HAProxy LoadBalancer em HA com Keepalived Automatizado

Toda a infraestrutura de TI hoje seja ela Cloud ou on-premise utiliza balanceadores de carga como a sua porta de entrada. Hoje acredito que todos os provedores de serviço de Cloud com salve exceções não fornecem esse tipo de serviço.

Mas e na infraestrutura on-premise como implementamos isso?

Nesse artigo trago a configuração do HAProxy em HA com Keepalived automatizado com ansible. Uma solução simples e bastante robusta dependendo da sua infraestrutura.

Requisitos

  • SSH
  • Ansible
  • 2 Máquinas virtuais
  • 2x
    • Ubuntu Server 18.04
    • 2 vCPU
    • 1GB RAM
    • 50GB Disco

Diretórios

Precisamos criar uma árvore de diretórios para que o ansible execute de forma correta. Na documentação do ansible existe um guia de como fazer isso. Link.

mkdir -p ansible-haproxy/{inventory/group_vars,roles,}
mkdir -p roles/haproxy
mkdir -p roles/haproxy/handlers
mkdir -p roles//haproxy/tasks
mkdir -p roles/haproxy/templates
tree -L 5 ansible-haproxy

ansible-haproxy
├── inventory
│   └── group_vars
└── roles
    └── haproxy
        ├── handlers
        ├── tasks
        └── templates

7 directories, 0 files

Arquivos

  • inventory.ini

Esse arquivo é responsável por armazenar os hosts que serão utilizados para configurar os servidores HAProxy.

Crie o arquivo no diretório ansible-haproxy/inventory.

[all]
haproxy-1 ansible_host=192.168.7.5  ip=192.168.7.5
haproxy-2 ansible_host=192.168.7.7  ip=192.168.7.7

[haproxy]
haproxy-1
haproxy-2
  • all.yaml

O arquivo all.yml corresponde as variáveis que serão utilizadas na role de insltação e configuração dos servidores.

vim inventory/group_vars/all.yaml
---
packages:
  to_install:
    - haproxy
    - keepalived
  to_remove:
    -

services:
  to_enable:
    - haproxy
    - keepalived
  to_disable:
    -
  • haproxy.yaml

Esse arquivo contem as variáveis para configuração do HAProxy e Keepalived.

vim inventory/group_vars/haproxy.yaml
---
# Virtual IP que será utilizado pelo Keepalived.
virtual_ipaddress: "192.168.246.254"

# Hostname do servidor primário haproxy, use o mesmo hostname do inventário.
# Ex.
# [all]
# haproxy-1 ansible_host=1.2.3.4
haproxy_master: "haproxy-1"

# Selecione o algoritmo de balanceamento: first, leastconn, static-rr or roundrobin.
balance_algorithm: "roundrobin"

# Selecione o modo HAProxy: layer_7 or layer_4
haproxy_mode: "layer_7"

# Dominio que está em uso.
# OBS: Essa configuração é para o modo Layer 7.
my_domain: "local.lab"

# Para configurar novos APP's basta repetir o bloco de variaveis conforme exemplo abaixo.
# Ex:
# new_app:
#   app_1:
#     name: "api_1"
#     port_front: 8070
#     port_back: 30870
#     backend: {
#       "hostname_1": "10.0.0.1",
#       "hostname_2": "172.16.0.1",
#       "hostname_3": "192.168.0.1"
#     }
#   app_2:
#     name: "api_2"
#     port_front: 8071
#     port_back: 30871
#     backend: {
#       "hostname_1": "10.0.0.1", # Finalize a linha com "," caso use mais de um backend.
#       "hostname_2": "172.16.0.1",
#       "hostname_3": "192.168.0.1"
#     }

new_app:
  app-1:
    name: "api-1"
    port_front: 8080
    port_back: 8080
    backend: {
      "webserver-1": "192.168.246.20",
    }
  app-2:
    name: "api-2"
    port_front: 8081
    port_back: 8081
    backend: {
      "webserver-1": "192.168.246.20",
    }
  • tasks - main.yaml

Nesse arquivo configuramos as tasks(tarefas) para o ansible fazer toda a sua mágica. A sintaxe do arquivo yaml é bem intuitiva visto que você está declarando toda a configuração.

vim roles/haproxy/tasks/main.yaml
---

- name: "Install Packages"
  apt:
    name: "{{ item }}"
    state: present
  loop: "{{ packages.to_install }}"

- name: "Configure New APP in HAProxy"
  template:
    src: haproxy.cfg.j2
    dest: /etc/haproxy/haproxy.cfg
    owner: root
    group: root
    mode: 0644
  with_dict: "{{ new_app }}"
  notify: "Apply new configuration for HAProxy"

- name: "Set up high availability with Keepalived"
  template:
    src: keepalived.conf.j2
    dest: /etc/keepalived/keepalived.conf
    owner: root
    group: root
    mode: 0644
  notify: "Apply new configuration for Keepalived"

- name: "Inicialize systemd services"
  systemd:
    name: "{{ item }}"
    state: started
    enabled: yes
  loop: "{{ services.to_enable }}"
  • handlers - main.yaml

Esse arquivo armazena as tasks handlers. Essas tasks só serão executadas caso tenha alguma modificação nas configurações que o ansible aplicar.

