DEV Community

Deep Datta
Deep Datta

Posted on

Deploying Nginx on AWS EC2 Using Terraform — Automating Infrastructure

Been meaning to do a proper Terraform project for a while. This weekend I finally did it.

The goal was simple: use Terraform to provision AWS infrastructure from scratch and have a running Nginx server at the end of it.


What I Built

Here's the architecture at a glance:

A browser hits an Internet Gateway, which routes traffic into a VPC containing a public subnet. Inside that subnet lives an EC2 instance running Nginx in a Docker container, exposed on port 8080.


Project Structure

.
├── main.tf
├── terraform.tfvars
└── entrypoint.sh
Enter fullscreen mode Exit fullscreen mode

Step 1: Variables

Instead of hardcoding values, I declared variables and supplied them via terraform.tfvars. This keeps things reusable and environment-aware.

terraform.tfvars

vpc_cidr_block      = "10.0.0.0/16"
subnet_1_cidr_block = "10.0.10.0/24"
env_prefix          = "dev"
avail_zone          = "ap-south-1a"
my_ip               = "YOUR_IP/32"   # replaced with my actual IP
instance_type       = "t2.micro"
Enter fullscreen mode Exit fullscreen mode

⚠️ Never commit your real IP or credentials to a public repo. Add terraform.tfvars to .gitignore.


Step 2: Provider

provider "aws" {
  region = "ap-south-1"
}
Enter fullscreen mode Exit fullscreen mode

This tells Terraform to use AWS and deploy everything into the Mumbai region (ap-south-1).


Step 3: Networking

VPC

resource "aws_vpc" "myapp-vpc" {
  cidr_block = var.vpc_cidr_block
  tags = {
    Name = "${var.env_prefix}-vpc"
  }
}
Enter fullscreen mode Exit fullscreen mode

This creates an isolated virtual network with the CIDR 10.0.0.0/16, providing me 65,536 private IP addresses to work with.

Subnet

resource "aws_subnet" "myapp-subnet-1" {
  vpc_id            = aws_vpc.myapp-vpc.id
  cidr_block        = var.subnet_1_cidr_block
  availability_zone = var.avail_zone
  tags = {
    Name = "${var.env_prefix}-subnet-1"
  }
}
Enter fullscreen mode Exit fullscreen mode

This carves out a /24 subnet (10.0.10.0/24) — 256 addresses — inside the VPC, pinned to ap-south-1a.

Internet Gateway

resource "aws_internet_gateway" "myapp-igw" {
  vpc_id = aws_vpc.myapp-vpc.id
  tags = {
    Name = "${var.env_prefix}-internet-gateway"
  }
}
Enter fullscreen mode Exit fullscreen mode

By default, a VPC is completely isolated. The Internet Gateway is what allows it to communicate with the outside world.

Route Table

resource "aws_default_route_table" "main-rtb" {
  default_route_table_id = aws_vpc.myapp-vpc.default_route_table_id
  route {
    cidr_block = "0.0.0.0/0"
    gateway_id = aws_internet_gateway.myapp-igw.id
  }
  tags = {
    Name = "${var.env_prefix}-main-rtb"
  }
}
Enter fullscreen mode Exit fullscreen mode

Rather than creating a new route table and manually associating it with the subnet, I modified the default route table that AWS automatically creates with the VPC. The 0.0.0.0/0 route sends all outbound traffic through the Internet Gateway.

Note: I initially wrote a custom aws_route_table resource, but switched to aws_default_route_table to keep things simpler — it's automatically associated with all subnets that don't have an explicit association.


Step 4: Security Group

resource "aws_default_security_group" "default-sg" {
  vpc_id = aws_vpc.myapp-vpc.id

  ingress {
    from_port   = 22
    to_port     = 22
    protocol    = "tcp"
    cidr_blocks = [var.my_ip]   # SSH only from your machine
  }

  ingress {
    from_port   = 80
    to_port     = 80
    protocol    = "tcp"
    cidr_blocks = ["0.0.0.0/0"] # HTTP open to the world
  }

  egress {
    from_port       = 0
    to_port         = 0
    protocol        = "-1"
    cidr_blocks     = ["0.0.0.0/0"]
    prefix_list_ids = []
  }

  tags = {
    Name = "${var.env_prefix}-default-sg"
  }
}
Enter fullscreen mode Exit fullscreen mode

Two ingress rules:

  • Port 22 (SSH) — locked down to your IP only. No random internet scanners getting in.
  • Port 80 (HTTP) — open to everyone so the web server is publicly accessible. All outbound traffic is allowed (protocol = "-1" means all protocols).

Step 5: Fetching the Latest Ubuntu AMI Dynamically

data "aws_ami" "ubuntu" {
  most_recent = true
  owners      = ["amazon"]

  filter {
    name   = "name"
    values = ["ubuntu/images/hvm-ssd/ubuntu-*"]
  }

  filter {
    name   = "virtualization-type"
    values = ["hvm"]
  }
}

output "ami_id" {
  value = data.aws_ami.ubuntu.id
}
Enter fullscreen mode Exit fullscreen mode

Instead of hardcoding an AMI ID (which is region-specific and goes stale), I used a data source to dynamically fetch the most recent Ubuntu HVM AMI. The output block prints the resolved AMI ID after terraform apply, which is handy for debugging.


Step 6: EC2 Instance

resource "aws_instance" "myapp-instance" {
  ami                    = data.aws_ami.ubuntu.id
  instance_type          = var.instance_type
  subnet_id              = aws_subnet.myapp-subnet-1.id
  vpc_security_group_ids = [aws_default_security_group.default-sg.id]
  availability_zone      = var.avail_zone

  user_data = file("entrypoint.sh")

  tags = {
    Name = "${var.env_prefix}-server"
  }
}
Enter fullscreen mode Exit fullscreen mode

The instance is a t2.micro (free-tier eligible), placed in my subnet with the security group I defined. The user_data field passes entrypoint.sh as a bootstrap script that runs once on first boot.


Step 7: The Bootstrap Script

entrypoint.sh

#!/bin/bash
sudo apt update -y
sudo apt install docker.io -y
sudo systemctl start docker
sudo usermod -aG docker ubuntu
sudo systemctl restart docker
sudo docker run -d -p 8080:80 nginx
Enter fullscreen mode Exit fullscreen mode

This script:

  1. Updates the package index
  2. Installs Docker
  3. Starts the Docker daemon
  4. Adds the ubuntu user to the docker group
  5. Pulls and runs the official Nginx image, mapping host port 8080 → container port 80

Deploying

# Initialize — downloads the AWS provider plugin
terraform init

# Preview what will be created
terraform plan

# Apply — this actually creates the resources
terraform apply
Enter fullscreen mode Exit fullscreen mode

Then visit http://<public-ip>:8080 in your browser — you should see the Nginx welcome page.


All the code for this project is available on github

Top comments (0)