← DevOps← DevOps
DevOpsDevOps18 Th7, 2026Jul 18, 202617 phút đọc14 min read

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ạnhProvisioning (Terraform, v.v.)Configuration management
Tạo raResource hạ tầng (VM, network, DB)Trạng thái phần mềm trên máy đã tồn tại
Mô hìnhDesired state của resource cloudDesired state của cấu hình OS/ứng dụng
Đơn vị điển hìnhResource trong API của providerPackage, 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

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ìnhCách hoạt độngVí dụĐánh đổi
PushMột control node kết nối ra ngoài (thường qua SSH/WinRM) và apply cấu hìnhAnsible, 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
PullAgent trên mỗi node định kỳ lấy cấu hình từ server và convergePuppet (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ìnhAgent?Điểm mạnh
AnsiblePlaybook YAML (+ Jinja2)Push qua SSHAgentlessRà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
PuppetPuppet DSL (khai báo)Pull (agent ↔ server)AgentMô hình model-driven trưởng thành, ép drift mạnh, báo cáo/compliance tốt ở quy mô lớn
ChefRuby DSL (“recipe”/“cookbook”)Pull (chef-client ↔ server)AgentSức mạnh lập trình đầy đủ, văn hóa test-driven infra (Test Kitchen, InSpec)
SaltStackYAML state + PythonPull (minion) hoặc push (salt-ssh); event bus (ZeroMQ)Cả haiRấ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

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ậnCách làmPhù hợp khiChi 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 gianVM sống lâu, ứng dụng legacy, đội máy không đồng nhất, thiết bị mạng, hardening complianceBoot-đế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ạnhCần pipeline build image; đổi cấu hình đòi hỏi rebuild + rollout
ContainerBất biến ở tầng ứng dụng: Dockerfile build môi trường app; orchestrator lên lịch chạyMicroservice, đóng gói dày đặc, dev-prod parity dễ mang theoHost 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 đổiSửa node đang chạy tại chỗBuild image mới, thay node
DriftCó thể; convergence chống lại nóBất khả thi theo thiết kế
RollbackRe-converge về state cũ (mờ)Deploy lại image trước đó (chính xác)
Boot-đến-sẵn-sàngChậm hơn (cấu hình lúc boot)Nhanh (image đã sẵn sàng)
Khả năng debugSSH vào và soi một máy sống lâuEphemeral; thu log/metric ra ngoài
Phù hợpDatabase, legacy, on-prem, applianceService 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:

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

# 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

  1. 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.
  2. 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.
  3. 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.
  4. 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.
  5. 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.
  6. 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.
  7. Rollout tuần tự kèm health check. serial: 25% cộng với task health check và max_fail_percentage biế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ó.
  8. Ư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.
  9. 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.
  10. 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ố.
  11. 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.
  12. 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.
  13. Ư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.
  14. 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

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

AspectProvisioning (Terraform etc.)Configuration management
CreatesInfrastructure resources (VMs, networks, DBs)Software state on existing machines
ModelDesired state of cloud resourcesDesired state of OS/application config
Typical unitResource in a provider APIPackage, file, service, user, cron job
CadenceOn 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

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

ModelHow it worksExamplesTrade-offs
PushA control node connects out (usually SSH/WinRM) and applies configAnsible, Salt (ssh mode)Simple, no agents to manage, great for orchestration and ordered rollouts; scaling requires care; nodes drift between runs
PullAn agent on each node periodically fetches config from a server and convergesPuppet (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

ToolLanguageModelAgent?Strengths
AnsibleYAML playbooks (+ Jinja2)Push over SSHAgentlessLowest barrier to entry, huge module collection, doubles as orchestration tool; also configures network devices
PuppetPuppet DSL (declarative)Pull (agent ↔ server)AgentMature model-driven approach, strong drift enforcement, good reporting/compliance at scale
ChefRuby DSL (“recipes”/“cookbooks”)Pull (chef-client ↔ server)AgentFull programming power, test-driven infra culture (Test Kitchen, InSpec)
SaltStackYAML states + PythonPull (minion) or push (salt-ssh); event bus (ZeroMQ)EitherVery 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

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:

ApproachHowBest whenCost
Runtime CM (Ansible/Puppet/Chef on live nodes)Configure machines after boot, re-converge over timeLong-lived VMs, legacy apps, heterogeneous fleets, network devices, compliance hardeningBoot-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 placeAutoscaling groups, uniform fleets, fast scale-out, strong reproducibilityImage build pipeline needed; config changes require a rebuild+rollout
ContainersImmutability at the app layer: Dockerfile builds the app environment; orchestrator schedules itMicroservices, dense packing, portable dev-prod parityHosts 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 modelModify the running node in placeBuild a new image, replace the node
DriftPossible; convergence fights itImpossible by construction
RollbackRe-converge to old state (fuzzy)Redeploy the previous image (exact)
Boot-to-readySlower (configure on boot)Fast (image is ready)
DebuggabilitySSH in and inspect a long-lived boxEphemeral; capture logs/metrics externally
FitDatabases, legacy, on-prem, appliancesStateless 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:

Never commit plaintext secrets, and never debug: a variable that holds one — it lands in logs.

Testing and validating configuration code

# 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

  1. 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.
  2. 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.
  3. 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.
  4. 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.
  5. 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.
  6. 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.
  7. Roll out serially with health checks. serial: 25% plus a health-check task and max_fail_percentage turns a config change into a rolling deploy with automatic stop-on-failure — a canary you already have.
  8. Prefer handlers over unconditional restarts. Restart services only when their config actually changed; restarting a healthy fleet on every run is self-inflicted downtime.
  9. 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.
  10. 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.
  11. 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.
  12. 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.
  13. 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.
  14. 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