Ansible Playbook 自動化部署

用 Ansible 一鍵完成 Keepalived、HAProxy 和 Docker Backend 的安裝與配置。

目錄結構

ansible/
├── inventory/
│   └── hosts.yml          # 定義 lb-master 和 lb-slave
├── group_vars/
│   └── all.yml            # 共用變數(VIP、認證密碼等)
├── host_vars/
│   ├── lb-master.yml      # Master 專屬變數(priority、state)
│   └── lb-slave.yml       # Slave 專屬變數
├── roles/
│   ├── keepalived/
│   │   ├── tasks/main.yml
│   │   ├── templates/keepalived.conf.j2
│   │   └── handlers/main.yml
│   ├── haproxy/
│   │   ├── tasks/main.yml
│   │   ├── templates/haproxy.cfg.j2
│   │   └── handlers/main.yml
│   └── docker_backend/
│       ├── tasks/main.yml
│       ├── templates/docker-compose.yml.j2
│       └── files/
│           └── api/app.py
└── site.yml               # 主 playbook

安裝 Ansible(在 Mac 上執行)

brew install ansible

# 確認版本
ansible --version
# ansible [core 2.x.x]

Inventory 設定

inventory/hosts.yml

all:
  children:
    lb_nodes:
      hosts:
        lb-master:
          ansible_host: 192.168.50.211
          ansible_user: ubuntu
        lb-slave:
          ansible_host: 192.168.50.212
          ansible_user: ubuntu

設定 SSH 免密碼登入

# 產生 SSH key(若尚未有)
ssh-keygen -t ed25519 -C "ansible"

# 複製 public key 到兩台 VM
ssh-copy-id ubuntu@192.168.50.211
ssh-copy-id ubuntu@192.168.50.212

# 測試連線
ansible lb_nodes -i inventory/hosts.yml -m ping
# 預期:
# lb-master | SUCCESS => {"ping": "pong"}
# lb-slave  | SUCCESS => {"ping": "pong"}

變數設定

group_vars/all.yml(兩台共用)

# VIP 設定
vrrp_vip: "192.168.50.250"
vrrp_interface: "ens4"        # 橋接網卡名稱(請確認)
vrrp_router_id: 51
vrrp_advert_int: 1
vrrp_auth_pass: "LB2024secret"

# HAProxy
haproxy_stats_user: "admin"
haproxy_stats_pass: "admin123"
haproxy_web_port: 80
haproxy_api_port: 8081
haproxy_stats_port: 8404

# Backend 服務
backend_web_port: 8080
backend_api_port: 8081
backend_servers:
  - name: lb-master
    ip: "192.168.50.211"
  - name: lb-slave
    ip: "192.168.50.212"

host_vars/lb-master.yml

keepalived_state: "MASTER"
keepalived_priority: 100
keepalived_preempt: true

host_vars/lb-slave.yml

keepalived_state: "BACKUP"
keepalived_priority: 90
keepalived_preempt: false

Keepalived Role

roles/keepalived/tasks/main.yml

- name: Install keepalived
  apt:
    name: keepalived
    state: present
    update_cache: yes
  become: true

- name: Deploy keepalived config
  template:
    src: keepalived.conf.j2
    dest: /etc/keepalived/keepalived.conf
    owner: root
    group: root
    mode: '0644'
  become: true
  notify: Restart keepalived

- name: Enable and start keepalived
  systemd:
    name: keepalived
    enabled: true
    state: started
  become: true

roles/keepalived/templates/keepalived.conf.j2

! Configuration File for keepalived - managed by Ansible

global_defs {
    router_id 
    script_user root
    enable_script_security
}

vrrp_script chk_haproxy {
    script "killall -0 haproxy"
    interval 2
    weight -20
    fall 2
    rise 2
}

vrrp_instance VI_1 {
    state 
    interface 
    virtual_router_id 
    priority 
    advert_int 

    nopreempt


    authentication {
        auth_type PASS
        auth_pass 
    }

    virtual_ipaddress {
        /24
    }

    track_script {
        chk_haproxy
    }
}

roles/keepalived/handlers/main.yml

- name: Restart keepalived
  systemd:
    name: keepalived
    state: restarted
  become: true

HAProxy Role

roles/haproxy/tasks/main.yml

- name: Install haproxy
  apt:
    name: haproxy
    state: present
    update_cache: yes
  become: true

- name: Deploy haproxy config
  template:
    src: haproxy.cfg.j2
    dest: /etc/haproxy/haproxy.cfg
    owner: root
    group: root
    mode: '0644'
  become: true
  notify: Restart haproxy

- name: Validate haproxy config
  command: haproxy -c -f /etc/haproxy/haproxy.cfg
  become: true
  changed_when: false

- name: Enable and start haproxy
  systemd:
    name: haproxy
    enabled: true
    state: started
  become: true

