Terraform in CI/CD
A pull request can tell you what Terraform code changed. It cannot by itself tell you what infrastructure that code will change. That is why Terraform CI/CD should put plan in the review process and make apply a controlled operation.
This chapter builds a practical Terraform delivery workflow and explains authentication, plan review, approvals, and production safeguards.
Why Run Terraform in CI/CD?
A developer can run:
terraform plan
terraform applyfrom a laptop.
That is useful for learning and development, but production infrastructure introduces problems:
- Which Terraform version did they use?
- Which credentials were used?
- Which workspace was selected?
- Was the plan reviewed?
- What exactly was applied?
- Who approved it?
- Was another operation running at the same time?
- What happens when the engineer leaves the company?
CI/CD provides a controlled execution environment.
A typical workflow is:
Developer
β
βΌ
Git Push / Pull Request
β
βΌ
terraform fmt
β
βΌ
terraform validate
β
βΌ
terraform plan
β
βΌ
Review
β
βΌ
Approval / Merge
β
βΌ
terraform apply
β
βΌ
InfrastructureThe important idea is not “put Terraform into YAML.”
It is:
Move infrastructure changes into a repeatable, reviewable, auditable process.
Separate Plan From Apply
Terraform’s plan is a proposed set of infrastructure changes.
That makes it useful as a review artifact.
For example:
Pull Request
β
βΌ
Terraform Plan
β
βββ 2 to add
βββ 1 to change
βββ 0 to destroy
β
βΌ
Human review
β
βΌ
ApplyThe reviewer should be able to ask:
- Is this resource change intentional?
- Is a resource being destroyed?
- Are production dependencies affected?
- Does the plan match the pull request?
- Is the scope larger than expected?
A plan is therefore not just a debugging command.
It is part of the change-review process.
Formatting and Validation
A basic CI pipeline can start with:
terraform fmt -check
terraform init
terraform validate
terraform planFormatting checks whether the Terraform files conform to Terraform’s formatting rules.
Validation checks whether the configuration is internally valid.
Planning evaluates the configuration against the provider and state to produce a proposed change.
These checks catch different classes of problems.
fmt
β
βββ formatting
validate
β
βββ configuration validity
plan
β
βββ proposed infrastructure changesA Complete GitHub Actions Example
The following is a small, platform-specific example using GitHub Actions.
It assumes:
- Terraform configuration is in the repository root
- AWS is the infrastructure provider
- AWS credentials are provided to the workflow through GitHub’s OIDC integration
- The Terraform backend is already configured
- The repository has an AWS IAM role prepared for CI
Create:
.github/
βββ workflows/
βββ terraform.ymlPut this in .github/workflows/terraform.yml:
name: Terraform
on:
pull_request:
push:
branches:
- main
permissions:
contents: read
id-token: write
jobs:
terraform:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup Terraform
uses: hashicorp/setup-terraform@v3
- name: Configure AWS credentials
uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: ${{ secrets.AWS_TERRAFORM_ROLE_ARN }}
aws-region: us-east-1
- name: Terraform Init
run: terraform init
- name: Terraform Format Check
run: terraform fmt -check -recursive
- name: Terraform Validate
run: terraform validate
- name: Terraform Plan
run: terraform plan -out=tfplan
- name: Terraform Apply
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
run: terraform apply -auto-approve tfplanThis is intentionally small.
It demonstrates the flow without introducing an application deployment system.
Note
GitHub Actions syntax is specific to GitHub. The same Terraform principles apply to GitLab CI, Azure DevOps, Jenkins, and other systems, but their configuration syntax and authentication mechanisms differ.
Why Save a Plan?
The workflow uses:
terraform plan -out=tfplanand later:
terraform apply -auto-approve tfplanThe saved plan represents the planned operation.
This is useful because it separates:
Planfrom:
ApplyThe exact mechanics of plan artifacts and their security need careful treatment in a production pipeline, because a saved plan can contain sensitive information.
Do not treat a plan file as harmless text.
It should be protected like other infrastructure artifacts.
Why Not Just Run terraform apply?
You can automate:
terraform apply -auto-approvebut that removes an important review boundary.
For production, a safer model is often:
Code change
β
βΌ
Plan
β
βΌ
Review
β
βΌ
Approval
β
βΌ
ApplyThe approval might be implemented through:
- Pull request review
- Protected branch rules
- Environment approvals
- CI manual gates
- HCP Terraform run approval
The implementation varies, but the operational principle remains the same.
Authentication: How Does CI Reach AWS?
Terraform running in CI needs AWS credentials.
There are two broad approaches.
Static Credentials
A CI system can store an access key and secret key.
Conceptually:
CI
β
βββ AWS_ACCESS_KEY_ID
βββ AWS_SECRET_ACCESS_KEYThis works, but long-lived credentials create risk.
They can:
- Expire unexpectedly
- Be copied
- Leak through logs or configuration
- Remain valid after a pipeline is no longer used
- Require manual rotation
Short-Lived or Federated Credentials
A better modern pattern is workload identity federation.
For GitHub Actions, the workflow can obtain an identity token and exchange it for temporary AWS credentials through an IAM role configured for OIDC.
Conceptually:
GitHub Actions
β
β OIDC identity
βΌ
AWS IAM Role
β
β temporary credentials
βΌ
Terraform
β
βΌ
AWSThe exact AWS trust-policy setup is outside this chapter’s scope.
The important lesson is:
CI should preferably receive short-lived credentials with only the permissions required for the Terraform operation.
Least Privilege Applies to CI
The CI role should not automatically be:
AdministratorAccesssimply because Terraform is running.
Its permissions should reflect what the configuration needs.
There is a second boundary as well:
AWS permissions
+
Terraform state permissionsFor example, a production CI role may be allowed to:
- Read and write the production state object
- Acquire and release its lock
- Manage the production resources
while having no access to development state.
That makes state isolation part of the security design.
State Locking Still Matters in CI
Suppose two pull requests merge close together:
PR A βββΊ CI βββΊ terraform apply
PR B βββΊ CI βββΊ terraform applyIf both target the same state, the backend must coordinate those operations.
Remote state with locking provides the synchronization boundary.
CI A βββ
β
CI B βββΌβββΊ Remote Backend + Lock
β
CI C βββCI does not replace state locking.
It makes locking even more important because automation can produce concurrent operations faster than humans.
Production Approval
A common production pattern is:
Pull Request
β
βΌ
Automated checks
β
βΌ
Terraform plan
β
βΌ
Human review
β
βΌ
Merge
β
βΌ
Production applyFor higher-risk environments, you can add another approval boundary before apply.
The goal is not to make every Terraform change painfully manual.
The goal is to make high-impact changes deliberate.
Destroy Protection
Destroy is particularly dangerous in automation.
A CI pipeline that can automatically destroy production infrastructure because a configuration changed unexpectedly is a serious operational risk.
You can reduce that risk through:
- Protected production environments
- Manual approvals
- Restricted IAM permissions
- Review policies
- Resource lifecycle controls where appropriate
- Separate production workflows
Do not rely on one mechanism alone.
What Happens When Apply Fails?
A failed Terraform apply does not necessarily mean “nothing changed.”
Terraform can successfully create several resources and then fail later.
For example:
Create VPC β
Create subnet β
Create database β
Create service βThe infrastructure may now be partially changed.
Terraform records state as operations progress.
The correct response is normally to inspect the resulting state and run another plan after resolving the underlying issue.
Do not immediately delete state or rerun destructive commands blindly.
A useful operational loop is:
Apply fails
β
βΌ
Inspect error
β
βΌ
Inspect state
β
βΌ
Fix cause
β
βΌ
terraform plan
β
βΌ
ContinuePlan Review Is Not Perfect
A plan is valuable, but it is not a guarantee that nothing unexpected can happen.
Providers can have behavior that depends on external conditions.
Infrastructure can also change outside Terraform.
CI therefore reduces risk; it does not eliminate the need for understanding.
A good Terraform pipeline preserves:
- Plan visibility
- Human review where appropriate
- State locking
- Strong authentication
- Least privilege
- Auditability
- Production boundaries
A Practical Pipeline Model
A mature workflow can be thought of as:
Git
β
βΌ
Pull Request
β
ββββββββ΄βββββββ
βΌ βΌ
fmt validate
β β
ββββββββ¬βββββββ
βΌ
plan
β
βΌ
review
β
βΌ
approval
β
βΌ
apply
β
βΌ
Remote State
β
βΌ
InfrastructureThis is the core Terraform CI/CD pattern.
The next chapter looks at what happens when you use a managed service designed specifically to provide this collaboration model: HCP Terraform.