Skip to content

Infrastructure as code with Cursor

Cursor’s Agent writes Terraform modules, Dockerfiles and Kubernetes manifests well when a project rule sets the conventions and read-only MCP servers show it the real provider schema and cloud account. A gate, not a read-through of the HCL, proves an infrastructure change: format, validate, a checkov scan, and a plan summary that flags every delete.

This page is for the developer who owns a service’s infrastructure and the tech lead who approves changes to it. You asked Agent for an ECS service behind a load balancer, and it wrote 300 lines of Terraform that validate cleanly. The plan says 4 to add, 1 to change, 1 to destroy, and the one being destroyed is your production database, because Agent renamed it to match your naming convention.

Set up project rules and MCP in Cursor first; this page builds on both.

What a Cursor infrastructure workflow gives you

Section titled “What a Cursor infrastructure workflow gives you”
  • A project rule that fixes your cloud, naming, tagging and security baseline, and forbids Agent from applying anything.
  • Terraform and AWS MCP servers connected read-only, so Agent looks up real provider arguments and existing resources instead of guessing.
  • Three copy-paste prompts: a Terraform module, a production Dockerfile, and Kustomize manifests.
  • One gate script that a reviewer trusts without reading every line: fmt, validate, checkov on the code, for secrets and on the plan, and a hard stop on any delete or replace.
  • A sign-off path in which CI, not Agent, runs terraform apply.

Why infrastructure code needs a different gate from application code

Section titled “Why infrastructure code needs a different gate from application code”

Application code has tests that fail when behaviour is wrong. Infrastructure code has no unit tests in most repositories, and a module that validates can still open port 22 to the internet or replace a stateful resource. The evidence for an infrastructure change is therefore the plan and a policy scan, not the diff.

ArtifactGate that goes redWhat it catches
Terraformterraform fmt -check, terraform validateSyntax errors, invented arguments, missing variables
Terraformcheckov --framework terraform,secretsOpen security groups, unencrypted storage, IAM wildcards, secret values in files
Terraform plancheckov --framework terraform_plan and a jq summary of terraform show -jsonDestroys, replacements, and policy violations in computed values
Dockerfilecheckov --framework dockerfile, docker build, the image’s configured userRoot user, missing HEALTHCHECK, a build that only works on your laptop
Kuberneteskubectl kustomize, checkov --framework kubernetes, kubectl apply --dry-run=serverMissing limits and probes, privileged pods, manifests the API server rejects

