Terraform changes via merge request

How to change infrastructure safely with Terraform or OpenTofu, with plan in the MR, apply after approval, and routines for drift, state and imports.

Updated 2026-11-11Raw markdown

Strategy sections
On this page

Use this page for any infrastructure change. If a resource is not in code yet, the change starts by importing it. Clicking in the console is reserved for break-glass, and even then it is followed by a code change the same day.

The flow

flowchart LR
  B[Branch] --> C[Change .tf]
  C --> F[fmt + validate + lint]
  F --> M[Open MR]
  M --> P[CI or Atlantis runs plan, posts it]
  P --> R[Human reviews plan]
  R --> A[Apply from MR or after merge]
  A --> V[Verify, close ticket]

Tools that run plan and apply from the MR: Atlantis, Spacelift, Terraform Cloud or Enterprise, env0, or a plain CI pipeline. Pick one and use it for every stack.

Before opening the MR

terraform fmt -recursive
terraform init -backend=false && terraform validate
tflint --recursive          # optional linter
checkov -d . --quiet        # or trivy config . ; policy and security checks
  • One logical change per MR. Do not mix a refactor with a behaviour change.
  • MR description: what, why, blast radius, rollback, ticket link.

Read a plan like a reviewer

  • Count: Plan: X to add, Y to change, Z to destroy. Any destroy needs a sentence explaining it.
  • -/+ (replace) on stateful resources (databases, volumes, buckets, load balancers) is a red flag. Find out which attribute forces it.
  • Changes to IAM, security groups, KMS key policies and route tables get a second reviewer.
  • Unexpected changes mean drift. Stop and investigate before applying.

In Atlantis the comments are:

atlantis plan -p <project>
atlantis apply -p <project>

Handle drift

# Read-only drift check: exit code 2 means changes exist
terraform plan -detailed-exitcode -lock=false -refresh=true
Drift cause Fix
Someone changed it in the console Decide which is right; update code or re-apply code
Provider default changed Pin the attribute explicitly
Resource deleted outside Terraform Remove from state or recreate via MR
Resource never in code Import it

Schedule a nightly drift plan per stack and report non-zero results to the team; that is good automation for an agent (see automation blueprint).

Import existing resources

import {
  to = aws_s3_bucket.logs
  id = "<bucket-name>"
}
terraform plan -generate-config-out=generated.tf   # review and tidy the generated code

State hygiene

  • Remote state with locking (for example S3 with native lockfile or a DynamoDB table), encrypted, versioned, and access-limited.
  • One state per environment per component. Small states plan fast and fail small.
  • terraform state mv and state rm are changes too: do them in an MR using moved {} and removed {} blocks where possible.
  • Never edit a state file by hand. Never commit state to git: it often contains secrets.

Rollback

Revert the MR and apply the revert through the same flow. For destroys, rollback means re-creating, which may lose data: that is why destroys of stateful resources need backups and a second approval (see backups and restore testing).

Pitfalls

  • terraform apply from a laptop "just this once". It bypasses review and the lock may conflict with CI.
  • -target as a habit. It hides the rest of the plan and leaves drift.
  • prevent_destroy missing on databases and buckets that hold data.
  • Secrets in .tfvars committed to git. Read them from a secret manager data source instead.