Configuration ManagementConfiguration Management
Thuộc bộ tài liệu DevOps Roadmap.
Tổng quan
Configuration management (CM — quản lý cấu hình) là kỷ luật đưa server về một trạng thái mong muốn, đã biết trước — package được cài, file cấu hình, user, service, thiết lập kernel — và giữ chúng ở đó, một cách tự động và lặp lại được. Nếu các tool provisioning (Terraform, CloudFormation) trả lời câu hỏi “máy/network/database này có tồn tại không?”, thì configuration management trả lời “phần mềm trên đó đã được cấu hình đúng chưa, và có giữ đúng như vậy không?”
Các tool CM đã thay thế thời kỳ SSH gõ tay và những sysadmin “anh hùng” bằng code: một playbook hay manifest mô tả trạng thái đích, được apply giống hệt nhau cho một node hay mười nghìn node. Lợi ích là tính nhất quán (không còn server “snowflake”), tốc độ (một web server mới được cấu hình trong vài phút), khả năng audit (thay đổi cấu hình nằm trong git), và khả năng khôi phục (dựng lại bất kỳ máy nào từ định nghĩa của nó).
Ngay cả trong thế giới container-first, CM vẫn quan trọng: vẫn phải có ai đó cấu hình các host chạy container, các đội VM cho database và ứng dụng legacy, thiết bị mạng, laptop, và các build agent mà CI của bạn chạy trên đó. Và những ý tưởng CM khai phá — desired state, idempotency, convergence — chính là những ý tưởng vận hành các controller của Kubernetes và GitOps.
Mô hình tư duy: mô tả điểm đến, không phải hành trình
Sự chuyển đổi tư duy mà CM đòi hỏi là từ script sang state. Một shell script nói “chạy các lệnh này”; một task CM nói “package này phải có mặt, file này phải có nội dung này, service này phải đang chạy và được enable.” Bạn mô tả điểm đến và để tool tự tìm cách — ở mỗi lần chạy, trên mỗi host, bất kể điểm xuất phát là gì — để đến đó. Chính tính chất đó làm cho CM an toàn khi chạy lặp lại, an toàn khi chạy trên một máy mới toanh hay một máy đã năm tuổi, và có thể báo cáo nó đã thay đổi gì thay vì mù quáng làm lại công việc.
Kiến thức nền tảng
Configuration management vs provisioning
| Khía cạnh | Provisioning (Terraform, v.v.) | Configuration management |
|---|---|---|
| Tạo ra | Resource hạ tầng (VM, network, DB) | Trạng thái phần mềm trên máy đã tồn tại |
| Mô hình | Desired state của resource cloud | Desired state của cấu hình OS/ứng dụng |
| Đơn vị điển hình | Resource trong API của provider | Package, file, service, user, cron job |
| Nhịp độ | Khi có thay đổi (plan/apply) | Khi có thay đổi và/hoặc liên tục (vòng lặp converge) |
Pattern phổ biến: Terraform tạo VM rồi chuyển cho Ansible (qua inventory hoặc provisioner) để cấu hình — hoặc ngày càng phổ biến hơn, Terraform khởi chạy một image đã bake sẵn và hoàn toàn không cần CM lúc runtime.
Idempotency và convergence
- Idempotency — apply cùng một cấu hình hai lần cho kết quả giống như apply một lần. “Đảm bảo nginx được cài” là idempotent; “chạy
apt-get install nginx… rồi append một dòng vào file” theo kiểu ngây thơ thì không (dòng append chạy mỗi lần). Các lần chạy idempotent báo cáo changed vs ok — đó chính là cách bạn nhìn thấy drift: một lần chạy báo có thay đổi trên một host bạn không đụng vào đang nói cho bạn biết host đó đã drift. - Convergence — tool đưa hệ thống từ bất kỳ trạng thái nào hiện tại về trạng thái đã khai báo, sửa drift ở mỗi lần chạy. Tool theo mô hình pull (Puppet, Chef) truyền thống converge theo lịch (ví dụ mỗi 30 phút); tool theo mô hình push converge khi bạn chạy chúng.
- Bài test idempotency. Cách thực tế để verify một task là idempotent: chạy playbook hai lần liên tiếp. Lần chạy thứ hai phải báo cáo zero thay đổi. Nếu không, bạn có một task mutate state vô điều kiện — một bước
command/shellthường là thủ phạm — và nó sẽ gây bất ngờ về sau bằng cách “luôn thay đổi” và che giấu drift thật.
Vì sao task non-idempotent nguy hiểm: chúng khiến mọi lần chạy trông giống một thay đổi, nên bạn không còn phân biệt được “không có gì drift” với “chúng ta vừa làm lại công việc”, và handler (như restart service) fire mỗi lần, biến một đội máy khỏe mạnh thành churn không cần thiết.
Mô hình push vs pull
| Mô hình | Cách hoạt động | Ví dụ | Đánh đổi |
|---|---|---|---|
| Push | Một control node kết nối ra ngoài (thường qua SSH/WinRM) và apply cấu hình | Ansible, Salt (chế độ ssh) | Đơn giản, không phải quản lý agent, rất tốt cho orchestration và rollout theo thứ tự; scale cần cẩn thận; node có thể drift giữa các lần chạy |
| Pull | Agent trên mỗi node định kỳ lấy cấu hình từ server và converge | Puppet (agent), Chef (client), Salt (minion) | Sửa drift liên tục, scale tới đội máy khổng lồ, hoạt động tốt khi node xuất hiện khó đoán (autoscaling); cần hạ tầng agent + server và quản lý certificate/key |
Chọn thế nào: push (Ansible) hợp với đội máy nhỏ-đến-vừa, rollout theo thứ tự, orchestration (“restart 200 host này, mỗi lượt 10 máy”), và các team muốn zero agent. Pull (Puppet/Chef) hợp với đội máy lớn cần ép drift liên tục và các node đến rồi đi qua autoscaling — nhưng bạn phải trả giá bằng server + agent + quản lý certificate. Trong các shop cloud-native, xu hướng là cả hai đều không chạy trên tầng app: immutable image và container thay thế CM lúc runtime, và CM thu nhỏ lại về tầng host/base-image.
So sánh công cụ
| Công cụ | Ngôn ngữ | Mô hình | Agent? | Điểm mạnh |
|---|---|---|---|---|
| Ansible | Playbook YAML (+ Jinja2) | Push qua SSH | Agentless | Rào cản gia nhập thấp nhất, kho module khổng lồ, kiêm luôn orchestration; cấu hình được cả thiết bị mạng |
| Puppet | Puppet DSL (khai báo) | Pull (agent ↔ server) | Agent | Mô hình model-driven trưởng thành, ép drift mạnh, báo cáo/compliance tốt ở quy mô lớn |
| Chef | Ruby DSL (“recipe”/“cookbook”) | Pull (chef-client ↔ server) | Agent | Sức mạnh lập trình đầy đủ, văn hóa test-driven infra (Test Kitchen, InSpec) |
| SaltStack | YAML state + Python | Pull (minion) hoặc push (salt-ssh); event bus (ZeroMQ) | Cả hai | Rất nhanh ở quy mô lớn, tự động hóa/khắc phục theo sự kiện (reactor, beacon) |
Ansible thống trị các dự án mới vì nó agentless (chỉ cần SSH và Python trên máy đích), YAML ai cũng đọc được, và cùng một tool xử lý cả cấu hình lẫn orchestration ad-hoc (“restart service này trên 200 host, mỗi lượt 10 máy”). Puppet và Chef vẫn giữ vị thế ở các đội máy lớn, sống lâu, nặng về compliance, nơi việc ép state liên tục theo mô hình pull và báo cáo phong phú biện minh được cho hạ tầng agent. Event bus của Salt khiến nó mạnh cho automation phản ứng, tự khắc phục.
Các khối xây dựng của Ansible
- Inventory — danh sách các host được quản lý, chia nhóm (
[webservers],[dbservers]), file tĩnh INI/YAML hoặc dynamic (lấy từ API của AWS/GCP/vCenter để danh sách không bao giờ lỗi thời dưới autoscaling). - Playbooks — file YAML ánh xạ nhóm host với danh sách task theo thứ tự (play).
- Modules — đơn vị công việc (
apt,template,service,user, hàng nghìn module khác); module chính là thứ làm cho task idempotent, vì mỗi module kiểm tra trạng thái hiện tại trước khi hành động. - Roles — cấu trúc thư mục chuẩn gom task, handler, template, file và biến mặc định để tái sử dụng (
roles/nginx/…); chia sẻ qua Ansible Galaxy. - Handlers — task chỉ được kích hoạt khi có thứ gì đó thay đổi (ví dụ chỉ restart nginx nếu file cấu hình của nó thay đổi); chúng chạy một lần, ở cuối play, bất kể có bao nhiêu task notify chúng.
- Variables & templates — template Jinja2 render file cấu hình theo từng host/nhóm từ các biến với quy tắc ưu tiên rõ ràng (role default yếu nhất, extra-vars trên dòng lệnh mạnh nhất).
- Collections — đơn vị đóng gói hiện đại (có namespace, ví dụ
ansible.builtin,community.aws) phân phối module, role và plugin cùng nhau.
Ví dụ Ansible playbook
# site.yml — cấu hình và deploy tầng web
- name: Configure web servers
hosts: webservers
become: true
serial: "25%" # rolling: một phần tư đội máy mỗi lượt
max_fail_percentage: 10 # dừng rollout nếu quá nhiều host fail
vars:
nginx_worker_connections: 2048
app_version: "1.4.2"
tasks:
- name: Install packages
ansible.builtin.apt:
name: [nginx, python3-venv]
state: present
update_cache: true
- name: Deploy nginx config from template
ansible.builtin.template:
src: templates/nginx.conf.j2
dest: /etc/nginx/nginx.conf
owner: root
mode: "0644"
validate: "nginx -t -c %s" # không bao giờ cài config lỗi cú pháp
notify: Reload nginx # handler chỉ chạy khi có thay đổi
- name: Ensure app user exists
ansible.builtin.user:
name: appuser
system: true
shell: /usr/sbin/nologin
- name: Ensure nginx is running and enabled
ansible.builtin.service:
name: nginx
state: started
enabled: true
- name: Wait for health endpoint
ansible.builtin.uri:
url: http://localhost/healthz
status_code: 200
retries: 5
delay: 3
register: health
until: health.status == 200
handlers:
- name: Reload nginx
ansible.builtin.service:
name: nginx
state: reloaded
Chạy ansible-playbook -i inventory/prod site.yml --check --diff trước (dry run hiển thị thay đổi), rồi chạy không có --check để apply thật. Bộ ba serial + max_fail_percentage + health check biến một thay đổi cấu hình thông thường thành một rolling deploy tự dừng khi lỗi — chính là tính chất an toàn mà canary mang lại trong CI/CD, được diễn đạt trong một playbook.
Template Jinja2 mà nó render
# templates/nginx.conf.j2
worker_processes auto;
events { worker_connections {{ nginx_worker_connections }}; }
http {
server {
listen 80;
# app_version={{ app_version }} được Ansible deploy
location / { proxy_pass http://127.0.0.1:8080; }
}
}
Vì template được render từ biến, cùng một role sinh ra config theo từng môi trường (worker count dev vs prod) mà không cần file trùng lặp.
Khái niệm chính
Config management vs baked image vs container
Ba cách đưa phần mềm lên compute, trải trên phổ từ mutable đến immutable:
| Cách tiếp cận | Cách làm | Phù hợp khi | Chi phí |
|---|---|---|---|
| CM lúc runtime (Ansible/Puppet/Chef trên node đang chạy) | Cấu hình máy sau khi boot, re-converge theo thời gian | VM sống lâu, ứng dụng legacy, đội máy không đồng nhất, thiết bị mạng, hardening compliance | Boot-đến-sẵn-sàng chậm; có thể drift giữa các lần chạy |
| Baked image / immutable infrastructure (Packer) | Build một golden AMI/VM image cài sẵn mọi thứ; deploy = thay instance bằng image mới; không bao giờ patch tại chỗ | Autoscaling group, đội máy đồng nhất, scale-out nhanh, tái tạo mạnh | Cần pipeline build image; đổi cấu hình đòi hỏi rebuild + rollout |
| Container | Bất biến ở tầng ứng dụng: Dockerfile build môi trường app; orchestrator lên lịch chạy | Microservice, đóng gói dày đặc, dev-prod parity dễ mang theo | Host vẫn cần cấu hình (nhẹ); các vấn đề stateful/tầng OS vẫn còn |
Chúng kết hợp với nhau chứ không cạnh tranh: Packer thường chạy Ansible làm provisioner để build golden image; image của container host được bake sẵn; CM lúc runtime thu nhỏ lại thành phần keo bootstrap (join cluster, đăng ký agent). Xu hướng chung của ngành — “pets vs cattle” — nghiêng về thay thế bất biến thay vì sửa tại chỗ vì nó loại bỏ drift ngay từ thiết kế: nếu bạn không bao giờ sửa một node đang chạy, nó không thể drift. Nhưng vẫn còn rất nhiều hệ thống thực tế (database trên VM, on-prem, appliance, hệ thống bị quản chế) cần CM kiểu convergence cổ điển, nên kỹ năng này không biến mất — nó dịch chuyển sang tầng base-image và host.
Mutable vs immutable — sự đánh đổi cốt lõi
| Mutable (CM lúc runtime) | Immutable (bake + thay thế) | |
|---|---|---|
| Mô hình thay đổi | Sửa node đang chạy tại chỗ | Build image mới, thay node |
| Drift | Có thể; convergence chống lại nó | Bất khả thi theo thiết kế |
| Rollback | Re-converge về state cũ (mờ) | Deploy lại image trước đó (chính xác) |
| Boot-đến-sẵn-sàng | Chậm hơn (cấu hình lúc boot) | Nhanh (image đã sẵn sàng) |
| Khả năng debug | SSH vào và soi một máy sống lâu | Ephemeral; thu log/metric ra ngoài |
| Phù hợp | Database, legacy, on-prem, appliance | Service stateless, đội máy autoscaling |
# packer.pkr.hcl — bake golden image bằng Ansible
source "amazon-ebs" "web" {
ami_name = "web-golden-{{timestamp}}"
instance_type = "t3.small"
region = "ap-southeast-1"
source_ami_filter {
filters = { name = "ubuntu/images/*ubuntu-jammy-22.04-amd64-server-*" }
owners = ["099720109477"]
most_recent = true
}
ssh_username = "ubuntu"
}
build {
sources = ["source.amazon-ebs.web"]
provisioner "ansible" {
playbook_file = "./site.yml"
}
# Tùy chọn: verify image đã bake trước khi publish AMI
provisioner "shell" {
inline = ["nginx -t", "systemctl is-enabled nginx"]
}
}
Đây là phần “đẹp” của hệ sinh thái: cùng một site.yml vừa cấu hình VM sống lúc runtime vừa bake golden image. Bạn không chọn Ansible hay Packer — Packer dùng Ansible, và bạn chọn cấu hình chạy ở đâu trong vòng đời.
Secret trong configuration management
Code cấu hình chắc chắn đụng tới secret (mật khẩu DB, API key, private key TLS). Các lựa chọn, đại khái tệ-đến-tốt:
- Ansible Vault / Hiera-eyaml / encrypted data bag — mã hóa secret lúc nghỉ (at rest) trong repo. Tốt hơn plaintext, nhưng key giải mã phải được phân phối và secret vẫn nằm trên đĩa.
- Secret manager bên ngoài lúc runtime — lấy từ Vault, AWS Secrets Manager, hay SSM Parameter Store trong lúc chạy (ví dụ lookup
community.hashi_vaultcủa Ansible). Secret không bao giờ được commit và có thể xoay vòng tập trung. Đây là trạng thái đích cần hướng tới.
Không bao giờ commit secret dạng plaintext, và không bao giờ debug: một biến chứa secret — nó sẽ nằm trong log.
Test và kiểm chứng code cấu hình
- Lint —
ansible-lint,puppet-lint,cookstylebắt lỗi style và các pattern đã biết là xấu trước khi merge. - Dry run —
--check --diff(Ansible),puppet agent --noopxem trước thay đổi mà không apply, để bạn review blast radius giống cách review một Terraform plan. - Integration test — Molecule (Ansible) hoặc Test Kitchen (Chef) dựng container/VM, apply role và kiểm chứng — bao gồm cả bài kiểm tra idempotence (apply hai lần, kỳ vọng zero thay đổi ở lần thứ hai) mà Molecule tự động hóa.
- Verification/compliance — InSpec, Testinfra, hoặc goss assert trên trạng thái hệ thống kết quả (“port 22 chỉ mở từ bastion”, “nginx chạy dưới user appuser”, “không có file world-writable trong /etc”).
# molecule/default/molecule.yml (trích)
verifier:
name: testinfra
# Kịch bản mặc định của Molecule chạy: create → converge → idempotence → verify → destroy
# molecule/default/tests/test_web.py (Testinfra)
def test_nginx_running(host):
svc = host.service("nginx")
assert svc.is_running and svc.is_enabled
def test_listens_on_80(host):
assert host.socket("tcp://0.0.0.0:80").is_listening
Best Practices
- Giữ toàn bộ code cấu hình trong version control kèm code review. Một thay đổi playbook tác động 500 server xứng đáng được soi kỹ ít nhất như thay đổi code ứng dụng — nó là một thay đổi cho 500 server.
- Làm mọi task idempotent — và verify bằng cách chạy hai lần. Lần chạy thứ hai phải báo cáo zero thay đổi; nếu không, bạn đang có một task non-idempotent ẩn che giấu drift và fire handler một cách thừa thãi.
- Dùng
--check --diff(dry run) trước khi apply lên production. Xem trước blast radius giống như cách bạn review một Terraform plan; một diff bất ngờ là cơ hội để dừng lại trước, chứ không phải sau. - Không bao giờ lưu secret dạng plaintext. Dùng Ansible Vault, Hiera-eyaml, hoặc encrypted data bag — hoặc tốt hơn, lấy từ secret manager bên ngoài lúc runtime để không có gì nhạy cảm bị commit hay log.
- Cấu trúc code thành các role/module nhỏ, đơn mục đích, có giá trị mặc định hợp lý. Khả năng tái sử dụng và dễ đọc sụp đổ khi một playbook làm mọi thứ; một role nên làm một việc và expose vài biến.
- Dùng dynamic inventory trong môi trường cloud. Danh sách host tĩnh lỗi thời ngay lập tức dưới autoscaling; lấy inventory từ tag/group qua API cloud để danh sách luôn cập nhật.
- Rollout tuần tự kèm health check.
serial: 25%cộng với task health check vàmax_fail_percentagebiến một thay đổi cấu hình thành rolling deploy có tự động dừng khi lỗi — một canary bạn đã sẵn có. - Ưu tiên handler thay vì restart vô điều kiện. Chỉ restart service khi cấu hình của nó thực sự thay đổi; restart cả đội máy đang khỏe ở mỗi lần chạy là tự gây downtime.
- Validate file cấu hình trước khi cài nó. Dùng hook
validate:của module (nginx -t,visudo -c,sshd -t) để một lỗi cú pháp làm fail task thay vì làm sập service lúc reload. - Chạy convergence (hoặc CM theo lịch) để chống drift trên node sống lâu. Cấu hình apply một lần rồi không bao giờ kiểm tra lại là tài liệu, không phải quản lý; drift tích tụ âm thầm cho đến khi thành sự cố.
- Test role trong CI với Molecule/Test Kitchen và assert bằng InSpec/Testinfra. Code cấu hình không có test sẽ fail lúc 2 giờ sáng trên đúng cái host hơi khác biệt so với nơi bạn phát triển.
- Giữ task dễ đọc và có tên. Mỗi task nên có
name:rõ ràng; output khi chạy chính là nhật ký vận hành của bạn, và “TASK [command]” chẳng nói gì cho kỹ sư on-call. - Ưu tiên immutable image khi nền tảng cho phép. Nếu một node có thể được thay thế thay vì sửa chữa, hãy bake nó bằng Packer và để nhiệm vụ của CM kết thúc ở lúc build image — drift bạn không thể tạo ra là drift bạn không bao giờ phải chống.
- Log và báo cáo mọi lần chạy về một chỗ tập trung. Biết node nào đã thay đổi, fail, hay drift (AWX/Ansible Automation Platform, PuppetDB, Chef Automate) là thứ biến automation thành vận hành mà bạn thực sự tin được.
Tài liệu tham khảo
- roadmap.sh — DevOps Roadmap
- Tài liệu Ansible
- Ansible — best practices / tips
- Ansible Galaxy (role/collection cộng đồng)
- Ansible Vault
- Molecule (test cho Ansible)
- Testinfra
- Tài liệu Puppet
- Tài liệu Chef
- Chef InSpec
- Tài liệu Salt Project
- Tài liệu Packer
- goss (kiểm chứng server)
- Martin Fowler — Immutable Server
- Martin Fowler — Snowflake Server
- Sách: Ansible: Up and Running — Bas Meijer, Lorin Hochstein, René Moser
- Sách: Infrastructure as Code — Kief Morris (bao quát CM vs pattern immutable)
Part of the DevOps Roadmap knowledge base.
Overview
Configuration management (CM) is the discipline of getting servers into a desired, known state — installed packages, config files, users, services, kernel settings — and keeping them there, automatically and repeatably. Where provisioning tools (Terraform, CloudFormation) answer “does this machine/network/database exist?”, configuration management answers “is the software on it set up correctly, and does it stay that way?”
CM tools replaced the era of hand-run SSH sessions and hero sysadmins with code: a playbook or manifest that describes the end state, applied identically to one node or ten thousand. The payoff is consistency (no snowflake servers), speed (a new web server configured in minutes), auditability (config changes in git), and recovery (rebuild any box from its definition).
Even in a container-first world, CM remains relevant: someone still configures the container hosts, VM fleets for databases and legacy apps, network gear, laptops, and the build agents your CI runs on. And the ideas CM pioneered — desired state, idempotency, convergence — are the same ideas that power Kubernetes controllers and GitOps.
The mental model: describe the destination, not the journey
The mindset shift that CM demands is from scripts to state. A shell script says “run these commands”; a CM task says “this package should be present, this file should have these contents, this service should be running and enabled.” You describe the destination and let the tool figure out — on every run, on every host, whatever the starting point — how to get there. That single property is what makes CM safe to run repeatedly, safe to run against a fresh box or a five-year-old one, and able to report what it changed rather than blindly re-doing work.
Fundamentals
Configuration management vs provisioning
| Aspect | Provisioning (Terraform etc.) | Configuration management |
|---|---|---|
| Creates | Infrastructure resources (VMs, networks, DBs) | Software state on existing machines |
| Model | Desired state of cloud resources | Desired state of OS/application config |
| Typical unit | Resource in a provider API | Package, file, service, user, cron job |
| Cadence | On change (plan/apply) | On change and/or continuously (converge loop) |
A common pattern: Terraform creates the VM and passes it to Ansible (via inventory or provisioner) to configure — or, increasingly, Terraform launches a pre-baked image and no runtime CM is needed at all.
Idempotency and convergence
- Idempotency — applying the same configuration twice produces the same result as applying it once. “Ensure nginx is installed” is idempotent; “run
apt-get install nginx… and append a line to a file” done naively is not (the append runs every time). Idempotent runs report changed vs ok, which is how you see drift: a run that reports changes on a host you did not touch is telling you that host drifted. - Convergence — the tool moves the system from whatever state it is in toward the declared state, correcting drift on every run. Pull-based tools (Puppet, Chef) traditionally converge on a schedule (e.g., every 30 minutes); push-based tools converge when you run them.
- The idempotency test. The practical way to verify a task is idempotent: run the playbook twice in a row. The second run must report zero changes. If it does not, you have a task that mutates state unconditionally — a
command/shellstep is the usual culprit — and it will surprise you later by “always changing” and by masking real drift.
Why non-idempotent tasks are dangerous: they make every run look like a change, so you can no longer distinguish “nothing drifted” from “we re-did work,” and handlers (like a service restart) fire every time, turning a healthy fleet into unnecessary churn.
Push vs pull models
| Model | How it works | Examples | Trade-offs |
|---|---|---|---|
| Push | A control node connects out (usually SSH/WinRM) and applies config | Ansible, Salt (ssh mode) | Simple, no agents to manage, great for orchestration and ordered rollouts; scaling requires care; nodes drift between runs |
| Pull | An agent on each node periodically fetches config from a server and converges | Puppet (agent), Chef (client), Salt (minion) | Continuous drift correction, scales to huge fleets, works when nodes appear unpredictably (autoscaling); requires agent + server infrastructure and certificate/key management |
How to choose: push (Ansible) fits small-to-mid fleets, ordered rollouts, orchestration (“restart these 200 hosts 10 at a time”), and teams that want zero agents. Pull (Puppet/Chef) fits large fleets that need continuous drift enforcement and nodes that come and go via autoscaling — but you pay for a server + agents + certificate management. In cloud-native shops the trend is that neither runs on the app tier at all: immutable images and containers replace runtime CM, and CM shrinks to the host/base-image layer.
Tool comparison
| Tool | Language | Model | Agent? | Strengths |
|---|---|---|---|---|
| Ansible | YAML playbooks (+ Jinja2) | Push over SSH | Agentless | Lowest barrier to entry, huge module collection, doubles as orchestration tool; also configures network devices |
| Puppet | Puppet DSL (declarative) | Pull (agent ↔ server) | Agent | Mature model-driven approach, strong drift enforcement, good reporting/compliance at scale |
| Chef | Ruby DSL (“recipes”/“cookbooks”) | Pull (chef-client ↔ server) | Agent | Full programming power, test-driven infra culture (Test Kitchen, InSpec) |
| SaltStack | YAML states + Python | Pull (minion) or push (salt-ssh); event bus (ZeroMQ) | Either | Very fast at scale, event-driven automation/remediation (reactors, beacons) |
Ansible dominates new adoption because it is agentless (only needs SSH and Python on targets), the YAML is readable by everyone, and the same tool handles both configuration and ad-hoc orchestration (“restart this service on 200 hosts, 10 at a time”). Puppet and Chef retain strongholds in large, long-lived, compliance-heavy fleets where continuous pull-based enforcement and rich reporting justify the agent infrastructure. Salt’s event bus makes it strong for reactive, auto-remediating automation.
Ansible building blocks
- Inventory — the list of managed hosts, grouped (
[webservers],[dbservers]), static INI/YAML files or dynamic (pulled from AWS/GCP/vCenter APIs so the list can never go stale under autoscaling). - Playbooks — YAML files mapping groups of hosts to ordered lists of tasks (plays).
- Modules — the units of work (
apt,template,service,user, ~thousands more); modules are what make tasks idempotent, because each one checks current state before acting. - Roles — a standard directory structure bundling tasks, handlers, templates, files, and default variables for reuse (
roles/nginx/…); shared via Ansible Galaxy. - Handlers — tasks triggered only when something changed (e.g., restart nginx only if its config file changed); they run once, at the end of the play, no matter how many tasks notified them.
- Variables & templates — Jinja2 templating renders per-host/per-group config files from variables with clear precedence rules (role defaults are weakest, extra-vars on the command line are strongest).
- Collections — the modern packaging unit (namespaced, e.g.
ansible.builtin,community.aws) distributing modules, roles, and plugins together.
Example Ansible playbook
# site.yml — configure and deploy the web tier
- name: Configure web servers
hosts: webservers
become: true
serial: "25%" # rolling: a quarter of the fleet at a time
max_fail_percentage: 10 # stop the rollout if too many hosts fail
vars:
nginx_worker_connections: 2048
app_version: "1.4.2"
tasks:
- name: Install packages
ansible.builtin.apt:
name: [nginx, python3-venv]
state: present
update_cache: true
- name: Deploy nginx config from template
ansible.builtin.template:
src: templates/nginx.conf.j2
dest: /etc/nginx/nginx.conf
owner: root
mode: "0644"
validate: "nginx -t -c %s" # never install a config that fails syntax check
notify: Reload nginx # handler runs only on change
- name: Ensure app user exists
ansible.builtin.user:
name: appuser
system: true
shell: /usr/sbin/nologin
- name: Ensure nginx is running and enabled
ansible.builtin.service:
name: nginx
state: started
enabled: true
- name: Wait for health endpoint
ansible.builtin.uri:
url: http://localhost/healthz
status_code: 200
retries: 5
delay: 3
register: health
until: health.status == 200
handlers:
- name: Reload nginx
ansible.builtin.service:
name: nginx
state: reloaded
Run with ansible-playbook -i inventory/prod site.yml --check --diff first (dry run showing changes), then without --check to apply. The serial + max_fail_percentage + health-check combination turns a plain config change into a rolling deploy that stops itself on failure — the same safety property a canary gives you in CI/CD, expressed in a playbook.
The Jinja2 template it renders
# templates/nginx.conf.j2
worker_processes auto;
events { worker_connections {{ nginx_worker_connections }}; }
http {
server {
listen 80;
# app_version={{ app_version }} deployed by Ansible
location / { proxy_pass http://127.0.0.1:8080; }
}
}
Because the template is rendered from variables, the same role produces per-environment configs (dev vs prod worker counts) without duplicated files.
Key Concepts
Config management vs baked images vs containers
Three ways to get software onto compute, on a spectrum from mutable to immutable:
| Approach | How | Best when | Cost |
|---|---|---|---|
| Runtime CM (Ansible/Puppet/Chef on live nodes) | Configure machines after boot, re-converge over time | Long-lived VMs, legacy apps, heterogeneous fleets, network devices, compliance hardening | Boot-to-ready is slow; drift possible between runs |
| Baked images / immutable infrastructure (Packer) | Build a golden AMI/VM image with everything installed; deploy = replace instances with a new image; never patch in place | Autoscaling groups, uniform fleets, fast scale-out, strong reproducibility | Image build pipeline needed; config changes require a rebuild+rollout |
| Containers | Immutability at the app layer: Dockerfile builds the app environment; orchestrator schedules it | Microservices, dense packing, portable dev-prod parity | Hosts still need (light) configuration; stateful/OS-level concerns remain |
They compose rather than compete: Packer commonly runs Ansible as its provisioner to build the golden image; the container host image is baked; runtime CM shrinks to bootstrap glue (join cluster, register agent). The industry direction — “pets vs cattle” — favors immutable replacement over in-place mutation because it eliminates drift by construction: if you never modify a running node, it cannot drift. But plenty of real estates (databases on VMs, on-prem, appliances, regulated systems) still need classic convergence-based CM, so the skill does not go away — it moves to the base-image and host layer.
Mutable vs immutable — the core trade-off
| Mutable (runtime CM) | Immutable (bake + replace) | |
|---|---|---|
| Change model | Modify the running node in place | Build a new image, replace the node |
| Drift | Possible; convergence fights it | Impossible by construction |
| Rollback | Re-converge to old state (fuzzy) | Redeploy the previous image (exact) |
| Boot-to-ready | Slower (configure on boot) | Fast (image is ready) |
| Debuggability | SSH in and inspect a long-lived box | Ephemeral; capture logs/metrics externally |
| Fit | Databases, legacy, on-prem, appliances | Stateless services, autoscaling fleets |
# packer.pkr.hcl — bake a golden image using Ansible
source "amazon-ebs" "web" {
ami_name = "web-golden-{{timestamp}}"
instance_type = "t3.small"
region = "ap-southeast-1"
source_ami_filter {
filters = { name = "ubuntu/images/*ubuntu-jammy-22.04-amd64-server-*" }
owners = ["099720109477"]
most_recent = true
}
ssh_username = "ubuntu"
}
build {
sources = ["source.amazon-ebs.web"]
provisioner "ansible" {
playbook_file = "./site.yml"
}
# Optional: verify the baked image before publishing the AMI
provisioner "shell" {
inline = ["nginx -t", "systemctl is-enabled nginx"]
}
}
This is the elegant part of the ecosystem: the same site.yml configures live VMs at runtime and bakes golden images. You do not choose Ansible or Packer — Packer uses Ansible, and you pick where in the lifecycle the configuration runs.
Secrets in configuration management
Config code inevitably touches secrets (DB passwords, API keys, TLS private keys). Options, roughly best-last:
- Ansible Vault / Hiera-eyaml / encrypted data bags — encrypt secrets at rest in the repo. Better than plaintext, but the decryption key must be distributed and the secret still lands on disk.
- External secret manager at runtime — pull from Vault, AWS Secrets Manager, or SSM Parameter Store during the run (e.g. Ansible’s
community.hashi_vaultlookup). The secret is never committed and can be rotated centrally. This is the target state.
Never commit plaintext secrets, and never debug: a variable that holds one — it lands in logs.
Testing and validating configuration code
- Lint —
ansible-lint,puppet-lint,cookstylecatch style and known-bad patterns before they merge. - Dry run —
--check --diff(Ansible),puppet agent --nooppreview changes without applying, so you review blast radius the way you review a Terraform plan. - Integration testing — Molecule (Ansible) or Test Kitchen (Chef) spin up containers/VMs, apply the role, and verify — including the idempotence check (apply twice, expect zero changes on the second run) that Molecule automates.
- Verification/compliance — InSpec, Testinfra, or goss assert on the resulting system state (“port 22 only from bastion”, “nginx running as appuser”, “no world-writable files in /etc”).
# molecule/default/molecule.yml (excerpt)
verifier:
name: testinfra
# Molecule's default scenario runs: create → converge → idempotence → verify → destroy
# molecule/default/tests/test_web.py (Testinfra)
def test_nginx_running(host):
svc = host.service("nginx")
assert svc.is_running and svc.is_enabled
def test_listens_on_80(host):
assert host.socket("tcp://0.0.0.0:80").is_listening
Best Practices
- Keep all configuration code in version control with code review. A playbook change to 500 servers deserves at least the scrutiny of an app code change — it is a change to 500 servers.
- Make every task idempotent — and verify by running twice. A second run should report zero changes; if it doesn’t, you have a hidden non-idempotent task that masks drift and fires handlers spuriously.
- Use
--check --diff(dry run) before applying to production. Preview blast radius the same way you review a Terraform plan; a surprising diff is a chance to stop before, not after. - Never store secrets in plaintext. Use Ansible Vault, Hiera-eyaml, or encrypted data bags — or better, pull from an external secret manager at runtime so nothing sensitive is committed or logged.
- Structure code as small, single-purpose roles/modules with sensible defaults. Reuse and readability collapse when one playbook does everything; a role should do one job and expose a few variables.
- Use dynamic inventory in cloud environments. Static host lists rot instantly under autoscaling; source inventory from cloud API tags/groups so the list is always current.
- Roll out serially with health checks.
serial: 25%plus a health-check task andmax_fail_percentageturns a config change into a rolling deploy with automatic stop-on-failure — a canary you already have. - Prefer handlers over unconditional restarts. Restart services only when their config actually changed; restarting a healthy fleet on every run is self-inflicted downtime.
- Validate config files before installing them. Use module
validate:hooks (nginx -t,visudo -c,sshd -t) so a syntax error fails the task instead of taking a service down on reload. - Run convergence (or scheduled CM runs) to fight drift on long-lived nodes. A config applied once and never re-checked is documentation, not management; drift accumulates silently until an incident.
- Test roles in CI with Molecule/Test Kitchen and assert with InSpec/Testinfra. Config code without tests fails at 2 a.m. on the one host that was slightly different from where you developed it.
- Keep tasks readable and named. Every task should have a clear
name:; the run output is your operational log, and “TASK [command]” tells the on-call engineer nothing. - Prefer immutable images where the platform allows it. If a node can be replaced instead of repaired, bake it with Packer and let CM’s job end at image-build time — drift you cannot create is drift you never fight.
- Log and report every run centrally. Knowing which nodes changed, failed, or drifted (AWX/Ansible Automation Platform, PuppetDB, Chef Automate) is what turns automation into operations you can actually trust.
References
- roadmap.sh — DevOps Roadmap
- Ansible documentation
- Ansible — best practices / tips
- Ansible Galaxy (community roles/collections)
- Ansible Vault
- Molecule (Ansible testing)
- Testinfra
- Puppet documentation
- Chef documentation
- Chef InSpec
- Salt Project documentation
- Packer documentation
- goss (server validation)
- Martin Fowler — Immutable Server
- Martin Fowler — Snowflake Server
- Book: Ansible: Up and Running — Bas Meijer, Lorin Hochstein, René Moser
- Book: Infrastructure as Code — Kief Morris (covers CM vs immutable patterns)