Production Checklist
The idea in one sentence
Section titled “The idea in one sentence”A production-ready Terraform and Terragrunt setup on Azure is not one single feature, but the sum of every practice this course has covered — remote state, pinned versions, reviewed plans, protected secrets, static analysis and policy gates, dependency-ordered units, and real tests — wired together into one CI-driven repository.
The checklist, one line of reasoning per item
Section titled “The checklist, one line of reasoning per item”Each item below traces back to a concrete failure mode a specific lesson in this course was built around:
- A remote
azurermbackend with blob-lease locking, from day one. Local state means one laptop’s disk is the only copy of the truth about your entire Azure footprint, and two engineers runningapplyat once can corrupt it — the State Management module’s whole reason for existing. - Provider versions pinned via
.terraform.lock.hcl, module versions pinned to a tag or version constraint. An unpinned provider or module can silently resolve to a new major version the next timeinitruns, changing behavior nobody asked for in the middle of an unrelated change. planreviewed in CI before everyapply, watching specifically for unexpected resource replacements. A-/+in a plan means destroy-then-recreate on a real, stateful Azure resource — reviewing a plan like a normal code diff misses exactly the line that matters most.- Sensitive values kept out of Terraform variables and state where a secrets-manager data source is available, and Azure RBAC tightly restricting who can read the state container.
sensitive = trueonly redacts CLI output; the real protection is restricting read access to the backend and minimizing what ever needs to be typed into a variable in the first place. tflintand a security scanner, and ideally policy as code, wired into CI. Static analysis catches known-bad HCL in seconds beforeplanruns at all; policy as code (Sentinel or OPA) then evaluates the actual computed plan against organizational rules that only make sense once a real change exists.- Terragrunt
dependencyblocks driving multi-unit rollout ordering. Splitting infrastructure into independent units (vnet,vm,database) is only safe if a unit that needs another unit’s output — a subnet ID, a resource group name — declares that dependency explicitly, so Terragrunt applies them in the right order instead of relying on humans to remember it. terraform testand.tftest.hclcoverage for any module doing something non-trivial.validatechecks syntax andplanchecks that Azure accepts the request, but neither one asserts that a module’s actual behavior is correct — that is whatterraform testis for.
Dependency blocks: the piece that orders a multi-unit rollout
Section titled “Dependency blocks: the piece that orders a multi-unit rollout”A Terragrunt dependency block is what lets one unit consume another unit’s real output without either unit knowing about the other’s internal implementation. A vm unit that needs the subnet a vnet unit created declares that dependency directly in its own terragrunt.hcl:
include "root" { path = find_in_parent_folders("root.hcl")}
dependency "vnet" { config_path = "../vnet"
mock_outputs = { subnet_id = "/subscriptions/00000000-0000-0000-0000-000000000000/mock-subnet" } mock_outputs_allowed_terraform_commands = ["plan"]}
terraform { source = "git::https://github.com/acme-corp/terraform-modules.git//vm?ref=v1.4.0"}
inputs = { subnet_id = dependency.vnet.outputs.subnet_id}Terragrunt reads this dependency block and figures out the correct apply order on its own: terragrunt run --all apply applies vnet first, reads its real subnet_id output once it exists, and only then runs vm with that real value as an input — nobody has to hardcode an ordering or remember to apply units in the right sequence by hand. The mock_outputs block matters specifically for plan: it lets a plan for vm run cleanly even before vnet has ever been applied, using a placeholder value instead of failing because the real output does not exist yet.
The end-to-end picture
Section titled “The end-to-end picture”Put every item together and a production Terragrunt repository on Azure looks like this: one root.hcl at the top declaring the shared remote_state backend and provider generate blocks, a handful of unit directories beneath it — vnet/, vm/ (or aks/), and sql/ are a realistic minimal set — each with its own terragrunt.hcl wiring in whatever dependency blocks it actually needs, and a CI pipeline that authenticates via OIDC federated credentials, running terragrunt run --all plan on every pull request and terragrunt run --all apply only after merge to main.
name: terraform
on: pull_request: paths: - 'infra/**' push: branches: - main paths: - 'infra/**'
permissions: id-token: write contents: read pull-requests: write
jobs: static-analysis: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: terraform-linters/setup-tflint@v4 - run: tflint --init && tflint --recursive - name: Run checkov uses: bridgecrewio/checkov-action@master with: directory: infra
plan: needs: static-analysis if: github.event_name == 'pull_request' runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: azure/login@v3 with: client-id: ${{ secrets.AZURE_CLIENT_ID }} tenant-id: ${{ secrets.AZURE_TENANT_ID }} subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }} - uses: hashicorp/setup-terraform@v3 - run: terragrunt run --all plan -no-color | tee plan.txt - run: gh pr comment ${{ github.event.pull_request.number }} --body-file plan.txt env: GH_TOKEN: ${{ github.token }}
apply: needs: static-analysis if: github.event_name == 'push' runs-on: ubuntu-latest environment: production steps: - uses: actions/checkout@v4 - uses: azure/login@v3 with: client-id: ${{ secrets.AZURE_CLIENT_ID }} tenant-id: ${{ secrets.AZURE_TENANT_ID }} subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }} - uses: hashicorp/setup-terraform@v3 - run: terragrunt run --all apply --non-interactiveNothing in that pipeline is new by this point in the course — every stage is a lesson you already worked through, assembled into one repository: vnet, vm/aks, and sql as independent units under one root.hcl; dependency blocks giving Terragrunt the real ordering between them; tflint and checkov failing fast before a plan is even attempted; OIDC replacing any stored client secret; a reviewed plan gating every apply; and apply itself reserved for after merge, behind an approval gate, run by the pipeline rather than a laptop.
flowchart TB root["root.hcl (remote_state + provider generate)"] --> vnet["vnet unit"] root --> vm["vm / aks unit"] root --> sql["sql unit"] vnet -->|dependency block| vm vnet -->|dependency block| sql pr["Pull request"] -->|OIDC auth| planj["terragrunt run --all plan"] planj -->|reviewed and merged| applyj["terragrunt run --all apply"] vnet -.-> planj vm -.-> planj sql -.-> planj