Automated Apply #
Automated apply is the end goal of a mature Terraform CI/CD setup — reviewed and merged configuration changes are automatically applied to the infrastructure without anyone needing to run a command manually. This removes the “wait for whoever has apply access” bottleneck, ensures consistency between code and infrastructure, and lets teams move faster. But “automatic” doesn’t mean “without control” — a good automated apply actually has more safeguards than a manual one.
flowchart TD
A["Merge to main"] --> B["CI: Plan"]
B --> C{"Auto checks"}
C -->|"pass"| D["Apply"]
C -->|"fail"| E["Block + Alert"]
D --> F["Verify"]
F -->|"drift"| G["Alert"]
F -->|"ok"| H["Done"]
style A fill:#3b82f6,stroke:#1e40af,color:#fff
style B fill:#3b82f6,stroke:#1e40af,color:#fff
style C fill:#f97316,stroke:#ea580c,color:#fff
style D fill:#10b981,stroke:#059669,color:#fff
style E fill:#ef4444,stroke:#dc2626,color:#fff
style F fill:#3b82f6,stroke:#1e40af,color:#fff
style G fill:#f59e0b,stroke:#d97706,color:#fff
style H fill:#10b981,stroke:#059669,color:#fffPrerequisites for Safe Automated Apply #
An automated apply without the right prerequisites is more dangerous than a manual apply. Here’s what must be in place before enabling automated apply.
AUTOMATED APPLY PREREQUISITES:
REVIEW PROCESS:
✓ All changes go through a PR / merge request
✓ At least 1 reviewer approves before merging
✓ The plan output appears in the PR so reviewers see the IMPACT, not just the code
STATE MANAGEMENT:
✓ Remote state with locking (S3 + DynamoDB or Terraform Cloud)
✓ State can't be accessed or modified directly by anyone other than the pipeline
CREDENTIALS:
✓ CI/CD gets credentials via OIDC (not stored secrets)
✓ Different credentials for plan and apply (plan: read-only, apply: full)
MONITORING:
✓ Notifications when apply succeeds or fails
✓ Apply logs stored and auditable
✓ Alerts for unexpected changes (drift detection)
GitOps: The Branch as Source of Truth #
GitOps is an approach where the state of a specific Git branch always reflects the desired state of the infrastructure. Every merge to that branch automatically triggers an apply.
GITOPS BRANCHING STRATEGY:
main branch → production environment
staging branch → staging environment
develop branch → development environment
Change flow:
feature-branch → PR to develop → auto-apply dev
develop → PR to staging → auto-apply staging (or with approval)
staging → PR to main → apply production (always with approval)
Each branch reflects its environment's state.
Git history = infrastructure audit trail.
# GitHub Actions: GitOps-style automated apply
# .github/workflows/terraform-gitops.yml
name: Terraform GitOps
on:
push:
branches: [develop, staging, main]
paths: ['infrastructure/**']
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Determine Environment
id: env
run: |
case "${{ github.ref_name }}" in
develop) echo "environment=dev" >> $GITHUB_OUTPUT
echo "auto_approve=true" >> $GITHUB_OUTPUT ;;
staging) echo "environment=staging" >> $GITHUB_OUTPUT
echo "auto_approve=true" >> $GITHUB_OUTPUT ;;
main) echo "environment=production" >> $GITHUB_OUTPUT
echo "auto_approve=false" >> $GITHUB_OUTPUT ;;
esac
- name: Configure AWS Credentials
uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: >
arn:aws:iam::${{ vars.AWS_ACCOUNT_ID }}:role/TerraformApplyRole-${{ steps.env.outputs.environment }}
aws-region: ap-southeast-1
- uses: hashicorp/setup-terraform@v3
with:
terraform_version: "1.7.0"
- name: Terraform Init
run: terraform init
working-directory: infrastructure/environments/${{ steps.env.outputs.environment }}
- name: Terraform Plan
run: terraform plan -out=tfplan -no-color
working-directory: infrastructure/environments/${{ steps.env.outputs.environment }}
- name: Terraform Apply (Auto-approve for non-production)
if: steps.env.outputs.auto_approve == 'true'
run: terraform apply tfplan
working-directory: infrastructure/environments/${{ steps.env.outputs.environment }}
- name: Terraform Apply (Production — requires environment approval)
if: steps.env.outputs.auto_approve == 'false'
run: terraform apply tfplan
working-directory: infrastructure/environments/${{ steps.env.outputs.environment }}
environment: production # Environment protection rules apply here
Safeguard: Detecting Dangerous Changes Before Applying #
A good pipeline has additional checks before the apply runs, especially to detect potentially destructive changes.
#!/bin/bash
# check-plan-safety.sh — run after terraform plan, before apply
PLAN_OUTPUT=$(terraform show -json tfplan)
# Count resources that will be destroyed
DESTROY_COUNT=$(echo $PLAN_OUTPUT | jq '[.resource_changes[] | select(.change.actions[] == "delete")] | length')
# Count resources that will be replaced (destroy + create)
REPLACE_COUNT=$(echo $PLAN_OUTPUT | jq '[.resource_changes[] | select(.change.actions | contains(["delete", "create"]))] | length')
echo "Resources to destroy: $DESTROY_COUNT"
echo "Resources to replace: $REPLACE_COUNT"
# Fail the pipeline if there are destroys in production
if [ "$ENVIRONMENT" = "production" ] && [ "$DESTROY_COUNT" -gt 0 ]; then
echo "ERROR: Destroy operations detected in the production plan!"
echo "Destroy operations require manual review and explicit override."
exit 1
fi
# Warn if there are replaces in production
if [ "$ENVIRONMENT" = "production" ] && [ "$REPLACE_COUNT" -gt 0 ]; then
echo "WARNING: Replace operations detected (resources will be destroyed and recreated)"
echo "This may cause downtime. Ensure this is intentional."
# No exit — just a warning, still needs human approval via environment protection
fi
# Integrate the check into the pipeline
- name: Check Plan Safety
run: bash ./scripts/check-plan-safety.sh
env:
ENVIRONMENT: ${{ steps.env.outputs.environment }}
working-directory: infrastructure/environments/${{ steps.env.outputs.environment }}
Rollback Strategies #
An automated apply failing midway can leave infrastructure in a partial state — some resources created, some not. Having a clear rollback strategy is very important.
TERRAFORM ROLLBACK STRATEGIES:
OPTION 1: Re-apply the previous version (safest)
- Revert the commit in Git
- The pipeline automatically triggers an apply of the old version
- Terraform will restore the infrastructure to its previous state
Note: This only works if the problematic resources can be changed back
(not all resources can be safely rolled back)
OPTION 2: Terraform apply from a specific state point
- terraform state pull > backup.tfstate (before applying)
- If apply fails: terraform apply -target=resource.that.needs.fixing
Note: -target is only for emergency fixes, not routine workflows
OPTION 3: Restore from a state backup
- If the state is corrupted or inconsistent
- Restore the tfstate from a previous backup
- Then run plan to see the differences
PREVENTION IS BETTER THAN ROLLBACK:
✓ Always back up state before a large apply
✓ Use create_before_destroy for critical resources
✓ Test changes in staging before production
✓ Save the plan output — it can be a reference when debugging
Notifications After Apply #
The team needs to know when infrastructure changes — whether it succeeds or fails.
# Notify Slack after apply
- name: Notify Slack on Success
if: success()
uses: slackapi/slack-github-action@v1
with:
payload: |
{
"text": "✅ Terraform apply succeeded",
"blocks": [
{
"type": "section",
"text": {
"type": "mrkdwn",
"text": "*✅ Terraform Apply Succeeded*\nEnvironment: `${{ steps.env.outputs.environment }}`\nCommit: `${{ github.sha }}`\nPR: ${{ github.event.pull_request.html_url }}"
}
}
]
}
env:
SLACK_WEBHOOK_URL: ${{ secrets.SLACK_INFRA_WEBHOOK }}
- name: Notify Slack on Failure
if: failure()
uses: slackapi/slack-github-action@v1
with:
payload: |
{
"text": "🚨 Terraform apply FAILED",
"blocks": [
{
"type": "section",
"text": {
"type": "mrkdwn",
"text": "*🚨 Terraform Apply FAILED*\nEnvironment: `${{ steps.env.outputs.environment }}`\nView the log: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}"
}
}
]
}
env:
SLACK_WEBHOOK_URL: ${{ secrets.SLACK_INFRA_WEBHOOK }}
flowchart LR
A["merge"] --> B["plan"]
B --> C["checks"]
C --> D["apply"]
D --> E["notify"]
style A fill:#3b82f6,stroke:#1e40af,color:#fff
style B fill:#f59e0b,stroke:#d97706,color:#fff
style C fill:#8b5cf6,stroke:#6d28d9,color:#fff
style D fill:#10b981,stroke:#059669,color:#fff
style E fill:#f97316,stroke:#ea580c,color:#fffRollback Strategy #
# If an apply fails or causes problems:
# 1. Check the state to see what has changed
terraform state list
terraform state show aws_instance.web
# 2. Use a state backup to roll back
terraform state pull > current-state.tfstate
terraform state push backup-before-apply.tfstate
# 3. Or: revert the code and apply again
git revert HEAD
terraform plan
terraform apply
Apply Monitoring #
# Monitor a running apply
terraform apply -auto-approve 2>&1 | tee apply.log
# Parse the log for errors
if grep -q "Error:" apply.log; then
echo "APPLY FAILED!"
grep "Error:" apply.log
# Send an alert
fi
# Verify after the apply
terraform output -json > post-apply-outputs.json
terraform state list | wc -l
# Make sure the resource count matches expectations
Apply Notifications #
# Send notifications after a successful/failed apply
# Notification script
terraform apply -auto-approve 2>&1 | tee apply.log
if [ $? -eq 0 ]; then
# Apply succeeded
curl -X POST $SLACK_WEBHOOK -d '{
"text": "✅ Terraform apply succeeded",
"attachments": [{"text": "'"$(tail -5 apply.log)"'"}]
}'
else
# Apply failed
curl -X POST $SLACK_WEBHOOK -d '{
"text": "❌ Terraform apply FAILED",
"attachments": [{"text": "'"$(grep Error apply.log)"'"}]
}'
fi
Summary #
- Automated apply prerequisites: remote state with locking, a review process through PRs, credentials via OIDC, and monitoring notifications.
- GitOps: a Git branch reflects the environment state — merging to
develop= apply dev, merging tomain= apply production.- Dev and staging can auto-apply after review; production always requires explicit approval through environment protection rules.
- Destroy-detection safeguards — prevent automatic applies in production if there are unexpected delete operations in the plan.
- The safest rollback is reverting the commit in Git and letting the pipeline apply the previous version — this is the advantage of GitOps.
- Notifications are always present — the team must know every time infrastructure changes, whether it succeeds or fails, with a link to auditable logs.