Lời nói đầu

Sai lầm phổ biến nhất của nhiều kỹ sư hệ tư duy là coi hai công cụ này có thể thay thế hoàn toàn cho nhau. Thực tế, chúng được sinh ra để bổ khuyết cho nhau trong mô hình Cơ sở hạ tầng dưới dạng mã (IaC).

Terraform quản lý hạ tầng bên ngoài (VPS, Network, DNS). Ansible kiểm soát cấu hình bên trong (User, Package, Nginx, Service). Terraform tạo ra máy chủ; Ansible biến máy chủ đó thành ứng dụng hoạt động.

Bản chất công cụ

Cả hai đều là công cụ Infrastructure as Code (IaC) mang tính khai báo (declarative), nhưng khác biệt cốt lõi nằm ở cách ghi nhớ trạng thái (state):

  • Terraform quản lý state qua file (terraform.tfstate): Ánh xạ từng dòng code với tài nguyên thực tế tạo ra từ API nhà cung cấp cloud. Xóa 5 dòng code đồng nghĩa với việc Terraform hiểu rằng nó cần xóa 1 cụm máy chủ tương ứng.
  • Ansible phi trạng thái (stateless giữa các lần chạy): Không lưu state file. Mỗi lần chạy, Ansible kết nối qua SSH, kiểm tra hiện trạng thực tế của máy chủ và chỉ thay đổi những gì chưa khớp với playbook (tính bất biến / idempotent).

Terraform thực sự làm gì?

Terraform giao tiếp với Cloud Provider qua API bằng plugin provider. Mỗi dịch vụ trên các nền tảng khác nhau sẽ có các thuộc tính khai báo riêng.

resource "cloud_server" "web" {
  name  = "web1"
  image = "ubuntu-24.04"
  type  = "small"
}

output "web_ip" {
  value = cloud_server.web.ipv4_address
}

Quy trình chuẩn hóa khi thực thi Terraform:

terraform init
terraform fmt -check
terraform validate
terraform plan -out=tfplan
terraform apply tfplan

Lưu ý sống còn với terraform plan: Luôn đọc kỹ dòng tổng kết (ví dụ: Plan: 1 to add, 0 to change, 0 to destroy). Nếu thuộc tính xuất hiện cờ # forces replacement, Terraform sẽ xóa sạch máy chủ hiện tại và tạo máy chủ mới rỗng để áp dụng thay đổi. Đây là nguyên nhân hàng đầu gây mất dữ liệu ngoài ý muốn.

Xuất plan ra file (-out=tfplan): Đảm bảo chính xác những gì bạn đã review là thứ được áp dụng, tránh trường hợp trạng thái hạ tầng bị ai đó thay đổi giữa lúc bạn gõ lệnh plan và apply.

Khóa State (Locking): File terraform.tfstate là linh hồn của Terraform. Hãy lưu nó trên Remote Backend (như S3 + DynamoDB) có cơ chế locking ngay khi làm việc từ hai người trở lên để tránh xung đột Error: Error acquiring the state lock.

(Ghi chú: Bản fork mã nguồn mở OpenTofu hoàn toàn tương thích cú pháp, bạn có thể thay terraform bằng tofu).

Ansible thực sự làm gì?

Ansible không cần cài agent trên server đích và không cần Cloud API. Nó chỉ cần một kết nối SSH: Ansible đẩy một Python module tạm thời lên máy chủ, chạy tác vụ rồi dọn dẹp sạch sẽ.

- name: Cấu hình Web Server cơ bản
  hosts: web
  become: true
  tasks:
    - name: Cài đặt Nginx
      ansible.builtin.apt:
        name: nginx
        state: present
        update_cache: true

    - name: Kích hoạt Nginx khởi động cùng hệ thống
      ansible.builtin.service:
        name: nginx
        state: started
        enabled: true

Thực thi và kiểm tra:

# Kiểm tra kết nối SSH, Python và quyền sudo
ansible -i inventory.ini web -m ansible.builtin.ping

