Workshop: ใช้ Ansible + Docker สำหรับ Microservices Deployment

เมื่อแอปพลิเคชันเติบโตจนต้องแยกเป็น Microservices หลายตัว การ deploy และดูแลระบบด้วยมือทีละเซิร์ฟเวอร์ทำให้เกิดความผิดพลาดและใช้เวลามาก Ansible และ Docker ทำงานเสริมกันได้ดี — Docker จัดการ packaging และ isolation ของแต่ละ service ส่วน Ansible ทำให้กระบวนการ deploy เป็นอัตโนมัติ ทำซ้ำได้ และตรวจสอบได้ในทุกสภาพแวดล้อม

Workshop นี้จะสร้าง Ansible Playbook สำหรับ deploy Microservices Stack ที่ประกอบด้วย API Gateway, User Service และ Product Service บน Docker ครอบคลุมตั้งแต่การติดตั้ง Docker, สร้าง Compose template ด้วย Jinja2, deploy services, ตรวจสอบ health check ไปจนถึงกลยุทธ์ rolling update และ rollback

โครงสร้างโปรเจกต์

จัดโครงสร้างโปรเจกต์ให้แต่ละ Role รับผิดชอบงานเดียวชัดเจน:

ansible-microservices/
├── inventory/
│   ├── hosts.ini
│   └── group_vars/
│       └── all.yml
├── roles/
│   ├── docker-install/
│   │   └── tasks/
│   │       └── main.yml
│   ├── deploy-microservices/
│   │   ├── tasks/
│   │   │   └── main.yml
│   │   └── templates/
│   │       └── docker-compose.yml.j2
│   └── healthcheck/
│       └── tasks/
│           └── main.yml
├── site.yml
└── rollback.yml

ขั้นตอนที่ 1 — เตรียม Inventory และ Variables

กำหนด host ที่จะ deploy microservices และตัวแปรที่ใช้ร่วมกัน:

inventory/hosts.ini

[microservices]
app01 ansible_host=10.0.1.10
app02 ansible_host=10.0.1.11

[microservices:vars]
ansible_user=ubuntu
ansible_python_interpreter=/usr/bin/python3

inventory/group_vars/all.yml

---
# Docker image versions
images:
  api_gateway: "nginx:1.25-alpine"
  user_service: "myapp/user-service:{{ user_service_version }}"
  product_service: "myapp/product-service:{{ product_service_version }}"

# Service versions (override ตอน deploy)
user_service_version: "1.0.0"
product_service_version: "1.0.0"

# Port mappings
ports:
  api_gateway: 80
  user_service: 8001
  product_service: 8002

# Deploy path
deploy_dir: /opt/microservices

# Docker registry
registry_url: registry.example.com
registry_user: deploy
registry_password: !vault |
  $ANSIBLE_VAULT;1.1;AES256
  38396265356365613632613062363...

# Health check settings
health_check_retries: 5
health_check_delay: 10

ขั้นตอนที่ 2 — ติดตั้ง Docker ด้วย Ansible Role

Role docker-install ติดตั้ง Docker Engine และ Compose Plugin จาก official repository:

roles/docker-install/tasks/main.yml

---
- name: ติดตั้ง dependencies สำหรับ Docker
  apt:
    name:
      - ca-certificates
      - curl
      - gnupg
      - lsb-release
    state: present
    update_cache: yes

- name: เพิ่ม Docker GPG key
  apt_key:
    url: https://download.docker.com/linux/ubuntu/gpg
    state: present

- name: เพิ่ม Docker repository
  apt_repository:
    repo: "deb [arch=amd64] https://download.docker.com/linux/ubuntu {{ ansible_distribution_release }} stable"
    state: present
    filename: docker

- name: ติดตั้ง Docker Engine และ Compose Plugin
  apt:
    name:
      - docker-ce
      - docker-ce-cli
      - containerd.io
      - docker-buildx-plugin
      - docker-compose-plugin
    state: present
    update_cache: yes

- name: เริ่ม Docker service
  systemd:
    name: docker
    enabled: yes
    state: started

- name: เพิ่ม deploy user เข้ากลุ่ม docker
  user:
    name: "{{ ansible_user }}"
    groups: docker
    append: yes

- name: ตรวจสอบ Docker version
  command: docker --version
  register: docker_ver
  changed_when: false

