From Zero to Continuous Delivery: Automating AWS EC2 and Infrastructure with Terraform
How many times have you watched a deployment turn into a late-night firefighting session because someone manually tweaked a security group in the AWS console? We have all been there, staring at a broken staging environment at 11 PM, wondering why the working branch on our local machine refuses to behave in the cloud. Moving from basic continuous integration to true continuous delivery is the single best upgrade you can give your engineering workflow, yet most teams treat infrastructure as an afterthought.
The Problem Everyone Ignores
Most engineering teams think they have a CI/CD pipeline because they run automated tests on every pull request and push green builds to a registry. But the moment code needs to land on a production AWS EC2 instance, the illusion shatters. Someone SSHs into the server, runs a quick git pull, restarts a systemd service, and calls it a day.
This manual drift is a ticking time bomb waiting for your next major release. When your infrastructure lives in someone's head or a hastily edited console configuration, reproducibility drops to zero. You cannot scale a team or audit a system when every server is a unique snowflake pet project.
The pain compounds exponentially when you need to spin up a fresh environment for a new microservice or disaster recovery. Without Infrastructure as Code, you spend days clicking through dashboards, missing crucial IAM policies, and misaligning VPC subnets. True delivery requires treating your servers and your pipeline scripts with the exact same rigor as your core application code.
What Actually Works
To achieve true continuous delivery, we need to decouple our application deployment from manual server management entirely. We combine Terraform for declarative infrastructure provisioning with a robust GitHub Actions workflow that handles both infrastructure validation and zero-downtime EC2 deployments.
Instead of mutating existing servers, we use Terraform to define our entire AWS footprint—VPCs, subnets, security groups, and EC2 launch configurations—as version-controlled code. When an update arrives, our pipeline provisions clean infrastructure or safely updates existing targets without human intervention.
By automating the state management and building immutable artifacts, you ensure that what runs in production is identical to what ran in staging. Let us look at how a foundational Terraform configuration sets up our target infrastructure cleanly and securely.
terraform {
required_version = ">= 1.5.0"
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 5.0"
}
}
}
provider "aws" {
region = var.aws_region
}
resource "aws_vpc" "main" {
cidr_block = "10.0.0.0/16"
enable_dns_hostnames = true
enable_dns_support = true
tags = {
Name = "production-vpc"
Environment = "production"
}
}
This configuration initializes our AWS provider and establishes a dedicated Virtual Private Cloud with DNS support enabled, giving us a secure networking foundation for our EC2 deployment.
Step-by-Step: Let's Build It Together
Let us walk through building a complete automated pipeline that provisions an EC2 instance via Terraform and deploys your application code whenever a pull request merges to main.
First, we need to define our EC2 instance resource and associate it with our custom security group and subnet to handle incoming traffic safely.
resource "aws_security_group" "web_sg" {
name = "web-server-sg"
description = "Security group for web servers"
vpc_id = aws_vpc.main.id
ingress {
from_port = 80
to_port = 80
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
}
egress {
from_port = 0
to_port = 0
protocol = "-1"
cidr_blocks = ["0.0.0.0/0"]
}
}
resource "aws_instance" "web" {
ami = "ami-0c55b159cbfafe1f0"
instance_type = "t3.micro"
subnet_id = aws_subnet.public.id
vpc_security_group_ids = [aws_security_group.web_sg.id]
tags = {
Name = "Production-Web-Server"
}
}
This snippet provisions a locked-down security group allowing standard HTTP traffic and spins up a t3.micro EC2 instance inside our public subnet.
Next, we configure our GitHub Actions workflow to automate the entire lifecycle, running terraform apply and deploying the application artifact directly to our running instance.
name: CI-CD-Pipeline
on:
push:
branches: [ "main" ]
jobs:
terraform-and-deploy:
runs-on: ubuntu-latest
steps:
- name: Checkout Code
uses: actions/checkout@v4
- name: Setup Terraform
uses: hashicorp/setup-terraform@v3
- name: Terraform Init & Apply
run: |
terraform init
terraform apply -auto-approve
env:
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
AWS_REGION: "us-east-1"
This workflow file checks out your repository, initializes the Terraform CLI, and automatically applies your infrastructure changes to AWS using securely stored repository secrets.
The Mistakes That Will Burn You
- Mistake 1: Storing your Terraform state file locally on the CI runner or your laptop, which inevitably leads to state lock conflicts and corrupted infrastructure when multiple engineers push updates.
- Mistake 2: Hardcoding sensitive AWS credentials or database passwords directly inside your repository files instead of utilizing encrypted GitHub Secrets and IAM roles.
- Mistake 3: Failing to implement proper health checks in your deployment script, causing the pipeline to report a successful release even when the web application fails to boot on the EC2 instance.
Production Checklist
- Use remote state storage: Always configure an S3 bucket with DynamoDB locking for your Terraform state backend before collaborating with a team.
- Implement automated rollbacks: Ensure your deployment script can revert to the previous application version if health checks fail post-deployment.
- Never do this: Run terraform apply without reviewing the execution plan output first in a staging or review environment.
Key Takeaways
- Treat your infrastructure definitions as immutable application code using Terraform.
- Automate your security groups, VPCs, and EC2 provisioning to eliminate configuration drift.
- Secure your continuous delivery pipeline with robust remote state management and encrypted secrets.
- Always validate infrastructure changes through a preview plan before executing production deployments.
Engr. Hamza | AI & MLOps Engineer | Building autonomous systems at the edge of possibility
Top comments (0)