# Chạy thử nghiệm để xem trước thay đổi (tương tự dry-run)
ansible-playbook -i inventory.ini site.yml --check --diff

# Thực thi cấu hình
ansible-playbook -i inventory.ini site.yml
  • Tính Idempotent: Khi chạy xong, dòng recap sẽ báo ok=... changed=.... Hãy chạy lại playbook đó lần thứ hai: kết quả lý tưởng phải là changed=0. Nếu một tác vụ liên tục báo changed sau mỗi lần chạy, thường là do bạn dùng module thô (command, shell) thay vì các module chuyên dụng của Ansible.

Điểm giao thoa và lý do không nên gộp chung

Nhiều người cố dùng một công cụ để giải quyết cả hai việc, dẫn đến các lỗi hệ thống khó chịu:

  • Không nên dùng Terraform remote-exec để cấu hình: Tài liệu HashiCorp coi đây là giải pháp đường cùng. Provisioner chỉ chạy đúng một lần lúc tạo resource. Nếu bạn sửa script cấu hình sau đó, terraform plan sẽ bỏ qua vì thấy server đã tồn tại. Nếu script lỗi giữa chừng, server bị đánh dấu là tainted và sẽ bị xóa/tạo lại ở lần apply kế tiếp. Chưa kể lỗi lệch pha thời gian: API vừa báo tạo server xong thì OS vẫn đang boot, dẫn đến connection refused cổng 22.
  • Không nên dùng Ansible Cloud Module để dựng hạ tầng lớn: Ansible tạo được VPS qua API, nhưng bạn sẽ mất hoàn toàn sơ đồ phụ thuộc (dependency graph) và state tracking. Khi bạn xóa một task tạo server trong playbook, máy chủ đó vẫn tiếp tục chạy ngầm và tính tiền trên cloud vì Ansible không lưu vết để biết cần phải destroy nó.

Quy tắc vàng: Để Terraform quản lý những gì sinh ra và mất đi thông qua Cloud API. Để Ansible quản lý mọi thứ bên trong hệ điều hành sau khi máy đã boot thành công.

Quy trình bàn giao thực tế (Handover Pipeline)

Bàn giao giữa hai công cụ là một ranh giới rõ ràng, không cần cơ chế tích hợp phức tạp: Terraform xong việc, nhả ra IP; Ansible nhận IP đó và bắt đầu cấu hình.

Cách 1: Xuất file tĩnh qua Shell Script

terraform apply -auto-approve
printf '[web]\n%s ansible_user=root\n' "$(terraform output -raw web_ip)" > inventory.ini

# Bước đệm kiểm tra kết nối cực kỳ quan trọng
ansible -i inventory.ini web -m ansible.builtin.ping
ansible-playbook -i inventory.ini site.yml

Lưu ý: Lệnh ping trung gian giúp phân biệt rõ ràng: lỗi do Terraform cấp sai IP hay do Playbook Ansible viết sai cú pháp.

Cách 2: Đọc trực tiếp State bằng dynamic inventory plugin Cài đặt collection:

ansible-galaxy collection install cloud.terraform

Tạo file terraform.yml:

plugin: cloud.terraform.terraform_provider
project_path: /path/to/terraform/code

Chạy Ansible trực tiếp từ state:

ansible-inventory -i terraform.yml --graph
ansible-playbook -i terraform.yml site.yml

(Cách này đòi hỏi bạn khai báo thêm resource ansible_host trong code Terraform để Ansible nhận diện được metadata).

Khi nào bạn thực sự cần Terraform?

Phần lớn trường hợp đơn giản chưa cần đến Terraform ngay lập tức:

  • Chỉ cần Ansible khi: Bạn chỉ quản trị một vài VPS có vòng đời dài (nuôi server 1–2 năm). Câu hỏi hàng ngày là “Máy chủ này đã cài đủ package, vá lỗi bảo mật chưa?” chứ không phải “Hôm nay tạo bao nhiêu máy chủ mới?”. Một playbook hardening duy nhất giúp bạn tiết kiệm hàng giờ cấu hình thủ công.
  • Cần Terraform khi: Bạn thường xuyên phải rebuild môi trường; môi trường Staging/Dev phải giống hệt Production; làm việc nhóm cần review thay đổi hạ tầng trước khi xóa; hoặc phạm vi quản lý mở rộng ra ngoài OS: VPC, Subnet, Firewall Rules, Load Balancer, DNS records.

