Infrastructure as code in the pipeline
Plan on pull requests, apply on merge, manage state and locking, detect drift, and keep infrastructure changes separate from application deploys.
Plan on pull request, apply on merge
name: infra
on:
pull_request:
paths: ['infra/**']
push:
branches: [main]
paths: ['infra/**']
permissions:
contents: read
id-token: write # OIDC, no long-lived cloud keys
jobs:
plan:
if: github.event_name == 'pull_request'
runs-on: ubuntu-latest
defaults:
run: { working-directory: infra }
steps:
- uses: actions/checkout@v4
- uses: hashicorp/setup-terraform@v3
with: { terraform_version: 1.9.8 }
- run: terraform fmt -check -recursive
- run: terraform init -backend-config=backend.hcl
- run: terraform validate
- run: terraform plan -out=tfplan -lock-timeout=5m
- run: terraform show -no-color tfplan > plan.txt
- uses: actions/github-script@v7
with:
script: |
const fs = require('fs');
const plan = fs.readFileSync('infra/plan.txt', 'utf8').slice(0, 60000);
await github.rest.issues.createComment({
...context.repo, issue_number: context.issue.number,
body: 'Terraform plan for ' + context.sha + ':\n\n\`\`\`\n' + plan + '\n\`\`\`'
});
apply:
if: github.ref == 'refs/heads/main'
runs-on: ubuntu-latest
environment: infrastructure
defaults:
run: { working-directory: infra }
steps:
- uses: actions/checkout@v4
- uses: hashicorp/setup-terraform@v3
- run: terraform init -backend-config=backend.hcl
- run: terraform apply -auto-approve -lock-timeout=5m- The plan belongs on the pull request, where a human can see that a rename deletes and recreates a database before it happens.
- Re-plan on apply rather than applying a stored plan file from a different run when the state may have moved.
- Use OIDC federation instead of cloud access keys so no long-lived credential exists in the repository.
- Restrict the infrastructure pipeline with a path filter so an application change cannot trigger a plan that reshapes the network.
State, locking and environments
# remote state with locking, one workspace per environment
terraform {
required_version = ">= 1.9"
backend "s3" {
bucket = "example-tfstate"
key = "app/terraform.tfstate"
region = "eu-west-2"
dynamodb_table = "tfstate-lock"
encrypt = true
}
}
# separate state per environment keeps the blast radius obvious
# terraform workspace new staging
# terraform workspace select production| Risk | Control | Failure mode |
|---|---|---|
| Two applies at once | State locking | A stuck lock from a killed run |
| Drift from manual changes | A scheduled plan that reports only | Nobody reads the report |
| Destroying production | Separate state and a protected environment | Both in one workspace |
| Secret in state | State is encrypted and access-controlled | State is treated as source code |
Terraform state contains every value it manages, including secrets. Treat the bucket as sensitive data, restrict read access, and never attach a plan output containing secrets to a public pull request.
Separating infrastructure from applications
repo/
infra/
modules/network/ reusable, versioned modules
envs/staging/ thin composition, own state
envs/production/
services/
api/
deploy/ Helm chart or manifests, no cloud resources
worker/
Rule: infrastructure pipelines change cloud resources.
application pipelines change running software.
They meet at a versioned interface: an image digest and a config contract.💡
When an application deploy needs a new cloud resource, that is a signal, not an exception. Add the resource through the infrastructure pipeline first, wait for it, then deploy the application that uses it — one change, two pipelines, in order.
FAQ
Terraform or Pulumi?
Terraform has the largest provider ecosystem and a plan format every reviewer recognises. Pulumi gives you a real programming language, which helps when you need loops and abstractions. Pick based on your team's comfort, and do not run both for the same resources.
How do I detect drift?
Run a scheduled plan that reports but does not apply, and alert on a non-empty plan for an environment nobody expected to change. That is also the cheapest way to catch a change made by hand during an incident.
Related
Environment promotion and release strategies Pipeline security and supply chain
Last refreshed 2026-09-18.