
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 tfplanLư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: trueThự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áochangedsau 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 plansẽ 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àtaintedvà 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 đếnconnection refusedcổ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.ymlLư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.terraformTạo file terraform.yml:
plugin: cloud.terraform.terraform_provider
project_path: /path/to/terraform/codeChạ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ụ
sshdsẵn sàng, gây lỗiConnection refused. Giải pháp là thêm taskansible.builtin.wait_for_connectionngay đầ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 plansẽ đòi destroy hoặc đổi thuộc tính ngoài dự kiến. Dùngterraform 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 modulecommandhoặcshell, hãy thêm điều kiện chặn nhưcreates: /path/to/filehoặc biểu thứcwhen:để đả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.ymlFile 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: trueTạ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 = FalseFile Pipeline GitHub Actions (.github/workflows/deploy.yml)
Pipeline được chia làm 2 jobs:
terraform: Khởi tạo hạ tầng, apply plan và export IP thành artifact.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.ymlThiết lập Secrets cần thiết
Vào repository trên GitHub: Settings > Secrets and variables > Actions > New repository secret:
| Secret Name | Mô tả |
SERVER_SSH_PRIVATE_KEY | Private 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:
- Khắc phục lỗi SSH
Connection Refused: Taskwait_for_connectiontrong 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ệnhsleepbừa bãi trong CI script. - Truyền IP an toàn không qua file trung gian: Sử dụng cơ chế
outputsgiữa các job của GitHub Actions ($GITHUB_OUTPUT), giúpansiblejob chỉ cần đọc biến môi trường để sinh inventory. - Thực thi an toàn với Plan File: Terraform chạy
plan -out=tfplanrồi mớiapply 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.

