Desplegando VPC, Subnet, Route Table, Internet Gateway, EC2 con Terraform en AWS

En este post, exploraremos cómo implementar infraestructura en AWS utilizando Terraform, desplegando componentes esenciales como una VPCSubnetsRoute Tables, un Internet Gateway y una instancia EC2 en cada subred. Aprenderás a definir y gestionar estos recursos de manera automatizada, asegurando una configuración escalable y reproducible en la nube. ¡Vamos a construir nuestra infraestructura como código paso a paso!

Pasos: Crear la VPC, Subnet Pública con su rango de IPs, Crear una Subnet Privada con su rango de IPs, Crear un Internet Gateway, Crear una Instancia EC2 dentro de la Subnet Pública, Crear una Instancia EC2 dentro de la Subnet Privada, Crear la Table de Rutas de ambas subredes, Configurar un VPC Endpoint.
El VPC Endpoint ayudará a la Instancia EC2 que se encuentra en la subred Privada a comunicarse con un Bucket S3 que reside en la cuenta de AWS.
Crear los archivos y directorios

Crear la VPC

crear el archivo main.tf
Dentro del main.tf crear un modulo para Networking y definir las variables para el cidr, nombre de la vpc, cidr de la subred publica, cidr de la subred privada, y la variable de la zona de disponiblidad

module "networking" {
  source               = "./networking"
  vpc_cidr             = var.vpc_cidr
  vpc_name             = var.vpc_name
  cidr_public_subnet   = var.cidr_public_subnet
  us_availability_zone = var.us_availability_zone
  cidr_private_subnet  = var.cidr_private_subnet
}

Crear una carpeta networking y dentro un main.tf. Definir las variables, los outputs

variable "vpc_cidr" {}
variable "vpc_name" {}
variable "cidr_public_subnet" {}
variable "us_availability_zone" {}
variable "cidr_private_subnet" {}

En el modulo podemos ver un directorio source = “./networking” y tenemos la carpeta networking. Dentro del directorio networking esta el archivo main.tf que contiene variables de salida y entre otras cosas la configuración de la VPC donde se requiere asignarle el rango de ip 11.0.0.0/16. Allí mismo definimos la VPC donde le pasamos la variable cidr_block = var.vpc_cidr que aun no hemos creado.

# Setup VPC
resource "aws_vpc" "dev_proj_1_vpc_us_east_1" {
  cidr_block = var.vpc_cidr
  tags = {
    Name = var.vpc_name
  }
}

Esta variable var.vpc_cidr será pasada al modulo networking del main principal
-> vpc_cidr = var.vpc_cidr

Creamos el archivo de variables -> variables.tf en la raíz del proyecto.
En el archivo variables.tf las definimos.

variable "vpc_cidr" {
  type        = string
  description = "Public Subnet CIDR values"
}

variable "vpc_name" {
  type        = string
  description = "WP Project 1 VPC 1"
}

variable "cidr_public_subnet" {
  type        = list(string)
  description = "Public Subnet CIDR values"
}

variable "cidr_private_subnet" {
  type        = list(string)
  description = "Private Subnet CIDR values"
}

variable "us_availability_zone" {
  type        = list(string)
  description = "Availability Zones"
}

Debemos crear el archivo terraform.tfvars también en la raíz del proyecto. Allí definimos los valores de las variables.

Dentro de terraform.tfvars le asignamos el valor a la variable -> vpc_cidr = “11.0.0.0/16” y de el resto de valores de las variables. Como queremos crear 2 subredes publicas y 2 subredes privadas enviamos los valores en forma de lista, asi como las 2 zonas de disponiblidad.

vpc_cidr             = "11.0.0.0/16"
vpc_name             = "dev_proj_1_vpc_us_east_1"
cidr_public_subnet   = ["11.0.1.0/24", "11.0.2.0/24"]
cidr_private_subnet  = ["11.0.3.0/24", "11.0.4.0/24"]
us_availability_zone = ["us-east-1a", "us-east-1b"]

En este momento podriamos hacer un terraform init. Es el primer comando que debes ejecutar cuando comienzas un nuevo proyecto en Terraform, y lo que hace es Descargar proveedores (providers)
y los instala cuando están definidos en el archivo main.tf

Abrimos la terminal, nos aseguramos de estar en la misma carpeta del proyecto y ejecutamos:

terraform init

Una vez terminado el proceso, se mostrara el mensaje Terraform has been successfully initialized!

Ahora ejecutamos el comando:

terraform plan

terraform plan analiza tu configuración (.tf) y el estado actual de la infraestructura (en el archivo terraform.tfstate) y muestra un resumen de lo que va a hacer antes de aplicar.

