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.
- 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;'
- 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.
- name: install dependency from source shell: curl -L https://example.com/tool.tar.gz | tar xz && make install
- 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:
- 1 Version control — does
.git/exist in the tree? - 2 Config decoupled — scan playbooks for hardcoded IPs and hosts.
- 3 Multi-env — count env-labelled subdirectories under
inventories/or*.tfvars. - 4 Idempotency — flag
shell:/command:tasks without a guard keyword. - 5 Modularity — flag playbooks >200 lines without
roles/. - 6 Deps — check for
requirements.ymlorversions.tf. - 9 Secrets — detect plaintext credential assignments outside vaulted files.
- 10 Simplicity — flag Jinja expressions with 3+ pipe filters.
- 11 Testing — check for
molecule/,.ansible-lint,.tflint.hcl. - 12 Docs — every
roles/*has a README.
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