vim roles/haproxy/handlers/main.yaml
---
- name: "Apply new configuration for HAProxy"
  systemd:
    name: haproxy
    state: restarted

- name: "Apply new configuration for Keepalived"
  systemd:
    name: keepalived
    state: restarted
  • templates

Com base nesses arquivos templates(modelo) o ansible cria os arquivos de configuração do HAProxy e do Keepalived dinamicamente, ou seja, toda alteração que for feita nesses arquivos será aplicada em ambos os servidores.

Com isso conseguimos trabalhar de forma idempotente.

  • haproxy.cfg.j2

vim roles/haproxy/templates/haproxy.cfg.j2
{% raw %}

global
  ulimit-n 100042
  maxconn     4000
  daemon

  # turn on stats unix socket
  stats socket /var/lib/haproxy/stats mode 0600 level admin

### Defaults
defaults
  log global
{% if haproxy_mode == "layer_7" %}
  mode http
{% endif %}
  option dontlognull
  option http-server-close
  option redispatch
  retries 3
  timeout http-request 10s
  timeout queue 1m
  timeout connect 10s
  timeout client 1m
  timeout server 1m
  timeout http-keep-alive 10s
  timeout check 10s
  maxconn 3000

### Stats
listen  stats
        bind *:9000
        stats enable
        stats realm Haproxy\ Statistics
        stats uri /haproxy_stats
        stats auth admin:password
        stats refresh 30
        stats show-node
        mode http

{% if haproxy_mode == "layer_7" %}
frontend app
  log /dev/log local0 debug
  bind *:80
  reqadd X-Forwarded-Proto:\ http

#
# ACL
{% for acl, app in new_app.items() %}
acl {{ acl }} hdr(host) -i {{ acl }}.{{ my_domain }}
{% endfor %}

{% for acl, app in new_app.items() %}
use_backend app-{{ app.name }} if {{ acl }}
{% endfor %}

{% for acl, app in new_app.items() %}
backend app-{{ app.name }}
  balance {{ balance_algorithm }}
  mode    http
  option httpchk HEAD /
{% for host_name, ip_addr in app.backend.items() %}
    server {{ host_name }} {{ ip_addr }}:{{ app.port_back }} check
{% endfor %}

{% endfor %}
{% endif %}

{% if haproxy_mode == "layer_4" %}
{% for acl, app in new_app.items() %}
#
### app-{{ app.name }}
frontend app-{{ app.name }}
  log /dev/log local0 debug
  bind *:{{ app.port_front }}
  default_backend app-{{ app.name }}

backend app-{{ app.name }}
  balance {{ balance_algorithm }}
  mode    tcp
{% for host_name, ip_addr in app.backend.items() %}
    server {{ host_name }} {{ ip_addr }}:{{ app.port_back }} check
{% endfor %}

{% endfor %}
{% endif %}

{% endraw %}

OBS Caso use esse exemplo, copie o conteúdo entre as tags {% raw %} e {% endraw %} do arquivo.

  • keepalived.conf.j2

vim roles/haproxy/templates/keepalived.conf.j2
vrrp_script chk_haproxy {
  script "/usr/bin/killall -0 haproxy"  # check the haproxy process
  interval 2 # every 2 seconds
  weight 2 # add 2 points if OK
}

vrrp_instance VI_1 {
  interface ens32 # interface to monitor
{% if inventory_hostname == haproxy_master %}
  state MASTER # MASTER on ha1, BACKUP on ha2
{% else %}
  state BACKUP # MASTER on ha1, BACKUP on ha2
{% endif -%}
  virtual_router_id 51
{% if inventory_hostname == haproxy_master %}
  priority 101 # 101 on ha1, 100 on ha2
{% else %}
  priority 100 # 101 on ha1, 100 on ha2
{% endif -%}
  advert_int 1
  unicast_src_ip {{ ansible_host }}
  virtual_ipaddress {
    {{ virtual_ipaddress }} # virtual ip address
  }
  track_script {
    chk_haproxy
  }
}
  • playbook - haproxy.yaml

Playbook que será utilizado pelo ansible para aplicar a role de configuração do HAProxy e Keepalived.

vim ansible-haproxy/haproxy.yaml
---
- name: "Configure a new infraestructure HAProxy HA with Keepalived"
  hosts:
    - haproxy

  roles:
    - {role: haproxy, tags: "haproxy"}

No final a estrutura de diretórios e arquivos fica assim.

tree -L 5 ansible-haproxy

ansible-haproxy
├── haproxy.yaml
├── inventory
│   ├── group_vars
│   │   ├── all.yaml
│   │   └── haproxy.yaml
│   └── inventory.ini
└── roles
    └── haproxy
        ├── handlers
        │   └── main.yaml
        ├── tasks
        │   └── main.yaml
        └── templates
            ├── haproxy.cfg.j2
            └── keepalived.conf.j2

7 directories, 8 files

<>

A configuração aplicada pelo ansible não será alterada até que você modifique. Idempotência :ˆD!

<>

Demo

<

Conclusão

Mesmo em uma infraestrutura on-premise é possível trabalhar com soluções simples e robustas. A principio parece trabalhoso codificar tudo isso, mas, quando você estiver trabalhando com dezenas ou até centenas de hosts o cenário é totalmente favorável.

Projeto: https://github.com/fabianoflorentino/ansible-haproxy

comments powered by Disqus