- name: แสดง Docker version
  debug:
    msg: "{{ docker_ver.stdout }}"

ขั้นตอนที่ 3 — สร้าง Docker Compose Template ด้วย Jinja2

ใช้ Jinja2 template สร้าง docker-compose.yml ที่มีค่าตัวแปรต่างกันตาม environment โดยอัตโนมัติ:

roles/deploy-microservices/templates/docker-compose.yml.j2

version: "3.8"

services:
  api-gateway:
    image: {{ images.api_gateway }}
    container_name: api-gateway
    ports:
      - "{{ ports.api_gateway }}:80"
    volumes:
      - ./nginx.conf:/etc/nginx/nginx.conf:ro
    depends_on:
      - user-service
      - product-service
    restart: unless-stopped
    networks:
      - microservices-net

  user-service:
    image: {{ registry_url }}/{{ images.user_service }}
    container_name: user-service
    ports:
      - "{{ ports.user_service }}:8001"
    environment:
      - DB_HOST={{ db_host | default('db') }}
      - DB_NAME=userdb
      - LOG_LEVEL={{ log_level | default('info') }}
    restart: unless-stopped
    networks:
      - microservices-net
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost:8001/health"]
      interval: 30s
      timeout: 10s
      retries: 3

  product-service:
    image: {{ registry_url }}/{{ images.product_service }}
    container_name: product-service
    ports:
      - "{{ ports.product_service }}:8002"
    environment:
      - DB_HOST={{ db_host | default('db') }}
      - DB_NAME=productdb
      - LOG_LEVEL={{ log_level | default('info') }}
    restart: unless-stopped
    networks:
      - microservices-net
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost:8002/health"]
      interval: 30s
      timeout: 10s
      retries: 3

networks:
  microservices-net:
    driver: bridge

ขั้นตอนที่ 4 — Deploy Microservices ด้วย Ansible

Role deploy-microservices ทำหน้าที่ login registry, สร้าง directory, copy compose file, pull images และ start services:

roles/deploy-microservices/tasks/main.yml

---
- name: สร้าง deploy directory
  file:
    path: "{{ deploy_dir }}"
    state: directory
    mode: "0755"

- name: Login Docker registry
  community.docker.docker_login:
    registry_url: "{{ registry_url }}"
    username: "{{ registry_user }}"
    password: "{{ registry_password }}"
  no_log: true

- name: Copy docker-compose.yml จาก template
  template:
    src: docker-compose.yml.j2
    dest: "{{ deploy_dir }}/docker-compose.yml"
    mode: "0644"
  notify: restart microservices

- name: Pull Docker images ล่าสุด
  community.docker.docker_compose_v2_pull:
    project_src: "{{ deploy_dir }}"

- name: Start microservices ด้วย Docker Compose
  community.docker.docker_compose_v2:
    project_src: "{{ deploy_dir }}"
    state: present
    pull: missing
  register: compose_result

- name: แสดงสถานะ services
  debug:
    var: compose_result.services

เพิ่ม Handler สำหรับ restart เมื่อ compose file เปลี่ยน:

---
# roles/deploy-microservices/handlers/main.yml

- name: restart microservices
  community.docker.docker_compose_v2:
    project_src: "{{ deploy_dir }}"
    state: restarted

ขั้นตอนที่ 5 — Health Check หลัง Deploy

Role healthcheck ตรวจสอบว่าทุก service ตอบสนองก่อนถือว่า deploy สำเร็จ:

roles/healthcheck/tasks/main.yml

---
- name: รอให้ API Gateway พร้อม
  uri:
    url: "http://{{ ansible_host }}/health"
    method: GET
    status_code: 200
  register: gw_result
  retries: "{{ health_check_retries }}"
  delay: "{{ health_check_delay }}"
  until: gw_result.status == 200

- name: ตรวจสอบ User Service health endpoint
  uri:
    url: "http://{{ ansible_host }}:{{ ports.user_service }}/health"
    method: GET
    status_code: 200
  register: user_result
  retries: "{{ health_check_retries }}"
  delay: "{{ health_check_delay }}"
  until: user_result.status == 200

- name: ตรวจสอบ Product Service health endpoint
  uri:
    url: "http://{{ ansible_host }}:{{ ports.product_service }}/health"
    method: GET
    status_code: 200
  register: product_result
  retries: "{{ health_check_retries }}"
  delay: "{{ health_check_delay }}"
  until: product_result.status == 200

