Terraform Part5

EC2 on AWS using Terraform

Overview

  • Deploy an EC2 instance on AWS running a simple Docker container (Nginx).
  • Create a custom VPC instead of using the default one.
  • Set up a subnet in one Availability Zone (AZ) for a simple use case with one server.
  • Connect the VPC to the Internet using an Internet Gateway, allowing inbound/outbound traffic.
  • Deploy EC2 instance inside the configured subnet.
  • Simulate a web application deployment by accessing the Nginx container via a web browser.
  • Configure firewall rules (Security Group):
  • Open HTTP (80) to access Nginx.
  • Open SSH (22) to connect to the server remotely.

Note: When you have a multi-server deployment, basically you just must create this whole network and infrastructure configuration once. So, you have just one VPC, maybe multiple subnets and you just deploy all the servers inside.

VPC s Subnet:

 

Create a custom VPC (myapp-vpc) and a subnet (myapp-subnet) inside an availability zone of the selected region.

Use input variables for:

  • vpc_cidr_block and subnet_cidr_block to define CIDR ranges dynamically.
  • availability_zone for flexibility.
  • env_prefix to prepend environment names (e.g., dev-vpc).

String Interpolation: Use ${var.variable_name} inside strings for dynamic naming (e.g., ${var.env_prefix}-vpc).

  •  

# Configure the AWS Provider provider “aws” {

}

#variables

variable “vpc_cidr_block” {} variable “subnet_cidr_block” {} variable “avail_zone” {} variable “env_prefix” {}

# Create a VPC

resource “aws_vpc” “terraform-vpc-demo” { cidr_block = var.vpc_cidr_block

tags = {

Name = “${var.env_prefix}-vpc”

}

}

# Create a VPC subnet under the VPC, we will create resource “aws_subnet” “terraform-subnet-1” {

vpc_id = aws_vpc.terraform-vpc-demo.id cidr_block = var.subnet_cidr_block availability_zone = var.avail_zone

tags = {

Name = “${var.env_prefix}-subnet-1″

}

}

terraform.tfvars:

vpc_cidr_block = “10.0.0.0/16”

subnet_cidr_block = “10.0.10.0/24”

avail_zone = “eu-central-1b”

env_prefix = “dev”

Route Table s Internet Gateway:

Internet Gateway will be used to expose the VPC to the Internet. AWS automatically generates a Route table and network ACL ,security Group for every new VPC. The

route table acts as a virtual router managing internal traffic, while the network ACL

(firewall for subnets) allows all traffic by default but can be customized.

Default Route Table:

  • Handles internal VPC traffic (e.g., EC2 instances) by routing within the VPC’s CIDR block (10.0.0.0/16).
  • No internet connectivity by default; all traffic stays inside the VPC.

Custom Route Table with Terraform:

  • Creating a new route table with two routes:
  1. Internal traffic (handled automatically).
  2. Internet-bound traffic (configured manually with an IGW).

Internet Gateway (IGW):

  • Required to connect the VPC to the internet.
  • Defined using aws_internet_gateway and linked to the VPC.
  • Referenced in the custom route table to allow external access.

By setting up a new route in new route table and IGW, the VPC will support both

internal communication and internet connectivity as per the Terraform configuration.

Route Table & Subnet Association:

  • A route table is created inside the VPC, but subnets must be explicitly associated with it.
  • By default, unassociated subnets are linked to the main route table of the VPC.

Terraform Implementation:

  • Terraform defines subnet-to-route table associations using two parameters: subnet ID and route table ID.
  • Once associated, all traffic from the subnet (including EC2 instances) will be handled by this route table.

Create new Internet Gateway , Route table and Route subnet association:

# Create internet gateway

resource “aws_internet_gateway” “terraform-gateway” { vpc_id = aws_vpc.terraform-vpc-demo.id

tags = {

Name = “${var.env_prefix}-gateway”

}

}

#Create route-table for VPC

resource “aws_route_table” “terraform-route-table” { vpc_id = aws_vpc.terraform-vpc-demo.id

route {

cidr_block = “0.0.0.0/0”

gateway_id = aws_internet_gateway.terraform-gateway.id

}

tags = {

Name = “${var.env_prefix}-route-table”

}

}

