Integrations

Ansible

Ansible is a configuration management solution implemented primarily in the Python programming language that you can use to manage your load balancer 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 the load balancer, 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 Jump to heading

  1. Install the latest version of Ansible onto your workstation. This will be the control node from which you manage your load balancer 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. Install socat on your load balancers so Ansible can invoke Runtime API commands:

    nix
    sudo apt-get install socat
    nix
    sudo apt-get install socat
    nix
    sudo yum install socat
    nix
    sudo yum install socat
    nix
    sudo zypper install socat
    nix
    sudo zypper install socat
    nix
    sudo pkg install socat
    nix
    sudo pkg install socat
  4. 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 the load balancer is installed and accept the host key:

    nix
    ssh lb1.example.com
    nix
    ssh lb1.example.com
    output
    text
    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
    output
    text
    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
  5. Update your Ansible inventory file, /etc/ansible/hosts, by adding your load balancer servers:

    ini
    [loadbalancers]
    lb1.example.com
    lb2.example.com
    ini
    [loadbalancers]
    lb1.example.com
    lb2.example.com
  6. Install Python and pip, the Python package manager, onto the load balancer servers. Ansible requires this on all nodes that it manages.

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

    nix
    ansible loadbalancers -u root -m ping
    nix
    ansible loadbalancers -u root -m ping
    output
    text
    loadbalancers | SUCCESS => {
    "changed": false,
    "ping": "pong"
    }
    output
    text
    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.

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

Ansible ad-hoc command usage Jump to heading

Unlike most configuration management engines, you can use Ansible to issue on-the-fly commands to reconfigure the 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:

nix
ansible loadbalancers -u root -m ping
nix
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:

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

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

nix
ansible loadbalancers -u root -m shell -a "netstat -tlpn | grep :80"
nix
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:

nix
ansible loadbalancers --become --ask-become-pass \
-m apt -a "name=socat state=present update_cache=yes"
nix
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.

nix
sudo visudo
nix
sudo visudo

Then edit the file as shown below and save it:

nix
# Allow members of group sudo to execute any command
%sudo ALL=(ALL:ALL) NOPASSWD: ALL
nix
# 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 load balancer 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 the load balancer is active, but not start it otherwise.

