Code as infrastructure: one reviewed path for every production change
Infrastructure as code solved half the problem: servers and networks became files. The other half, the policies, pipelines and now the permissions handed to AI coding agents, often still lives in consoles, tickets and people's heads. Treat all of it as code and you get one reviewed, tested, repeatable path for every change to production, whoever or whatever proposes it.
What "code as infrastructure" means
Infrastructure as code (IaC) means describing servers, networks and managed services in files that a tool such as Terraform applies. "Code as infrastructure" turns the idea around: the code in your repositories is the infrastructure's control system. The desired state of the platform, the rules it must obey, the pipeline that changes it and the access granted to automated agents are all versioned, reviewable artefacts.
The OpenGitOps project describes the operating model in four principles [1]:
- Declarative: the desired state is expressed declaratively.
- Versioned and immutable: desired state is stored with a complete version history.
- Pulled automatically: software agents pull the desired state from the source.
- Continuously reconciled: agents observe the actual state and try to apply the desired state.
Most teams adopt the first two and stop. The value comes from all four, plus two additions this post covers: policy that runs as code before anything is applied, and agent permissions that are declared in the repository rather than granted ad hoc.
Why it matters more now that agents write code
Coding agents have made writing code cheap. Simon Willison's Agentic Engineering Patterns guide opens its principles with chapters titled "Writing code is cheap now" and "AI should help us produce better code" [2]. When code is cheap, the scarce resources are review and verification. A team that relies on a senior engineer eyeballing a console change cannot absorb many more proposed changes. A team whose changes all flow through plan, policy and review can, because most of the checking is automated and the human reviews a small, well-presented diff.
Agents also need access to systems, and that access is standardising. The Model Context Protocol (MCP) is an open protocol for connecting AI applications to tools and data. When Anthropic donated MCP to the new Agentic AI Foundation, a directed fund under the Linux Foundation co-founded with Block and OpenAI, it reported more than 10,000 active public MCP servers and adoption in ChatGPT, Cursor, Gemini, Microsoft Copilot and Visual Studio Code [3]. More connections mean more credentials. The MCP security best-practices document is explicit about the risks: it forbids "token passthrough", where a server forwards tokens not issued to it, and recommends a minimal initial scope set with elevation only when a privileged operation is attempted [4]. Those are exactly the kind of decisions that belong in a reviewed file, not a chat window.
Before and after
| Concern | Ticket and console operations | Code as infrastructure |
|---|---|---|
| Making a change | Ticket, then someone clicks in a console | Pull request against desired-state files |
| Knowing the impact | Experience and hope | A saved plan showing every create, update and delete |
| Enforcing rules | A wiki page and a reviewer's memory | Policy files that fail the build |
| Audit trail | Ticket comments, console logs | Git history, plan artefacts, approvals |
| Out-of-band changes | Discovered during the next incident | Detected by a scheduled plan |
| Agent access | Personal tokens pasted into tools | Declared, scoped, reviewed configuration |
The playbook
The steps below use Terraform and Open Policy Agent (OPA) as examples. The same shape works with OpenTofu, Pulumi, Kubernetes manifests and Argo CD or Flux.
- List every way production changes today. Consoles, scripts on laptops, CI jobs, vendor dashboards, agents with tokens. Each one is either moved into the loop or written down as an exception with an owner.
- Put desired state in Git. Infrastructure in
infra/, workload manifests ink8s/, policy inpolicy/, pipelines in.github/workflows/. - Plan in CI and save the plan.
terraform plan -out=FILEwrites a plan thatterraform applycan execute later. HashiCorp describes this two-step flow as intended for running Terraform in automation, and warns that saved plans can contain sensitive values in cleartext, so store them as protected artefacts [5]. - Check the plan against policy. Convert it to JSON with
terraform show -json, whoseresource_changesarray lists each resource's address, type and planned actions [6], then test it with Conftest, which runs Rego policies against structured configuration [7]. OPA's own Terraform tutorial uses the same plan-to-JSON approach [8]. - Review the plan, not just the code. Post the plan summary on the pull request. A reviewer reading "1 to destroy" catches what a reviewer reading HCL might miss.
- Apply the saved plan from automation only. Humans approve; the pipeline applies. Remove standing write access from people where you can.
- Detect drift on a schedule. Run a plan against the main branch nightly. Changes found there were made outside the loop.
- Declare agent access as code. Keep the list of tools, servers and scopes each agent may use in the repository, start read-only, and require review to widen it, in line with MCP's scope-minimisation guidance [4].
Plan, convert, test
terraform init -input=false
terraform plan -input=false -out=tfplan.binary
terraform show -json tfplan.binary > tfplan.json
conftest test tfplan.json --policy policy/
A policy that blocks accidental database deletion
Conftest looks for policies in a policy directory by default and treats any message in a deny set as a failure [7]. This rule fails the build if the plan would delete an AWS RDS instance, including a replacement, whose actions are ["delete", "create"] or ["create", "delete"] [6].
package main
deny contains msg if {
some rc in input.resource_changes
rc.type == "aws_db_instance"
"delete" in rc.change.actions
msg := sprintf("%s would be deleted or replaced; needs an approved exception", [rc.address])
}
A nightly drift check
With -detailed-exitcode, terraform plan exits 0 when there are no changes, 1 on error and 2 when changes are present [5]. Run it against the main branch after the last apply, and a 2 means something changed outside the loop.
#!/usr/bin/env bash
set -uo pipefail
terraform plan -input=false -no-color -detailed-exitcode > drift.txt
rc=$?
case "$rc" in
0) echo "No drift." ;;
2) echo "Drift detected; see drift.txt"; exit 2 ;;
*) echo "Plan failed"; exit 1 ;;
esac
When drift is legitimate, for example an emergency fix during an incident, bring the code up to date with a pull request. Terraform's -refresh-only mode exists for the related case of updating state to match changes made outside the normal workflow [5].
Checklist
- Every production environment has its desired state in Git, and the repository is the documented source of truth.
- CI produces a saved plan for every pull request and stores it as a protected artefact.
- At least one policy runs against the plan and can fail the build.
- Applies run from the pipeline using the saved plan, not from laptops.
- A scheduled drift check runs against main and alerts a named owner.
- Agent tool access is declared in the repository, read-only by default, and widened only through review.
- Exceptions (manual changes, break-glass access) are logged and followed by a pull request.
Common mistakes
- Applying a fresh plan instead of the reviewed one. If the pipeline re-plans at apply time, the reviewer approved something different from what ran.
- Policy as documentation. A rule that cannot fail a build is a suggestion.
- Treating all drift as an error. Some drift is an emergency fix. The goal is to reconcile it into code quickly, not to punish it.
- Giving agents personal tokens. Use identities issued for the agent, with narrow scopes, so actions are attributable and revocable. MCP's guidance on token passthrough and scope minimisation explains why [4].
- Leaking secrets through plans. Saved plans can contain sensitive values in cleartext [5]. Restrict who can download them and expire them.
- Trying to move everything at once. Start with the system that hurts most when it breaks.
What to do this week
- Write down every path by which production changed in the last month, including agent and vendor access.
- Add
terraform plan -outandterraform show -jsonto one pipeline and publish the plan summary on pull requests. - Write one Rego rule for your most expensive mistake, such as deleting a database or opening a security group to the internet, and run it with Conftest.
- Schedule a nightly
-detailed-exitcodeplan for one environment and route exit code 2 to an owner. - Move one agent's tool configuration into the repository with read-only scopes.
Getting the loop right takes some up-front work, but it is what lets a small platform team keep pace with engineers and agents proposing far more changes than before. If you want help running that loop day to day, including drift checks and change review, see our SysOps 24/7 service.
References
- OpenGitOps: GitOps principles
- Simon Willison, Agentic Engineering Patterns
- Anthropic, Donating the Model Context Protocol and establishing the Agentic AI Foundation (9 December 2025)
- Model Context Protocol: Security Best Practices
- HashiCorp, Terraform CLI: terraform plan
- HashiCorp, Terraform JSON output format
- Conftest documentation
- Open Policy Agent: Terraform