Ansible Vault + CI/CD: จัดการ Secrets ใน GitHub Actions Pipeline

การ integrate Ansible กับ CI/CD pipeline อย่าง GitHub Actions ต้องจัดการ vault password อย่างระมัดระวัง — บน local เราใส่ password ใน file ที่ไม่ได้ commit แต่ใน CI/CD environment ไม่มี file นั้น ต้องส่ง vault password ผ่าน environment variables หรือ secret store ของ CI/CD แทน

บทความนี้ครอบคลุมวิธีใช้ Ansible Vault ใน GitHub Actions อย่างปลอดภัย: การตั้งค่า GitHub Secrets, 3 วิธีส่ง vault password ให้ ansible-playbook, การใช้ ansible-vault สำหรับ secrets ใน pipeline, การ cache dependencies, และ best practices สำหรับ CI/CD secrets management

หลักการจัดการ Vault Password ใน CI/CD

ใน CI/CD pipeline ต้องส่ง vault password ให้ Ansible โดยไม่ hardcode ใน workflow file และไม่เก็บใน repository — GitHub Actions Secrets คือที่เหมาะสมที่สุดสำหรับเก็บ vault password

# หลักการพื้นฐาน
# 1. vault password เก็บใน GitHub Secrets (ไม่ใช่ repository)
# 2. Workflow อ่าน secret ผ่าน env variable
# 3. ส่ง vault password ให้ ansible-playbook 3 วิธี:
#    a. --vault-password-file (ใช้ temp file)
#    b. ANSIBLE_VAULT_PASSWORD_FILE environment variable
#    c. stdin (pipe password)

# ตัวอย่าง: ส่ง vault password ผ่าน stdin
echo "$VAULT_PASSWORD" | ansible-playbook site.yml --vault-password-file /dev/stdin

# ตัวอย่าง: ใช้ temp file (ปลอดภัยกว่า stdin)
echo "$VAULT_PASSWORD" > /tmp/vault_pass
chmod 600 /tmp/vault_pass
ansible-playbook site.yml --vault-password-file /tmp/vault_pass
rm -f /tmp/vault_pass

ตั้งค่า GitHub Secrets

ก่อนเขียน workflow ต้องตั้งค่า secrets ใน GitHub repository ก่อน — ไปที่ Settings → Secrets and variables → Actions → New repository secret

# GitHub Secrets ที่ต้องตั้งค่า

# ANSIBLE_VAULT_PASSWORD — vault password สำหรับ decrypt vault files
# ตั้งค่าที่: Settings → Secrets → New repository secret
# Name: ANSIBLE_VAULT_PASSWORD
# Value: [vault_password_ที่ใช้ encrypt ไฟล์]

# สำหรับ multiple environments — ตั้งแยกตาม environment:
# ANSIBLE_VAULT_PASSWORD_PRODUCTION
# ANSIBLE_VAULT_PASSWORD_STAGING

# SSH private key สำหรับ connect ไป managed nodes
# ANSIBLE_SSH_PRIVATE_KEY
# Value: [private key content]

# ข้อควรระวัง:
# - ห้ามใส่ค่า secret ใน workflow YAML โดยตรง
# - ห้าม print หรือ echo secrets ใน workflow
# - ใช้ environment ต่างกันสำหรับ production/staging

GitHub Actions Workflow — Deploy ด้วย Ansible Vault

ตัวอย่าง workflow สมบูรณ์สำหรับ deploy ด้วย Ansible Playbook ที่ใช้ vault secrets — รองรับทั้ง staging และ production ด้วย vault password แยกกัน

# .github/workflows/deploy.yml
name: Deploy with Ansible

on:
  push:
    branches:
      - main         # deploy production เมื่อ merge ไป main
      - staging      # deploy staging เมื่อ push ไป staging

env:
  ANSIBLE_HOST_KEY_CHECKING: false
  ANSIBLE_STDOUT_CALLBACK: yaml

jobs:
  deploy:
    runs-on: ubuntu-22.04

    steps:
      - name: Checkout code
        uses: actions/checkout@v4

      - name: Set up Python
        uses: actions/setup-python@v5
        with:
          python-version: '3.11'
          cache: pip

      - name: Install Ansible
        run: |
          pip install ansible==9.0.0

      - name: Install Ansible requirements
        run: |
          ansible-galaxy install -r requirements.yml

      - name: Set up SSH key
        run: |
          mkdir -p ~/.ssh
          echo "${{ secrets.ANSIBLE_SSH_PRIVATE_KEY }}" > ~/.ssh/id_rsa
          chmod 600 ~/.ssh/id_rsa
          ssh-keyscan -H ${{ secrets.TARGET_HOST }} >> ~/.ssh/known_hosts

      - name: Set vault password file
        run: |
          echo "${{ secrets.ANSIBLE_VAULT_PASSWORD }}" > /tmp/vault_pass
          chmod 600 /tmp/vault_pass

      - name: Run Ansible Playbook
        run: |
          ansible-playbook \
            -i inventory/production \
            --vault-password-file /tmp/vault_pass \
            site.yml

      - name: Clean up vault password
        if: always()
        run: rm -f /tmp/vault_pass