Recibimos el resumen de lo que terraform hará y si todo esta conforme a lo solicitado, procedemos con el comando:

terraform apply -auto-approve

El comando terraform apply es el paso final del flujo principal de Terraform. Después de terraform init y terraform plan, este comando se encarga de crear, modificar o destruir recursos en tu proveedor de infraestructura (como AWS, Azure, GCP, etc.), según lo definido en tus archivos .tf.

Deberas tener configuradas tus credenciales de acceso a AWS desde la terminal. (aws configure)

Ten cuidado con el parametro -auto-approve, es un modificador para terraform apply que le dice a Terraform que ejecute el plan sin pedir confirmación del usuario.

Crear 2 Subredes publicas y 2 privadas

Agregar en el main.tf de el modulo networking

# Setup public subnet
resource "aws_subnet" "dev_proj_1_public_subnets" {
  count             = length(var.cidr_public_subnet)
  vpc_id            = aws_vpc.dev_proj_1_vpc_us_east_1.id
  cidr_block        = element(var.cidr_public_subnet, count.index)
  availability_zone = element(var.us_availability_zone, count.index)

  tags = {
    Name = "dev-proj-public-subnet-${count.index + 1}"
  }
}

# Setup private subnet
resource "aws_subnet" "dev_proj_1_private_subnets" {
  count             = length(var.cidr_private_subnet)
  vpc_id            = aws_vpc.dev_proj_1_vpc_us_east_1.id
  cidr_block        = element(var.cidr_private_subnet, count.index)
  availability_zone = element(var.us_availability_zone, count.index)

  tags = {
    Name = "dev-proj-private-subnet-${count.index + 1}"
  }
}

Te explico algunas líneas:

count = length(var.cidr_public_subnet)

Crea múltiples subnets según la cantidad de CIDR definidos en la variable var.cidr_public_subnet.
count es una meta-argumento de Terraform que permite hacer una cantidad dinámica de recursos.
length(var.cidr_public_subnet) determina cuántas subnets crear. Por ejemplo, si cidr_public_subnet tiene 3 elementos, se crearán 3 subnets.

cidr_block = element(var.cidr_public_subnet, count.index)

Asigna un rango CIDR específico a cada subnet.
element() toma un valor de la lista var.cidr_public_subnet usando el índice actual (count.index).
Esto asegura que cada subnet tenga un rango distinto, como: [“11.0.1.0/24”, “11.0.2.0/24”, “11.0.3.0/24”].

Revisar y aplicar

terraform plan
terraform apply.

Ve a AWS -> VPC y mira la magia -> Se creará una Route Table por defecto.

Crear Internet Gateway

Deberá ser adjuntada a la VPC
En el main.tf del modulo networking ->

# Setup Internet Gateway
resource "aws_internet_gateway" "dev_proj_1_public_internet_gateway" {
  vpc_id = aws_vpc.dev_proj_1_vpc_us_east_1.id
  tags = {
    Name = "dev-proj-1-igw"
  }
}

Con la línea vpc_id = aws_vpc.dev_proj_1_vpc_us_east_1.id queda adjuntado a la VPC
Aplicamos terraform plan
Aplicamos terraform apply -auto-approve

Verificamos los cambios en AWS

Crear una Route Table

Una RT para las subredes publicas y una RT para las subredes privadas

En el archivo main.tf del módulo Networking

# Public Route Table
resource "aws_route_table" "dev_proj_1_public_route_table" {
  vpc_id = aws_vpc.dev_proj_1_vpc_us_east_1.id
  route {
    cidr_block = "0.0.0.0/0" # internet
    gateway_id = aws_internet_gateway.dev_proj_1_public_internet_gateway.id
  }
  tags = {
    Name = "dev-proj-1-public-rt"
  }
}

# Private Route Table
resource "aws_route_table" "dev_proj_1_private_route_table" {
  vpc_id = aws_vpc.dev_proj_1_vpc_us_east_1.id
  #depends_on = [aws_nat_gateway.nat_gateway]
  tags = {
    Name = "dev-proj-1-private-rt"
  }
}

En la Public Route Table, se debe adjuntar el ID de la VPC utilizando:

vpc_id = aws_vpc.dev_proj_1_vpc_us_east_1.id

Además, en el CIDR block, se debe definir el rango de salida hacia y desde internet como 0.0.0.0/0
Finalmente, es necesario adjuntar el ID del Internet Gateway.
En el caso de la Private Route Table de la subred privada, es mas sencillo porque no requiere internet Gateway.

Asociar las Route Table a las subnets en el mismo main.tf del módulo Networking

