27-05-2020 / blog / Jeroen van de Lockand

Ansible Automation - HAProxy Loadbalancer met NGINX

In dit blog laat ik zien hoe de installatie van een HAProxy load-balancer geautomatiseerd kan worden met Ansible.

 

Als Site Reliability Engineer(SRE) zijn belangrijke doelen het creëren van schaalbare en zeer betrouwbare oplossingen voor applicaties. HAProxy biedt een hoge beschikbaarheid load-balancer en proxy-server voor TCP en HTTP gebaseerde applicaties die verzoeken over meerdere servers kan verdelen.

 

Ansible helpt een SRE met het automatiseren van reperterende taken, configuratie management, uitrollen van applicaties en het schaalbaar houden van infrastructuur. Ansible bestaat uit eenvoudig leesbare YAML bestanden. Hoe je Ansible in kunt zetten voor het uitrollen van HAProxy en NGINX laat ik je in dit blog zien.

 

De code en playbooks zijn beschikbaar op de Conclusion Xforce GitLab pagina hier.

 

Ik begin met een omgeving waarbij het Operating System geïnstalleerd is en SSH-keys zijn geïnstalleerd om de servers met Ansible aan te kunnen sturen.

 

Mijn omgeving ziet er als volgt uit:

- 2 HAProxy loadbalancers met keepalived

- 3 NGINX webservers

Inventory

We beginnen met het definieren van de omgeving in een inventory. Een inventory is een lijst met hosts die gemanaged worden door Ansible. Deze lijst kan onderverdeeld worden in meerder groepen. Het meest voorkomende bestandsformaat voor een inventory is INI of YAML.

 

Voor mijn omgeving ziet de inventory er als volgt uit.

 

 

[loadbalancers]
lb1 ansible_host=192.168.122.53 
lb2 ansible_host=192.168.122.196 

[webserver1] 
nginx1 ansible_host=192.168.122.45

[webserver2] 
nginx2 ansible_host=192.168.122.149

[webserver3] 
nginx3 ansible_host=192.168.122.142
 
[webservers:children]
webserver1
webserver2
webserver3

[all:vars]
ansible_connection=ssh
ansible_user=automation
ansible_ssh_pass=conclusionxforce
ansible_become_pass=conclusionxforce

De loadbalancers zijn onderverdeeld in een groep loadbalancers. De webservers apart gedefinieerd en samengevoegd onder de groep webservers.

 

Verder worden nog een aantal variablen gebruikt om aan te geven hoe Ansible verbinding moet maken met de servers.

 

Dit bestand sla ik op als production in een aparte directory inventory. Dit ziet er als volgt uit.

# tree inventory/

inventory/
└── production

0 directories, 1 file

Om te testen dat de inventory goed geconfigureerd is, gaan we ons eerste Ansible commando uitvoeren.

# ansible -m ping -i inventory/production all

nginx3 | SUCCESS => {
    "ansible_facts": {
        "discovered_interpreter_python": "/usr/bin/python3"
    },
    "changed": false,
    "ping": "pong"
}
nginx1 | SUCCESS => {
    "ansible_facts": {
        "discovered_interpreter_python": "/usr/bin/python3"
    },
    "changed": false,
    "ping": "pong"
}
nginx2 | SUCCESS => {
    "ansible_facts": {
        "discovered_interpreter_python": "/usr/bin/python3"
    },
    "changed": false,
    "ping": "pong"
}
lb1 | SUCCESS => {
    "ansible_facts": {
        "discovered_interpreter_python": "/usr/bin/python3"
    },
    "changed": false,
    "ping": "pong"
}
lb2 | SUCCESS => {
    "ansible_facts": {
        "discovered_interpreter_python": "/usr/bin/python3"
    },
    "changed": false,
    "ping": "pong"
}

Hier gebruik ik Ansible met de module ping om te verifieren dat ik kan verbinden met alle servers en er een bruikbare python versie geïnstalleerd is. Als dit zo is dan reageert de server met "pong".

