Skip to content

Infrastructure as code with AI agents works when the agent writes and plans the change but never applies it. Claude Code, Codex or Cursor draft Terraform, CloudFormation, CDK or Pulumi from current provider schemas; deterministic gates, a machine-read plan and scheduled drift checks prove the result; and CI applies it after a named owner approves the plan.

You inherit a Terraform repository with no documentation, the platform engineer who wrote it has left, and you need a read replica in a second region by Friday. An agent can read the modules and write the change in minutes. The risk is everything around it: a provider argument invented from stale training data, a rename that the plan turns into a destroy, or an agent with your admin profile running terraform apply because a prompt said “deploy it”.

This page is for developers who make infrastructure changes with an agent, and for tech leads who decide what the agent may touch.

What you get from an agent-driven IaC workflow

Section titled “What you get from an agent-driven IaC workflow”
  • A split of work in which the agent writes and plans, CI applies, and a person approves a plan rather than reading every line of HCL.
  • Guardrails for all three tools: a read-only cloud identity, repository instructions, and command rules that block apply and destroy.
  • MCP and skill setup that grounds the agent in the current Terraform Registry and AWS IaC documentation, with the package names that are wrong.
  • A gate stack per IaC tool (Terraform or OpenTofu, CloudFormation, CDK, Pulumi) that runs before anyone looks at the change.
  • Four copy-paste prompts: a constraint brief, a gated change, a plan summary for the reviewer, and a drift triage.
  • Recovery steps for state locks, provider-pin breakage, hidden replacements and drift.

Who does what: the agent plans, CI applies

Section titled “Who does what: the agent plans, CI applies”

The agent’s speed is only safe when the dangerous steps are out of its reach. Decide the split before the first prompt:

StepAgentCIPerson
Read the repo, look up provider schemasYes
Write HCL, templates or CDK/Pulumi codeYes
fmt, validate, lint, security scan, testsRuns them locallyRuns them again as the gate
plan / preview / synthLocally, with a read-only identityAuthoritative plan on the pull request
Summarise the plan and flag deletesYesFails the build on unapproved deletesChecks the summary against the plan
apply / up / deployNeverAfter approval, from the saved planApproves; owns the rollback
State surgery (force-unlock, state rm)Never without askingDecides

The person who approves is the owner of the affected workspace or stack, not the author of the prompt. That approval covers the plan (what will change in the cloud), which is a smaller and more honest artifact than the diff.

Set the guardrails before the first prompt

Section titled “Set the guardrails before the first prompt”
  1. Give the agent a read-only identity. Create a cloud role or profile that can read resources and state but not write them, and make it the only credential in the environment where the agent runs. No production admin profile, no long-lived deploy key on the machine. For AWS, a role with the ReadOnlyAccess managed policy plus access to the state bucket is enough to plan (with use_lockfile, a plan also writes a lock object next to the state). Cloud MCP servers with read-only identities covers the same setup for AWS, Azure and Google Cloud.

  2. Write the rules into the repository instructions. Put them in AGENTS.md for Codex, in CLAUDE.md for Claude Code (which falls back to AGENTS.md only when no CLAUDE.md exists, from v2.1.277, on both release channels), and in a project rule under .cursor/rules/ for Cursor (Cursor project rules covers the file format and frontmatter):

    ## Infrastructure rules
    - Never run terraform apply, destroy, import or force-unlock, pulumi up or destroy, or cdk deploy. CI applies.
    - Before writing a resource, look up its arguments for the provider version pinned in versions.tf.
    - Every change must pass: terraform fmt -check, terraform validate, tflint, trivy config, terraform test.
    - Renaming or moving a resource requires a moved block. A plan with an unexplained replace is a failed change.
    - No wildcard IAM actions, no 0.0.0.0/0 ingress, encryption at rest on every storage resource.
  3. Back the rules with command rules. Instructions can be ignored; command rules cannot. The rules differ per tool:

    In .claude/settings.json:

    {
    "permissions": {
    "deny": [
    "Bash(terraform apply)",
    "Bash(terraform apply *)",
    "Bash(terraform destroy *)",
    "Bash(terraform import *)",
    "Bash(terraform force-unlock *)",
    "Bash(tofu apply)",
    "Bash(tofu apply *)",
    "Bash(tofu destroy *)",
    "Bash(tofu import *)",
    "Bash(tofu force-unlock *)",
    "Bash(pulumi up)",
    "Bash(pulumi up *)",
    "Bash(pulumi destroy *)",
    "Bash(cdk deploy)",
    "Bash(cdk deploy *)",
    "Bash(cdk destroy *)",
    "Bash(npx cdk deploy)",
    "Bash(npx cdk deploy *)",
    "Bash(npx cdk destroy *)"
    ],
    "ask": [
    "Bash(terraform state rm *)",
    "Bash(terraform state mv *)",
    "Bash(terraform state push *)",
    "Bash(tofu state rm *)",
    "Bash(tofu state mv *)",
    "Bash(tofu state push *)"
    ]
    }
    }

    The list names both the bare command (terraform apply) and the form with arguments (terraform apply *), so the rule does not depend on whether a trailing wildcard also matches a command with no arguments. The ask rules make Claude Code stop and ask before state surgery, as the Codex prompt rule below does.

