Playbook ที่ทำงานได้ถูกต้องบนเครื่องทดสอบแต่เกิดปัญหาใน production อาจมาจากการจัดการ secrets ไม่ดีพอ, tasks ที่ไม่เป็น idempotent หรือการจัดการ error ที่หลุดรอด — ทั้งสามเรื่องนี้คือต้นเหตุของ incident ที่พบบ่อยในทีมที่เพิ่งเริ่มใช้ Ansible อย่างจริงจัง
บทความนี้อธิบาย best practices สามด้านที่สำคัญที่สุด: การจัดการ secrets ด้วย Ansible Vault และ environment variables, การเขียน tasks ให้เป็น idempotent อย่างแท้จริง และ pattern สำหรับ error handling ที่ทำให้ deployment หยุดอย่างถูกต้องเมื่อเกิดปัญหา
Security: จัดการ Secrets ด้วย Ansible Vault
Ansible Vault เข้ารหัส secrets ด้วย AES-256 โดยตรงในไฟล์ — ทำให้เก็บ passwords, API keys และ certificates ใน Git repository ได้อย่างปลอดภัย
# สร้างไฟล์ vault ใหม่ (เปิด editor ให้กรอกเนื้อหา)
ansible-vault create group_vars/all/vault.yml
# เข้ารหัสไฟล์ที่มีอยู่แล้ว
ansible-vault encrypt group_vars/all/secrets.yml
# ถอดรหัสชั่วคราวเพื่อดูเนื้อหา
ansible-vault view group_vars/all/vault.yml
# แก้ไขไฟล์ที่เข้ารหัสแล้ว (เปิด editor อัตโนมัติ)
ansible-vault edit group_vars/all/vault.yml
# เปลี่ยน vault password
ansible-vault rekey group_vars/all/vault.yml
รูปแบบที่แนะนำสำหรับ group_vars — แยกไฟล์ plain และ vault ออกจากกัน เพื่อให้ diff ใน Git อ่านง่าย
# group_vars/appservers/vars.yml — ตัวแปรทั่วไป (plain text)
app_name: myapp
app_port: 8000
db_name: myapp_production
# group_vars/appservers/vault.yml — secrets (encrypted)
# เนื้อหาจริงของไฟล์ vault:
vault_db_password: "S3cur3P@ssw0rd!"
vault_secret_key: "django-insecure-xxx..."
vault_registry_token: "glpat-xxxxxxxxxxxx"
# vars.yml อ้างอิง vault variables
db_password: "{{ vault_db_password }}"
secret_key: "{{ vault_secret_key }}"
registry_token: "{{ vault_registry_token }}"
# รัน playbook พร้อม vault password
ansible-playbook deploy.yml --ask-vault-pass
# ใช้ password file (สำหรับ CI/CD)
ansible-playbook deploy.yml --vault-password-file ~/.vault_pass
# ใช้ script เพื่อดึง password จาก secret manager
ansible-playbook deploy.yml --vault-password-file ./scripts/get_vault_pass.py
# หลาย vault IDs (สำหรับ multi-environment)
ansible-playbook deploy.yml \
--vault-id dev@~/.vault_pass_dev \
--vault-id prod@~/.vault_pass_prod
Security: หลีกเลี่ยง Secret Leaks
แม้ใช้ Vault แล้ว ยังมีโอกาสที่ secrets จะปรากฏใน log output — ใช้ no_log และ diff_mode เพื่อป้องกัน
# no_log: true ป้องกัน task output แสดง secret
- name: Set application password
ansible.builtin.lineinfile:
path: /etc/myapp/config.ini
regexp: '^password='
line: "password={{ db_password }}"
no_log: true
# ป้องกัน diff แสดง secret เมื่อรัน --diff
- name: Deploy SSL certificate
ansible.builtin.copy:
content: "{{ ssl_private_key }}"
dest: /etc/ssl/private/myapp.key
mode: "0600"
no_log: true
# Debug ที่ปลอดภัย — แสดงแค่ว่ามีค่า ไม่แสดงค่าจริง
- name: Verify secret is set
ansible.builtin.debug:
msg: "DB password is {{ 'set' if vault_db_password else 'NOT SET' }}"
# ใน ansible.cfg — ปิด diff สำหรับ production
[defaults]
diff = false
Idempotency: เขียน Tasks ให้รัน N ครั้งได้ผลเหมือนกัน
Idempotency คือหัวใจของ Ansible — task ที่ idempotent รันซ้ำกี่ครั้งก็ได้ผลเหมือนกัน ไม่เกิด side effects การทำความเข้าใจว่า task ใดที่ไม่ idempotent โดยธรรมชาติช่วยป้องกันปัญหาใน production
# ❌ ไม่ idempotent — เพิ่ม line ซ้ำทุกครั้ง
- name: Add to config (WRONG)
ansible.builtin.shell: echo "MaxConnections=100" >> /etc/myapp.conf
# ✅ idempotent — ใช้ lineinfile แทน
- name: Set MaxConnections
ansible.builtin.lineinfile:
path: /etc/myapp.conf
regexp: '^MaxConnections='
line: 'MaxConnections=100'
# ❌ ไม่ idempotent — สร้าง user ซ้ำ = error
- name: Create user (WRONG)
ansible.builtin.command: useradd myapp
# ✅ idempotent — user module จัดการ state เอง
- name: Create application user
ansible.builtin.user:
name: myapp
shell: /bin/bash
system: true
create_home: false
# ❌ ไม่ idempotent — install แม้ติดตั้งแล้ว
- name: Install package (WRONG)
ansible.builtin.command: apt install nginx
# ✅ idempotent — apt module ตรวจ state ก่อน
- name: Install Nginx
ansible.builtin.apt:
name: nginx
state: present
Idempotency: ใช้ changed_when และ creates
เมื่อต้องใช้ command หรือ shell module ที่ไม่มี idempotency built-in ให้ใช้ changed_when, creates และ when เพื่อควบคุมพฤติกรรม
# changed_when: false — task นี้ไม่เปลี่ยนแปลง state (read-only)
- name: Check application version
ansible.builtin.command: myapp --version
register: app_version
changed_when: false
# changed_when ตาม output — mark changed เฉพาะเมื่อ migrate จริง
- name: Run database migrations
ansible.builtin.command: python manage.py migrate --noinput
register: migrate_result
changed_when: "'No migrations to apply' not in migrate_result.stdout"
# creates — ข้ามถ้าไฟล์มีอยู่แล้ว
- name: Extract application archive
ansible.builtin.command:
cmd: tar -xzf /opt/myapp-1.0.tar.gz -C /opt/myapp
creates: /opt/myapp/bin/myapp
# removes — รันเฉพาะเมื่อไฟล์มีอยู่
- name: Cleanup old lock file
ansible.builtin.command:
cmd: rm -f /var/run/myapp.lock
removes: /var/run/myapp.lock
# when — guard condition ป้องกันรันซ้ำ
- name: Initialize database (first time only)
ansible.builtin.command: myapp db:init
when: not db_initialized.stat.exists
Error Handling: block, rescue, always
block/rescue/always คือ try/catch/finally ของ Ansible — ใช้สำหรับ rollback เมื่อ deployment ล้มเหลว หรือ cleanup ที่ต้องทำเสมอไม่ว่าจะสำเร็จหรือไม่
- name: Deploy application with rollback
block:
# tasks ใน block — ถ้า task ใด failed จะข้ามไป rescue
- name: Pull new Docker image
community.docker.docker_image:
name: "registry.example.com/myapp:{{ new_version }}"
source: pull
- name: Stop old container
community.docker.docker_container:
name: myapp
state: stopped
- name: Start new container
community.docker.docker_container:
name: myapp
image: "registry.example.com/myapp:{{ new_version }}"
state: started
- name: Verify new container is healthy
community.docker.docker_container_info:
name: myapp
register: container_info
until: container_info.container.State.Health.Status == "healthy"
retries: 5
delay: 10
rescue:
# รันเมื่อ block มี error — rollback กลับ version เดิม
- name: Rollback to previous version
community.docker.docker_container:
name: myapp
image: "registry.example.com/myapp:{{ current_version }}"
state: started
- name: Notify team of rollback
ansible.builtin.uri:
url: "{{ slack_webhook_url }}"
method: POST
body_format: json
body:
text: "⚠️ Deployment failed on {{ inventory_hostname }}, rolled back to {{ current_version }}"
# ทำให้ playbook fail อยู่ดี (ไม่ถือว่า rescue = success)
- name: Fail after rollback
ansible.builtin.fail:
msg: "Deployment failed and rolled back to {{ current_version }}"
always:
# รันเสมอ ไม่ว่า block/rescue จะสำเร็จหรือไม่
- name: Record deployment attempt
ansible.builtin.lineinfile:
path: /var/log/deployments.log
line: "{{ ansible_date_time.iso8601 }} {{ inventory_hostname }} {{ new_version }}"
create: true
Error Handling: ignore_errors, failed_when, any_errors_fatal
ควบคุมพฤติกรรมเมื่อเกิด error ในระดับ task และ play — บางกรณีต้องการให้ error ไม่หยุด playbook, บางกรณีต้องการหยุดทันทีทั้ง inventory
# ignore_errors — ดำเนินต่อแม้ task fail (ระวัง overuse)
- name: Stop service (may not exist)
ansible.builtin.systemd:
name: old-service
state: stopped
ignore_errors: true
# failed_when — กำหนดเงื่อนไข failure เอง
- name: Check disk space
ansible.builtin.command: df -BG /var
register: disk_check
failed_when:
- disk_check.rc != 0
- "'100%' in disk_check.stdout"
# failed_when กับ multiple conditions (AND)
- name: Validate application response
ansible.builtin.uri:
url: "http://localhost:8000/health"
return_content: true
register: health_check
failed_when:
- health_check.status != 200
- "'ok' not in health_check.content"
# any_errors_fatal — หยุดทั้ง play ทันทีเมื่อ host ใด host หนึ่ง fail
# (ไม่รอให้ครบทุก host ก่อน)
- name: Deploy to production
hosts: appservers
any_errors_fatal: true
tasks:
- name: Run pre-deploy checks
ansible.builtin.script: scripts/pre_deploy_check.sh
# max_fail_percentage — ยอมให้ fail ได้กี่ % ก่อนหยุด play
- name: Rolling update
hosts: appservers
max_fail_percentage: 20 # ถ้า fail เกิน 20% ของ hosts ให้หยุด
serial: 2
tasks:
- name: Update application
# ...
Error Handling: Validation ก่อน Deploy
Pre-deployment validation ช่วยหยุด playbook ก่อนเกิดความเสียหาย — ตรวจสอบ prerequisites ด้วย assert และ pre_tasks
---
- name: Deploy application
hosts: appservers
become: true
pre_tasks:
# ตรวจสอบ OS version ก่อนเริ่ม
- name: Verify supported OS
ansible.builtin.assert:
that:
- ansible_os_family == "Debian"
- ansible_distribution_major_version | int >= 22
fail_msg: "Unsupported OS: {{ ansible_distribution }} {{ ansible_distribution_version }}"
success_msg: "OS check passed: {{ ansible_distribution }} {{ ansible_distribution_version }}"
# ตรวจสอบ disk space
- name: Check disk space for deployment
ansible.builtin.assert:
that:
- (ansible_mounts | selectattr('mount', 'equalto', '/') | first).size_available > 2147483648
fail_msg: "Insufficient disk space: less than 2GB available on /"
# ตรวจสอบว่า required variables ครบ
- name: Verify required variables
ansible.builtin.assert:
that:
- app_version is defined
- app_version | length > 0
- vault_db_password is defined
fail_msg: "Required variable not set: app_version or vault_db_password"
# ตรวจสอบ network connectivity
- name: Check registry connectivity
ansible.builtin.uri:
url: "https://registry.example.com/v2/"
status_code: [200, 401]
register: registry_check
failed_when: registry_check.status not in [200, 401]
tasks:
- name: Deploy application
# ...
Secrets บน CI/CD Pipeline
ใน CI/CD pipeline ควรส่ง vault password ผ่าน environment variable หรือ secret manager — ไม่เก็บ vault password ในไฟล์ที่อยู่ใน repository
# GitHub Actions — vault password จาก GitHub Secrets
- name: Deploy with Ansible
env:
ANSIBLE_VAULT_PASSWORD: ${{ secrets.VAULT_PASSWORD }}
run: |
echo "$ANSIBLE_VAULT_PASSWORD" > /tmp/vault_pass
chmod 600 /tmp/vault_pass
ansible-playbook deploy.yml --vault-password-file /tmp/vault_pass
rm -f /tmp/vault_pass
# หรือใช้ ANSIBLE_VAULT_PASSWORD_FILE environment variable
- name: Deploy with Ansible
env:
VAULT_PASS: ${{ secrets.VAULT_PASSWORD }}
run: |
python3 -c "import os; print(os.environ['VAULT_PASS'])" > /tmp/.vault_pass
ANSIBLE_VAULT_PASSWORD_FILE=/tmp/.vault_pass ansible-playbook deploy.yml
# ansible.cfg — กำหนด vault password file path
[defaults]
vault_password_file = ~/.vault_pass
# หรือ script ที่ดึงจาก AWS Secrets Manager / HashiCorp Vault
vault_password_file = scripts/get_vault_password.py
สรุป
Security, Idempotency และ Error Handling เป็นสามเสาหลักของ Ansible playbook คุณภาพสูง — ใช้ Ansible Vault เก็บ secrets ใน Git อย่างปลอดภัย, ใช้ no_log: true ป้องกัน secrets รั่วใน log, เขียน tasks ให้เป็น idempotent ด้วย built-in modules แทน shell commands, และใช้ block/rescue/always สำหรับ deployment ที่ rollback ได้อัตโนมัติ
Pattern ที่ช่วยได้มากที่สุดในทางปฏิบัติคือ: pre_tasks + assert สำหรับ validation ก่อน deploy, any_errors_fatal เพื่อหยุดทันทีเมื่อเกิดปัญหา และ changed_when เพื่อให้ idempotency ของ command module ถูกต้อง — ทั้งสามสิ่งนี้รวมกันทำให้ playbook รันซ้ำได้อย่างมั่นใจในทุก environment