Nu onze inventory klaar is gaan we verder met 't maken van de volgende rollen:

 

  1. Installeren en configureren van haproxy met keepalived voor hoge beschikbaarheid.
  2. Installeren en configureren van NGINX webservers.

 

Voor de rollen maak ik een aparte directory die ik roles noem en in deze directory maak ik nog twee directories haproxy-keepalived en nginx.

 

 

haproxy-keepalived

Binnen de rol haproxy-keepalived maak ik een task definitie roles/haproxy-keepalived/tasks/main.yml. Hierin include ik twee taken die uitgevoerd moeten worden.

 

  1. keepalived.yml
  2. haproxy.yml

 

HAProxy

Binnen de rol haproxy-keepalived maak ik een task definitie roles/haproxy-keepalived/tasks/haproxy.yml. Deze task installeert en configureert HAProxy.

- name: Put SELinux in permissive mode, logging actions that would be blocked.
  selinux:
    state: disabled

- name: Ensure dependencies are installed
  yum: 
    name: "{{ packages }}"
    state: latest
  vars:
    packages:
      - openssl-devel 
      - pcre-devel
      - make
      - gcc
      - socat
  when: ansible_os_family == "RedHat"

- name: Ensure haproxy
  yum: 
    name: haproxy
    state: latest
  when: ansible_os_family == "RedHat"

- name: Create haproxy config file
  template:
    src: templates/haproxy.cfg.j2
    dest: /etc/haproxy/haproxy.cfg
    backup: yes
    owner: root
    mode: 0644
  notify:
    - restart haproxy    

- name: Configure kernel parameters
  sysctl:
    name: net.ipv4.ip_nonlocal_bind
    value: '1'
    sysctl_set: yes
    state: present
    reload: yes

- name: Configure HAProxy to start at boot
  service: 
    name: haproxy 
    enabled: yes 
    state: started

Keepalived

Keepalived is een service die processen en systemen kan monitoren en automatisch failover kan initieren mochten problemen zich voor doen.

 

Binnen de rol haproxy-keepalived maak ik een task definitie roles/haproxy-keepalived/tasks/keepalived.yml. Deze task installeert en configureert keepalived. 

- name: Ensure keepalived
  yum: 
    name: "{{ packages }}"
    state: latest
  vars:
    packages:
      - keepalived
  when: ansible_os_family == "RedHat"

- name: Create keepalived config file
  template:
    src: templates/keepalived.conf.j2
    dest: /etc/keepalived/keepalived.conf
    backup: yes
    owner: root
    group: root
    mode: 0644
  notify:
    - restart keepalived

- name: Configure keepalived to start at boot
  service: 
    name: keepalived 
    enabled: yes 
    state: started

De taken om haproxy en keepalived te installeren en configureen zijn nu klaar. Van deze taken is er een stuk code (uit de keepalived.yml) dat ik nader wil toelichten.

- name: Create keepalived config file
  template:
    src: templates/keepalived.conf.j2
    dest: /etc/keepalived/keepalived.conf
    backup: yes
    owner: root
    group: root
    mode: 0644
  notify:
    - restart keepalived

Dit stuk code uit de keepalived.yml gebruikt de template module van Ansible.

De template staat in een aparte directory binnen de rol genaamd templates en worden door Ansible verwerkt in de Jinja2 template taal en op de servers gezet. Hiermee configureren we de keepalived daemon.

 

In deze template gebruik ik een aantal variablen bijvoorbeeld "

{{ keepalived_interval }}". Deze variablen zet ik een apart bestand in de directory host_vars. Ik gebruik alleen voor de load-balancers extra variablen de bestanden in de host_vars directory zijn hier naar vernoemd lb1 en lb2.

 

Nadat Ansible de configuratie op de server heeft gezet, moet de daemon herstart worden om de configuratie te gebruiken.

 

Het "notify: - restart keepalived" roept een handler aan om te zorgen dat keepalived herstart wordt. Een handler is een lijst met taken die eigenlijk niet zo veel verschilt met reguliere taken, maar die middels de notify aangeroepen worden. Kijk maar eens naar de roles/haproxy-keepalived/handlers/main.yml. Hierin beschrijf ik het herstarten van haproxy en keepalived.

 

