HAProxy Enterprise Documentation 2.1r1

Ansible

Ansible is a configuration management solution implemented primarily in the Python programming language that you can use to manage your HAProxy Enterprise deployment. Unlike other configuration management software, Ansible offers an ad-hoc mode in which tasks can be run manually. In many ways, this is similar to running commands via shell scripts or manually via SSH.

This guide shows how to run ad-hoc Ansible commands to control HAProxy Enterprise, as well as how to organize more complex tasks into Ansible playbooks, which are YAML-formatted task definitions executed with the ansible-playbook command.

Install Ansible

  1. Install the latest version of Ansible onto your workstation. This will be the the control node from which you manage HAProxy Enterprise nodes. Note that Ansible is not supported on Windows.

  2. Install Ansible Lint onto your workstation, which helps to identify syntax and spacing errors and contains style recommendations and deprecation warnings.

  3. Ansible uses SSH for communication with the remote Linux servers. Therefore, it expects that you have examined and accepted the remote server's SSH host key. Connect via SSH to the machines where HAProxy Enterprise is installed and accept the host key:

    $ ssh lb1.example.com
    
       The authenticity of host 'lb1.example.com (100.200.1.6)' can't be established.
       ECDSA key fingerprint is SHA256:dzUE7CyUTeE98A5WKUT8DyNwvNqFO3CcJtRQFvsa4xk.
       Are you sure you want to continue connecting (yes/no)? yes
  4. Update your Ansible inventory file, /etc/ansible/hosts, by adding your HAProxy Enterprise servers:

    [loadbalancers]
    lb1.example.com
    lb2.example.com
  5. Install Python and pip, the Python package manager, onto the HAProxy Enterprise servers. Ansible requires this on all nodes that it manages.

  6. After Python is installed, run the the following Ansible ping command to verify that everything is working:

    $ ansible loadbalancers -u root -m ping
    
    loadbalancers | SUCCESS => {
       "changed": false,
       "ping": "pong"
    }

    The -u flag defines the remote user for the SSH connection and -m tells Ansible which module should be used. Ping is one of the pre-installed modules.

  7. Many of the Ansible commands you'll use require the Runtime API. To enable it, see this guide.

  8. Install socat on the load balancers so that Ansible can invoke Runtime API commands:

    $ ansible loadbalancers -u root -m apt -a "name=socat state=present update_cache=yes"
    
    # or, if you are on RHEL7/CentOS:
    $ ansible loadbalancers -u root -m yum -a "name=socat state=present"

Ansible ad-hoc command usage

Unlike most configuration management engines, you can use Ansible to issue on-the-fly commands to reconfigure state on multiple machines. It shows you the state of the infrastructure and allows you to perform runtime changes to multiple machines easily.

The most basic command, which we introduced in the last section, uses the ping module to tell you whether the machine is alive:

$ ansible loadbalancers -u root -m ping

You can run shell commands on the remote machine by specifying the -a argument. Below, we invoke the netstat command on the remote load balancer servers:

$ ansible loadbalancers -u root -a "netstat -tlpn"

To use shell features like pipes and output redirects, you can use the shell module:

$ ansible loadbalancers -u root -m shell -a "netstat -tlpn | grep :80"

One thing to note is that if you choose to run as a non-root user, some commands will require sudo privileges to work properly. In that case it is necessary to use the --become flag (short form: -b) before executing commands. Below, we specify --become to ensure that the socat package is installed on the system:

$ ansible loadbalancers --become --ask-become-pass \
    -m apt -a "name=socat state=present update_cache=yes"

The --ask-become-pass argument prompts you to enter your sudo password. Please note that for --ask-become-pass to work correctly, all machines must have the same password for the sudo user.

Rather than prompting for the sudo password, you can enable passwordless sudo. To enable passwordless sudo, add the NOPASSWD: directive to your user or group using the visudo command.

$ sudo visudo

Then edit the file as shown below and save it:

# Allow members of group sudo to execute any command
%sudo   ALL=(ALL:ALL) NOPASSWD: ALL

Then you can use --become without --ask-become-pass.

Next, you can check if the HAProxy Enterprise service is running on a node. The --check argument can be used with most modules to see what changes would have been made without actually doing them. In this case, we only want to see if HAProxy Enterprise is active, but not start it otherwise.

$ ansible loadbalancers -u root -m service -a "name=hapee-2.1-lb state=started" --check