Lộ trình học chuẩn: Học Ansible từ máy chủ đầu tiên bạn sở hữu. Bắt đầu học Terraform khi bạn chuẩn bị phải dựng lại môi trường thứ ba.

Xử lý các lỗi bàn giao kinh điển

  • Máy chủ chưa kịp khởi động SSH: API Cloud trả về IP trước khi dịch vụ sshd sẵn sàng, gây lỗi Connection refused. Giải pháp là thêm task ansible.builtin.wait_for_connection ngay đầu playbook thay vì dùng lệnh sleep thủ công.
  • Lỗi Host key verification failed: Thường xảy ra khi Terraform destroy và tạo lại một VPS mới nhưng Cloud Provider cấp lại đúng địa chỉ IP cũ. Giải pháp: Xóa SSH fingerprint cũ bằng lệnh ssh-keygen -R <IP_SERVER>.
  • Hạ tầng bị lệch trạng thái (Drift): Khi ai đó bấm tay thay đổi cấu hình trên Web Console của Cloud, terraform plan sẽ đòi destroy hoặc đổi thuộc tính ngoài dự kiến. Dùng terraform plan -refresh-only để cập nhật riêng phần sai lệch vào state và điều chỉnh lại code cho chuẩn xác trước khi apply.
  • Ansible task luôn báo changed: Nếu bắt buộc phải dùng module command hoặc shell, hãy thêm điều kiện chặn như creates: /path/to/file hoặc biểu thức when: để đảm bảo lệnh chỉ chạy khi thực sự cần thiết.

Mẫu CI/CD tự động hóa luồng Terraform kết hợp Ansible

Dưới đây là mẫu pipeline GitHub Actions hoàn chỉnh, chuẩn hóa theo nguyên tắc: Tách biệt ranh giới giữa tạo hạ tầng (Terraform) và cấu hình bên trong (Ansible), đồng thời xử lý triệt để race condition (lỗi máy chủ chưa kịp mở SSH).

Cấu trúc thư mục dự án khuyến nghị

├── .github/
│   └── workflows/
│       └── deploy.yml
├── terraform/
│   ├── main.tf
│   ├── outputs.tf
│   └── versions.tf
└── ansible/
    ├── ansible.cfg
    └── site.yml

File Ansible Playbook (ansible/site.yml)

Để pipeline không bị gãy do Terraform tạo xong VPS nhưng hệ điều hành chưa kịp nạp SSH daemon, playbook bắt buộc phải có task chờ SSH (wait_for_connection):

- name: Cấu hình Web Server sau khi provision
  hosts: web
  become: true
  gather_facts: false # Tắt tạm thời vì SSH chưa chắc đã kết nối được ngay

  tasks:
    - name: Chờ SSH daemon khởi động hoàn tất
      ansible.builtin.wait_for_connection:
        delay: 5
        timeout: 120

    - name: Thu thập facts sau khi đã thông SSH
      ansible.builtin.setup:

    - name: Cập nhật cache và cài đặt Nginx
      ansible.builtin.apt:
        name: nginx
        state: present
        update_cache: true

    - name: Đảm bảo service Nginx đang chạy
      ansible.builtin.service:
        name: nginx
        state: started
        enabled: true

Tạo thêm file ansible/ansible.cfg để bỏ qua cảnh báo host key trong môi trường runner tự động:

[defaults]
host_key_checking = False
retry_files_enabled = False

File Pipeline GitHub Actions (.github/workflows/deploy.yml)

Pipeline được chia làm 2 jobs:

  1. terraform: Khởi tạo hạ tầng, apply plan và export IP thành artifact.
  2. ansible: Nhận IP, tạo dynamic inventory, nạp SSH key và cấu hình hệ thống.
