Despliega un servidor Nginx en AWS con Terraform
Tags: infrastructureascode, devops, tutorial, aws
Published: 2023-11-27 18:36:56

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](https://developer.hashicorp.com/terraform/install) 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):

```terraform
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](https://registry.terraform.io/browse/providers))

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):

1. Colocar los valores directamente en el bloque del proveedor (no recomiendo esta forma):
    ```terraform
        provider "aws" {
            region = "us-east-1"
            access_key = "clave-de-acceso"
            secret_key = "clave-secreta"
        }
    ```
2. Usando variables de entorno (`AWS_ACCESS_KEY_ID` y `AWS_SECRET_ACCESS_KEY` y opcionalmente `AWS_SESSION_TOKEN`)
3. Configurando el archivo de credenciales. Podes hacerlo con la herramienta de `awscli` y la instrucción `aws configure`. Terraform buscara el archivo donde, según la documentación, deberían estar. Pero podemos indicarle nosotros mismos donde buscar con lo siguiente:
    ```terraform
        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:

```terraform
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í:
```terraform
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:

1. 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.
2. El ambiente para el que esta siendo desplegado (dev, stg, prd).
3. 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. 

```terraform
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.

```terraform
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.

```terraform
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 🌐

```terraform
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.

```terraform
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. 

```terraform
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?

```terraform
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](https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/finding-an-ami.html) 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. 

```terraform
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:

```bash
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

```terraform
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:
```terraform
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️⃣:
```terraform
  ...
  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:

```bash
#!/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:

```terraform
  ...
  user_data = file('init.sh')
  ...
```

> El archivo de `init.sh` debe estar al mismo nivel que el archivo `main.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:

1. `.terraform.lock.hcl`: este archivo registra la version del proveedor que vamos a usar para el despliegue.
2. `.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

![Información de la instancia EC2 creada](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/a3tewj4pkdcyjt8ns2zk.png)

Ya tenemos una instancia creada! 
Copiemos la IP y vayamos al navegador para corroborar que todo funciona:

![Captura del navegador mostrando la respuesta exitosa de Nginx](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/vodn8c1slle86v4c2s5r.png)

![is alive](https://media.giphy.com/media/vmv47p4zksWDC/giphy-downsized-large.gif)

## Destruir lo creado

Para borrar todos los recursos creados y evitar un costo extra en nuestra factura de AWS, apliquemos `terraform destroy`

![Explosiones](https://media.giphy.com/media/3oKIPwoeGErMmaI43S/giphy.gif)

## 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

```terraform
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
    ```terraform
    # 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
    ```terraform
    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 en `variables.tf`. Por ejemplo, podemos darle un valor de la variable aws_region (si la tuvieramos definida)
    ```terraform
        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 el `terraform.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](https://github.com/gareisdev/aws-nginx-server-with-terraform) 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!