GitHub Actions Pipeline: Automated Plan & Apply Workflows
In a collaborative engineering organization, running terraform apply directly from a developer's local laptop is an anti-pattern. If an engineer applies changes locally, there is no centralized log of the deployment, no automated validation of HCL syntax, and no guarantee that the local environment matches the remote master branch.
To achieve operational consistency, we must automate our infrastructure delivery using a GitOps CI/CD pipeline via GitHub Actions.
[ Developer PR ] ──> [ GitHub Actions ] ──> Runs: fmt, validate, security, plan
│
Comment plan in PR for review
▼
[ Merge to Main ] ──> [ Manual Gate ] ── Approved? ──> [ 'terraform apply' ]
Key Stages of a Production GitOps Pipeline
- Linting and Validation: Catch syntax errors (
terraform validate) and formatting drift (terraform fmt -check) before allocating computing resources. - Security Audits: Run tools like
tfsecortrivyto detect common security misconfigurations (e.g. open ports, unencrypted S3 buckets). - Pull Request Plans: Automatically calculate the infrastructure changes and post the visual diff directly inside the Pull Request conversation.
- Gated Continuous Deployment: Apply the changes only when code is merged to the main branch, utilizing GitHub Environments to enforce manual peer-approval gates.
Hands-on: Building the GitHub Actions Workflow
Create a file .github/workflows/terraform.yml in your repository:
# .github/workflows/terraform.yml
name: "Terraform GitOps Pipeline"
on:
pull_request:
branches: [ main ]
paths:
- 'infrastructure/**'
push:
branches: [ main ]
paths:
- 'infrastructure/**'
permissions:
id-token: write # Required to fetch temporary AWS credentials via OIDC
contents: read # Required to checkout code
pull-requests: write # Required to write plan comments back to PR threads
jobs:
validate-and-plan:
name: "1. Lint, Validate & Plan"
runs-on: ubuntu-latest
if: github.event_name == 'pull_request'
steps:
- name: Checkout Code
uses: actions/checkout@v4
- name: Configure AWS Credentials (OIDC)
uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: ${{ secrets.AWS_TERRAFORM_ROLE_ARN }} # temporary passwordless credentials
aws-region: us-east-1
- name: Setup Terraform
uses: hashicorp/setup-terraform@v3
with:
terraform_version: "1.7.0"
- name: Format Check
run: terraform fmt -check
working-directory: infrastructure/environments/prod
- name: Initialize Workspace
run: terraform init
working-directory: infrastructure/environments/prod
- name: Validate Syntax
run: terraform validate
working-directory: infrastructure/environments/prod
- name: Security Scan (tfsec)
uses: aquasecurity/tfsec-action@v1.0.0
with:
working_directory: infrastructure/environments/prod
- name: Calculate Infrastructure Plan
id: plan
run: terraform plan -no-color -out=tfplan
working-directory: infrastructure/environments/prod
# Inject plan logs back into the PR conversation thread
- name: Post Plan Comments to PR
uses: actions/github-script@v7
with:
script: |
const plan = `${{ steps.plan.outputs.stdout }}`;
const body = `#### 🤖 Terraform Execution Plan Result:
\`\`\`hcl
${plan.substring(0, 60000)}
\`\`\``;
github.rest.issues.createComment({
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
body: body
})
deploy-production:
name: "2. Deploy to Production"
runs-on: ubuntu-latest
if: github.ref == 'refs/heads/main' && github.event_name == 'push'
environment: production # Enforces manual organization approval gate
steps:
- name: Checkout Code
uses: actions/checkout@v4
- name: Configure AWS Credentials (OIDC)
uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: ${{ secrets.AWS_TERRAFORM_ROLE_ARN }}
aws-region: us-east-1
- name: Setup Terraform
uses: hashicorp/setup-terraform@v3
with:
terraform_version: "1.7.0"
- name: Initialize Workspace
run: terraform init
working-directory: infrastructure/environments/prod
- name: Execute Changes (Apply)
run: terraform apply -auto-approve
working-directory: infrastructure/environments/prod
Enforcing the Safety Gates
- GitHub Environment: Inside your GitHub repository settings, create an environment named
production. Check Required Reviewers and add your team leads. - Branch Protection Rules: Enforce that the
mainbranch requires at least one approving review, and all status checks (1. Lint, Validate & Plan) must pass successfully before code can be merged.
By locking down direct production write access and funneling all deployments through an audited, multi-stage GitOps pipeline, you ensure absolute architectural stability and eliminate operational errors across your cloud environments.
Next Steps
Your pipeline is operational. However, as developers operate on AWS, changes will inevitably occur outside our pipeline—either through manual console fixes during outages or external automation. This is called Infrastructure Drift.
In the next lesson, we will explore how to detect drift automatically, run surgical State Surgery using CLI tools, and import untracked resources safely.