nix
ansible loadbalancers -u root -m service -a "name=hapee-2.8-lb state=started" --check
nix
ansible loadbalancers -u root -m service -a "name=hapee-2.8-lb state=started" --check
output
text
haproxy-ams | SUCCESS => {
"changed": false,
"name": "hapee-2.8-lb",
"state": "started",
...
output
text
haproxy-ams | SUCCESS => {
"changed": false,
"name": "hapee-2.8-lb",
"state": "started",
...

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

nix
ansible loadbalancers -u root -m service -a "name=hapee-2.8-lb state=reloaded"
nix
ansible loadbalancers -u root -m service -a "name=hapee-2.8-lb state=reloaded"

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

nix
ansible loadbalancers -u root -m copy -a "src=/home/user/hapee-lb.cfg dest=/etc/hapee-2.8/hapee-lb.cfg"
nix
ansible loadbalancers -u root -m copy -a "src=/home/user/hapee-lb.cfg dest=/etc/hapee-2.8/hapee-lb.cfg"
nix
ansible loadbalancers -u root -m service -a "name=hapee-2.8-lb state=reloaded"
nix
ansible loadbalancers -u root -m service -a "name=hapee-2.8-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:

nix
ansible loadbalancers -u root -m shell -a "echo 'disable server bk_www/www-01-server' | socat stdio unix-connect:/var/run/hapee-2.8/hapee-lb.sock"
nix
ansible loadbalancers -u root -m shell -a "echo 'disable server bk_www/www-01-server' | socat stdio unix-connect:/var/run/hapee-2.8/hapee-lb.sock"

Or, to show different debugging statistics:

  • show stat

    nix
    ansible loadbalancers -u root -m shell -a "echo 'show stat' | socat stdio unix-connect:/var/run/hapee-2.8/hapee-lb.sock"
    nix
    ansible loadbalancers -u root -m shell -a "echo 'show stat' | socat stdio unix-connect:/var/run/hapee-2.8/hapee-lb.sock"
  • show info

    nix
    ansible loadbalancers -u root -m shell -a "echo 'show info' | socat stdio unix-connect:/var/run/hapee-2.8/hapee-lb.sock"
    nix
    ansible loadbalancers -u root -m shell -a "echo 'show info' | socat stdio unix-connect:/var/run/hapee-2.8/hapee-lb.sock"
  • show fd

    nix
    ansible loadbalancers -u root -m shell -a "echo 'show fd' | socat stdio unix-connect:/var/run/hapee-2.8/hapee-lb.sock"
    nix
    ansible loadbalancers -u root -m shell -a "echo 'show fd' | socat stdio unix-connect:/var/run/hapee-2.8/hapee-lb.sock"
  • show activity

    nix
    ansible loadbalancers -u root -m shell -a "echo 'show activity' | socat stdio unix-connect:/var/run/hapee-2.8/hapee-lb.sock"
    nix
    ansible loadbalancers -u root -m shell -a "echo 'show activity' | socat stdio unix-connect:/var/run/hapee-2.8/hapee-lb.sock"
  • show pools

    nix
    ansible loadbalancers -u root -m shell -a "echo 'show pools' | socat stdio unix-connect:/var/run/hapee-2.8/hapee-lb.sock"
    nix
    ansible loadbalancers -u root -m shell -a "echo 'show pools' | socat stdio unix-connect:/var/run/hapee-2.8/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 Jump to heading

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

nix
ansible loadbalancers -u root -m copy -a "src=/home/user/hapee-lb.cfg dest=/etc/hapee-2.8/hapee-lb.cfg"
nix
ansible loadbalancers -u root -m copy -a "src=/home/user/hapee-lb.cfg dest=/etc/hapee-2.8/hapee-lb.cfg"
nix
ansible loadbalancers -u root -m service -a "name=hapee-2.8-lb state=reloaded"
nix
ansible loadbalancers -u root -m service -a "name=hapee-2.8-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:

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

Multiple configuration files

If you have multiple configuration files in your application, be sure the hapee-lb -c command checks them all in the correct order.

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:

    yaml
    - name: Check if there are no errors in configuration file
    command: "/opt/hapee-2.8/sbin/hapee-lb -c -f /etc/hapee-2.8/hapee-lb.cfg"
    register: hapee_check
    yaml
    - name: Check if there are no errors in configuration file
    command: "/opt/hapee-2.8/sbin/hapee-lb -c -f /etc/hapee-2.8/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.

    yaml
    - name: Reload load balancer if the check passed
    service:
    name: hapee-2.8-lb
    state: reloaded
    when: hapee_check is success and not ansible_check_mode
    yaml
    - name: Reload load balancer if the check passed
    service:
    name: hapee-2.8-lb
    state: reloaded
    when: hapee_check is success and not ansible_check_mode

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

Ansible Lint Jump to heading

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

nix
ansible-lint first-haproxy-deploy.yml
nix
ansible-lint first-haproxy-deploy.yml
output
text
[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
output
text
[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:

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

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

Finally, to run the playbook execute:

nix
ansible-playbook first-haproxy-deploy.yml
nix
ansible-playbook first-haproxy-deploy.yml

Jinja templates Jump to heading

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:

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

yaml
---
# updates ACLs on remote load balancer nodes 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 unix-connect:/var/run/hapee-2.8/hapee-lb.sock"
- name: check ACL file for presence/absence of the IP address
shell: "echo 'show acl {{ acl_path }}' | socat stdio unix-connect:/var/run/hapee-2.8/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
yaml
---
# updates ACLs on remote load balancer nodes 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 unix-connect:/var/run/hapee-2.8/hapee-lb.sock"
- name: check ACL file for presence/absence of the IP address
shell: "echo 'show acl {{ acl_path }}' | socat stdio unix-connect:/var/run/hapee-2.8/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:

nix
ansible-playbook update-acl-playbook.yml \
-e "acl_action=add acl_path=/etc/hapee-2.8/ip-whitelist.acl `acl_ip_address=10.10.10.10"`
nix
ansible-playbook update-acl-playbook.yml \
-e "acl_action=add acl_path=/etc/hapee-2.8/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 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 Jump to heading

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

ini
[loadbalancers]
lb1.example.com
lb2.example.com
ini
[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:

ini
[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
ini
[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:

nix
ansible loadbalancers-europe -u root -m ping
nix
ansible loadbalancers-europe -u root -m ping

Do you have any suggestions on how we can improve the content of this page?