
Organizations implementing cross-team Terraform PR automation report 40-70% reductions in infrastructure provisioning time. Deutsche Bank's platform team now enables hundreds of development teams to provision compliant infrastructure within minutes, while Spotify successfully migrated 1,200+ microservices using automated Terraform workflows.
The hard decisions are in the details, though: which files trigger which workspaces, which commit gets planned when a PR has five of them, and what happens when the webhook pipeline stops processing without an error. This guide covers the tooling choices — open-source options you maintain yourself versus managed platforms — alongside the failure modes we've watched teams hit in production.
PR-based workflows provide several key advantages:
Terraform pull request automation typically follows one of two patterns: merge-before-apply or apply-before-merge.
This is the most recommended and safest approach:
terraform plan runsterraform apply executes via CI/CDBenefits:
Trade-offs:
There's a third, less obvious trade-off: the post-merge trigger itself can fail in ways nobody notices until production lags behind main. A team we worked with at Scalr running a large Azure DevOps monorepo found that after merges to main, roughly 60% of affected workspaces didn't auto-trigger — and only on large merges. One offending commit touched 72 folders and 234 files. Webhooks were delivered, and config-as-code drift was ruled out; the root cause was a pagination limit in the Azure DevOps commit file-list API that dropped some changed paths on very large commits, so workspaces whose directories fell past the cutoff never saw a trigger. The bug took weeks to fix, but the diagnostic lesson is durable: when post-merge runs go missing in a monorepo, check whether your VCS API actually returns the full file list for the merge commit before you start second-guessing trigger patterns. The same team had disabled plan-on-PR entirely because it "generated triple the number of runs" across their workspace fan-out — monorepo trigger scoping is a cost decision as much as a correctness one.
Some teams apply before merging to verify changes in a real environment. This approach critically depends on sophisticated tooling like Atlantis or Scalr.
Key requirements:
Benefits:
Trade-offs:
Every pull request should trigger an automatic terraform plan to show reviewers what changes are coming. This visibility is non-negotiable.
What is negotiable — and routinely underestimated — is how the trigger evaluates the diff. A customer running Terraform out of a /terraform directory hit this directly: the dry run on PR open worked as expected, but every review-fix commit after that re-triggered a full dry run, even when the commit touched zero files under /terraform. They described it as "racking up a number of runs, and cost." The cause is that most platforms evaluate the entire PR diff (head vs. base) on every push, so the original /terraform change keeps matching the trigger no matter what the new commit contains. GitHub Actions' on: pull_request: paths filter behaves the same way, which surprises teams who assume it scopes to the latest push. That customer's feature request became a product setting about six months later: a selectable commit strategy that evaluates only the files changed since the previous commit.
The previous-commit strategy has its own edge cases, found by another customer who stress-tested it. A push containing multiple commits only evaluated the last one — a relevant change buried mid-batch triggered nothing — and /scalr plan comment commands only produced runs when the most recent commit matched the trigger patterns, because the -force flag overrides disabled dry runs but not pattern matching. The fix that shipped: comment commands now bypass the commit strategy entirely and run for any workspace affected by any commit in the PR. Whichever diff-evaluation strategy you choose, test it with a multi-commit push and a review-only commit before depending on it.
Here's a production-ready workflow incorporating best practices:
name: Terraform PR Automation
on:
pull_request:
paths:
- 'terraform/**'
- '.github/workflows/terraform.yml'
permissions:
id-token: write
contents: read
pull-requests: write
jobs:
terraform-check:
runs-on: ubuntu-latest
strategy:
matrix:
environment: [dev, staging, prod]
steps:
- uses: actions/checkout@v4
- name: Configure AWS Credentials
uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: arn:aws:iam::${{ secrets.AWS_ACCOUNT_ID }}:role/github-actions
aws-region: us-east-1
- name: Setup Terraform
uses: hashicorp/setup-terraform@v3
with:
terraform_version: 1.6.0
terraform_wrapper: false
- name: Terraform Format Check
run: terraform fmt -check -recursive
- name: Terraform Init
working-directory: terraform/${{ matrix.environment }}
run: |
terraform init \
-backend-config="bucket=${{ secrets.TF_STATE_BUCKET }}" \
-backend-config="key=${{ matrix.environment }}/terraform.tfstate"
- name: Terraform Validate
working-directory: terraform/${{ matrix.environment }}
run: terraform validate
- name: Run Security Scan
uses: aquasecurity/tfsec-pr-commenter-action@v1
with:
working_directory: terraform/${{ matrix.environment }}
github_token: ${{ github.token }}
- name: Terraform Plan
id: plan
working-directory: terraform/${{ matrix.environment }}
run: |
terraform plan -out=tfplan -no-color 2>&1 | tee plan_output.txt
echo "exitcode=$?" >> $GITHUB_OUTPUTGitLab's native Terraform integration simplifies state management:
stages:
- validate
- plan
variables:
TF_ROOT: ${CI_PROJECT_DIR}/terraform
TF_STATE_NAME: ${CI_ENVIRONMENT_NAME}
.terraform-base:
image: hashicorp/terraform:1.6
before_script:
- cd ${TF_ROOT}/${CI_ENVIRONMENT_NAME}
- terraform init
validate:
extends: .terraform-base
stage: validate
script:
- terraform fmt -check -recursive
- terraform validate
rules:
- if: $CI_MERGE_REQUEST_ID
plan:dev:
extends: .terraform-base
stage: plan
environment:
name: development
script:
- terraform plan -out=tfplan
- terraform show -json tfplan > plan.json
artifacts:
paths:
- ${TF_ROOT}/${CI_ENVIRONMENT_NAME}/tfplan
- ${TF_ROOT}/${CI_ENVIRONMENT_NAME}/plan.json
reports:
terraform: ${TF_ROOT}/${CI_ENVIRONMENT_NAME}/plan.json
rules:
- if: $CI_MERGE_REQUEST_IDname: 'Terraform Apply on Merge'
on:
push:
branches: [ main ]
jobs:
apply:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: hashicorp/setup-terraform@v3
- run: terraform init
working-directory: ./terraform
- run: terraform plan -input=false -out=tfplan
working-directory: ./terraform
- run: terraform apply -auto-approve tfplan
working-directory: ./terraformWhen to use:
Platforms like Scalr (or tools like Atlantis) manage apply triggers based on workspace settings and PR interactions:
/atlantis apply, /scalr apply) triggers controlled apply with platform managing locks and stateWhen to use:
Atlantis established the pattern most Terraform PR automation now follows: plan and apply driven by comments, directly inside the pull request.
atlantis planatlantis apply; Atlantis runs the apply, doneversion: 3
projects:
- name: production
dir: environments/prod
terraform_version: v1.5.0
autoplan:
when_modified: ["*.tf", "*.tfvars"]
enabled: true
apply_requirements: ["approved", "mergeable"]
- name: staging
dir: environments/staging
terraform_version: v1.5.0
autoplan:
when_modified: ["*.tf", "*.tfvars"]
enabled: trueScalr takes the Atlantis-style PR workflow and wraps it in a complete enterprise platform.
Link a workspace to a repo branch, and Scalr gets notified of PRs and merges. It supports two main GitOps flows:
# See the plan
/scalr plan
# Approve a waiting run
/scalr approve -workspace-id=ws-xxxxxxxxxx
# Apply changes (requires permission)
/scalr apply
# Target specific workspace
/scalr apply -workspace-id=ws-xxxxxxxxxxFeedback comes right back into the PR with smart summaries for clean plans and detailed reports for errors.
Comment commands interact with workspace trigger settings, and the interactions deserve a test run before you depend on them. A team we worked with at Scalr running an Atlantis-style workflow on tag-based workspaces hit two at once. First, /scalr plan stopped producing runs after they disabled plan-on-PR-open — by design, since the command respects that setting unless you pass -force. Second, and more confusing, plans fired for workspaces whose trigger patterns didn't match the changed files at all: a workspace scoped to core-eks-manifests/config/**, profiles/k8s/**, and services/*/core-eks-service.yaml triggered on a commit that touched only profiles/local/README.md and a root main.tf. The root cause was that tag-based workspaces computed the PR diff against the commit SHA of the workspace's current state version — correct for tag-pushed applies, wrong for plan-only PR runs — and broad patterns shared across many workspaces fanned that one miscomputed diff out into a pile of zero-change runs. Scalr fixed the diff computation and credited the account 2,000 runs to cover the plans it should never have generated.