Multi-Environment Workflow

workflow ที่รองรับหลาย environment พร้อม vault password แยกกัน — ใช้ GitHub Environments feature สำหรับ approval workflow และ environment-specific secrets

# .github/workflows/deploy-multi-env.yml
name: Deploy Multi-Environment

on:
  workflow_dispatch:
    inputs:
      environment:
        description: 'Target environment'
        required: true
        default: 'staging'
        type: choice
        options:
          - staging
          - production

jobs:
  deploy:
    runs-on: ubuntu-22.04
    environment: ${{ github.event.inputs.environment }}

    steps:
      - uses: actions/checkout@v4

      - name: Install Ansible
        run: pip install ansible==9.0.0

      - name: Set vault password
        run: |
          echo "${{ secrets.VAULT_PASSWORD }}" > /tmp/vault_pass
          chmod 600 /tmp/vault_pass
        # secrets.VAULT_PASSWORD มาจาก GitHub Environment secrets
        # production environment มี VAULT_PASSWORD ต่างจาก staging

      - name: Deploy to ${{ github.event.inputs.environment }}
        run: |
          ansible-playbook \
            -i inventory/${{ github.event.inputs.environment }} \
            --vault-password-file /tmp/vault_pass \
            site.yml \
            -e "target_env=${{ github.event.inputs.environment }}"

      - name: Cleanup
        if: always()
        run: rm -f /tmp/vault_pass

Dry Run ก่อน Deploy จริง

เพิ่ม dry run step ก่อน deploy จริง — ใช้ --check flag เพื่อให้ Ansible จำลองการทำงานโดยไม่เปลี่ยนแปลงอะไรจริง ตรวจสอบว่า playbook parse ได้และ vault decrypt ได้ก่อน

# .github/workflows/deploy-with-check.yml
jobs:
  validate:
    runs-on: ubuntu-22.04
    steps:
      - uses: actions/checkout@v4
      - name: Install Ansible
        run: pip install ansible==9.0.0

      - name: Set vault password
        run: |
          echo "${{ secrets.ANSIBLE_VAULT_PASSWORD }}" > /tmp/vault_pass
          chmod 600 /tmp/vault_pass

      - name: Syntax check
        run: |
          ansible-playbook \
            --vault-password-file /tmp/vault_pass \
            --syntax-check \
            site.yml

      - name: Dry run (check mode)
        run: |
          ansible-playbook \
            -i inventory/staging \
            --vault-password-file /tmp/vault_pass \
            --check \
            site.yml

      - name: Cleanup
        if: always()
        run: rm -f /tmp/vault_pass

  deploy:
    needs: validate      # รอ validate ผ่านก่อน
    runs-on: ubuntu-22.04
    environment: production

    steps:
      - uses: actions/checkout@v4
      - name: Install Ansible
        run: pip install ansible==9.0.0

      - name: Set vault password
        run: |
          echo "${{ secrets.ANSIBLE_VAULT_PASSWORD }}" > /tmp/vault_pass
          chmod 600 /tmp/vault_pass

      - name: Deploy
        run: |
          ansible-playbook \
            -i inventory/production \
            --vault-password-file /tmp/vault_pass \
            site.yml

      - name: Cleanup
        if: always()
        run: rm -f /tmp/vault_pass

ใช้ ansible-vault ตรวจสอบใน CI

เพิ่ม CI job สำหรับตรวจสอบว่า vault files ทั้งหมดถูก encrypt และ decrypt ได้จริง — ช่วยป้องกัน vault files ที่ encrypt ด้วย password ผิด หรือ plain-text secrets ที่หลุดเข้า repository

# .github/workflows/vault-check.yml
name: Vault Integrity Check

on:
  pull_request:
    paths:
      - '**/vault.yml'
      - 'requirements.yml'

jobs:
  vault-check:
    runs-on: ubuntu-22.04
    steps:
      - uses: actions/checkout@v4

      - name: Install Ansible
        run: pip install ansible==9.0.0

      - name: Check vault files are encrypted
        run: |
          FAILED=0
          find . -name "vault.yml" -not -path "./.git/*" | while read f; do
            if ! head -1 "$f" | grep -q '^\$ANSIBLE_VAULT'; then
              echo "ERROR: $f is NOT encrypted!"
              FAILED=1
            else
              echo "OK: $f"
            fi
          done
          exit $FAILED

      - name: Verify vault files can be decrypted
        run: |
          echo "${{ secrets.ANSIBLE_VAULT_PASSWORD }}" > /tmp/vault_pass
          chmod 600 /tmp/vault_pass

          find . -name "vault.yml" -not -path "./.git/*" | while read f; do
            ansible-vault view --vault-password-file /tmp/vault_pass "$f" > /dev/null
            echo "Decryption OK: $f"
          done

          rm -f /tmp/vault_pass