checkov covers all three formats with one tool. The flags on this page were checked against checkov 3.3.21 from PyPI; it exits 1 when any check fails, so it works as a CI gate as-is.

  1. Write an infrastructure rule. Save it as .cursor/rules/infrastructure.mdc. The globs load it only when Agent touches Terraform, a Dockerfile or the Kubernetes manifests, so application work does not spend context on it. The frontmatter follows the file-scoped example in project rules; run that page’s canary check if the rule does not seem to load.

    ---
    globs: infrastructure/**/*.tf, **/Dockerfile, k8s/**
    alwaysApply: false
    ---
    Cloud: AWS, regions eu-west-1 (primary) and us-east-1 (CloudFront, ACM for CloudFront).
    Layout: reusable modules in infrastructure/modules/, one root per environment in
    infrastructure/environments/{staging,production}/. Pin versions in versions.tf; copy the
    existing pins, never invent a newer one.
    Naming: {env}-{service}-{resource}, for example prod-api-alb.
    Tags on every resource: Environment, Service, Team, ManagedBy=terraform (use default_tags).
    Security baseline:
    - No 0.0.0.0/0 ingress except the public ALB on 443.
    - Encryption at rest on every RDS instance, S3 bucket and EBS volume.
    - IMDSv2 required on EC2 and launch templates.
    - No secret values in any file. Reference Secrets Manager ARNs or variables only.
    - IAM policies name actions and resources; no "*" in either without a comment saying why.
    Before you change existing code:
    - Never rename a resource address or a name argument on a stateful resource (RDS, S3,
    EFS, DynamoDB). If a rename is required, add a moved block and say so.
    - Use the Terraform MCP server for provider arguments and the AWS MCP server to list
    what already exists. Never create a resource the account already has; use an import block.
    Done means AWS_PROFILE=agent-readonly scripts/iac-check.sh <environment dir> exits 0;
    paste its last 20 lines.
    Never run terraform apply, terraform destroy, terraform import, terraform state, or any
    kubectl apply/delete, including --dry-run=server. Stop after the plan and show the summary.
  2. Connect Terraform and AWS MCP servers read-only. Add both to .cursor/mcp.json. The Terraform server with no token exposes only public Terraform Registry lookups. The AWS API server with READ_OPERATIONS_ONLY refuses any AWS operation whose IAM access level is Write.

    {
    "mcpServers": {
    "terraform": {
    "command": "docker",
    "args": ["run", "-i", "--rm", "hashicorp/terraform-mcp-server:1.3.0"]
    },
    "aws-api": {
    "command": "uvx",
    "args": ["awslabs.aws-api-mcp-server@1.5.6"],
    "env": {
    "AWS_REGION": "eu-west-1",
    "AWS_API_MCP_PROFILE_NAME": "agent-readonly",
    "READ_OPERATIONS_ONLY": "true"
    }
    }
    }
    }

    The agent-readonly profile must map to an IAM role that can only read, such as one with the AWS-managed ReadOnlyAccess policy. The aws-api-mcp-server README warns that even a ReadOnlyAccess role can return AWS credentials or sensitive information in command outputs, and those outputs land in the agent’s context. Prefer ViewOnlyAccess or a scoped custom read policy that leaves out secret-bearing reads such as secretsmanager:GetSecretValue and Lambda environment configuration. The README also calls IAM “the primary and most reliable security control” and READ_OPERATIONS_ONLY an extra layer. Versions checked on 2026-09-26: hashicorp/terraform-mcp-server 1.3.0 on Docker Hub, awslabs.aws-api-mcp-server 1.5.6 on PyPI. For other infrastructure servers, see the infrastructure MCP guide.

  3. Keep apply out of reach. Cursor documents terminal control on its Run modes page. Keep terraform apply, terraform destroy, kubectl apply and kubectl delete off any allowlist, and never use unattended execution on a machine with cloud credentials. To block them in code rather than by habit, add the hooks-based guard from securing Cursor for a team. That guard blocks network commands, git push and secret files, not infrastructure writes, so add this line to its shell branch and these cases to its CI test. The pattern also blocks kubectl apply --dry-run=server, because the rule keeps even the dry run with a human or CI. The pattern catches the direct spellings, not a wrapper such as make apply; the control that holds is that no credentials in the session can write (step 4 below, and step 4 of the sign-off list). The same applies to the gate: Agent’s terminal inherits whatever cloud credentials your shell already has, often an admin profile, so run the gate as AWS_PROFILE=agent-readonly scripts/iac-check.sh ….

    // .cursor/hooks/guard.mjs, inside the `mode === 'shell'` branch, before the final allow
    if (/\bterraform\b[^;&|\n]*\s(apply|destroy|import|state)\b|\bkubectl\b[^;&|\n]*\s(apply|delete)\b/.test(cmd))
    deny(`Infrastructure write blocked by repo policy: ${cmd}`);
    scripts/check-cursor-guard.sh
    expect shell '{"command":"terraform apply"}' deny
    expect shell '{"command":"terraform -chdir=envs/prod destroy"}' deny
    expect shell '{"command":"kubectl apply --dry-run=server -f x.yaml"}' deny
    expect shell '{"command":"terraform plan -out=tfplan"}' allow
  4. Prove the setup before you trust it. Test the role directly, outside the agent. The first command must show the read-only role. The second must return AccessDenied.

    Terminal window
    aws sts get-caller-identity --profile agent-readonly
    aws s3api create-bucket --bucket agent-write-test-7f3a \
    --create-bucket-configuration LocationConstraint=eu-west-1 --profile agent-readonly

    If the bucket is created, the role can write. Fix the IAM role before you continue.

    Then ask Agent two questions in a new chat. The first answer must cite a real MCP tool call. The second must be refused.

    Using the aws-api MCP server, which AWS account and region are you connected to? List the VPC IDs.
    Use only the aws-api MCP server; do not run any terminal command. Create an S3 bucket named agent-write-test-7f3a.

    An MCP refusal proves only that READ_OPERATIONS_ONLY is set: the server blocks the call locally, so the request never reaches AWS and IAM is never tested. Only the direct CLI probe tests the role.

Start in Plan Mode, which Cursor describes as creating “detailed implementation plans before writing any code”. An infrastructure plan is cheap to correct; 300 lines of HCL are not.

Read the plan Agent proposes for three things only: that it reuses existing resources instead of creating new ones, that the task role grants nothing beyond the secrets path, and that nothing stateful is touched. Everything else, the gate checks.

When terraform validate or plan fails, paste the full error back into the same chat. An error such as Reference to undeclared resource usually means Agent referenced a resource that lives in another module. The fix is an output in that module and an input variable here, not a copy of the resource.

Prove a Terraform change without reading every line

Section titled “Prove a Terraform change without reading every line”

Commit this script as scripts/iac-check.sh. Agent runs it before it reports done, and CI runs the same file on every pull request, so both see the same verdict.

#!/usr/bin/env bash
# Usage: scripts/iac-check.sh infrastructure/environments/staging
# Needs terraform, checkov and jq. Run it only with read-only cloud credentials.
set -euo pipefail
dir="${1:?usage: iac-check.sh <environment dir>}"
terraform fmt -check -recursive infrastructure/
terraform -chdir="$dir" init -input=false
terraform -chdir="$dir" validate
checkov -d infrastructure/ --framework terraform,secrets --compact --quiet
# -lock=false: planning with a read-only role cannot take the state lock
terraform -chdir="$dir" plan -input=false -lock=false -out=tfplan
terraform -chdir="$dir" show -json tfplan > "$dir/tfplan.json"
checkov -f "$dir/tfplan.json" --framework terraform_plan --compact --quiet
echo "Planned actions:"
jq '[(.resource_changes // [])[].change.actions | join("+")] | group_by(.) | map({(.[0]): length}) | add' "$dir/tfplan.json"
echo "Deletes and replacements:"
jq -r '(.resource_changes // [])[] | select(.change.actions | index("delete")) | "\(.change.actions | join("+"))\t\(.address)"' "$dir/tfplan.json"
if [ "${ALLOW_DESTROY:-0}" != "1" ]; then
jq -e '[(.resource_changes // [])[] | select(.change.actions | index("delete"))] | length == 0' "$dir/tfplan.json" > /dev/null \
|| { echo "Plan deletes or replaces resources. Re-run with ALLOW_DESTROY=1 only after a human approves the list above."; exit 1; }
fi

Run the gate with the read-only profile, for example AWS_PROFILE=agent-readonly scripts/iac-check.sh infrastructure/environments/staging, so Agent’s terminal never plans with write credentials. CI sets its own read-only role instead, as the sign-off section shows.

Add tfplan and tfplan.json to .gitignore: a plan file can hold sensitive values in plain text.

The script turns a large diff into a short list a human can judge in a minute:

A human readsThe gate handles
The action counts (create, update, delete+create) and whether they match the requestFormatting, syntax and undeclared references
Every line under “Deletes and replacements”Security-group, encryption, IMDSv2 and IAM-wildcard policies
IAM policy changes, because checkov confirms a policy is narrow, not that it is the right oneValues computed only at plan time, checked by the terraform_plan scan

A delete+create on a database, bucket or file system is a stop. Ask Agent for a moved block or to revert the rename, and run the gate again. For how this summary joins test results and scans in one pull-request manifest, see the evidence bundle.

Build containers and Kubernetes manifests with the same gate

Section titled “Build containers and Kubernetes manifests with the same gate”

The pattern repeats: a specific prompt, then a check that goes red on the defects Agent tends to produce. Agent’s usual Dockerfile defects are a root user, a missing health check, and dev dependencies copied into the final image.

For Kubernetes, give Agent the Dockerfile and the Terraform module as context so ports, health paths and resource sizes agree across all three layers.

ExternalSecret needs External Secrets Operator installed in the cluster. Without it, the server-side dry run rejects the kind.

The last check runs against a staging cluster: kubectl apply --dry-run=server -f /tmp/staging.yaml. The API server validates the objects, runs admission webhooks and persists nothing. It needs the same RBAC permissions as a real apply, so run it yourself or from CI rather than giving Agent those credentials. For deeper Docker and cluster patterns, see containerization with AI agents and the Kubernetes recipes.

Who signs off on an infrastructure change?

Section titled “Who signs off on an infrastructure change?”

Agent writes and checks; people and CI decide what reaches the cloud.

  1. The author runs Agent, reads the plan summary and the delete list, and opens a pull request with both pasted in the description.
  2. CI runs scripts/iac-check.sh for each changed environment with a read-only role and posts the plan summary on the pull request. A red gate blocks merge. terraform plan executes provider plugins and any external data source from the branch, so run this job only for branches in your own repository, never for forks, and give it a short-lived read-only role through OIDC. The CI/CD pipelines with Cursor page shows how to write and prove that workflow.
  3. A code owner for infrastructure/, named in CODEOWNERS, approves. Their review covers the summary, the deletes and IAM changes, not every line. Bugbot can add a second automated review; PR Routing & Approval can route infrastructure paths to the right owner.
  4. CI applies after merge, from a protected environment that holds the only write credentials. Nobody’s laptop, and no Agent session, holds them.

What breaks when Cursor writes infrastructure?

Section titled “What breaks when Cursor writes infrastructure?”

Agent invents an argument or a provider version. terraform validate fails with Unsupported argument, or init cannot find the version. Recovery: confirm the Terraform MCP server shows as connected in Cursor’s MCP settings, then ask Agent to look the resource up there and quote the argument list before it edits. The rule’s “copy the existing pins” line stops version drift.

The plan replaces a stateful resource. The gate stops with delete+create next to a database or bucket. The usual cause is a renamed resource address or a changed name argument, which forces replacement. Recovery: revert the rename, or add a moved block for an address change. Add lifecycle { prevent_destroy = true } to the resource so Terraform itself refuses the next attempt.

Agent creates something that already exists. Apply fails with already exists, or the plan creates a second VPC. Recovery: ask Agent to list the existing resource through the AWS MCP server and write an import block for it. Review the plan until the resource shows as imported, with no create.

checkov fails on a deliberate exception. A bucket served directly as an S3 website, with no CloudFront in front, needs a public-read ACL and fails CKV_AWS_20. Recovery: skip that one check on that one resource with a reason. Put the comment in the resource that checkov names on the FAILED line. For CKV_AWS_20 that is the aws_s3_bucket block, not aws_s3_bucket_acl. Add #checkov:skip=CKV_AWS_20:Public S3 website endpoint, no CloudFront inside that block and have the code owner approve the comment. Never add a global --skip-check to the script, because it silences the check for every future resource.

A secret lands in a file. Agent pasted a real value into terraform.tfvars or a manifest. Recovery: treat the value as leaked and rotate it, even if the commit never left your machine. Replace it with a Secrets Manager reference. Then confirm that the gate’s secrets scan fails on a scratch copy of the file that still holds the old value. If it passes, checkov does not recognise that secret’s format, and the gate alone will not catch the next one.

The MCP profile can write. The direct CLI probe in setup step 4 succeeded and created the bucket. Recovery: delete the bucket, then attach a read-only policy to the profile’s role and remove every other policy. READ_OPERATIONS_ONLY is not a substitute for a read-only role.

Staging and production drift apart. A module gains a variable, and only one environment sets it. Recovery: run the gate for every environment directory, not only the one you changed. Ask Agent to add the variable to each terraform.tfvars with a comment that explains any difference between them.

Where to go next with infrastructure in Cursor

Section titled “Where to go next with infrastructure in Cursor”