haproxy-ams | SUCCESS => {
  "changed": false,
  "name": "hapee-2.1-lb",
  "state": "started",
  ...

To perform a hitless reload, use state=reloaded and omit the --check parameter:

$ ansible loadbalancers -u root -m service -a "name=hapee-2.1-lb state=reloaded"

We can combine the above commands with the copy module to sync an arbitrary configuration to multiple hosts and then reload HAProxy Enterprise to apply the changes:

$ ansible loadbalancers -u root -m copy -a "src=/home/user/hapee-lb.cfg dest=/etc/hapee-2.1/hapee-lb.cfg"
$ ansible loadbalancers -u root -m service -a "name=hapee-2.1-lb state=reloaded"

Doing this for more than a few commands is quite tedious, which is why you might define an Ansible playbook instead. However, in a pinch, the ad-hoc commands are quite useful.

Ad-hoc commands can be used to interact directly with the HAProxy Runtime API.

For example, to disable a specific server from a specific backend:

$ ansible loadbalancers -u root -m shell -a "echo 'disable server bk_www/www-01-server' | socat stdio /var/run/hapee-2.1/hapee-lb.sock"

Or, to show different debugging statistics:

$ ansible loadbalancers -u root -m shell -a "echo 'show stat' | socat stdio /var/run/hapee-2.1/hapee-lb.sock"
$ ansible loadbalancers -u root -m shell -a "echo 'show info' | socat stdio /var/run/hapee-2.1/hapee-lb.sock"
$ ansible loadbalancers -u root -m shell -a "echo 'show fd' | socat stdio /var/run/hapee-2.1/hapee-lb.sock"
$ ansible loadbalancers -u root -m shell -a "echo 'show activity' | socat stdio /var/run/hapee-2.1/hapee-lb.sock"
$ ansible loadbalancers -u root -m shell -a "echo 'show pools' | socat stdio /var/run/hapee-2.1/hapee-lb.sock"

See the Runtime API documentation for more examples.

The ad-hoc commands are also useful for prototyping the commands which are going to become part of a larger playbook, so it's useful to be comfortable with running ad-hoc commands before beginning to write complex playbooks.

Write your first Ansible Playbook

In the last section we described a way to transfer an HAProxy Enterprise configuration to multiple hosts by using the ansible command; The commands used were the following:

$ ansible loadbalancers -u root -m copy -a "src=/home/user/hapee-lb.cfg dest=/etc/hapee-2.1/hapee-lb.cfg"
$ ansible loadbalancers -u root -m service -a "name=hapee-2.1-lb state=reloaded"

In this section, we will show how to adapt these ad-hoc commands into a playbook that can achieve the same result. The equivalent playbook would be the following:

- hosts: loadbalancers
  remote_user: root
  tasks:
    - name: Copy HAProxy Enterprise configuration
      copy:
        src: "/home/user/hapee-lb.cfg"
        dest: "/etc/hapee-2.1/hapee-lb.cfg"
        owner: root
        group: hapee
        mode: 0644
    - name: Check if there are no errors in configuration file
      command: "/opt/hapee-2.1/sbin/hapee-lb -c -f /etc/hapee-2.1/hapee-lb.cfg"
      register: hapee_check
    - name: Reload HAProxy Enterprise if the check passed
      service:
        name: hapee-2.1-lb
        state: reloaded
      when: hapee_check is success and not ansible_check_mode

You might notice a few things in the above YAML snippet when moving from ad-hoc commands to a playbook:

  • Under tasks, each separate command needs to be named; This name is displayed during playbook execution.

  • One addition not present in the ad-hoc commands is the register keyword, which is used to store the success/fail result of the remote command:

    - name: Check if there are no errors in configuration file
      command: "/opt/hapee-2.1/sbin/hapee-lb -c -f /etc/hapee-2.1/hapee-lb.cfg"
      register: hapee_check

    Then, the when line in the next block verifies that the configuration syntax check executed correctly. The block executes only upon success.

    - name: Reload HAProxy Enterprise if the check passed
      service:
         name: hapee-2.1-lb
         state: reloaded
      when: hapee_check is success and not ansible_check_mode

    If the check did not pass, we skip reloading HAProxy Enterprise. It also checks that Ansible is not running in dry-run mode by verifying ansible_check_mode.

Ansible Lint

Save the above file as first-haproxy-deploy.yml and check its syntax with ansible-lint:

$ ansible-lint first-haproxy-deploy.yml

[301] Commands should not change things if nothing needs doing
first-playbook.yml:14
Task/Handler: Check if there are no errors in configuration file

You can ignore the [ 301 ] warning in this case; We are only interested in making sure there are no obvious errors, the most common being missing spaces in the YAML file.

The linter is useful, however it generally only catches Ansible syntax errors. Therefore, it is recommended to run playbooks with the --check flag to catch some Ansible runtime errors. Run the ansible-playbook command with the --check flag:

$ ansible-playbook first-haproxy-deploy.yml --check

This step only validates that there are no obvious errors in the playbook itself, and not the HAProxy Enterprise configuration file.

Finally, to run the playbook execute:

$ ansible-playbook first-haproxy-deploy.yml

Jinja templates

You can utilize Jinja templates in both the playbook YAML file itself and in configuration files it manages.

To use Jinja templates to manage an external file you can use the template module. In the snippet below, the tasks block now includes a template block:

- hosts: loadbalancers
  remote_user: root
  tasks:
    - name: Sync main HAPEE configuration
      template:
        src: ../templates/hapee-lb.cfg.j2
        dest: /etc/hapee-2.1/hapee-lb.cfg
        owner: root
        group: hapee
        mode: 0664
    - name: Check if there are no errors in configuration file
      command: "/opt/hapee-2.1/sbin/hapee-lb -c -f /etc/hapee-2.1/hapee-lb.cfg"
      register: hapee_check
    - name: Reload HAPEE if the check passed
      service:
        name: hapee-2.1-lb
        state: reloaded
      when: hapee_check is success and not ansible_check_mode

To start using Jinja templates, it is enough to rename a file to have a .j2 extension. Then you can optionally introduce Jinja templating patterns into the file.

To use Jinja templates inside your playbooks directly, you can look at the following example playbook:

---
# updates ACLs on remote HAProxy Enterprise instances via the Runtime API
- hosts: loadbalancers
  remote_user: ubuntu
  become: true
  become_user: root
  become_method: sudo
  tasks:
    - name: update ACL file
      shell: "echo '{{ acl_action }} acl {{ acl_path }} {{ acl_ip_address }}' | socat stdio /var/run/hapee-2.1/hapee-lb.sock"
    - name: check ACL file for presence/absence of the IP address
      shell: "echo 'show acl {{ acl_path }}' | socat stdio /var/run/hapee-2.1/hapee-lb.sock | grep {{ acl_ip_address }}"
      register: socket_show
      failed_when: "socket_show.rc != 0 and socket_show.rc != 1"
    - debug: var=socket_show.stdout_lines

Jinja patterns are enclosed in quotes and curly brackets {{ }}; The variables inside of the brackets will be expanded and used as strings. Variables are set when you run the ansible-playbook command, as shown below:

$ ansible-playbook update-acl-playbook.yml \
   -e "acl_action=add acl_path=/etc/hapee-2.1/ip-whitelist.acl `acl_ip_address=10.10.10.10"`

External variables are defined via the -e flag. However, Jinja templates can be used with various different variables like host variables, group variables, Ansible facts or custom variables set via the register keyword. See more examples in the Ansible Templating (Jinja2) guide.

Ansible inventory file configuration

In our previous inventory file, we defined two load balancer nodes that Ansible manages:

[loadbalancers]
lb1.example.com
lb2.example.com

You can create hierarchies of load balancer groups, such as to update all load balancers, only load balancers in Europe, or only load balancers in the Americas. One node can appear in multiple groups. The group names can contain any valid DNS record or IP addresses; As a suffix, the port on which SSH is listening can also be specified. A more complex inventory file could look similar to this:

[loadbalancers:children]
loadbalancers-europe
loadbalancer-americas

[loadbalancers-europe]
lb01.eu.example.com
lb02.eu.example.com

[loadbalancers-americas]
lb01.us.example.com
lb02.ca.example.com
lb02.mx.example.com

[loadbalancers-staging]
# Using IP address with SSH ports
10.10.12.10:2222
10.10.12.11:2223

The :children keyword creates a new group that inherits the nodes of two other groups. Set the group when calling an ad-hoc command. Below, we check whether the European load balancers are up:

$ ansible loadbalancers-europe -u root -m ping

Next up

Dynamic Data Updates