Day 27: Terraform Infrastructure with GitHub Actions

Managing cloud infrastructure manually is time-consuming, error-prone, and difficult to scale. As organizations adopt Infrastructure as Code (IaC) with tools like Terraform, automating its deployment becomes crucial. In this comprehensive guide, we'll walk through setting up a complete CI/CD pipeline for Terraform using GitHub Actions, complete with security scanning, environment management, and a production-ready two-tier AWS architecture—based on practical implementation from our Terraform Full Course.
Why Automate Terraform with CI/CD?
Manual Terraform execution creates several challenges: inconsistent deployments, security vulnerabilities slipping through, and the dreaded "works on my machine" syndrome. A CI/CD pipeline addresses these by providing standardized, repeatable processes that include security checks, peer review, and automated state management. If you're new to Terraform concepts, I recommend checking out our Day-24-Blog which covers scalable and fault-tolerant application design fundamentals.
Our pipeline will transform how we manage infrastructure by implementing:
Pull Request Workflow: All changes go through code review
Automated Testing: Linting and security scanning on every change
Environment Separation: Distinct dev, test, and production environments
State Management: Secure, locked state files in S3
Drift Detection: Automated monitoring for infrastructure consistency
Building the Two-Tier Architecture
Before we automate, let's understand our infrastructure. We're deploying a classic two-tier architecture on AWS, as implemented in our Terraform-Full-Course-Aws repository. You can find the complete code under lessons/day27/code:
Public Layer: An Application Load Balancer (ALB) in public subnets accepts user traffic on port 80
Private Layer: EC2 instances in private subnets handle application logic
Connectivity: A NAT Gateway enables private instances to fetch updates while maintaining security
Traffic Flow: User → ALB → Private EC2 → NAT Gateway → User
This design balances accessibility with security—only the ALB and NAT Gateway face the internet, while application servers remain isolated.
The CI/CD Pipeline in Action: From Setup to Deployment
Our automation journey begins when a developer creates a feature branch and submits a pull request. Here's what happens next:
1. The Setup Foundation: Remote State Management
First, we configure remote state management using an S3 bucket. Unlike local state files that risk being lost or overwritten, S3 provides durability and native locking capabilities. Our setup is automated through a bash script located at lessons/day30/code/scripts/setup-backend.sh:
# Give execute permissions
chmod +x setup-backend.sh
# Run the setup file
./setup-backend.sh
This script creates our S3 bucket and configures Terraform to use it. Important note: We're using S3 because it supports native state file locking without needing DynamoDB, simplifying our architecture while maintaining reliability. After running the script, don't forget to update backend.tf with your newly created bucket name.
2. Environment and Secret Configuration
GitHub Environments give us granular control over deployment stages. We create three environments—dev, test, and prod—each with its own Terraform variables and approval requirements. The production environment requires manual approval from designated reviewers, adding a critical safety checkpoint before live deployment.
Security is paramount in our pipeline setup. We store AWS credentials as GitHub Secrets:
Go to your repository → Settings → Secrets and variables → Actions
Create two repository secrets:
AWS_ACCESS_KEY_IDwith your IAM user's access keyAWS_SECRET_ACCESS_KEYwith the corresponding secret key
These credentials should come from an IAM user with sufficient permissions to provision the necessary resources.
3. The Workflow Execution: YAML Configuration
Our pipeline is defined through three key YAML files in the .github/workflows/ directory:
day27-github-provisioning.yaml - Main provisioning workflow
destroy-day-27.yaml - Controlled resource destruction
drift-detection.yaml - Infrastructure consistency monitoring
When a pull request is approved, our GitHub Actions workflow executes a carefully orchestrated sequence:
Terraform Linting (tflint): Scans our Terraform code for best practices, style consistency, and potential errors
Security Scanning (Trivy): Detects vulnerabilities in container images and IaC configurations—you can test this by checking the Trivy section in your workflow runs to see all security vulnerabilities
Terraform Plan: Shows exactly what changes will be applied, providing transparency
Terraform Apply: Executes the approved changes, provisioning or updating infrastructure
State Management: Automatically saves the updated state to our S3 backend
Testing the Pipeline: Real-World Validation
The true test of any pipeline is in execution. Here's how to validate our setup:
Make Environment-Specific Changes: We have multiple
tfvarsfiles for different environments. Change something in thetfvarsfile for the dev environmentCreate and Push Feature Branch: Create a feature branch and push your modified code
Initiate Pull Request: Issue a pull request from your feature branch to the main branch
Review and Approve: Have an approver review and accept the pull request from the GitHub UI
Monitor Execution: Go to the Actions tab to watch the workflow execute in real-time
Verify Deployment: Check the Load Balancer DNS (available in AWS Console or Terraform outputs) to verify application health
Advanced Pipeline Features and Best Practices
Beyond basic provisioning, our implementation includes sophisticated capabilities:
Drift Detection: The separate drift detection workflow regularly compares actual infrastructure with Terraform state, alerting us to any manual changes or configuration drift. This ensures our IaC remains the single source of truth.
Safe Destruction Workflow: When environments are no longer needed, the destruction workflow provides a controlled process to remove all resources, preventing unexpected cloud costs. Like production deployments, this requires approvals for critical environments.
Multi-environment Strategy: Each environment uses its own variable files (.tfvars), allowing different configurations for dev, test, and prod while maintaining identical Terraform code structure.
Lessons Learned and Implementation Insights
Through this implementation, several key insights emerged:
Remote State is Non-Negotiable: Local state files simply don't work in collaborative, automated environments. Our S3 backend provides the reliability teams need.
Security Scanning Should Be Early and Often: Trivy's integration catches vulnerabilities before they reach production, turning security from an afterthought into a core part of our workflow.
Human Oversight Matters: Requiring manual approval for production changes ensures automation serves teams rather than replacing human judgment for critical decisions.
Modular Workflow Design: Separating provisioning, destruction, and drift detection into different YAML files keeps each process focused and maintainable.
Video
Conclusion
Automating Terraform with GitHub Actions transforms infrastructure management from a manual, risky process to a streamlined, secure workflow. By combining IaC principles with CI/CD automation, teams gain consistency, security, and speed—deploying infrastructure changes with the same confidence as application code.
The pipeline handles the entire lifecycle: from initial code review through security scanning to deployment and ongoing monitoring. As your infrastructure needs grow, this foundation scales with you, ensuring that managing cloud resources remains predictable, secure, and efficient.
Whether you're managing a handful of servers or hundreds of microservices, investing in Terraform automation pays dividends in reliability, security, and team productivity. Start with the basics outlined here, expand as your needs evolve, and remember: the best infrastructure is the one you can deploy confidently at 2 AM knowing your automation has your back.