Branch-aware protection:
Controlled applies:
[skip scalr] or [skip ci] in merge messageruns:apply permission can executeThese safeguards exist because the failure modes are real — including in Scalr itself. One we caught and fixed ourselves: with draft-PR triggers disabled, commenting /scalr plan on a draft PR planned the last commit made before the PR was marked draft, producing a green check against code that no longer matched the branch. We shipped a fix in May 2026 so the command always plans the latest commit. If your tooling treats draft PRs differently from open ones, check which commit SHA appears in the run metadata before trusting the status check.
Scalr lets you inject scripts at different run phases: pre-init, pre-plan, post-plan, pre-apply, post-apply.
Example pre-plan hook to run tflint:
#!/bin/bash
echo "--- Running TFLint ---"
if ! command -v tflint &> /dev/null; then
mkdir -p /tmp/bin
curl -s https://raw.githubusercontent.com/terraform-linters/tflint/master/install_linux.sh | TFLINT_INSTALL_PATH=/tmp/bin bash
export PATH="$PATH:/tmp/bin"
fi
if [ -f ".tflint.hcl" ]; then
tflint --config=.tflint.hcl --recursive .
else
tflint --recursive .
fiWrite policies in Rego, store in Git, and Scalr checks them pre-plan and post-plan:
package terraform
import input.tfrun as tfrun
# Deny if estimated monthly cost delta exceeds $100
deny[reason] {
cost_delta := tfrun.cost_estimate.delta_monthly_cost
max_allowed_cost_delta := 100.00
cost_delta > max_allowed_cost_delta
reason := sprintf("Cost increase is $%.2f. We allow up to $%.2f.",
[cost_delta, max_allowed_cost_delta])
}Policies can be advisory (warning only), soft-mandatory (requires approval), or hard-mandatory (blocks run).
Account (top level)
├── Environment (groups of workspaces)
│ └── Workspace (where Terraform runs)
Standards set at Account/Environment level flow down to all workspaces, enabling consistent governance across teams.
A critical challenge in Terraform PR automation is preventing state corruption from changes applied before PR merge.
State Corruption and Conflicts: Multiple developers applying from unmerged PRs to shared state causes race conditions, overwrites, and inconsistent state.
Infrastructure Drift: The main branch becomes the single source of truth. Applying from unmerged PRs creates divergence where live infrastructure doesn't match main's definition.
Compromised Main Branch Integrity: Hidden issues in pre-applied changes, provider errors, or API limits can cause actual infrastructure to differ from intended state.
Traceability Challenges: Audit trails become obscured, and rollbacks become complex when changes weren't merged into main first.
"Apply After Merge" Gold Standard: Ensures main is source of truth and changes remain linear.
"Apply Before Merge" with Sophisticated Tooling: Requires PR-level locking, automated plan/apply via comments, and remote state backends with locking.
Essential Supporting Practices:
terraform plan in all PRsModern platforms like Scalr provide built-in safeguards:


- and ~ changes are intentional# GitHub branch protection rules
Required reviewers: 2
Require status checks to pass before merging:
- Terraform Plan (all environments)
- Security Scan (tfsec, checkov)
- OPA Policy Checks| Feature | Atlantis | Spacelift | Terraform Cloud | Scalr |
|---|---|---|---|---|
| Apply-Before-Merge (Native) | Yes (PR Comments) | Yes (Proposed Runs) | Limited | Yes (PR Comments) |
| Merge-Before-Apply | No | Yes (Default) | Yes (Primary) | Yes (Default) |
| Workflow Customization (Hooks) | Limited | Extensive | Limited | Extensive (5 hooks) |
| OPA Policy Depth | Manual Integration | Deep OPA | Sentinel (Proprietary) | Deep OPA |
| State Backend Choice | User's Choice | Managed or User's | TFC Managed Only | Scalr or User's |
| PR Comment Quality | Basic Plan Output | Detailed, Customizable | Basic Status Checks | Rich & Contextual |
| Multi-IaC Support | Terraform | Multiple Tools | Terraform, OpenTofu | Terraform, OpenTofu, Terragrunt |
| Self-Hosted Execution | Yes (Default) | Yes (Worker Pools) | Yes (TFE Agents) | Yes (Custom Images) |
| RBAC Granularity | Auth-dependent | Granular | Limited System Roles | 120+ Permissions |
The matrix above intentionally leaves pricing out — each platform's model differs enough that lining them up as a single column would mislead. The three approaches in this space are concurrency-based pricing, resource-based pricing (HCP Terraform's RUM model), and usage-based, per-run pricing. The concurrency model has a structural problem worth flagging for PR-heavy workflows: you buy a fixed number of parallel run slots, and there's no setting that's right — too few and PRs queue during a release, too many and you're renting idle capacity. The slot cap bites hardest during incident response, when many fixes ship in parallel across workspaces. How to evaluate IaC platform pricing models walks through each model across six evaluation dimensions.
terraform plan automaticallySmall Teams (1-10):
Growing Teams (10-50):
Enterprise (50-200+):
Stage 1 - Manual: Local Terraform runs, shared credentials, manual state locking Stage 2 - Basic Automation: CI/CD pipeline, GitHub Actions, remote state Stage 3 - PR-Driven: Atlantis-style automation, plan-on-PR, manual policy checks Stage 4 - Enterprise Governance: OPA policies, RBAC, cost management, compliance Stage 5 - Self-Service Platform: Module marketplace, policy as code, team autonomy
One more pitfall deserves its own paragraph because it produces no error to ignore: PR automation dies when a VCS token expires. Webhook processing stops, plans stop appearing on PRs, and nothing pages anyone — the absence of a status check is far easier to miss than a red one. Across Scalr's own fleet, in a single 30-day window as of mid-2026, we measured 121 broken VCS connections, including 79 fully-broken VCS providers among paid customers. Treat the VCS connection as production infrastructure: monitor for the absence of expected runs, alert on webhook delivery failures, and put token rotation on a calendar rather than waiting for the first PR that goes unplanned.

Terraform pull request automation has evolved from experimental practice to enterprise necessity. The journey typically starts with basic CI/CD checks, progresses to Atlantis-style PR-based automation, and matures into governed platforms like Scalr for organizations at scale.
Key takeaways:
The right platform choice depends on your organization's size, toolchain, and governance needs. Starting simple with GitHub Actions and scaling to specialized platforms as requirements grow is the pragmatic approach that has worked for hundreds of organizations.
