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

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.