Lesson 23 of 30
Terraform applied by hand from someone's laptop is a single point of failure with root access. Running plan on every PR and apply only on merge turns infrastructure changes into a reviewable diff.
15 min read
Every option explains itself — why the right one is right, and why each wrong one is wrong. Miss one and it comes back later on a spaced-repetition schedule.
This topic is about judgment and practical tradeoffs more than a single right answer — a mock interview is the better way to test yourself on it.
Be interviewed on this →Hints without spoilers, and a plain explanation of why a wrong answer was wrong — answered only from this lesson. Free account, no card.
Sign up freeWhat interviewers actually ask about this, and what a strong answer has to cover.
A strong answer covers
Likely follow-ups
Infrastructure as Code means your servers, databases, load balancers, and DNS records are defined in files (Terraform, Pulumi, CloudFormation) instead of clicked together in a cloud console. That alone is a big improvement — it's versioned, diffable, reviewable. But it's only half the win if someone still runs terraform apply from their own laptop with their own cloud credentials whenever they feel like it. That's the same "champion clicks deploy from their machine" problem the deployment-strategies lesson covers, just for infrastructure instead of application code.
The fix is the same shape as everything else in this track: put the change through a pipeline instead of a person. Specifically, split the two Terraform commands across two different trigger points — plan runs automatically and safely on every pull request, apply runs only after a human has reviewed that plan and the change lands on the main branch.
terraform plan is read-only — it compares your .tf files against the real infrastructure and prints what would change, without touching anything. That makes it safe to run on every single PR, even from an untrusted contributor. terraform apply is the dangerous one — it actually creates, modifies, or destroys real infrastructure — so it should only run after review, on merge, ideally gated by a manual approval too.
name: Terraform
on:
pull_request:
paths: ["infra/**"]
push:
branches: [main]
paths: ["infra/**"]
jobs:
plan:
if: github.event_name == 'pull_request'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: hashicorp/setup-terraform@v3
- run: terraform -chdir=infra init
- run: terraform -chdir=infra plan -no-color -out=tfplan
- name: Post the plan as a PR comment
uses: actions/github-script@v7
with:
script: |
const { execSync } = require('child_process');
const plan = execSync('terraform -chdir=infra show -no-color tfplan').toString();
github.rest.issues.createComment({
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
body: "### Terraform plan\n```\n" + plan.slice(0, 60000) + "\n```",
});
apply:
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
needs: []
runs-on: ubuntu-latest
environment: production # requires a reviewer to approve, see next lesson
steps:
- uses: actions/checkout@v4
- uses: hashicorp/setup-terraform@v3
- run: terraform -chdir=infra init
- run: terraform -chdir=infra apply -auto-approve -input=false
Notice the two jobs are gated on two different if: conditions tied to two different events — plan fires on pull_request and never touches real infrastructure, apply fires only on push to main, which only happens after the PR (and its posted plan) has been reviewed and merged. A reviewer sees exactly what will change — "add 1 S3 bucket, modify 2 security group rules, destroy 0 resources" — as a normal PR comment, the same way they'd review a code diff, before it's ever applied for real.
terraform plan is not fully side-effect-free. It refreshes state by querying real cloud APIs to see current resource status — safe to run freely, but it does make network calls and needs real (usually read-scoped) credentials, it's not a pure local computation.apply jobs run concurrently against the same state (a rare but real risk with re-triggered pipelines), Terraform's state locking is what prevents them from corrupting each other — remote state backends like S3+DynamoDB or Terraform Cloud provide this; local state files don't.The plan-then-apply gate is branch protection's required-checks idea aimed at infrastructure instead of application code — a check must post its result before the thing it's checking is allowed to reach production.