- name: สรุปผล Health Check
  debug:
    msg:
      - "API Gateway: {{ gw_result.status }}"
      - "User Service: {{ user_result.status }}"
      - "Product Service: {{ product_result.status }}"

ขั้นตอนที่ 6 — Main Playbook และ Rolling Update

site.yml รวม Role ทั้งหมดและรองรับการ deploy แบบ rolling (ทีละเซิร์ฟเวอร์) เพื่อไม่ให้ระบบหยุดทำงาน:

---
- name: ติดตั้ง Docker บนทุก server
  hosts: microservices
  become: yes
  gather_facts: yes
  roles:
    - docker-install

- name: Deploy Microservices (Rolling Update)
  hosts: microservices
  become: yes
  serial: 1          # deploy ทีละ 1 เซิร์ฟเวอร์
  max_fail_percentage: 0   # หยุดทันทีถ้า server ใดล้มเหลว
  vars_prompt:
    - name: user_service_version
      prompt: "User Service version (e.g. 1.2.0)"
      default: "1.0.0"
      private: no
    - name: product_service_version
      prompt: "Product Service version (e.g. 1.2.0)"
      default: "1.0.0"
      private: no
  roles:
    - deploy-microservices
    - healthcheck

รัน deploy ด้วยคำสั่ง:

ansible-playbook -i inventory/hosts.ini site.yml --ask-vault-pass

หรือกำหนด version ผ่าน extra-vars โดยไม่ต้องป้อนทาง prompt:

ansible-playbook -i inventory/hosts.ini site.yml \
  --ask-vault-pass \
  -e "user_service_version=1.2.0 product_service_version=1.2.0"

ขั้นตอนที่ 7 — Rollback เมื่อ Deploy ล้มเหลว

ไฟล์ rollback.yml ใช้คืนค่า version เดิมเมื่อ deploy ใหม่มีปัญหา โดยระบุ version ก่อนหน้าผ่าน extra-vars:

---
- name: Rollback Microservices
  hosts: microservices
  become: yes
  serial: 1
  vars_prompt:
    - name: user_service_version
      prompt: "Rollback User Service to version"
      private: no
    - name: product_service_version
      prompt: "Rollback Product Service to version"
      private: no
  roles:
    - deploy-microservices
    - healthcheck

รัน rollback:

ansible-playbook -i inventory/hosts.ini rollback.yml \
  --ask-vault-pass \
  -e "user_service_version=1.1.0 product_service_version=1.1.0"

เทคนิคเพิ่มเติม

นอกจากโครงสร้างหลัก ยังมีเทคนิคที่ช่วยให้ workflow สมบูรณ์ขึ้น ได้แก่ การใช้ Tags เพื่อ deploy เฉพาะ service ที่ต้องการ (ansible-playbook site.yml --tags user-service), การใช้ Ansible Vault เพื่อเก็บ registry credentials อย่างปลอดภัย, และการสร้าง Staging environment ด้วย inventory แยก (inventory/staging/hosts.ini) เพื่อทดสอบก่อน deploy production

สำหรับ environment ที่มี services หลายตัว แนะนำให้ใช้ community.docker.docker_compose_v2 module แทนการ shell docker compose up โดยตรง เพราะ module รองรับ idempotency และให้ข้อมูล diff ของ services ที่เปลี่ยนแปลงกลับมาใน register variable ซึ่งนำไปใช้ใน subsequent tasks หรือ notification ได้

สรุป

Workshop นี้สร้าง Ansible Playbook ที่ครบวงจรสำหรับ Microservices บน Docker ตั้งแต่ติดตั้ง Docker Engine ด้วย Role ที่ reusable, สร้าง docker-compose.yml ด้วย Jinja2 template ที่ปรับ version และ config ตาม environment, deploy แบบ rolling update ทีละเซิร์ฟเวอร์เพื่อไม่ให้ระบบหยุด, ตรวจสอบ health check ทุก service ด้วย uri module ไปจนถึง rollback อย่างรวดเร็วด้วย playbook แยก

การผสาน Ansible กับ Docker ช่วยให้ทีมมี deployment workflow ที่ standardized, ตรวจสอบได้ด้วย version control และปรับขนาดได้โดยเพียงเพิ่ม host ในไฟล์ inventory — โดยไม่ต้องเขียน script ใหม่ทุกครั้ง