# Public Route Table and Public Subnet Association
resource "aws_route_table_association" "dev_proj_1_public_rt_subnet_association" {
  count          = length(aws_subnet.dev_proj_1_public_subnets)
  subnet_id      = aws_subnet.dev_proj_1_public_subnets[count.index].id
  route_table_id = aws_route_table.dev_proj_1_public_route_table.id
}

# Private Route Table and private Subnet Association
resource "aws_route_table_association" "dev_proj_1_private_rt_subnet_association" {
  count          = length(aws_subnet.dev_proj_1_private_subnets)
  subnet_id      = aws_subnet.dev_proj_1_private_subnets[count.index].id
  route_table_id = aws_route_table.dev_proj_1_private_route_table.id
}

Con estas dos líneas se crea la asociación entre la tabla de rutas y la subnet

subnet_id = aws_subnet.dev_proj_1_public_subnets[count.index].id
route_table_id = aws_route_table.dev_proj_1_public_route_table.id

Crear los Outputs en el mismo main.tf modulo networking

output "dev_proj_1_vpc_id" {
  value = aws_vpc.dev_proj_1_vpc_us_east_1.id
}

output "dev_proj_1_public_subnets_ids" {
  value = aws_subnet.dev_proj_1_public_subnets.*.id
}

output "dev_proj_1_private_subnets_ids" {
  value = aws_subnet.dev_proj_1_private_subnets.*.id
}

output "public_subnet_cidr_block" {
  value = aws_subnet.dev_proj_1_public_subnets.*.cidr_block
}

output "dev_proj_1_private_route_table_ids" {
  value = aws_route_table.dev_proj_1_private_route_table.id
}

Hasta acá se ha creado la configuración de red mínima.

Aplicamos terraform plan
Aplicamos terraform apply -auto-approve

Configurar las instancias EC2. Una en la subred publica y la otra en la subred privada

Crear un modulo en el main.tf principal

module "ec2-public-subnet" {
  source                   = "./ec2"
  ami_id                   = var.ec2_ami_id
  instance_type            = "t2.medium"
  tag_name                 = "EC2 Instance: Public Subnet"
  public_key               = var.public_key
  subnet_id                = tolist(module.networking.dev_proj_1_public_subnets_ids )[0]
  sg_for_jenkins           = [module.security_group.sg_ec2_sg_ssh_http_id, module.security_group.sg_ec2_jenkins_port_8080]
  enable_public_ip_address = true
  key_name                 = "aws_ec2_terraform_public"
}

module "ec2-private-subnet" {
  source                   = "./ec2"
  ami_id                   = var.ec2_ami_id
  instance_type            = "t2.medium"
  tag_name                 = "EC2 Instance: Private Subnet"
  public_key               = var.public_key
  subnet_id                = tolist(module.networking.dev_proj_1_private_subnets_ids)[0]
  sg_for_jenkins           = [module.security_group.sg_ec2_sg_ssh_http_id, module.security_group.sg_ec2_jenkins_port_8080]
  enable_public_ip_address = false
  key_name                 = "aws_ec2_terraform_private"
  #user_data_install_jenkins = templatefile("./jenkins-runner-script/jenkins-installer.sh", {})
}

Las instancias EC2 requieren: el id de una ami, el id de la VPC, un id de subred, un security group, y un public_key.
Para generar un par de llaves se puede hacer con ssh-keygen y luego pasar las credenciales manualmente a terraform en terraform.tfvars.

Se debe tener en cuenta que las instancias t2.medium tienen costo. No forma parte del free tier.

Asignar una subred a la instancia ec2 en la subred pública.
subnet_id = tolist(module.networking.dev_proj_1_public_subnets_ids )[0]
Acá se hace una lista de las subredes publicas que se están creando, luego se toma el primer elemento de la lista y se asigna el id a la instancia EC2.
Asignar una subred a la instancia ec2 en la subred privada.

subnet_id = tolist(module.networking.dev_proj_1_private_subnets_ids)[0]

La instancia EC2 en la subred pública debe tener la opcion enable_public_ip_address = true mientras que la instancia EC2 en la subred debe colocarse en false ya que no es necesario.

Crear el Security Group

module "security_group" {
  source              = "./security-groups"
  ec2_sg_name         = "SG for EC2 to enable SSH(22), HTTPS(443), HTTP(8080) and HTTP(80)"
  vpc_id              = module.networking.dev_proj_1_vpc_id
  ec2_jenkins_sg_name = "Allow port 8080 for jenkins"
}

Cada modulo debe tener su respectiva carpeta con el nombre del recurso

source                   = "./ec2"
source              = "./security-groups"

