«این سرور را چه کسی و با چه تنظیماتی راه انداخت؟» اگر جواب این سؤال در ذهن یک نفر است، نه در Git، زیرساخت شما شکننده است. زیرساخت بهعنوان کد (IaC) یعنی سرور، شبکه، فایروال و تنظیمات سیستمعامل همه در کد تعریف شوند؛ قابل بازبینی، قابل تکرار و قابل بازگشت.
دو ابزار اصلی این حوزه Terraform و Ansible هستند. سؤال رایج این است که کدام را انتخاب کنیم. جواب کوتاه: هر دو را، هر کدام برای کار خودش.
تقسیم کار
| Terraform | Ansible | |
|---|---|---|
| کار اصلی | ساختن منابع (Provisioning) | پیکربندی داخل سرور (Configuration) |
| مدل | Declarative با State | Procedural و Idempotent، بدون State |
| مثال | VM، شبکه، فایروال، DNS، Bucket | نصب Docker، Hardening، کاربرها، containerd |
| نقطهی قوت | دانستن تفاوت وضعیت فعلی با وضعیت مطلوب | کار روی هر سروری که SSH دارد، حتی VMware قدیمی |
بخش اول: Terraform
Provider و منابع
مثال زیر سه سرور، یک شبکهی خصوصی و یک فایروال روی Hetzner Cloud میسازد:
terraform {
required_providers {
hcloud = { source = "hetznercloud/hcloud", version = "~> 1.48" }
}
}
variable "hcloud_token" { sensitive = true }
provider "hcloud" { token = var.hcloud_token }
resource "hcloud_network" "private" {
name = "app-net"
ip_range = "10.10.0.0/16"
}
resource "hcloud_network_subnet" "nodes" {
network_id = hcloud_network.private.id
type = "cloud"
network_zone = "eu-central"
ip_range = "10.10.1.0/24"
}
resource "hcloud_firewall" "base" {
name = "base"
rule {
direction = "in"
protocol = "tcp"
port = "22"
source_ips = ["203.0.113.10/32"] # office / VPN only
}
}
resource "hcloud_server" "node" {
count = 3
name = "node-${count.index + 1}"
server_type = "cpx31"
image = "ubuntu-24.04"
location = "fsn1"
ssh_keys = ["devops"]
firewall_ids = [hcloud_firewall.base.id]
labels = { role = "k8s-worker", env = "production" }
network {
network_id = hcloud_network.private.id
}
depends_on = [hcloud_network_subnet.nodes]
}
output "nodes" {
value = { for s in hcloud_server.node : s.name => s.ipv4_address }
}
State روی MinIO
State فایل حساسی است و باید بین اعضای تیم مشترک و محافظتشده باشد. اگر S3 ابری ندارید، MinIO داخلی کاملاً جوابگوست:
terraform {
backend "s3" {
bucket = "terraform-state"
key = "production/infra.tfstate"
region = "us-east-1" # ignored by MinIO, but required
endpoints = { s3 = "https://minio.example.ir" }
use_path_style = true
skip_credentials_validation = true
skip_region_validation = true
skip_requesting_account_id = true
skip_metadata_api_check = true
}
}
بخش دوم: از Terraform به Ansible
هیچ IPی نباید دستی در Inventory وارد شود. با یک local_file، Terraform خودش Inventory را میسازد:
resource "local_file" "inventory" {
filename = "${path.module}/../ansible/inventory/production.ini"
content = <<-EOT
[k8s_workers]
%{for s in hcloud_server.node~}
${s.name} ansible_host=${s.ipv4_address}
%{endfor~}
[all:vars]
ansible_user=root
EOT
}
بخش سوم: Ansible Role برای Hardening
# roles/base/tasks/main.yml
- name: Install base packages
ansible.builtin.apt:
name: [curl, chrony, fail2ban, unattended-upgrades]
state: present
update_cache: true
- name: Disable SSH password login
ansible.builtin.lineinfile:
path: /etc/ssh/sshd_config
regexp: "^#?PasswordAuthentication"
line: "PasswordAuthentication no"
notify: restart ssh
- name: Kernel parameters for Kubernetes
ansible.posix.sysctl:
name: "{{ item.key }}"
value: "{{ item.value }}"
sysctl_file: /etc/sysctl.d/90-k8s.conf
loop: "{{ sysctl_params | dict2items }}"
- name: Point containerd to the Harbor mirror
ansible.builtin.template:
src: hosts.toml.j2
dest: /etc/containerd/certs.d/docker.io/hosts.toml
mode: "0644"
notify: restart containerd
# playbooks/site.yml
- hosts: k8s_workers
become: true
roles: [base, containerd, kubernetes-node]
اجرای Playbook را همیشه اول با --check --diff انجام دهید تا قبل از اعمال، دقیقاً ببینید چه چیزی تغییر میکند.
بخش چهارم: Pipeline برای زیرساخت
کد زیرساخت هم مثل کد برنامه باید Review شود:
stages: [validate, plan, apply, configure]
.tf:
image: { name: ghcr.io/opentofu/opentofu:1.8, entrypoint: [""] }
before_script:
- cd terraform && tofu init -input=false
validate:
extends: .tf
stage: validate
script: [tofu fmt -check, tofu validate]
plan:
extends: .tf
stage: plan
script: [tofu plan -input=false -out=plan.bin]
artifacts: { paths: [terraform/plan.bin], expire_in: 1 week }
apply:
extends: .tf
stage: apply
script: [tofu apply -input=false plan.bin]
rules:
- if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
when: manual
environment: { name: infra-production }
configure:
stage: configure
image: cytopia/ansible:latest-tools
needs: [apply]
script:
- ansible-playbook -i ansible/inventory/production.ini ansible/playbooks/site.yml
apply دقیقاً همان plan.binی را اجرا میکند که در Merge Request دیده و تأیید شده است؛ نه یک plan جدید که ممکن است فرق کند.
اشتباهات رایج
- تغییر دستی در کنسول ابری: هر تغییر دستی باعث Drift میشود. اگر مجبور شدید، بلافاصله در کد هم اعمالش کنید.
- Secret در فایلهای
.tfvars: از متغیرهای Protected در CI یا HashiCorp Vault استفاده کنید. - یک State برای همهچیز: State را بر اساس محیط و لایه (شبکه، کلاستر، DNS) جدا کنید تا یک اشتباه همهچیز را تحت تأثیر قرار ندهد.
- Ansible غیر Idempotent: استفادهی زیاد از
shellوcommandباعث میشود اجرای دوم نتیجهی متفاوتی بدهد. تا جای ممکن از ماژولهای اختصاصی استفاده کنید.
جمعبندی
Terraform و Ansible رقیب نیستند، مکملاند. Terraform اسکلت زیرساخت را میسازد، Ansible آن را آمادهی کار میکند و GitLab CI هر دو را قابل بازبینی و تکرارپذیر نگه میدارد. نتیجه زیرساختی است که اگر فردا کاملاً از بین برود، در چند ساعت دوباره ساخته میشود.