DEV Community

Cover image for πŸ—οΈ Provision AWS EC2 Instances with Terraform and Set Up Docker via User Data
Lester Diaz Perez
Lester Diaz Perez

Posted on

πŸ—οΈ Provision AWS EC2 Instances with Terraform and Set Up Docker via User Data

πŸ“‹Workflow

1️⃣ πŸ“ Structure
2️⃣ 🌱 Root module
3️⃣ πŸ’» EC2 module
4️⃣ πŸ›‘οΈ Security Group module

πŸ”—Link to project


1️⃣ πŸ“ Structure

β”œβ”€β”€ dev.tfvars
β”œβ”€β”€ main.tf
β”œβ”€β”€ provider.tf
β”œβ”€β”€ modules
β”‚   β”œβ”€β”€ ec2
β”‚   β”‚   β”œβ”€β”€ install_docker.sh
β”‚   β”‚   β”œβ”€β”€ main.tf
β”‚   β”‚   β”œβ”€β”€ output.tf
β”‚   β”‚   └── variables.tf
β”‚   └── security_group
β”‚       β”œβ”€β”€ igw.tf
β”‚       β”œβ”€β”€ output.tf
β”‚       β”œβ”€β”€ rt.tf
β”‚       β”œβ”€β”€ sg.tf
β”‚       β”œβ”€β”€ subnet.tf
β”‚       └── vpc.tf

Enter fullscreen mode Exit fullscreen mode

Why it's important to split terraform into modules?

  1. πŸ”„ Code reuse
  2. πŸ“ˆ Improved scalability
  3. 🧩 Modularity and abstraction
  4. πŸ‘₯ Clear separation of responsibilities.
  5. πŸ› οΈ Simplified maintenance
  6. πŸ“– Improved readability
  7. πŸ”„ Efficient version control

2️⃣ 🌱 Root module

# main.tf
variable "ami" {}
variable "instance_type" {}
variable "key_name" {}


#Replace the resource with a module
module "ec2_instance" {
  #References file location of new module
  source = "./modules/ec2"

  ami = var.ami
  instance_type = var.instance_type
  key_name = var.key_name

  subnet_id = module.security_group.subnet_id  # Call the name of variable output 

  sg_id = module.security_group.sg_id


}

# Specify the name for the security group
module "security_group" {
  source = "./modules/security_group"

}
Enter fullscreen mode Exit fullscreen mode
#provider.tf
terraform {
  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "~> 5.52.0"
    }
  }

}


provider "aws" {
  region  = "us-east-2"
}
Enter fullscreen mode Exit fullscreen mode
# dev.tfvars
ami = "ami-0b4624933067d393a" # each ami is region specific 
instance_type = "t2.micro"
key_name = "ec2_key"
Enter fullscreen mode Exit fullscreen mode

3️⃣ πŸ’» EC2 module

# modules/ec2/main.tf
variable "ami" {}
variable "instance_type" {}
variable "key_name" {}

resource "aws_instance" "ec2" {
  ami           = var.ami
  instance_type = var.instance_type
  key_name      = var.key_name

  vpc_security_group_ids = [var.sg_id]
  subnet_id = var.subnet_id
  user_data = "${file("./modules/ec2/install_docker.sh")}"

  tags = {
    Name = "traefik-demo"
  }
}

Enter fullscreen mode Exit fullscreen mode
# modules/ec2/variables.tf
variable "sg_id" {
  description = "Security Group ID"
  type        = string
}
variable "subnet_id" {
  description = "Subnet ID"
  type        = string
}
Enter fullscreen mode Exit fullscreen mode

πŸ”‘ Key points.
The EC2 module requires two essential fields:

  • πŸ›‘οΈ Security Group ID.
  • 🌐 Subnet ID
# modules/ec2/output.tf
output "instance_id" {
  value = aws_instance.ec2.id
}

output "public_ip" {
  value = aws_instance.ec2.public_ip
}
Enter fullscreen mode Exit fullscreen mode
#!/bin/bash
# Update and install Docker
sudo yum update -y
sudo yum install -y jq docker


# Enable Docker service
sudo service docker start
sudo usermod -a -G docker ec2-user

# Get the latest version of Docker Compose 
DOCKER_COMPOSE_VERSION=$(curl -s https://api.github.com/repos/docker/compose/releases/latest | jq -r .tag_name)

# Install Docker Compose
sudo curl -L "https://github.com/docker/compose/releases/download/${DOCKER_COMPOSE_VERSION}/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose
sudo chmod +x /usr/local/bin/docker-compose
Enter fullscreen mode Exit fullscreen mode

4️⃣ πŸ›‘οΈ Security Group module

vpc.tf

#modules/security_group/vpc.tf
resource "aws_vpc" "traefik_vpc" {
  cidr_block = "10.0.0.0/16"

  tags = {
    Name = "main-vpc"
  }
}
Enter fullscreen mode Exit fullscreen mode
#modules/security_group/subnet.tf
resource "aws_subnet" "traefik_subnet" {
    vpc_id                  = aws_vpc.traefik_vpc.id
    cidr_block              = "10.0.1.0/24"
    map_public_ip_on_launch = true

    tags = {
        Name = "example-subnet"
    }
}
Enter fullscreen mode Exit fullscreen mode
#modules/security_group/sg.tf
resource "aws_security_group" "traefik_sg" {
  name        = "traefik-security-group"
  vpc_id = aws_vpc.traefik_vpc.id
  description = "Allow HTTP and HTTPS traffic"

  ingress {
    from_port   = 80
    to_port     = 80
    protocol    = "tcp"
    cidr_blocks = ["0.0.0.0/0"]
  }

  ingress {
    from_port   = 443
    to_port     = 443
    protocol    = "tcp"
    cidr_blocks = ["0.0.0.0/0"]
  }
   ingress {
    from_port   = 22
    to_port     = 22
    protocol    = "tcp"
    cidr_blocks = ["0.0.0.0/0"] # Allow ssh from internet
  }

  egress {
    from_port   = 0
    to_port     = 0
    protocol    = "-1" # Allow all traffic
    cidr_blocks = ["0.0.0.0/0"]
  }

  tags = {
    Name = "traefik_sg"
  }
}

Enter fullscreen mode Exit fullscreen mode
#modules/security_group/rt.tf
resource "aws_route_table" "traefik_route_table" {
  vpc_id = aws_vpc.traefik_vpc.id

  route {
    cidr_block = "0.0.0.0/0"
    gateway_id = aws_internet_gateway.traefik_igw.id
  }

  tags = {
    Name = "route_table"
  }
}
resource "aws_route_table_association" "a" {
  subnet_id      = aws_subnet.traefik_subnet.id
  route_table_id = aws_route_table.traefik_route_table.id
}
Enter fullscreen mode Exit fullscreen mode
#modules/security_group/igw.tf
resource "aws_internet_gateway" "traefik_igw" {
  vpc_id = aws_vpc.traefik_vpc.id

  tags = {
    Name = "traefik_IGW"
  }
}
Enter fullscreen mode Exit fullscreen mode
#modules/security_group/output.tf
output "subnet_id" {
  value = aws_subnet.traefik_subnet.id
}

output "sg_id" {
  value = aws_security_group.traefik_sg.id
}
Enter fullscreen mode Exit fullscreen mode

πŸ—οΈ Terraform deploy

 terraform init
 terraform validate --var-file=dev.tfvars
 terraform plan --var-file=dev.tfvars
 terraform apply --var-file=dev.tfvars -auto-approve
Enter fullscreen mode Exit fullscreen mode

Top comments (0)