Command Palette

Search for a command to run...

Hectal

Mission 7.4 · Stage 7 — Team Workflow

Day 2: Drift Detection, Upgrades, Stuck Locks, and Teardown

Goal: A nightly job reports drift in each environment, you know how to upgrade Terraform and providers safely and clear a stuck lock, and you finish by tearing all of ShopLite down in the right order.

45 min Free — and after this mission, nothing keeps costing anything 3 steps 2 break-it drills

By the end of this mission

  • Detect drift on a schedule with plan -detailed-exitcode
  • Upgrade providers and Terraform deliberately, with a reviewable lock-file diff
  • Recover from a stale lock left by a killed run
  • Destroy a multi-environment, multi-config project in the correct order

Part 1

Understand it first

Drift doesn't announce itself

Someone fixes an incident in the console at 3 a.m.; AWS changes a default; another tool edits a tag. Nobody notices until the next unrelated PR's plan shows surprising changes, or reverts the fix. A scheduled plan per environment with -detailed-exitcode turns drift into an alert: exit code 0 means no changes, 2 means changes exist, 1 means an error. The job opens an issue or posts to chat when it sees a 2.

Upgrades are changes too

Provider minor versions add resources and fix bugs, but can also change defaults, so a new provider can produce a different plan for the same code. Treat upgrades as their own PRs: terraform init -upgrade rewrites the lock file, the PR shows the lock-file diff plus plans for every environment, and it merges only when those plans are clean or explained. Terraform CLI upgrades likewise: bump required_version, the CI version, and laptops together.

Make the lock file cover every platform your team uses (terraform providers lock -platform=...), or a teammate on a different OS gets checksum surprises.

Teardown order matters

Dependencies run across configurations too: analytics depends on ShopLite's published parameters, apps depend on the network, and everything depends on the state bucket. Destroy consumers before producers, and the bootstrap state bucket LAST, since without it no other configuration can even plan its own destroy. Guardrails you added on purpose (prevent_destroy, deletion protection, force_destroy = false) must be lifted deliberately, one reviewed change at a time.

Teardown orderdiagram
Rendering diagram…

Part 2

Your project after this mission · 3 files change

shoplite/
  • .github/
    • workflows/
      • drift.ymlnew
  • bootstrap/
    • main.tf
  • infra/
    • envs/
      • dev.s3.tfbackend
      • dev.tfvars
      • prod.s3.tfbackend
      • prod.tfvars
    • .terraform.lock.hclmodified
    • database.tf
    • ecs.tf
    • main.tf
    • providers.tf
    • security.tf
    • tf
    • versions.tfmodified
  • modules/
    • ecs-service/
      • main.tf
      • variables.tf
    • network/
      • main.tf

Part 3

