Despliega un servidor Nginx en AWS con Terraform
Leonel Gareis
Posted on November 27, 2023
Hoy vamos a aprender como podemos desplegar una instancia EC2 e instalar Nginx de forma rápida y sencilla con Terraform.
Desde el inicio: Que es Terraform? 🤔
Terraform es una herramienta que nos permite crear, modificar y versionar la infraestructura de nuestro proyecto de forma fácil a partir de manifiestos (archivos). A esto se le conoce como IaC (Infrastructure as Code)
Terraform no es la única herramienta para IaC, pero si me arriesgo a decir que es la más popular.
Terraform vs Manual ❗🟰
Características | Terraform | Configuración Manual |
---|---|---|
Sintaxis y Lenguaje | HCL (HashiCorp Configuration Lang.) | Interfaz gráfica o CLI proveedor |
Declarativo vs. Imperativo | Declarativo | Imperativo |
Gestión de Estado | Sí (Archivo de Estado) | No (Depende de documentación) |
Escalabilidad | Escala bien con infraestructuras | Propenso a complejidad y errores |
Control de Versiones | Sí (Archivo de Configuración) | No (Depende de documentación) |
Gestión de Cambios | Planificación antes de aplicar | Riesgo de cambios no documentados |
Reproducibilidad | Alta | Variable |
Curva de Aprendizaje | Moderada | Baja (Interfaz gráfica) |
Colaboración | Facilita la colaboración en equipos | Dificultad para compartir contextos |
Flexibilidad y Abstracción | Permite abstracciones complejas | Limitado a la interfaz del proveedor |
Automatización | Totalmente automatizable | Limitado a capacidades de proveedor |
Que seria el "versionado"? 🤔
Al declarar toda la infraestructura en archivos, estos pueden estar versionados tranquilamente con Git.
Es conveniente hacer todo con Terraform? 🤔
Esto depende mucho de lo que estes haciendo. A veces, para proyectos muy chicos/personales es mas rápido utilizar la consola del proveedor.
Instalación 🖥️
Si no tenes Terraform instalado, te sugiero que entres aca y sigas la guía.
Creando nuestra infraestructura ☁️
Empecemos creando nuestro archivo main.tf
con el siguiente contenido (voy a ir explicando para que es cada bloque):
provider "aws" {
region = "us-east-1"
}
El archivo
main.tf
es el archivo de configuración principal. Acá vamos a definir todo lo que queremos crear.
El bloque provider le permite saber a Terraform con que proveedor de la nube vamos a trabajar. Existen varios proveedores (podes ver la lista completa en este link)
Cada proveedor establece que valores deberemos indicar en este bloque. Hay cosas que son obligatorias, y otras que son opcionales.
Autenticación 🔐
Si, es necesario que Terraform se identifique con el proveedor para crear los recursos solicitados. Como logramos esto?
Para el caso de AWS existen 3 formas principales (en la doc hay mas):
-
Colocar los valores directamente en el bloque del proveedor (no recomiendo esta forma):
provider "aws" { region = "us-east-1" access_key = "clave-de-acceso" secret_key = "clave-secreta" }
Usando variables de entorno (
AWS_ACCESS_KEY_ID
yAWS_SECRET_ACCESS_KEY
y opcionalmenteAWS_SESSION_TOKEN
)-
Configurando el archivo de credenciales. Podes hacerlo con la herramienta de
awscli
y la instrucciónaws configure
. Terraform buscara el archivo donde, según la documentación, deberían estar. Pero podemos indicarle nosotros mismos donde buscar con lo siguiente:
provider "aws" { shared_config_files = ["/tf_user/.aws/conf"] shared_credentials_files = ["/tf_user/.aws/creds"] profile = "mi-perfil-para-test" }
En mi caso, yo tengo las credenciales como variables de entorno así que no voy a colocar parámetros extras en el bloque del proveedor.
Creamos una VPC ☁️
Lo primero que vamos a crear es una VPC.
Si estas viendo Terraform, es porque algo de conocimiento tenés sobre AWS y ciertos recursos como VPC, EC2, etc. Si no es el caso, tranquilo/a que repasamos algunos conceptos clave para que entiendas 😃
Una VPC nos permite crear una red virtual privada para aislar recursos de forma lógica en la nube. Con esto podremos lanzar y gestionar recursos como instancias EC2, bases de datos en RDS, etc.
Escribamos esto en nuestro main.tf
y analicemos que es:
resource "aws_vpc" "my-vpc" {
cidr_block = "10.0.0.0/16"
tags = {
Name = "aws-nginx-server-with-terraform"
Environment = "test"
Terraform = true
}
}
En Terraform debemos crear un bloque de resource
por cada recurso que deseemos. Se define así:
resource "tipo_de_recurso" "nombre_del_recurso" {
atributo = valor
}
Donde:
- tipo_de_recurso: indica el tipo de recurso que estamos creando (esto se define en la documentación del proveedor). Por ejemplo: aws_instance, aws_vpc, aws_eks_cluster, etc.
- nombre_del_recurso: nombre que le damos a ese recurso para usarlo en los siguientes bloques en caso de ser necesario (spoiler)
Los Tags
El atributo tags
es algo que se encuentra presente en casi todos los recursos que podemos crear. Yo normalmente agrego estas 3 etiquetas:
- El nombre del proyecto para el que estoy creando la infra. Es mejor si es el nombre del repositorio donde estas por almacenar los manifiestos.
- El ambiente para el que esta siendo desplegado (dev, stg, prd).
- El tag de
Terraform = true
para saber que ese recurso esta siendo gestionado por esa herramienta y comunicarle así a mi equipo (o a mi yo del futuro) que evite modificarlo manualmente.
Creamos un Internet Gateway 🌐
Este recurso es fundamental en la infraestructura de red de AWS, ya que proporciona una conexión entre una red privada en la nube y la red publica de internet.
resource "aws_internet_gateway" "internet-gw" {
vpc_id = aws_vpc.dev-vpc.id
tags = {
Name = "aws-nginx-server-with-terraform",
Environment = "test",
Terraform = true
}
}
Fijate en el siguiente detalle: No estamos escribiendo el id del VPC porque no lo tenemos ya que no existe. Así que le estamos pidiendo a Terraform que lo defina por nosotros cuando cree ese recurso.
Siempre que tengamos que referirnos a un recurso que va a ser creado en el mismo manifiesto, lo vamos a hacer de esta forma: tipo_de_recurso.nombre_del_recurso.atributo
Creamos el Route Table 📄
El Route Table nos permite definir como se dirige el trafico entre las subredes de la VPC y, en particular, como se enruta el trafico desde y hacia internet mediante el Internet Gateway.
resource "aws_route_table" "route-table" {
vpc_id = aws_vpc.my-vpc.id
route {
cidr_block = "0.0.0.0/0"
gateway_id = aws_internet_gateway.internet-gw.id
}
route {
ipv6_cidr_block = "::/0"
gateway_id = aws_internet_gateway.internet-gw.id
}
tags = {
Name = "aws-nginx-server-with-terraform",
Environment = "test",
Terraform = true
}
}
Este bloque de configuración está estableciendo reglas para decirle a AWS cómo manejar el tráfico dentro de una VPC, permitiendo que las cosas en esa VPC se comuniquen con el Internet y viceversa. La tabla de rutas es como un mapa que dice a las instancias de la VPC hacia dónde enviar o recibir datos.
Creamos una subnet en la VPC 🌐
Una subnet es una porción de una red IP mas grande. En AWS, una subnet esta asociada a una VPC y se usa para organizar y segmentar los recursos de red.
resource "aws_subnet" "subnet-1a" {
vpc_id = aws_vpc.my-vpc.id
cidr_block = "10.0.1.0/24"
availability_zone = "us-east-1a"
tags = {
Name = "aws-nginx-server-with-terraform",
Environment = "test",
Terraform = true
}
}
Creamos la asociación entre la subnet y la route table 🌐
resource "aws_route_table_association" "association-1a" {
subnet_id = aws_subnet.subnet-1a.id
route_table_id = aws_route_table.route-table.id
}
Este bloque nos va a permitir establecer una relación entre la subred que creamos mas arriba y la tabla de enrutamiento también creada con anterioridad. Esto significa, básicamente, que la subred va a usar esas reglas de enrutamiento.
Creamos un security group 🔓
Los "security group" son como un firewall. Estos permiten definir que conexiones entrantes y salientes se aceptan, y cuales no.
resource "aws_security_group" "security-group" {
name = "sg-internet"
description = "Allow ingress and egress connection to the internet"
vpc_id = aws_vpc.my-vpc.id
ingress {
description = "Allow HTTPS connection"
from_port = 443
to_port = 443
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
}
ingress {
description = "Allow HTTP connection"
from_port = 80
to_port = 80
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
}
ingress {
description = "Allow SSH connection"
from_port = 22
to_port = 22
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"]
}
tags = {
Name = "aws-nginx-server-with-terraform",
Environment = "test",
Terraform = true
}
}
Este bloque nos permite definir las reglas necesarias para permitir las conexiones hacia los puertos 80 (HTTP), 443 (HTTPS) y 22 (SSH) desde cualquier IP. Si, se que no es seguro permitir conexiones desde cualquier lado por SSH, pero es solo para el ejemplo.
Creamos una network interface 🌐
Las "network interfaces" (interfaces de red) son un componente clave para permitir la conexión de instancias EC2 a la red.
resource "aws_network_interface" "network-interface" {
subnet_id = aws_subnet.subnet-1a.id
private_ips = ["10.0.1.50"]
security_groups = [aws_security_group.security-group.id]
tags = {
Name = "aws-nginx-server-with-terraform",
Environment = "test",
Terraform = true
}
}
Este recurso nos permite configurar también la IP privada que asignaremos a la interfaz.
Creamos una elastic IP 🌐
En AWS, una elastic IP es una dirección IP publica estática que sera asociada a una instancia EC2. Lo que la diferencia de las IPs publicas tradicionales que son asignadas dinámicamente a nuestras instancias EC2, es que las IPs otorgadas por este recurso persisten aun cuando las instancias son reiniciadas o detenidas. Increíble, no?
resource "aws_eip" "eip" {
domain = "vpc"
network_interface = aws_network_interface.network-interface.id
associate_with_private_ip = "10.0.1.50"
depends_on = [aws_internet_gateway.internet-gw]
tags = {
Name = "aws-nginx-server-with-terraform",
Environment = "test",
Terraform = true
}
}
Algo interesante de este bloque es el depends_on
que nos permite decirle a Terraform que debe crear primero de forma explícita. En nuestro caso, este atributo asegura que la Elastic IP se cree después de que el Internet Gateway asociado esté disponible.
Finalmente: creamos el servidor 🖥️
Las instancias EC2 (Elastic Cloud Computing) son básicamente maquinas virtuales con recursos de computo limitados según el tipo de instancia elegida. Como es una maquina virtual, debemos seleccionar el sistema operativo que correrá en la misma. Es lo que en AWS se conocen como AMI (Amazon Machine Image).
Tenemos AMI's para Windows, Linux y MacOS.
Para este tutoria, voy a usar una imagen de Ubuntu Server 20.04 aunque vos podes usar el que quieras.
El recurso necesita que le indiquemos el ID de la AMI. Como lo obtenemos? Existen dos formas: buscar en la consola de AWS, o definir un bloque de data
para que haga la búsqueda por nosotros.
Si vamos por la consola, al darle "Crear instancia" podremos seleccionar el tipo de AMI y copiar el ID. También podes usar esta guía de AWS.
El bloque data
Este bloque se usa para obtener y utilizar información existente antes de la creación o modificación de los recursos.
data "aws_ami" "ubuntu-ami" {
most_recent = true
filter {
name = "name"
values = ["ubuntu/images/hvm-ssd/ubuntu-focal-20.04-amd64-server-*"]
}
filter {
name = "virtualization-type"
values = ["hvm"]
}
}
Es importante que si vamos a usar esta opción, aceptemos los términos y condiciones de AWS Marketplace. Si no lo hacemos, obtendremos un error como este:
creating EC2 Instance: OptInRequired: In order to use this AWS Marketplace product you need to accept terms and subscribe. To do so please visit https://<URL>
Por último, creamos la instancia de EC2
resource "aws_instance" "nginx-server" {
ami = data.aws_ami.ubuntu-ami.id
instance_type = "t2.micro"
network_interface {
device_index = 0
network_interface_id = aws_network_interface.network-interface.id
}
tags = {
Name = "aws-nginx-server-with-terraform",
Environment = "test",
Terraform = true
}
}
Si no usaste data
, podes hacerlo de esta forma:
resource "aws_instance" "nginx-server" {
ami = "ami-0fc5d935ebf8bc3bc"
instance_type = "t2.micro"
network_interface {
device_index = 0
network_interface_id = aws_network_interface.network-interface.id
}
tags = {
Name = "aws-nginx-server-with-terraform",
Environment = "test",
Terraform = true
}
}
En este bloque estamos definiendo: el tipo de instancia, la AMI que debe usar, la interfaz de red (que creamos mas arriba) y los tags de siempre.
Que nos falta? 🤔
Esto nos creara una instancia con un sistema operativo y nada mas. Que pasa con Nginx? Debemos instalarlo a mano a través de SSH? 🤔
Por suerte no. Si has trabajado con la consola, te acordaras de un input llamado "user data". Este input nos permitía definir un script de bash (en su mayoría) que seria ejecutado cuando la instancia de EC2 sea creada.
Para agregar un user_data
en el bloque, tenemos dos formas:
Forma 1️⃣:
...
user_data=<<EOF
#!/bin/bash
sudo apt update
sudo apt install nginx -y
sudo systemctl start nginx
sudo systemctl enable nginx
EOF
...
Forma 2️⃣ (mi favorita)
Creamos un archivo llamado init.sh
(o como prefieras) y definimos el script ahi:
#!/bin/bash
sudo apt update
sudo apt install nginx -y
sudo systemctl start nginx
sudo systemctl enable nginx
Ahora, en el main.tf
definimos el user_data de la siguiente forma:
...
user_data = file('init.sh')
...
El archivo de
init.sh
debe estar al mismo nivel que el archivomain.tf
Despliegue 🚀
Ahora que tenemos toda la infraestructura definida, solo nos resta decirle a Terraform que aplique lo solicitado.
El primer comando que vamos a ejecutar donde se encuentra el main.tf
es: terraform init
. Este comando incializará los archivos de estado y descargara los archivos necesarios para interactuar con el proveedor (AWS en nuestro caso).
Al ejecutarlo, notaremos que se han creado algunas cosas en el directorio donde estamos:
-
.terraform.lock.hcl
: este archivo registra la version del proveedor que vamos a usar para el despliegue. -
.terraform
: este directorio contiene varios directorios, pero en resumen, acá se guarda el binario del proveedor.
Si no estamos seguro de que es lo que va a hacer Terraform, este nos provee de un comando para la visualización del plan de ejecución. Simplemente debemos ejecutar terraform plan
.
Si estamos seguros, podemos ejecutar terraform apply
para aplicar el plan. Este comando primero nos mostrara todo lo que va a hacer (misma salida que el terraform plan
), y nos pedirá que ingresemos una palabra para confirmar los cambios.
Mientras el plan se ejecuta, notaremos cambios en nuestro directorio. Se ha creado un archivo llamado terraform.tfstate
. Este es MUY importante para Terraform ya que con el rastrea y gestiona el estado de los recursos que fueron creados por la herramienta.
Cuidado! En este
terraform.tfstate
, Terraform muestra secretos y cosas sensibles en texto plano. Así que si estas recuperando secretos de Secrets Manager para inyectarlos en los recursos que estes creando/modificando, tené por seguro que van a aparecer ahi sin protección alguna.Abordaremos cuestiones de seguridad y buenas practicas en otro post.
Revisamos la consola de AWS
Para saber que todo fue bien, entramos a la consola y nos vamos al apartado de EC2
Ya tenemos una instancia creada!
Copiemos la IP y vayamos al navegador para corroborar que todo funciona:
Destruir lo creado
Para borrar todos los recursos creados y evitar un costo extra en nuestra factura de AWS, apliquemos terraform destroy
Extras (lo que no vimos)
Existen mas cosas de Terraform que no cubrimos en este mini tutorial.
Bloque output
Este bloque se utiliza para declarar valores que deben ser mostrados después de que Terraform haya aplicado los cambios. Puede ser información útil, identificadores, direcciones IP, o cualquier otro dato que deseemos exponer después de que la infraestructura ha sido creada o modificada.
En nuestro caso podriamos agregar el siguiente bloque para obtener la IP pública y evitar acceder a la consola
output "public_ip_nginx_server" {
value = aws_instance.nginx-server.public_ip
}
Archivos
Si bien el main.tf
es el mas importante, existen otros archivos que podemos definir para mantener el main lo mas limpio posible:
-
variables.tf
: acá definimos variables que pueden ser usadas para parametrizar la configuracion de los recursos. Por ejemplo: el environment y el tipo de instancia
# variables.tf variable "instance_type" { type = string default = "t2.micro" } variable "environment" { type = string default = "test" } # main.tf resource "aws_instance" "nginx-server" { ami = "ami-0fc5d935ebf8bc3bc" instance_type = var.instance_type # <-- esto cambió network_interface { device_index = 0 network_interface_id = aws_network_interface.network-interface.id } user_data = file("init.sh") tags = { Name = "aws-nginx-server-with-terraform", Environment = var.environment, # <-- esto cambió Terraform = true } }
-
outputs.tf
: Acá definimos todas las salidas que necesitamos
output "public_ip_nginx_server" { value = aws_instance.nginx-server.public_ip }
-
terraform.tfvars
: Este archivo se usa para asignar valores a las variables definidas envariables.tf
. Por ejemplo, podemos darle un valor de la variable aws_region (si la tuvieramos definida)
aws_region = "us-east-1"
backend.tf
: Este archivo se usa para definir un backend remoto, es decir, el lugar donde se va a almacenar elterraform.tfstate
, pero esto mejor dejémoslo para otro post.
Hasta acá el post de hoy! Espero realmente que te haya gustado.
Todo comentario o sugerencia es mas que bienvenido.
Te dejo el link al repositorio por si queres dar un vistazo mas general, forkearlo (spanglish++) o revisar mis otros repositorios.
No te olvides de seguirme en redes (Twitter: LeoGareis)
Hasta la próxima coder!
Posted on November 27, 2023
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.