En la carpeta ec2 -> main.tf creamos las variables

variable "ami_id" {}
variable "instance_type" {}
variable "tag_name" {}
variable "public_key" {}
variable "subnet_id" {}
variable "sg_for_jenkins" {}
variable "enable_public_ip_address" {}
variable "user_data_install_jenkins" {
  default = ""
}

output "jenkins_ec2_instance_ip" {
  value = aws_instance.jenkins_ec2_instance_ip.id
}

output "dev_proj_1_ec2_instance_public_ip" {
  value = aws_instance.jenkins_ec2_instance_ip.public_ip
}

resource "aws_instance" "jenkins_ec2_instance_ip" {
  ami           = var.ami_id
  instance_type = var.instance_type
  tags = {
    Name = var.tag_name
  }
  key_name                    = var.key_name
  subnet_id                   = var.subnet_id
  vpc_security_group_ids      = var.sg_for_jenkins
  associate_public_ip_address = var.enable_public_ip_address

  
  metadata_options {
    http_endpoint = "enabled"  # Enable the IMDSv2 endpoint
    http_tokens   = "required" # Require the use of IMDSv2 tokens
  }
}

resource "aws_key_pair" "jenkins_ec2_instance_public_key" {
  key_name   = var.key_name
  public_key = var.public_key
}

Se debe tener un par de llaves y se deben asignar a la instancia EC2 para poder acceder a ellas por ssh.

En el archivo variables.tf agregamos las sigientes líneas:

variable "public_key" {
  type        = string
  description = "WP Project 1 Public key for EC2 instance"
} 

variable "ec2_ami_id" {
  type        = string
  description = "WP Project 1 AMI Id for EC2 instance"
}

También agregamos la variable para la AMI.

En el archivo terraform.tfvars se indican los valores estas dos variables.

public_key = "ssh-rsa TU_LLAVE_PUBLICA"
ec2_ami_id = "ami-084568db4383264d4"

En la carpeta security-groups->main.tf

variable "ec2_sg_name" {}
variable "vpc_id" {}
variable "ec2_jenkins_sg_name" {}

output "sg_ec2_sg_ssh_http_id" {
  value = aws_security_group.ec2_sg_ssh_http.id
}

output "sg_ec2_jenkins_port_8080" {
  value = aws_security_group.ec2_jenkins_port_8080.id
}

resource "aws_security_group" "ec2_sg_ssh_http" {
  name        = var.ec2_sg_name
  description = "Enable the Port 22(SSH) & Port 80(http)"
  vpc_id      = var.vpc_id

  # ssh for terraform remote exec
  ingress {
    description = "Allow remote SSH from anywhere"
    cidr_blocks = ["0.0.0.0/0"]
    from_port   = 22
    to_port     = 22
    protocol    = "tcp"
  }

  # enable http
  ingress {
    description = "Allow HTTP request from anywhere"
    cidr_blocks = ["0.0.0.0/0"]
    from_port   = 80
    to_port     = 80
    protocol    = "tcp"
  }

  # enable http
  ingress {
    description = "Allow HTTP request from anywhere"
    cidr_blocks = ["0.0.0.0/0"]
    from_port   = 443
    to_port     = 443
    protocol    = "tcp"
  }

  #Outgoing request
  egress {
    description = "Allow outgoing request"
    from_port   = 0
    to_port     = 0
    protocol    = "-1"
    cidr_blocks = ["0.0.0.0/0"]
  }

  tags = {
    Name = "Security Groups to allow SSH(22) and HTTP(80)"
  }
}

resource "aws_security_group" "ec2_jenkins_port_8080" {
  name        = var.ec2_jenkins_sg_name
  description = "Enable the Port 8080 for jenkins"
  vpc_id      = var.vpc_id

  # ssh for terraform remote exec
  ingress {
    description = "Allow 8080 port to access jenkins"
    cidr_blocks = ["0.0.0.0/0"]
    from_port   = 8080
    to_port     = 8080
    protocol    = "tcp"
  }

  tags = {
    Name = "Security Groups to allow SSH(22) and HTTP(80)"
  }
}

Aplicamos terraform plan

Es posible que en este punto nos pida aplicar terraform init para instalar algunos módulos
Aplicamos terraform apply -auto-approve

Luego de unos minutos, en este punto, deberían estar las 2 instancias EC2 en ejecución. Con todas las opciones que configuramos.

Para eliminar toda la implementación: terraform destroy -> yes

Repositorio Github:

https://github.com/richardaguirre1/aws-terraform-vpc.git

Agregar un comentario

Tu dirección de correo electrónico no será publicada. Los campos requeridos están marcados *