Automated Terraform Plan/Apply in CI/CD Pipeline Setup
Terraform is run manually from an engineer's laptop — a classic situation in teams where infrastructure code exists but processes around it are unestablished. The problem isn't just unreliability (though that too). The issue is that terraform.tfstate lives on someone's computer, apply can be run without review, and the infrastructure change history is the git history of one person who commits "once a week when I remember." This practice leads to state drift, lock conflicts, and unplanned downtime. According to HashiCorp, automating plan/apply reduces MTTR by 70%.
Over 7+ years and 50+ projects, we've developed an approach that turns chaos into a manageable process. Integrating Terraform into CI/CD automates plan on every PR, apply after approval, and provides a full history in the pipeline. Below is a proven scheme with remote state, auto-comments, and OIDC security. Get in touch for an audit of your IaC — we'll assess your project and scope the work.
Why Integrate Terraform into CI/CD?
Manual infrastructure management is a direct path to state drift and human error. An automated pipeline guarantees:
- Code review before every apply — changes visible to the whole team.
- State stored in a central remote backend — no conflicts or loss.
- Full change history: who, when, and what changed.
- Apply only after a successful plan check — protection against accidental breakage.
Experience shows: switching to GitOps and CI/CD for Terraform reduces deployment time by 60% and reduces incident risk by 3 times compared to the manual approach.
Architecture: Where to Store State and How to Work with Workspaces
Before setting up CI/CD, you need to address remote state. S3 + DynamoDB for AWS, GCS for GCP, Azure Blob for Azure, or Terraform Cloud. Local state in CI is an antipattern — it dies with the runner. According to official Terraform documentation, remote state with locking is mandatory for team collaboration.
Example backend.tf configuration
# backend.tf
terraform {
backend "s3" {
bucket = "mycompany-terraform-state"
key = "production/main.tfstate"
region = "eu-west-1"
dynamodb_table = "terraform-state-lock"
encrypt = true
}
}dynamodb_table for State Locking is critical. Without locking, two concurrent terraform apply (e.g., two PRs merged simultaneously) can lead to corrupt state or conflicting infrastructure changes.
How to Protect the Production Environment During Automatic Apply?
Key principle — two-stage scheme: plan automatically, apply manually with production rights. We add when: manual to the apply stage and use environment: production for access separation. Additionally, RBAC in GitLab/GitHub is configured so only certain users can confirm apply.
For critical changes (resource deletion), you can add approval rules — for example, request a separate approval from the team lead.
Pipeline: Plan on PR, Apply After Merge
Standard two-stage scheme with validate:
# .gitlab-ci.yml
stages:
- validate
- plan
- apply
terraform:validate:
stage: validate
image: hashicorp/terraform:1.7
script:
- terraform init -backend=false
- terraform validate
- terraform fmt -check -recursive
rules:
- if: $CI_PIPELINE_SOURCE == "merge_request_event"
- if: $CI_COMMIT_BRANCH == "main"
terraform:plan:
stage: plan
image: hashicorp/terraform:1.7
script:
- terraform init
- terraform plan -out=tfplan -var-file="environments/${ENV}/terraform.tfvars"
- terraform show -json tfplan > tfplan.json
artifacts:
paths:
- tfplan
- tfplan.json
expire_in: 1 week
rules:
- if: $CI_PIPELINE_SOURCE == "merge_request_event"
- if: $CI_COMMIT_BRANCH == "main"
terraform:apply:
stage: apply
image: hashicorp/terraform:1.7
script:
- terraform init
- terraform apply -auto-approve tfplan
dependencies:
- terraform:plan
rules:
- if: $CI_COMMIT_BRANCH == "main"
when: manual
environment:
name: production
when: manual on apply is mandatory for production. Plan runs automatically, apply requires a button click from a person with the right permissions.
How to Set Up Auto-Comment of Plan Results in Pull Request?
The most valuable thing in a terraform pipeline is seeing the diff directly in the PR, without going to CI. For this, we use Atlantis or a custom script with GitHub/GitLab API. Atlantis provides 2x faster feedback thanks to real-time comments compared to built-in solutions.
In GitHub Actions using dflook/terraform-github-actions:
---
- name: Terraform Plan
uses: dflook/terraform-plan@v1
with:
path: infrastructure/
var_file: |
environments/production/terraform.tfvars
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
The action automatically adds a comment to the PR with the full terraform plan output: what will be added, changed, deleted. On subsequent commits to the branch, the comment is updated, not recreated.
Atlantis for Team Work — Automated Terraform Setup
If the team is large and needs a full workflow with /atlantis plan and /atlantis apply commands directly from PR comments — Atlantis handles it out of the box. It runs as a separate service (Helm chart in a cluster or standalone VM), subscribes to GitHub/GitLab webhooks.
---
# atlantis.yaml version: 3
projects:
- name: production
dir: infrastructure/environments/production
workspace: production
autoplan:
when_modified: ["*.tf", "../modules/**/*.tf"]
enabled: true
apply_requirements: [approved, mergeable]
---apply_requirements: [approved, mergeable] — apply is impossible without PR approval and "mergeable" status (all required checks passed).
| Characteristic | Atlantis | GitLab CI native |
|---|---|---|
| Setup | Helm chart / standalone | built into GitLab |
| PR comments | /atlantis plan |
via API (dflook/action) |
| Access control | apply_requirements |
when: manual + RBAC |
| Multiple repos support | out of the box | requires per-repo setup |
| OIDC | via IAM role | via GitLab CI variables |
Security: Credentials in CI
Static AWS credentials in CI Variables are acceptable to start, but the best option is OIDC integration. The CI system obtains a temporary token via OIDC and exchanges it for an IAM role through aws sts assume-role-with-web-identity. No long-lived secrets.
---
- name: Configure AWS credentials via OIDC
uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: arn:aws:iam::123456789:role/github-actions-terraform
aws-region: eu-west-1
---
The IAM role is configured with a trust policy limited to a specific repository and branch. The token lives for 1 hour and cannot leak from the pipeline.
What's Included in the Work (Deliverables)
| Stage | Result |
|---|---|
| Audit of current IaC | Report with recommendations, state drift assessment |
| Remote state setup | S3/DynamoDB or equivalent with locking, backups |
| Pipeline (validate+plan+apply) | Working CI/CD in your system |
| OIDC integration | IAM role, trust policy, tokens |
| Auto-comments | PR notifications with diff |
| Documentation and training | README, runbook, 2-hour session |
| Warranty | Free support for 1 month after implementation |
Timeline and How to Start
Basic plan/apply integration in GitLab CI with remote state — from 1 day. Setup with Atlantis, OIDC, auto-comments, and workspace strategy for multiple environments — from 2 to 3 days. Cost is calculated individually after an audit of your current IaC infrastructure. Get a consultation — our engineers will assess your project and propose the optimal solution.