roles/haproxy/templates/haproxy.cfg.j2

#---------------------------------------------------------------------
# Global - managed by Ansible
#---------------------------------------------------------------------
global
    log /dev/log local0
    log /dev/log local1 notice
    chroot /var/lib/haproxy
    stats socket /run/haproxy/admin.sock mode 660 level admin
    stats timeout 30s
    user haproxy
    group haproxy
    daemon
    maxconn 50000

defaults
    mode http
    log global
    option httplog
    option dontlognull
    option http-server-close
    option forwardfor except 127.0.0.0/8
    retries 3
    timeout http-request    10s
    timeout queue           1m
    timeout connect         10s
    timeout client          1m
    timeout server          1m
    timeout check           10s

#---------------------------------------------------------------------
# Stats
#---------------------------------------------------------------------
frontend stats
    bind *:
    stats enable
    stats uri /stats
    stats refresh 10s
    stats show-node
    stats auth :

#---------------------------------------------------------------------
# Web Frontend
#---------------------------------------------------------------------
frontend web_front
    bind *:
    default_backend web_back

backend web_back
    balance roundrobin
    option httpchk GET /


#---------------------------------------------------------------------
# API Frontend
#---------------------------------------------------------------------
frontend api_front
    bind *:
    default_backend api_back

backend api_back
    balance roundrobin
    option httpchk GET /health
    http-check expect status 200

roles/haproxy/handlers/main.yml

- name: Restart haproxy
  systemd:
    name: haproxy
    state: restarted
  become: true

Docker Backend Role

roles/docker_backend/tasks/main.yml

- name: Install Docker
  shell: curl -fsSL https://get.docker.com | sudo sh
  args:
    creates: /usr/bin/docker
  become: true

- name: Add ubuntu user to docker group
  user:
    name: ubuntu
    groups: docker
    append: yes
  become: true

- name: Create backend directory
  file:
    path: /home/ubuntu/backend
    state: directory
    owner: ubuntu
    mode: '0755'

- name: Create web directory
  file:
    path: /home/ubuntu/backend/web
    state: directory
    owner: ubuntu

- name: Create api directory
  file:
    path: /home/ubuntu/backend/api
    state: directory
    owner: ubuntu

- name: Deploy index.html
  template:
    src: index.html.j2
    dest: /home/ubuntu/backend/web/index.html
    owner: ubuntu

- name: Deploy API server script
  copy:
    src: api/app.py
    dest: /home/ubuntu/backend/api/app.py
    owner: ubuntu

- name: Deploy docker-compose.yml
  template:
    src: docker-compose.yml.j2
    dest: /home/ubuntu/backend/docker-compose.yml
    owner: ubuntu

- name: Start backend services
  community.docker.docker_compose_v2:
    project_src: /home/ubuntu/backend
    state: present
  become: true

roles/docker_backend/templates/docker-compose.yml.j2

version: '3.8'

services:
  web-server:
    image: nginx:alpine
    container_name: web-server
    ports:
      - ":80"
    volumes:
      - ./web/index.html:/usr/share/nginx/html/index.html:ro
    restart: unless-stopped

  api-server:
    image: python:3.11-alpine
    container_name: api-server
    ports:
      - ":"
    volumes:
      - ./api/app.py:/app/app.py:ro
    working_dir: /app
    command: python app.py
    restart: unless-stopped

主 Playbook

site.yml

---
- name: Deploy Load Balancer Stack
  hosts: lb_nodes
  gather_facts: true
  become: false

  roles:
    - role: docker_backend
      tags: [backend]

    - role: haproxy
      tags: [haproxy]

    - role: keepalived
      tags: [keepalived]

執行 Playbook

cd ansible/

# 先 dry-run 確認沒問題
ansible-playbook -i inventory/hosts.yml site.yml --check

# 正式執行
ansible-playbook -i inventory/hosts.yml site.yml

# 只執行特定 role
ansible-playbook -i inventory/hosts.yml site.yml --tags keepalived
ansible-playbook -i inventory/hosts.yml site.yml --tags haproxy
ansible-playbook -i inventory/hosts.yml site.yml --tags backend

預期輸出:

PLAY RECAP ******************************************
lb-master : ok=12  changed=8  unreachable=0  failed=0
lb-slave  : ok=12  changed=8  unreachable=0  failed=0

部署後驗證

# 確認 VIP 在 Master
ansible lb-master -i inventory/hosts.yml -m command \
  -a "ip addr show ens4" | grep inet

# 確認服務狀態
ansible lb_nodes -i inventory/hosts.yml -m command \
  -a "systemctl is-active keepalived haproxy"

# 從 Mac 測試 VIP
curl http://192.168.50.250        # Web
curl http://192.168.50.250:8081/api/info  # API
open http://192.168.50.250:8404/stats     # HAProxy Stats