Cache Ansible Dependencies

Cache pip packages และ Ansible roles เพื่อลดเวลา CI/CD pipeline — สำคัญเมื่อมีหลาย Galaxy roles ที่ต้องดาวน์โหลดทุก run

# ตัวอย่าง: cache pip และ ansible roles
steps:
  - uses: actions/checkout@v4

  - name: Set up Python with cache
    uses: actions/setup-python@v5
    with:
      python-version: '3.11'
      cache: pip
      cache-dependency-path: requirements-ansible.txt

  - name: Cache Ansible roles
    uses: actions/cache@v4
    with:
      path: ~/.ansible/roles
      key: ansible-roles-${{ hashFiles('requirements.yml') }}
      restore-keys: |
        ansible-roles-

  - name: Install Ansible
    run: pip install -r requirements-ansible.txt

  - name: Install Ansible requirements
    run: ansible-galaxy install -r requirements.yml

# requirements-ansible.txt
# ansible==9.0.0
# boto3==1.34.0  (สำหรับ AWS dynamic inventory)

ป้องกัน Secret Leaking ใน Logs

GitHub Actions จะ mask secrets โดยอัตโนมัติใน logs แต่ก็ยังต้องระวังไม่ให้ vault content ถูก print ออกมา — ใช้ no_log: true ใน tasks ที่อาจแสดง sensitive values

# ansible tasks ที่จัดการ secrets ต้องใช้ no_log
- name: Create database with password
  community.postgresql.postgresql_user:
    name: appuser
    password: "{{ vault_db_password }}"
    priv: "appdb.*:ALL"
  no_log: true    # ป้องกัน password แสดงใน Ansible output

- name: Deploy app with secret config
  template:
    src: config.j2
    dest: /etc/myapp/config.yml
    mode: '0600'
  no_log: true    # ป้องกัน template content แสดงใน diff output

# GitHub Actions: ห้ามใช้ debug: msg สำหรับ vault variables
- name: Debug app config
  debug:
    msg: "App is configured"    # ✅ ไม่แสดง secret
  # msg: "{{ vault_app_secret_key }}"  ❌ อย่าทำ!

# ถ้าต้องการ debug ให้ใช้ conditional
- name: Show config status (not secret)
  debug:
    msg: "vault_db_password is {{ 'defined' if vault_db_password is defined else 'MISSING' }}"

Ansible Vault + HashiCorp Vault ใน CI/CD

สำหรับ production enterprise environment ที่มีความต้องการความปลอดภัยสูง สามารถดึง vault password จาก HashiCorp Vault แทนการเก็บใน GitHub Secrets ตรงๆ — ทำให้ rotate vault password ได้โดยไม่ต้องแก้ GitHub Secrets

# .github/workflows/deploy-with-hcvault.yml
# ดึง Ansible vault password จาก HashiCorp Vault

jobs:
  deploy:
    runs-on: ubuntu-22.04
    steps:
      - uses: actions/checkout@v4

      # authenticate กับ HashiCorp Vault ด้วย GitHub OIDC
      - name: Import Secrets from HashiCorp Vault
        uses: hashicorp/vault-action@v3
        with:
          url: https://vault.company.com
          method: jwt
          role: github-actions-deploy
          secrets: |
            secret/data/ansible/vault password | ANSIBLE_VAULT_PASSWORD ;
            secret/data/ansible/ssh private_key | ANSIBLE_SSH_KEY

      - name: Install Ansible
        run: pip install ansible==9.0.0

      - name: Set vault password
        run: |
          echo "$ANSIBLE_VAULT_PASSWORD" > /tmp/vault_pass
          chmod 600 /tmp/vault_pass
          echo "$ANSIBLE_SSH_KEY" > ~/.ssh/id_rsa
          chmod 600 ~/.ssh/id_rsa

      - name: Deploy
        run: |
          ansible-playbook \
            -i inventory/production \
            --vault-password-file /tmp/vault_pass \
            site.yml

      - name: Cleanup
        if: always()
        run: rm -f /tmp/vault_pass ~/.ssh/id_rsa

สรุป

การใช้ Ansible Vault ใน CI/CD ต้องการแนวทางที่ชัดเจน: เก็บ vault password ใน GitHub Secrets หรือ secret manager ไม่ใช่ใน repository, ส่ง vault password ผ่าน temp file ที่ลบหลังใช้งาน, เพิ่ม vault integrity check ใน PR pipeline เพื่อตรวจสอบก่อน merge และใช้ no_log: true กับ tasks ที่จัดการ sensitive data

Pattern ที่แนะนำ: ทำ dry run ก่อน deploy จริงเสมอ, แยก GitHub Environment secrets สำหรับ production/staging, และเพิ่ม if: always() ใน cleanup step เพื่อให้ลบ vault password file แม้ workflow fail กลางทาง