# Associate the route-table we create to subnet

resource “aws_route_table_association” “a-rtb-subnet” { subnet_id = aws_subnet.terraform-subnet-1.id route_table_id = aws_route_table.terraform-route-table.id

}

Note:

Tags help identify resources and associate them with a specific environment. Terraform automatically determines the order of resource creation, ensuring dependencies are managed without manual ordering.

The route table acts as a virtual router, while the Internet Gateway serves as a virtual modem

This setup ensures internal communication within the VPC while enabling external connectivity via Terraform-managed resources.

Use the Main(Default) Route Table

Option to Use Default Route Table:

  • Instead of creating a new route table, we can modify the default route table provided by AWS.
  • Terraform allows referencing the default route table ID instead of defining a new VPC ID.

Subnet Association Behaviour:

  • No need for explicit subnet associations since all unassociated subnets are automatically linked to the main route table.
  • This simplifies configuration while ensuring proper traffic routing.

#Use default route table

resource “aws_default_route_table” “terraform-default-rtb” { default_route_table_id = aws_vpc.terraform-vpc-demo.default_route_table_id route {

cidr_block = “0.0.0.0/0”

gateway_id = aws_internet_gateway.terraform-gateway.id

}

tags = {

Name = “${var.env_prefix}-route-table”

}

}

Security group

Purpose of Security Group:

  • Allows SSH access (port 22) and NGINX web access (port 8080).
  • Associated with VPC to secure EC2 instances inside it.

Security Group Rules:

  • Ingress (Incoming Traffic Rules):

      • SSH (Port 22) → Restricted to specific IP (laptop IP).

      • HTTP (Port 8080) → Open to all IPs for web access.

  • Egress (Outgoing Traffic Rules):

      • Allows all outbound traffic for software installations, updates, and Docker image pulls.

variable “my_ip” {}

# Create security group

resource “aws_security_group” “terraform-sg” { name = “terraform-sg”

description = “Allow TLS inbound traffic” vpc_id = aws_vpc.terraform-vpc-demo.id ingress {

description = “TLS from VPC” from_port = 22

to_port = 22

protocol = “tcp” cidr_blocks = [var.my_ip]

}

ingress {

description = “TLS from VPC” from_port = 8080

to_port = 8080

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”] ipv6_cidr_blocks = [“::/0”]

}

tags = {

Name = “${var.env_prefix}-sg”

}

}

Security group is now ready for EC2 deployment with correct firewall rules . Possible to reuse the default security group instead of creating a new one. Default security groups start with all traffic blocked, requiring modifications

Use default Security Group

variable “my_ip” {}

# Use default security group

resource “aws_default_security_group” “terraform-default-sg” { vpc_id = aws_vpc.terraform-vpc-demo.id

ingress {

description = “TLS from VPC” from_port = 22

to_port = 22

protocol = “tcp” cidr_blocks = [var.my_ip]

}

ingress {

description = “TLS from VPC” from_port = 8080

to_port = 8080

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”] ipv6_cidr_blocks = [“::/0”]

}

tags = {

Name = “${var.env_prefix}-sg”

}

}

Amazon Machine Image for EC2

variable “instance_type” {} variable “public_key_location” {}

#Filter a AMI

data “aws_ami” “latest-ubuntu-linux” { most_recent = true

owners = [“099720109477”] # Ubuntu

filter {

name = “name”

values = [“ubuntu/images/hvm-ssd/ubuntu-jammy-*-amd64-server-*”]

}

filter {

name = “virtualization-type” values = [“hvm”]

}

}

#Provides an EC2 key pair resource

resource “aws_key_pair” “terraform-key-pair” { key_name = “terraform-key-pair”

public_key = file(var.public_key_location)

}

#EC2 instance

resource “aws_instance” “web” {

ami = data.aws_ami.latest-ubuntu-linux.id instance_type = var.instance_type

subnet_id = aws_subnet.terraform-subnet-1.id

vpc_security_group_ids = [aws_default_security_group.terraform-default-sg.id] availability_zone = var.avail_zone

associate_public_ip_address = true

key_name = aws_key_pair.terraform-key-pair.key_name #Excute command in the EC2 instance

user_data = file(“entry-script.sh”) # user_data = <<EOF

# #!/bin/bash

# sudo apt update -y

# sudo snap install docker.io

# sudo snap start docker

# sudo usermod -aG docker ubuntu

# sudo service ssh reload

# sudo docker run -p 8080:80 nginx

# EOF

tags = {

Name = “${var.env_prefix}-terraform-demo”

}

}

