
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 shift from manual infrastructure management to automated workflows has become essential. Teams face a critical decision: choosing between open-source solutions requiring significant maintenance effort or commercial platforms offering managed services with advanced features.
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:
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.
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 revolutionized Terraform PR automation by bringing automation directly into pull requests.
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.

Branch-aware protection:
Controlled applies:
[skip scalr] or [skip ci] in merge messageruns:apply permission can executeScalr 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 |
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

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.