Ground the agent in current provider schemas

Section titled “Ground the agent in current provider schemas”

Models write provider arguments from training data, which lags the providers. The AWS provider for Terraform was on the 6.x line in September 2026, per its changelog; a model that emits ~> 5.0 is guessing. An MCP server that reads the registry, plus the vendor’s skills, fixes most of that.

StackGround it withWhat it adds
Terraform / OpenTofuHashiCorp Terraform MCP server (Docker image hashicorp/terraform-mcp-server)Registry lookups: provider versions, resource docs, modules. HCP Terraform workspaces with a token
Terraform styleHashiCorp skills (hashicorp/agent-skills)terraform-style-guide, terraform-test, refactor-module, terraform-search-import
CloudFormation / CDKawslabs.aws-iac-mcp-server (PyPI)cfn-lint validation, cfn-guard compliance checks, failed-stack troubleshooting, CDK docs and samples
Pulumi@pulumi/mcp-server (npm 0.2.0, last published 2025-09)Registry lookups, preview, refresh. It also exposes pulumi-cli-up and deploy-to-aws (which returns instructions that steer the agent into provisioning AWS resources; both checked in the 0.2.0 package), and shell command rules catch neither: deny the tools themselves (in Claude Code, add mcp__pulumi__pulumi-cli-up and mcp__pulumi__deploy-to-aws to permissions.deny, assuming you registered the server as pulumi, for example with claude mcp add pulumi -- npx @pulumi/mcp-server@0.2.0 stdio; the prefix is mcp__<server-name>__) or leave the server unregistered, and rely on the read-only identity, which blocks it either way
Live AWS readsAgent Toolkit for AWSAccount reads through the managed AWS MCP Server

Register the two servers most IaC work needs:

Terminal window
# Terminal, repo root. Registry-only Terraform server, pinned image
claude mcp add terraform -s project -- docker run -i --rm hashicorp/terraform-mcp-server:1.3.0
# CloudFormation and CDK validation; credentials from the read-only profile
claude mcp add aws-iac -s project -e AWS_PROFILE=agent-readonly -- uvx awslabs.aws-iac-mcp-server@latest
claude mcp list

Add HashiCorp’s skills with claude plugin marketplace add hashicorp/agent-skills then claude plugin install terraform@hashicorp in Claude Code, or one at a time with npx skills add hashicorp/agent-skills/plugins/terraform/skills/terraform-style-guide elsewhere. The HCP Terraform token setup, tool allowlists and the context cost of each server are on infrastructure MCP: Docker, Kubernetes and Terraform.

Brief the change as constraints, not resources

Section titled “Brief the change as constraints, not resources”

“Create an EKS cluster with three nodes” gets a generic cluster. Describe the business constraints and ask the agent to name its assumptions before it writes code. This prompt works the same in all three tools.

Item 4 is the one that saves the week. It surfaces where the agent is guessing (is the source database encrypted with a key that can be shared cross-region? is the budget per month or per environment?) while correcting course costs one message. Item 5 forces the agent to think about the blast radius before it touches anything.

Let the agent write the change and run every deterministic check until they pass. The exit codes decide; the agent’s report of them does not.

StackGates the agent runs and CI repeats
Terraformterraform fmt -check -recursive · terraform validate · tflint --recursive · trivy config . or checkov -d . · terraform test
OpenTofuThe same with tofu in place of terraform
CloudFormationcfn-lint template.yaml · cfn-guard validate -d template.yaml -r rules/
CDKcdk synth with cdk-nag registered, so security findings fail the synth
Pulumipulumi preview --policy-pack ./policy with a CrossGuard policy pack

For CDK, cdk-nag 3.x registers as a validation plugin: Validations.of(app).addPlugins(new AwsSolutionsChecks(app)); in the app entry point.

A grounded Terraform root config pins what models most often get wrong:

terraform {
required_version = ">= 1.11"
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 6.0"
}
}
backend "s3" {
bucket = "acme-tf-state-prod"
key = "platform/terraform.tfstate"
region = "us-east-1"
encrypt = true
use_lockfile = true # S3-native locking; a new project does not need a DynamoDB lock table
}
}

Security scanners over-flag (a WAF demanded on an internal load balancer) and the agent under-flags (a wildcard IAM action it wrote itself). Keep the scanner as the gate, and handle each suppression as a reviewed line in the scanner’s config with a reason, never as a silent skip the agent adds to make the build green.

