Table of Contents
Introduction
Automating infrastructure deployments is a critical practice for modern DevOps teams. By integrating Terraform with GitHub Actions, you can create powerful CI/CD pipelines that automatically plan, validate, and apply infrastructure changes whenever code is pushed to your repository. This approach eliminates manual deployment steps, reduces human error, and ensures consistent infrastructure across environments.
In this comprehensive guide, we’ll walk through setting up a production-ready GitHub Actions workflow for Terraform, covering everything from basic configuration to advanced patterns like environment-specific deployments and pull request previews.
Prerequisites
Before getting started, ensure you have the following:
- A GitHub repository containing your Terraform configuration
- An AWS account (or another cloud provider) with appropriate permissions
- Terraform state stored remotely (e.g., in an S3 bucket)
- Basic familiarity with GitHub Actions and Terraform
Setting Up AWS Credentials
The first step is to securely store your cloud provider credentials as GitHub Secrets. Navigate to your repository’s Settings → Secrets and variables → Actions and add the following secrets:
AWS_ACCESS_KEY_ID— Your AWS access keyAWS_SECRET_ACCESS_KEY— Your AWS secret keyAWS_REGION— Your preferred AWS region (e.g.,us-east-1)
For production environments, consider using OpenID Connect (OIDC) instead of long-lived credentials. This approach is more secure and eliminates the need to rotate access keys.
OIDC Configuration for AWS
# oidc.tf — Create an OIDC provider for GitHub Actions
resource "aws_iam_openid_connect_provider" "github" {
url = "https://token.actions.githubusercontent.com"
client_id_list = ["sts.amazonaws.com"]
thumbprint_list = ["6938fd4d98bab03faadb97b34396831e3780aea1"]
}
resource "aws_iam_role" "github_actions" {
name = "github-actions-terraform"
assume_role_policy = jsonencode({
Version = "2012-10-17"
Statement = [
{
Effect = "Allow"
Principal = {
Federated = aws_iam_openid_connect_provider.github.arn
}
Action = "sts:AssumeRoleWithWebIdentity"
Condition = {
StringEquals = {
"token.actions.githubusercontent.com:aud" = "sts.amazonaws.com"
}
StringLike = {
"token.actions.githubusercontent.com:sub" = "repo:your-org/your-repo:*"
}
}
}
]
})
}
Basic GitHub Actions Workflow
Create a workflow file at .github/workflows/terraform.yml:
name: Terraform CI/CD
on:
push:
branches: [main]
pull_request:
branches: [main]
permissions:
contents: read
pull-requests: write
id-token: write
env:
TF_VERSION: "1.7.0"
AWS_REGION: "us-east-1"
jobs:
terraform:
name: Terraform Plan & Apply
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Configure AWS credentials
uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: arn:aws:iam::role/github-actions-terraform
aws-region: ${{ env.AWS_REGION }}
- name: Setup Terraform
uses: hashicorp/setup-terraform@v3
with:
terraform_version: ${{ env.TF_VERSION }}
- name: Terraform Format Check
run: terraform fmt -check -recursive
- name: Terraform Init
run: terraform init
- name: Terraform Validate
run: terraform validate
- name: Terraform Plan
id: plan
run: terraform plan -no-color -out=tfplan
continue-on-error: true
- name: Terraform Apply
if: github.ref == 'refs/heads/main' && github.event_name == 'push'
run: terraform apply -auto-approve tfplan
Adding Pull Request Comments
One of the most valuable features is posting the Terraform plan output as a comment on pull requests. This allows team members to review infrastructure changes before they’re applied:
- name: Comment PR with Plan
if: github.event_name == 'pull_request'
uses: actions/github-script@v7
with:
script: |
const plan = `${{ steps.plan.outputs.stdout }}`;
const truncated = plan.length > 60000
? plan.substring(0, 60000) + '\n\n... (truncated)'
: plan;
const body = `## Terraform Plan Output
\`\`\`hcl
${truncated}
\`\`\`
*Plan: ${{ steps.plan.outcome }}*`;
github.rest.issues.createComment({
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
body: body
});
Multi-Environment Deployments
For organizations managing multiple environments (development, staging, production), you can use a matrix strategy or separate workflows:
jobs:
terraform:
name: Deploy to ${{ matrix.environment }}
runs-on: ubuntu-latest
strategy:
matrix:
environment: [dev, staging, prod]
max-parallel: 1
environment: ${{ matrix.environment }}
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup Terraform
uses: hashicorp/setup-terraform@v3
- name: Terraform Init
run: |
terraform init \
-backend-config="key=${{ matrix.environment }}/terraform.tfstate"
- name: Terraform Plan
run: |
terraform plan \
-var-file="environments/${{ matrix.environment }}.tfvars" \
-out=tfplan
- name: Terraform Apply
if: github.ref == 'refs/heads/main'
run: terraform apply -auto-approve tfplan
Adding Security Scanning
Integrate security scanning tools to catch misconfigurations before they reach production:
- name: Run tfsec
uses: aquasecurity/[email protected]
with:
soft_fail: true
- name: Run Checkov
uses: bridgecrewio/checkov-action@v12
with:
directory: .
framework: terraform
soft_fail: true
State Locking and Backend Configuration
Ensure your Terraform backend is properly configured for CI/CD:
# backend.tf
terraform {
backend "s3" {
bucket = "my-terraform-state"
key = "infrastructure/terraform.tfstate"
region = "us-east-1"
dynamodb_table = "terraform-locks"
encrypt = true
}
}
Best Practices
1. Never Store Secrets in Code
Always use GitHub Secrets or a secrets manager like AWS Secrets Manager or HashiCorp Vault. Never commit credentials, API keys, or sensitive values to your repository.
2. Use Remote State with Locking
Always use a remote backend with state locking enabled. This prevents concurrent modifications and ensures state consistency when multiple pipeline runs occur.
3. Require Plan Approval for Production
For production environments, use GitHub’s environment protection rules to require manual approval before terraform apply runs:
environment:
name: production
url: https://console.aws.amazon.com
4. Pin Provider and Terraform Versions
Always pin your Terraform and provider versions to avoid unexpected changes:
terraform {
required_version = "~> 1.7.0"
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 5.30"
}
}
}
5. Use Terraform Workspaces Carefully
While workspaces can be useful, prefer separate state files per environment for better isolation and security in CI/CD pipelines.
Troubleshooting Common Issues
Pipeline Timeout
Terraform operations on large infrastructures can take a long time. Increase the job timeout:
jobs:
terraform:
timeout-minutes: 60
State Lock Conflicts
If a pipeline fails mid-apply, the state might remain locked. Use terraform force-unlock <LOCK_ID> carefully, or configure automatic lock expiration on your DynamoDB table.
Plan Output Too Large
GitHub has a 65,536 character limit for PR comments. Truncate the plan output or upload it as an artifact instead.
Conclusion
Integrating Terraform with GitHub Actions creates a robust, automated infrastructure deployment pipeline that enforces code review, security scanning, and consistent deployments across environments. By following the patterns and best practices outlined in this guide, you can build a CI/CD workflow that scales with your infrastructure needs while maintaining security and reliability.
Start with the basic workflow and gradually add features like multi-environment support, security scanning, and PR comments as your team’s needs evolve. The investment in automation pays dividends in reduced deployment errors and faster iteration cycles.

