Introduction
Keepalived implements VRRP (Virtual Router Redundancy Protocol) for Linux — it provides virtual IP (VIP) failover between servers, making load balancers, databases, and any service highly available. Ansible automates Keepalived deployment: primary/backup pairs, health check scripts, notification hooks, and integration with HAProxy and Nginx.
Basic VRRP Setup
---
- name: Deploy Keepalived
hosts: ha_pair
become: true
vars:
keepalived_vip: 10.0.0.100
keepalived_interface: eth0
keepalived_router_id: 51
keepalived_auth_pass: "{{ vault_keepalived_password }}"
tasks:
- name: Install Keepalived
ansible.builtin.package:
name: keepalived
state: present
- name: Deploy keepalived.conf
ansible.builtin.template:
src: keepalived.conf.j2
dest: /etc/keepalived/keepalived.conf
mode: '0644'
notify: restart keepalived
- name: Enable non-local IP binding
ansible.posix.sysctl:
name: net.ipv4.ip_nonlocal_bind
value: '1'
sysctl_set: true
reload: true
- name: Start Keepalived
ansible.builtin.service:
name: keepalived
state: started
enabled: true
handlers:
- name: restart keepalived
ansible.builtin.service:
name: keepalived
state: restarted
Config Template
# templates/keepalived.conf.j2
global_defs {
router_id {{ inventory_hostname }}
script_user root
enable_script_security
}
vrrp_script check_service {
script "{{ keepalived_check_script | default('/usr/local/bin/check_service.sh') }}"
interval 2
weight -20
fall 3
rise 2
}
vrrp_instance VI_1 {
state {{ 'MASTER' if inventory_hostname == groups['ha_pair'][0] else 'BACKUP' }}
interface {{ keepalived_interface }}
virtual_router_id {{ keepalived_router_id }}
priority {{ 101 if inventory_hostname == groups['ha_pair'][0] else 100 }}
advert_int 1
authentication {
auth_type PASS
auth_pass {{ keepalived_auth_pass }}
}
virtual_ipaddress {
{{ keepalived_vip }}/24 dev {{ keepalived_interface }}
}
track_script {
check_service
}
{% if keepalived_unicast | default(false) %}
unicast_src_ip {{ ansible_default_ipv4.address }}
unicast_peer {
{% for host in groups['ha_pair'] %}
{% if host != inventory_hostname %}
{{ hostvars[host].ansible_default_ipv4.address }}
{% endif %}
{% endfor %}
}
{% endif %}
{% if keepalived_notify_script is defined %}
notify {{ keepalived_notify_script }}
{% endif %}
}
HAProxy + Keepalived HA
---
- name: Deploy HA load balancer pair
hosts: ha_lb
become: true
vars:
keepalived_vip: 10.0.0.100
keepalived_interface: eth0
tasks:
- name: Install HAProxy and Keepalived
ansible.builtin.package:
name: [haproxy, keepalived]
state: present
- name: Deploy HAProxy check script
ansible.builtin.copy:
dest: /usr/local/bin/check_service.sh
content: |
#!/bin/bash
# Check if HAProxy is running and responding
if ! pidof haproxy > /dev/null; then
exit 1
fi
if ! curl -sf http://localhost:8404/stats > /dev/null 2>&1; then
exit 1
fi
exit 0
mode: '0755'
- name: Deploy Keepalived config
ansible.builtin.template:
src: keepalived.conf.j2
dest: /etc/keepalived/keepalived.conf
mode: '0644'
notify: restart keepalived
Nginx + Keepalived HA
- name: Deploy Nginx health check
ansible.builtin.copy:
dest: /usr/local/bin/check_service.sh
content: |
#!/bin/bash
if ! pidof nginx > /dev/null; then
exit 1
fi
if ! curl -sf http://{{ inventory_hostname }}/index.html > /dev/null 2>&1; then
exit 1
fi
exit 0
mode: '0755'
Multiple VIPs
# templates/keepalived-multi.conf.j2
{% for vip in keepalived_vips %}
vrrp_instance VI_{{ loop.index }} {
state {{ 'MASTER' if (loop.index0 % 2 == 0 and inventory_hostname == groups['ha_pair'][0]) or (loop.index0 % 2 == 1 and inventory_hostname == groups['ha_pair'][1]) else 'BACKUP' }}
interface {{ vip.interface | default(keepalived_interface) }}
virtual_router_id {{ vip.router_id }}
priority {{ 101 if (loop.index0 % 2 == 0 and inventory_hostname == groups['ha_pair'][0]) or (loop.index0 % 2 == 1 and inventory_hostname == groups['ha_pair'][1]) else 100 }}
advert_int 1
authentication {
auth_type PASS
auth_pass {{ keepalived_auth_pass }}
}
virtual_ipaddress {
{{ vip.address }}/24
}
track_script {
check_service
}
}
{% endfor %}
# Active-active with distributed VIPs
keepalived_vips:
- { address: 10.0.0.100, router_id: 51 } # MASTER on node1
- { address: 10.0.0.101, router_id: 52 } # MASTER on node2
Notification Script
- name: Deploy notification script
ansible.builtin.copy:
dest: /usr/local/bin/keepalived-notify.sh
content: |
#!/bin/bash
TYPE=$1 # INSTANCE or GROUP
NAME=$2 # VI_1
STATE=$3 # MASTER, BACKUP, FAULT
PRIORITY=$4
case $STATE in
"MASTER")
echo "$(date) - Became MASTER on $(hostname)" >> /var/log/keepalived-state.log
# Restart services if needed
systemctl restart haproxy
;;
"BACKUP")
echo "$(date) - Became BACKUP on $(hostname)" >> /var/log/keepalived-state.log
;;
"FAULT")
echo "$(date) - FAULT state on $(hostname)" >> /var/log/keepalived-state.log
;;
esac
mode: '0755'
vars:
keepalived_notify_script: /usr/local/bin/keepalived-notify.sh
Health Check
- name: Check which node holds VIP
ansible.builtin.command: "ip addr show {{ keepalived_interface }}"
register: ip_output
changed_when: false
- name: Report VIP location
ansible.builtin.debug:
msg: "{{ inventory_hostname }} {{ 'HOLDS' if keepalived_vip in ip_output.stdout else 'does NOT hold' }} the VIP {{ keepalived_vip }}"
- name: Verify VIP is reachable
ansible.builtin.command: "ping -c 1 {{ keepalived_vip }}"
register: vip_ping
changed_when: false
delegate_to: localhost
Troubleshooting
Split-Brain Prevention
# Use unicast instead of multicast to prevent split-brain in cloud environments
keepalived_unicast: true
VIP Not Floating
- name: Check keepalived logs
ansible.builtin.command: journalctl -u keepalived --no-pager -n 50
register: kd_logs
changed_when: false
Related Articles
Conclusion
Keepalived provides sub-second failover for any service using VRRP virtual IPs. Ansible templates the config from inventory — the first host in the ha_pair group becomes MASTER, the rest are BACKUP. Health check scripts track service state and trigger failover automatically. Use unicast mode in cloud environments, multiple VIPs for active-active, and notification scripts for logging state transitions. High availability as code.