name: "IaC: Provision & Configure Infrastructure"

on:
  push:
    branches:
      - main
  workflow_dispatch:

permissions:
  contents: read

jobs:
  # ==========================================
  # JOB 1: TERRAFORM PROVISION
  # ==========================================
  terraform:
    name: "Terraform Provisioning"
    runs-on: ubuntu-latest
    defaults:
      run:
        working-directory: terraform
    outputs:
      server_ip: ${{ steps.export-ip.outputs.SERVER_IP }}

    steps:
      - name: Checkout Source Code
        uses: actions/checkout@v4

      - name: Setup Terraform
        uses: hashicorp/setup-terraform@v3
        with:
          terraform_version: "1.9.5"

      # Cấu hình credentials của Cloud Provider (Ví dụ: AWS / DigitalOcean / Linode...)
      - name: Configure Cloud Credentials
        env:
          AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
          AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
        run: echo "Credentials configured."

      - name: Terraform Init
        run: terraform init

      - name: Terraform Validate
        run: terraform validate

      - name: Terraform Plan
        id: plan
        run: terraform plan -out=tfplan -input=false

      - name: Terraform Apply
        run: terraform apply -input=false -auto-approve tfplan

      - name: Export Server IP
        id: export-ip
        run: |
          IP=$(terraform output -raw web_ip)
          echo "SERVER_IP=$IP" >> $GITHUB_OUTPUT

  # ==========================================
  # JOB 2: ANSIBLE CONFIGURATION
  # ==========================================
  ansible:
    name: "Ansible Configuration"
    needs: terraform
    runs-on: ubuntu-latest
    defaults:
      run:
        working-directory: ansible

    steps:
      - name: Checkout Source Code
        uses: actions/checkout@v4

      - name: Setup Python
        uses: actions/setup-python@v5
        with:
          python-version: "3.11"

      - name: Install Ansible
        run: |
          python -m pip install --upgrade pip
          pip install ansible

      - name: Setup SSH Key
        env:
          SSH_PRIVATE_KEY: ${{ secrets.SERVER_SSH_PRIVATE_KEY }}
        run: |
          mkdir -p ~/.ssh
          echo "$SSH_PRIVATE_KEY" > ~/.ssh/deploy_key
          chmod 600 ~/.ssh/deploy_key

      - name: Generate Dynamic Inventory
        env:
          TARGET_IP: ${{ needs.terraform.outputs.server_ip }}
        run: |
          echo "[web]" > inventory.ini
          echo "$TARGET_IP ansible_user=ubuntu ansible_ssh_private_key_file=~/.ssh/deploy_key" >> inventory.ini
          cat inventory.ini

      - name: Run Ansible Playbook
        run: |
          ansible-playbook -i inventory.ini site.yml

Thiết lập Secrets cần thiết

Vào repository trên GitHub: Settings > Secrets and variables > Actions > New repository secret:

Secret NameMô tả
SERVER_SSH_PRIVATE_KEYPrivate SSH Key (khớp với Public Key đã nạp cho VPS trong Terraform).
AWS_ACCESS_KEY_ID
AWS_SECRET_ACCESS_KEY
Hoặc Token của Cloud Provider bạn đang dùng.

Tóm tắt:

  1. Khắc phục lỗi SSH Connection Refused: Task wait_for_connection trong playbook sẽ tự động ping thử cổng 22 liên tục trong tối đa 120 giây cho đến khi OS boot xong, không cần chèn lệnh sleep bừa bãi trong CI script.
  2. Truyền IP an toàn không qua file trung gian: Sử dụng cơ chế outputs giữa các job của GitHub Actions ($GITHUB_OUTPUT), giúp ansible job chỉ cần đọc biến môi trường để sinh inventory.
  3. Thực thi an toàn với Plan File: Terraform chạy plan -out=tfplan rồi mới apply tfplan, loại bỏ nguy cơ lệch trạng thái giữa lúc tạo plan và lúc áp dụng.

Leave a Reply

This site uses cookies to offer you a better browsing experience. By browsing this website, you agree to our use of cookies.