Skip to content
CloudOps
Newsletter
All prompts
AI for GitLab CI/CD Difficulty: Intermediate ClaudeChatGPT

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

  1. Use GitLab-managed state for simple cases; S3/Terraform Cloud for scale or cross-org.
  2. Plan on every MR; require review of the report widget.
  3. Apply manually on default branch — never auto.
  4. 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-approve in 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

Newsletter

Free: the DevOps AI Incident-Triage Cheat Sheet

Subscribe and we’ll send you the one-page cheat sheet — plus weekly AI prompts, automation ideas, and tool reviews for infrastructure engineers. One email a week. No spam, unsubscribe anytime.

  • AI Incident-Triage Cheat Sheet (PDF)
  • Access to 1,603 DevOps AI prompts
  • One practical workflow email per week