The reviewer does not read the HCL line by line. The reviewer reads the plan, which says what will happen in the cloud, and CI makes sure the plan contains no surprise the reviewer did not approve.

  1. Save the plan as JSON in CI. The job runs with a plan-only role that reaches nothing beyond reading state and resources and writing the lock object:

    Terminal window
    terraform plan -out tfplan
    terraform show -json tfplan > plan.json

    A plan executes code from the pull request: provider binaries, data "external" programs and module sources. So run the credentialed plan job only for branches in the same repository, on the pull_request trigger, which gives forks no secrets. Never use pull_request_target to check out the pull request head. Plan a contributor’s fork only after a maintainer has read the change and pushed it to a branch in this repository, or not at all: a label that starts a credentialed plan of the fork’s head runs the fork’s code with the plan role.

  2. Fail the build on any delete or replace that is not declared. Replacements show up as a delete plus a create in resource_changes:

    Terminal window
    jq -r '.resource_changes[] | select(any(.change.actions[]; . == "delete")) | .address' plan.json \
    > deletes.txt
    if [ -s deletes.txt ] && ! grep -q '^allow-delete:' pr-body.txt; then
    echo "Plan deletes or replaces:"; cat deletes.txt; exit 1
    fi

    pr-body.txt is the pull request description, written to a file by an earlier step. The author opts in with an allow-delete: line that names the addresses and the reason. When an agent opened the pull request, it wrote that description too, so the line is a declaration, not a control: the check only makes a delete visible, and the workspace owner’s required approval of the plan is what stops it. The owner compares every address in deletes.txt with the allow-delete: line before approving. For richer rules (tags, instance sizes, regions) run a policy engine such as Conftest over the same plan.json.

  3. Have the agent summarise the plan for the reviewer. The summary is a map, not the evidence: the reviewer checks it against the plan output.

  4. The workspace owner approves, CI applies the saved plan. CI runs terraform apply tfplan from the same plan file, so what was approved is what runs. Rollback is a revert of the pull request followed by the same pipeline; state operations stay with the owner.

This is the infrastructure version of the evidence bundle an agent’s pull request must carry: gate exit codes, the saved plan, the delete check and the summary. How a reviewer triages that bundle without reading every line is on reviewing an agent’s pull request, and wiring the jobs into a pipeline is on CI/CD pipelines with AI.

Detect drift on a schedule and triage it with the agent

Section titled “Detect drift on a schedule and triage it with the agent”

Drift (a console hotfix, a manual security-group rule) is where infrastructure code quietly stops being the truth. Detect it with the IaC tool itself on a schedule, then give the agent the output to triage. The detection is deterministic; the agent only ranks and explains.

Terminal window
# Exit code 2 means the real infrastructure differs from state
terraform plan -refresh-only -detailed-exitcode -out drift.tfplan
terraform show -json drift.tfplan > drift.json

Before you act on a finding, confirm two things the agent cannot know: that the “expected” value matches the code on the default branch, not an old plan, and that the manual change was not an undocumented fix someone made during an incident. Ask the stack owner; then the chosen remediation goes through the same pull request and plan review as any other change.

What breaks when agents write infrastructure code?

Section titled “What breaks when agents write infrastructure code?”

terraform apply in CI hangs on “Acquiring state lock”. Two runs overlapped, or an earlier run crashed mid-apply. Read the lock holder and time from the error first. Run terraform force-unlock LOCK_ID only after you confirm that run is dead; unlocking a live apply can corrupt state. Stop the overlap with a concurrency group on the apply job.

The plan replaces a resource nobody meant to touch. The agent renamed a resource or moved it into a module, so Terraform sees a delete and a create. The delete check in CI catches it. Ask the agent to add a moved block from the old address to the new one and re-plan until the replace disappears.

terraform init fails after the agent changed the provider pin. The lock file still pins the old major version. Do not let the agent bump a provider major version in the same change as a feature; do it in its own pull request after reading the provider’s upgrade guide. Run terraform init -upgrade to update .terraform.lock.hcl, then terraform providers lock -platform=linux_amd64 -platform=darwin_arm64 so CI and laptops share the same hashes.

terraform validate rejects an argument the agent wrote. The agent skipped the registry lookup or read the latest docs against an older pinned provider. Make the lookup a separate turn and name the pinned version in the prompt, as in the constraint brief above.

The gates pass but the change is permissive. Generated IAM policies drift toward wildcard actions and broad principals, which many scanners only warn about. Add a policy rule that fails the plan on "Action": "*" and on 0.0.0.0/0 ingress, and keep security-sensitive paths (IAM, networking, KMS) on a list that always needs the owner’s approval.

The MCP server returns nothing, or stale data. Check the package first (see the caution box above: the npm terraform-mcp-server and the deprecated AWS Labs servers are common culprits), then the credentials: AWS_PROFILE and region in the server’s env, and the profile’s read permissions. claude mcp list or codex mcp list shows whether the server connected. More in MCP connection issues.

A blanket drift revert removes a real fix. Someone opened a port during an incident and the revert closes it again. Never revert drift on a schedule without triage; route each finding through option (c) of the drift prompt when the change is recent.

The agent follows an instruction hidden in plan output or a resource tag. Plan output, tags and descriptions are data that reach the model. Keep the agent read-only while it reads them, and keep write-capable servers out of the same session. See MCP security.

Where to go next with infrastructure as code

Section titled “Where to go next with infrastructure as code”