Build it, step by step

  1. 1

    Nightly drift detection

    Runs at 02:30 UTC per environment with the READ-ONLY plan role. It needs a trust statement for scheduled runs on main: add repo:you/shoplite:ref:refs/heads/main to the plan role's sub values. Exit code 2 opens an issue with the plan attached. Note the careful exit-code capture; see Break it for why.

    .github/workflows/drift.ymlwhole fileyaml
    name: drift
    on:
      schedule: [{ cron: "30 2 * * *" }]
      workflow_dispatch: {}
    
    permissions:
      id-token: write
      contents: read
      issues: write
    
    jobs:
      drift:
        runs-on: ubuntu-latest
        strategy:
          fail-fast: false
          matrix: { env: [dev, prod] }
        steps:
          - uses: actions/checkout@v4
          - uses: hashicorp/setup-terraform@v3
            with: { terraform_version: 1.13.3, terraform_wrapper: false }
          - uses: aws-actions/configure-aws-credentials@v4
            with:
              role-to-assume: arn:aws:iam::${{ vars[format('{0}_ACCOUNT_ID', matrix.env)] }}:role/shoplite-ci-plan
              aws-region: ap-south-1
          - name: plan
            id: plan
            working-directory: infra
            run: |
              set +e
              ./tf ${{ matrix.env }} plan -detailed-exitcode -no-color -lock=false > plan.txt
              code=$?
              set -e
              echo "exitcode=$code" >> "$GITHUB_OUTPUT"
              [ "$code" -eq 1 ] && exit 1 || exit 0
          - name: open issue on drift
            if: steps.plan.outputs.exitcode == '2'
            env: { GH_TOKEN: "${{ github.token }}" }
            run: |
              gh issue create --title "Drift detected in ${{ matrix.env }} ($(date -u +%F))" \
                --label drift --body-file infra/plan.txt
  2. 2

    Upgrade providers deliberately

    On a branch: re-select the newest allowed versions and record checksums for every platform the team and CI use. The lock-file diff shows exactly which versions changed. The PR's plans then show whether the upgrade changes anything real.

    terminal
    $ cd infra
    terraform init -upgrade -backend=false >/dev/null
    terraform providers lock -platform=linux_amd64 -platform=darwin_arm64 -platform=windows_amd64
    git diff --stat .terraform.lock.hcl
    ── expected output ──
    - Fetching hashicorp/aws 6.21.0 for linux_amd64...
    - Retrieved hashicorp/aws 6.21.0 for linux_amd64 (signed by HashiCorp)
    - Fetching hashicorp/aws 6.21.0 for darwin_arm64...
    ...
    Success! Terraform has updated the lock file.
     
    infra/.terraform.lock.hcl | 46 +++++++++++++++++++++++-----------------------
    1 file changed, 23 insertions(+), 23 deletions(-)
  3. 3

    Tear it all down, in order

    When you're done with the course, remove everything so nothing keeps billing. First lift the guardrails: remove prevent_destroy from the database, and in prod set deletion_protection/final snapshots as you see fit. Then destroy consumers first, and bootstrap last, where you also remove prevent_destroy on the state bucket and empty its versions.

    terminal
    $ (cd analytics && terraform destroy -auto-approve)
    (cd infra && ./tf prod destroy) # if you applied prod
    (cd infra && ./tf dev destroy)
    # bootstrap last: remove prevent_destroy, empty all object versions, then:
    (cd bootstrap && terraform destroy)
    ── expected output ──
    Destroy complete! Resources: 1 destroyed.
    ...
    Destroy complete! Resources: 83 destroyed.
    ...
    Destroy complete! Resources: 11 destroyed.
    Also delete the ECR images' repository if it survived (prod had force_delete = false), the GitHub OIDC provider in each account, and the billing SNS subscription.

Checkpoint — you should now have

  • ✓drift.yml runs nightly per environment and opens an issue when exit code is 2.
  • ✓Provider upgrades happen in their own PR with a multi-platform lock file.
  • ✓You can clear a stale lock safely (see Break it).
  • ✓ShopLite is fully destroyed, bootstrap last, and your AWS bill for it has stopped.

Part 4

Break it on purpose

Make each change, run the command, and read the error before revealing the diagnosis. Recognising these messages on sight is what makes you fast on a real team. Undo the change afterwards.

Break #1

Let `set -e` swallow drift

Simplify the drift step to just ./tf ${{ matrix.env }} plan -detailed-exitcode > plan.txt (GitHub runs bash -e by default), then create some drift and run the workflow.

terminal
$ (GitHub Actions log — plan step)
── what you'll see ──
Run ./tf dev plan -detailed-exitcode -no-color -lock=false > plan.txt
Error: Process completed with exit code 2.

Break #2

Clear a lock left by a killed run

Reproduce Mission 7.1's cancelled apply (or kill a local ./tf dev apply with kill -9 mid-run), then run ./tf dev plan.

terminal
$ ./tf dev plan
── what you'll see ──
╷
│ Error: Error acquiring the state lock
│
│ Error message: operation error S3: PutObject, https response error
│ StatusCode: 412, ... PreconditionFailed
│ Lock Info:
│ ID: 6f0c2a91-3b7e-4d15-a8c0-92e4f1b7d3aa
│ Path: shoplite-tfstate-c41e/shoplite/dev/terraform.tfstate
│ Operation: OperationTypeApply
│ Who: runner@fv-az812-431
│ Created: 2026-09-26 18:02:11 +0000 UTC
╵

Part 5

Interview questions from this mission

01

How do you detect infrastructure drift with Terraform?

02

How do you upgrade the AWS provider across many environments safely?

03

A CI job was killed during terraform apply. What do you do?

0/4 · 0%