De role voor haproxy-keepalived ziet er als volgt uit.

# tree roles/haproxy-keepalived/

roles/haproxy-keepalived/
├── handlers
│   └── main.yml
├── meta
│   └── main.yml
├── README.md
├── tasks
│   ├── haproxy.yml
│   ├── keepalived.yml
│   └── main.yml
└── templates
    ├── haproxy.cfg.j2
    └── keepalived.conf.j2

4 directories, 8 files

nginx

Binnen de rol haproxy-keepalived maak ik een task definitie roles/nginx/tasks/main.yml. Hierin include ik een taak die uitgevoerd moet worden.

 

  1. nginx.yml

 

NGINX

Binnen de rol haproxy-keepalived maak ik een task definitie roles/nginx/tasks/nginx.yml. Deze task installeert en configureert de webservers met NGINX.

- name: Ensure nginx
  yum: 
    name: nginx
    state: latest
  when: ansible_os_family == "RedHat"

- name: Configure NGINX to start at boot
  service: 
    name: nginx 
    enabled: yes 
    state: started

- name: Dynamic Firewall Manager - Allow HTTP
  firewalld:
    service: http
    permanent: yes
    state: enabled

- name: Dynamic Firewall Manager - Allow HTTPS
  firewalld:
      service: https
      permanent: yes
      state: enabled

Deze task is vrij klein en installeert NGINX en zet TCP poort 80 en TCP poort 443 open in de firewall.

 

De rol voor NGINX ziet er als volgt uit.

# tree roles/nginx/

roles/nginx/
├── handlers
│   └── main.yml
├── meta
│   └── main.yml
├── README.md
└── tasks
    ├── main.yml
    └── nginx.yml

3 directories, 5 files


# tree roles/

roles/
├── haproxy-keepalived
│   ├── handlers
│   │   └── main.yml
│   ├── meta
│   │   └── main.yml
│   ├── README.md
│   ├── tasks
│   │   ├── haproxy.yml
│   │   ├── keepalived.yml
│   │   └── main.yml
│   └── templates
│       ├── haproxy.cfg.j2
│       └── keepalived.conf.j2
└── nginx
    ├── handlers
    │   └── main.yml
    ├── meta
    │   └── main.yml
    ├── README.md
    └── tasks
        ├── main.yml
        └── nginx.yml

9 directories, 13 files

Klaar voor de start

Nu de rollen klaar zijn willen we dit uitvoeren op de servers. De loadbalancers moeten de rol haproxy-keepalived krijgen en de webservers de rol NGINX.

 

Dit alles voeg ik samen in een bestand site.yml. In de site.yml geef ik per host groep aan welke role het krijgt.

---
- hosts: all

- hosts: loadbalancers
  gather_facts: false
  become: yes
  roles:
    - haproxy-keepalived

- hosts: webservers
  gather_facts: false
  become: yes
  roles:
    - nginx

Voer het ansible-playbook commando uit met de inventory file production.

# ansible-playbook -i inventory/production site.yml

PLAY RECAP ****************************************************************************************************************
lb1                        : ok=10   changed=2    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0   
lb2                        : ok=10   changed=2    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0   
nginx1                     : ok=5    changed=1    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0   
nginx2                     : ok=5    changed=1    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0   
nginx3                     : ok=5    changed=1    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0   

De bovenstaande output is ingekort, maar om te controleren of alles goed gegaan is kan ik in mijn browser naar het IP adres van de server op poort 22002 gaan waar de HAProxy statistieken pagina beschikbaar is.

Als ik naar het virtuele IP adres van de loadbalancers ga dan zou je een webpagina met de hostnaam van de webserver moeten zien.

Ververs de pagina een aantal keren met F5 om te zien dat de verzoeken op alle 3 de webservers terecht komen.

Hiermee zijn we aan het einde gekomen van dit blog. In dit blog heb ik laten zien hoe je met Ansible een hoog beschikbare loadbalancer kunt installeren.

 

In het volgende blog zal ik laten zien hoe de "day 2 operations" eruit zien door zonder downtime deze setup te updaten.