# Get the output of the public IP for the EC2 instance

output “public_ip_address” {

value = aws_instance.web.public_ip

}

EC2 Instance Setup Two required attributes: AMI (Amazon Machine Image) and

Instance Type.

Filter an AMI:

AMI is the OS image for the EC2 instance. AMI varies across regions; instead of hardcoding, query AWS dynamically using the data “aws_ami” block.

Fetching Latest AMI Dynamically:

  • Use most_recent = true to get the latest image.
  • Specify owners to filter official AWS images.
  • Use filters to refine the search by image name patterns.

Instance Type Configuration:

  • Use T2 micro (free tier eligible).
  • Make it configurable using a Terraform variable.

Customizing EC2 Deployment:

  • By default, EC2 launches in a default VPC and random subnet.
  • Specify subnet ID and security group to deploy EC2 inside a custom VPC.
  • Assign availability zone using a variable.
  • Enable public IP for SSH and web access.

SSH Key Pair for Access:

We can attach SSH key pair access in two ways.

One way:

We can Create a key pair (server-key-pair) in AWS and Download and secure the PEM file (~/.ssh directory, restrict permissions).

Use key_name to associate EC2 with the key pair. Example: key_name = “server-key-pair”

Other Way:

Steps to Automate Key Pair Creation and SSH Access Using Terraform

  • Generate SSH Key Pair using ssh-keygen (id_rsa C id_rsa.pub).
  • Extract the public key from id_rsa.pub and Store it as a variable (my_public_key) so each user can override it
  • Alternatively read it from a file:

variable “my_public_key” {

default = file(“~/.ssh/id_rsa.pub”)

}

  • Create an AWS Key Pair in Terraform by referring my_public_key variable

resource “aws_key_pair” “my_key” { key_name = “my-key”

public_key = var.my_public_key

}

  • In the EC2 resource, replace hardcoded key reference:

key_name = aws_key_pair.my_key.key_name

 

Advantages of Automating Key Pair Creation

Automating key creation in Terraform prevents manual errors, ensures consistency by allowing developers to use their own keys without modifying code, and simplifies clean-up by removing all resources, including keys, during terraform destroy. It enables easy replication across development, staging, and production environments while eliminating forgotten manual steps, ensuring everything is managed within the Terraform code, reducing the need for extra documentation.

Use user_data for Automatic Execution

Avoid manual work by defining commands as part of the Terraform configuration. Commands should execute immediately after the EC2 instance is created. user_data attribute allows running scripts on the EC2 instance at startup.

This ensures the setup process is part of the infrastructure code. Use multi-line string syntax (<<EOF … EOF) to write the script.

We can write shell script in separate file, and we can refer in user_data Ex: user_data = file(“entry-script.sh”)

Ensure Terraform Recreates Instance on user_data Changes

  • Enable replace_on_change flag for automatic instance recreation if the user data script is modified.
  • This ensures a clean, consistent state for the EC2 instance.

Terraform Output Public IP

  • Modify Terraform to output the instance’s public IP automatically.
  • Avoids manually retrieving it from the AWS Console.

Apply Configuration & Access EC2

  • Run terraform apply to deploy changes.
  • Use the displayed public IP to SSH into the instance and verify NGINX is running.
  • Now, accessing publicIP:8080 in the browser should display the NGINX homepage.

Terraform’s Role vs. Configuration Management Tools

Terraform is used for provisioning and managing infrastructure like networking, EC2 instances, and security groups, but its role ends once the infrastructure is created. It cannot debug or manage software installation, and user_data scripts execute commands without feedback, requiring manual SSH access for troubleshooting. To automate post-provisioning tasks like software installation and configuration, tools like Ansible, Puppet, or Chef are needed. Combining Terraform for infrastructure provisioning with Ansible for application deployment ensures a fully automated, scalable, and consistent setup.

Git repositories https://github.com/NeevPrashanth/neev_terraform/tree/main/ec2_aws_poc