GitLab CI/CD + Terraform Backend & MR Plan Workflow Prompt
Build GitLab-managed Terraform state, MR-driven `terraform plan` review, apply gating, and module versioning patterns.
- Target user
- DevOps engineers running Terraform via GitLab CI
- Difficulty
- Intermediate
- Tools
- Claude, ChatGPT
The prompt
You are a senior DevOps engineer who has built Terraform CI/CD pipelines in GitLab — managed state backend, MR-driven plans, apply gates, multi-environment workspaces.
I will provide:
- The Terraform layout (single root, multi-env, module repo)
- State backend (GitLab managed, S3, Terraform Cloud)
- Current pipeline (if exists)
- The goal: design new pipeline / improve existing / debug a failure
Your job:
1. **GitLab-managed Terraform state**:
- GitLab has a built-in HTTP backend at `https://gitlab.example.com/api/v4/projects/<id>/terraform/state/<name>`
- Auth via personal token, project token, or `$CI_JOB_TOKEN`
- Includes locking (built-in)
- Versioning, history, encrypted at rest
- Configure in Terraform with `terraform { backend "http" {} }`
2. **Pipeline shape (recommended)**:
```
validate → init → plan → (MR comment) → apply (manual)
```
- `validate` and `fmt -check` on every push
- `plan` on every MR; output saved as artifact
- GitLab integration: plan output posted as MR widget
- `apply` on default-branch merge, with `when: manual`
3. **For MR plan reporting**:
- Use `gitlab-terraform plan` (wrapper from GitLab) or run `terraform plan -out=tfplan` then `terraform show -json tfplan > plan.json`
- GitLab's terraform plan report integration: `artifacts:reports:terraform: plan.json`
- Renders a summary widget on the MR (add/change/destroy counts)
4. **For multi-environment**:
- Terraform workspaces OR directory-per-env
- GitLab CI environment-scoped variables for AWS creds
- Plan/apply jobs per environment with appropriate `rules:`
5. **For module versioning**:
- Modules in a separate project
- Pin via `git::https://gitlab.example.com/group/modules.git//module-name?ref=v1.2.0` (tag)
- CI: tag pipeline tags + publishes modules
- Consumer pipelines pin to a specific tag; bump via MR
6. **For state locking issues**:
- Built-in GitLab backend supports locking
- Stuck lock: API call to release
- Failed apply mid-state can leave lock; manual release required
7. **For drift detection**:
- Periodic `terraform plan` in scheduled pipeline; alert if drift
- Drift = state differs from real cloud → manual investigation
- Common causes: manual cloud-console changes, other tools modifying resources
8. **For dangerous-change protection**:
- Use `terraform plan` JSON to detect deletes
- `jq '.resource_changes[] | select(.change.actions | contains(["delete"]))'` → list deletes
- Require additional approval for deletes (see [terraform-dangerous-changes-review](/prompts/terraform-dangerous-changes-review/))
Mark DESTRUCTIVE: `terraform apply -auto-approve` in CI without review, `terraform destroy` jobs, state surgery (`terraform state rm` / `mv`) without backup, modifying shared backend config while applies are in flight.
---
Terraform layout: [single root / multi-env / module repo]
State backend: [GitLab managed / S3 / Terraform Cloud / Atlantis]
Current pipeline (if any):
```yaml
[PASTE]
```
Goal: [design / improve / debug]
Why this prompt works
GitLab + Terraform integration is well-documented but easy to misconfigure (state security, drift, dangerous changes). This prompt walks the modern pattern with MR plans + manual apply.
How to use it
- Use GitLab-managed state for simple cases; S3/Terraform Cloud for scale or cross-org.
- Plan on every MR; require review of the report widget.
- Apply manually on default branch — never auto.
- Pin module versions via tags.
Useful commands
# Configure GitLab-managed backend (in terraform code)
cat > backend.tf <<EOF
terraform {
backend "http" {}
}
EOF
# Init with GitLab backend (from CI)
TF_HTTP_ADDRESS="https://gitlab.example.com/api/v4/projects/${CI_PROJECT_ID}/terraform/state/${ENV}"
terraform init \
-backend-config="address=${TF_HTTP_ADDRESS}" \
-backend-config="lock_address=${TF_HTTP_ADDRESS}/lock" \
-backend-config="unlock_address=${TF_HTTP_ADDRESS}/lock" \
-backend-config="username=gitlab-ci-token" \
-backend-config="password=${CI_JOB_TOKEN}" \
-backend-config="lock_method=POST" \
-backend-config="unlock_method=DELETE" \
-backend-config="retry_wait_min=5"
# Force-unlock (only for stuck locks)
curl --request DELETE --header "PRIVATE-TOKEN: $TOKEN" \
"https://gitlab.example.com/api/v4/projects/<id>/terraform/state/<name>/lock"
# State versions list
curl --header "PRIVATE-TOKEN: $TOKEN" \
"https://gitlab.example.com/api/v4/projects/<id>/terraform/state/<name>/versions" | jq
# Generate plan JSON for GitLab MR widget
terraform plan -out=tfplan
terraform show -json tfplan | jq -r '
{
create: ([.resource_changes[] | select(.change.actions == ["create"])] | length),
update: ([.resource_changes[] | select(.change.actions == ["update"])] | length),
delete: ([.resource_changes[] | select(.change.actions == ["delete"])] | length)
}
' > plan.json
Pipeline pattern
stages: [validate, plan, apply, destroy]
variables:
TF_ROOT: ${CI_PROJECT_DIR}/terraform
TF_STATE_NAME: ${CI_ENVIRONMENT_SLUG}
TF_VERSION: 1.9.0
default:
image: registry.gitlab.com/gitlab-org/terraform-images/stable:latest
tags: [docker]
before_script:
- cd "${TF_ROOT}"
validate:
stage: validate
script:
- gitlab-terraform fmt -check
- gitlab-terraform validate
rules:
- if: $CI_PIPELINE_SOURCE == "merge_request_event"
- if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
plan:
stage: plan
script:
- gitlab-terraform plan
- gitlab-terraform plan-json
artifacts:
name: plan
paths:
- ${TF_ROOT}/plan.cache
reports:
terraform: ${TF_ROOT}/plan.json
expire_in: 1 week
environment:
name: production
action: prepare
rules:
- if: $CI_PIPELINE_SOURCE == "merge_request_event"
- if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
apply:
stage: apply
script:
- gitlab-terraform apply
resource_group: production-terraform # prevents concurrent applies
environment:
name: production
rules:
- if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
when: manual
needs: [plan]
Module repo pattern
# Module repo: tag-based releases
release-module:
stage: release
script:
- terraform fmt -check -recursive
- terraform validate
# publish via Git tag; consumers pin to tag
rules:
- if: $CI_COMMIT_TAG =~ /^v\d+\.\d+\.\d+$/
# In consumer's terraform code:
# module "vpc" {
# source = "git::https://gitlab.example.com/group/modules.git//vpc?ref=v1.2.0"
# }
Drift detection (scheduled pipeline)
# Add as scheduled pipeline (Schedule daily)
drift-check:
stage: plan
script:
- gitlab-terraform plan
- gitlab-terraform plan-json
- |
CHANGES=$(jq '[.resource_changes[] | select(.change.actions != ["no-op"])] | length' ${TF_ROOT}/plan.json)
if [ "$CHANGES" -gt 0 ]; then
echo "Drift detected: $CHANGES changes"
# send Slack notification
exit 1
fi
rules:
- if: $CI_PIPELINE_SOURCE == "schedule"
Dangerous-change detection (block deletes without approval)
detect-deletes:
stage: plan
needs: [plan]
script:
- |
DELETES=$(jq '[.resource_changes[] | select(.change.actions | contains(["delete"]))] | length' ${TF_ROOT}/plan.json)
if [ "$DELETES" -gt 0 ]; then
echo "Plan contains $DELETES delete operations"
echo "PROTECTED_DELETE=true" >> deploy.env
fi
artifacts:
reports:
dotenv: deploy.env
# Apply gated on delete count
apply-with-delete-approval:
stage: apply
script: gitlab-terraform apply
needs: [detect-deletes]
rules:
- if: $PROTECTED_DELETE == "true"
when: manual
allow_failure: false
Common findings this catches
- State stored in S3 without versioning → no recovery from accidental destroy.
- Concurrent applies on same state → lock contention, state corruption risk.
- Module pinned to
main→ silent breaking changes; pin to tag. -auto-approvein CI → no human review.- State locking ignored (locking disabled) → race conditions.
- Plan output not posted to MR → reviewers can’t see what will change.
- Workspace-based “isolation” between envs sharing state → not isolation.
When to escalate
- State corruption — engage Terraform-experienced engineer; restore from version history.
- Cross-org state migration (GitLab → S3 → Terraform Cloud) — staged plan; backup first.
- Compliance requiring detailed change audit — integrate with audit log or change-management system.
Related prompts
-
GitLab CD: Blue/Green, Canary & Rolling Deployment Patterns Prompt
Design GitLab CD pipelines implementing blue/green, canary, and rolling deployment strategies for Kubernetes, VM, and serverless targets.
-
Dangerous Terraform Changes Review Prompt
Scan a `terraform plan` output for changes that will silently destroy data, cause outages, or trigger irreversible mutations.
-
Terraform Module Review Prompt
Get a senior-engineer review of a Terraform module — variable hygiene, state safety, security defaults, drift resistance.