IaC Quality

12-Factor IaC: A Practical Manifesto for Reliable Infrastructure Automation

April 20, 2026 11 min read Ansible · Terraform · K8s

In 2011, Heroku published The Twelve-Factor App. It framed what "good" looked like for deployable services at a moment when most teams were still shipping monolithic tarballs. Fifteen years later, Infrastructure-as-Code has arrived at the same maturity wall application code hit back then. Some teams ship playbooks that are version-controlled, tested, modular and documented. Others ship 800-line monoliths with hardcoded IPs, no tests, and a README that says "Ask Daniel."

This post proposes a practical manifesto — twelve factors for building reliable Infrastructure-as-Code — with concrete, auditable checks you can enforce in CI today. It applies to Ansible, Terraform, Kubernetes manifests and, honestly, any declarative automation.

Why now? IaC has quietly become the most privileged code in the organization. A broken web endpoint hurts one service; a broken Ansible role can brick a fleet. The gap between "my code runs" and "my code is trustworthy" is exactly what a twelve-factor approach closes.

Factor 1 — Version Control

The rule

Every playbook, role, module, manifest and inventory lives in git. No exceptions for "quick fixes" on the jumpbox.

Why: traceability, rollback, code review, provenance. Any IaC outside git is a ticking incident.

Factor 2 — Config Decoupled from Code

The rule

Variables live in group_vars/, host_vars/, .tfvars or external secret managers — never hardcoded into the task.

Why: the same role should run unchanged across staging and production. Hardcoded hosts make that impossible and invite environment drift.

anti-pattern
- hosts: 10.0.1.42
  tasks:
    - name: install nginx
      apt: name=nginx state=present
    - name: set worker count
      lineinfile:
        path: /etc/nginx/nginx.conf
        regexp: '^worker_processes'
        line: 'worker_processes 8;'
twelve-factor
- hosts: "{{ web_group }}"
  vars_files:
    - "group_vars/{{ env }}/web.yml"
  roles:
    - role: nginx
      nginx_worker_processes: "{{ nginx_workers }}"

Factor 3 — Multi-Environment Inventories

The rule

At minimum inventories/dev/, inventories/staging/, inventories/prod/. For Terraform: dev.tfvars, staging.tfvars, prod.tfvars or an environments/ tree.

Why: if you can't test a change in staging before prod, you are one keystroke from an incident.

Factor 4 — Idempotency

The rule

Running the playbook twice leaves the system in the same state as running it once. Every shell: or command: task has a creates:, removes:, when: or changed_when: guard.

Why: declarative automation loses all its value the moment reruns are unsafe. Non-idempotent tasks silently drift the infrastructure every cron execution.

anti-pattern
- name: install dependency from source
  shell: curl -L https://example.com/tool.tar.gz | tar xz && make install
twelve-factor
- name: install dependency from source
  shell: curl -L https://example.com/tool.tar.gz | tar xz && make install
  args:
    creates: /usr/local/bin/tool

Factor 5 — Role / Module Modularity

The rule

Break complex logic into reusable roles (Ansible) or modules (Terraform). A 200+ line monolithic playbook is a smell.

Why: reuse, testability, ownership. A role that only one team touches can evolve independently from a role a platform team manages.

Factor 6 — Dependency Management

The rule

Every external role, collection and provider is pinned in requirements.yml (Ansible) or versions.tf (Terraform).

Why: implicit dependencies are the slowest kind of bug to diagnose. "It works on my laptop" usually means "I have an older version of a collection the CI doesn't have."

Factor 7 — Logs and Visibility

The rule

Structured output, callback plugins for CI visibility, no_log: true only on tasks that genuinely contain secrets — not as a lazy way to silence a task.

Why: you cannot improve what you cannot see. Post-incident reviews die when the playbook ran silent.

Factor 8 — Reproducible, Disposable Executions

The rule

A run should succeed from a clean slate. Avoid dependencies on state left behind by previous runs. If a task relies on an earlier one, express it explicitly with when: predicates.

Why: disposable executions are the difference between automation and arcane knowledge. If only the original author can rerun it, it isn't automation.

Factor 9 — Secrets Separation

The rule

Secrets live in Ansible Vault, HashiCorp Vault, AWS Secrets Manager, Azure Key Vault or equivalent. Never in plain YAML. Never in git history.

Why: this one is not a stylistic preference. Plaintext secrets in git are the single most common root cause of credential theft.

Factor 10 — Declarative Simplicity

The rule

Prefer declarative modules over procedural shell:. Prefer a variable over a three-filter Jinja expression. When in doubt, break the logic out into a role variable or a custom filter plugin.

Why: clever automation is unmaintainable automation. Future-you will not remember what {{ x | default([]) | union(y) | difference(z) | sort }} does at 3 AM.

Factor 11 — Testing and Linting

The rule

Ansible roles have molecule/ scenarios. Playbooks run through ansible-lint. Terraform has .tflint.hcl and ideally terratest. Kubernetes manifests pass kubeval and policy checks.

Why: you don't let application code merge without tests. IaC is the code with the widest blast radius — it deserves more tests, not fewer.

Factor 12 — Documentation as Code

The rule

Every role and module has a README: purpose, variables (with defaults), dependencies, example usage.

Why: a role without a README is guess-work. A role with a good README is leverage — other teams can adopt it without a meeting.

Enforcing the manifesto

A manifesto is only as useful as what you can enforce. These twelve factors are all auditable — not all of them are easy to audit, but each one reduces to a concrete check a script or scanner can run:

Security Factor 365 ships all of these as an 11th scanner engine called the 12-Factor IaC Engine. Findings land in the same portal as your SAST and SCA results, tagged with Category = "12-Factor IaC" so you can track progress factor-by-factor.

The bigger claim

Reliable infrastructure is not an accident. It is the consequence of a team that decided ahead of time what good looks like and then baked the checks into CI. The twelve-factor framing gives you that shared vocabulary. The tools — ansible-lint, molecule, tflint, and a compliance engine that rolls up the results — let you enforce it.

If your team can only adopt three of the twelve today, pick: 9 Secrets Separation, 4 Idempotency, and 11 Testing. Those three alone will pay for themselves within a quarter.

Grade your IaC against the 12 factors

SF365's 12-Factor IaC Engine scans Ansible, Terraform and Kubernetes and grades every factor automatically. Bundled with the Full scan.

See the Engine