Despliegue y prueba de servicios creados con Terraform en AWS
Tags: aws, terraform
Published: 2022-08-16 15:00:15

Terraform es una herramienta de IaC multicloud, basado en el lenguaje de configuración Hansicorp que es un tipo de JSON.

Las plantillas de Terraform generalmente se distribuyen de la siguiente forma:

![Template Structure]
(https://github.com/cecamilo00/posts/blob/circleci-project-setup/terraformAWS/estructura%20template.png?raw=true)

**Provider**: Se define la nube en la cual vamos a trabajar.
**DataSources-Parameters**: Se definen los parámetros o variables que vamos a utilizar dentro de nuestra plantilla.
**Resources**: Definimos los recursos a desplegar.

La siguiente arquitectura muestra los recursos que vamos a desplegar en AWS con Terraform.

![Arquitectura]
(https://github.com/cecamilo00/posts/blob/circleci-project-setup/terraformAWS/arquitectura.png?raw=true)

[plantilla](https://github.com/cecamilo00/posts/blob/circleci-project-setup/terraformAWS/main.tf)

```
# PROVIDER

provider "aws" {
    region = "${var.aws_regions[0]}"
    #Se definen los tags comunes a todos los resources
    default_tags {
        tags = {
            despliegue = "Terraform AWS"
        }
    }
}

#DATA - PARAMETERS - VARIABLES


locals {
  tags = {
    despliegue = "Terraform AWS"
  }
}
variable "amiinstance" {
    default = "ami-090fa75af13c156b4"
    type = string  
}

variable "aws_regions" {
    default = [
        "us-east-1",
        "us-east-2"
    ]
    type = list
}

variable "aws_instance_size" {
    type = map
    default= {
        small = "t2.micro",
        medium = "t2.medium",
        large = "t2.large"
    }
}

variable "aws_instancedb_size" {
    type = map
    default= {
        small = "db.t2.micro",
        medium = "db.t2.small"
    }
}

variable "tag" {
    default = "template_terraform"
    type = string
}

variable "cidr_subnets" {
    default = [
        "10.0.1.0/24",#public
        "10.0.2.0/28",#private
        "10.0.3.0/28"#private
    ]
    type = list
}

#RESOURCES

resource "aws_vpc" "vpcterraform" {
    cidr_block = "10.0.0.0/16"
    enable_dns_hostnames = true
    tags = {
        Name = "${var.tag}"
    }
}

resource "aws_internet_gateway" "igwterraform" {
    vpc_id = "${aws_vpc.vpcterraform.id}"
    tags = {
        Name = "routeTableTFPublic_${var.tag}"
    }
}

resource "aws_route_table" "routeTableTFPublic" {
    vpc_id = "${aws_vpc.vpcterraform.id}"
    route {
        cidr_block = "0.0.0.0/0"
        gateway_id = "${aws_internet_gateway.igwterraform.id}"
    }
    tags = {
        Name = "routeTableTFPublic_${var.tag}"
    }
}

resource "aws_route_table" "routeTablerivate" {
    vpc_id = "${aws_vpc.vpcterraform.id}"
    tags = {
        Name = "routeTableTFPrivate_${var.tag}"
    }
    
}

resource "aws_subnet" "publicsubnet1" {
    vpc_id = "${aws_vpc.vpcterraform.id}"
    cidr_block = var.cidr_subnets[0]
    map_public_ip_on_launch = true
    availability_zone = "us-east-1a"
    tags = {
        Name = "publicsubnet1_${var.tag}"
    }
}

resource "aws_route_table_association" "routeTableAssociationTF" {
    subnet_id = "${aws_subnet.publicsubnet1.id}"
    route_table_id = "${aws_route_table.routeTableTFPublic.id}"
}

resource "aws_subnet" "privatesubnet1" {
    vpc_id = "${aws_vpc.vpcterraform.id}"
    cidr_block = var.cidr_subnets[1]
    availability_zone = "us-east-1b"
    tags = {
        Name = "privatesubnet1_${var.tag}"
    }
}

resource "aws_subnet" "privatesubnet2" {
    vpc_id = "${aws_vpc.vpcterraform.id}"
    cidr_block = var.cidr_subnets[2]
    availability_zone = "us-east-1c"
    tags = {
        Name = "privatesubnet2_${var.tag}"
    }
}

resource "aws_db_subnet_group" "dbprivatesubnetgroup"{
    name = "dbprivatesubnetgroup"
    subnet_ids = ["${aws_subnet.privatesubnet1.id}","${aws_subnet.privatesubnet2.id}"]
    tags = {
        Name = "dbprivatesubnetgroup_${var.tag}"
    }
}

resource "aws_iam_role" "instance_role" {
    assume_role_policy = jsonencode({
        "Version": "2012-10-17",
        "Statement": [
        {
            "Effect": "Allow",
            "Action": [
                "sts:AssumeRole"
            ],
            "Principal": {
                "Service": [
                    "ec2.amazonaws.com"
                ]
            }
        }
        ]
    })

    tags = {
        tag-key = "role_${var.tag}"
    }
}

resource "aws_iam_policy" "policy"{
    policy = jsonencode({
        "Version": "2012-10-17",
        "Statement": [
            {
                "Effect": "Allow",
                "Action": [
                    "cloudwatch:PutMetricData",
                    "ds:CreateComputer",
                    "ds:DescribeDirectories",
                    "ec2:DescribeInstanceStatus",
                    "logs:*",
                    "ssm:*",
                    "ec2messages:*"
                ],
                "Resource": "*"
            },
            {
                "Effect": "Allow",
                "Action": "iam:CreateServiceLinkedRole",
                "Resource": "arn:aws:iam::*:role/aws-service-role/ssm.amazonaws.com/AWSServiceRoleForAmazonSSM*",
                "Condition": {
                    "StringLike": {
                        "iam:AWSServiceName": "ssm.amazonaws.com"
                    }
                }
            },
            {
                "Effect": "Allow",
                "Action": [
                    "iam:DeleteServiceLinkedRole",
                    "iam:GetServiceLinkedRoleDeletionStatus"
                ],
                "Resource": "arn:aws:iam::*:role/aws-service-role/ssm.amazonaws.com/AWSServiceRoleForAmazonSSM*"
            },
            {
                "Effect": "Allow",
                "Action": [
                    "ssmmessages:CreateControlChannel",
                    "ssmmessages:CreateDataChannel",
                    "ssmmessages:OpenControlChannel",
                    "ssmmessages:OpenDataChannel"
                ],
                "Resource": "*"
            }
        ]
    })
    tags = {
        tag-key = "policy_${var.tag}"
    }
}

resource "aws_iam_role_policy_attachment" "policy_attachment" {
    role = "${aws_iam_role.instance_role.name}"
    policy_arn = "${aws_iam_policy.policy.arn}"
}

resource "aws_iam_instance_profile" "instance_profile" {
    role  = "${aws_iam_role.instance_role.name}"
}

resource "aws_security_group" "sg_instance" {
    vpc_id = "${aws_vpc.vpcterraform.id}"
    ingress {
        from_port = 22
        to_port = 22
        protocol ="tcp"
        cidr_blocks = ["0.0.0.0/0"]
    }
    ingress {
        from_port   = 80
        to_port     = 80
        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 = "sg_instance_${var.tag}"
    }
}

resource "aws_instance" "instancia" {
    ami= "${var.amiinstance}"
    instance_type = "${var.aws_instance_size.small}"
    subnet_id = "${aws_subnet.publicsubnet1.id}"
    vpc_security_group_ids = [ "${aws_security_group.sg_instance.id}" ]
    tags = {
        Name = "instancia_${var.tag}"
    }
    iam_instance_profile = "${aws_iam_instance_profile.instance_profile.id}"
    depends_on = [aws_internet_gateway.igwterraform]
    user_data = <<EOF
    #!/bin/bash
    yum update -y
    yum install httpd -y
    service httpd start
    chkconfig httpd on
    cd /var/www/html
    echo "<html><body><h1>Terraform Instancia: $(hostname -f)</h1></body></html>" > index.html
    EOF

}

resource "aws_security_group" "sgbd" {
    name = "sg"
    vpc_id = "${aws_vpc.vpcterraform.id}"
    ingress {
        from_port = 3306
        to_port = 3306
        protocol ="tcp"
        cidr_blocks = ["${aws_instance.instancia.private_ip}/32"]
    }
    tags = {
        Name = "sgbd_${var.tag}"
    }
}

resource "aws_db_instance" "dbinstance" {
    db_subnet_group_name = "${aws_db_subnet_group.dbprivatesubnetgroup.name}"
    engine = "mysql"
    db_name = "dbterraform"
    engine_version = "5.7.28"
    instance_class = "${var.aws_instancedb_size.small}"
    username = "admin"
    password = "admin12345678"
    allocated_storage = 10
    skip_final_snapshot = true
    vpc_security_group_ids =  [ "${aws_security_group.sgbd.id}" ]
    tags = {
        Name = "dbinstance_${var.tag}"
    }
}

output "endpoint" {
    value = "${aws_instance.instancia.public_ip}"
}

```


Terraform tiene comandos para iniciar, validar,  desplegar y destruir los recursos definidos en nuestras plantillas (Antes de usar los siguientes comandos recuerden configurar sus access keys con el comando _aws configure_).

1.**terraform init**: Configura el directorio donde se encuentra nuestra plantilla para que pueda ser usado con Terraform.

![Init]
(https://github.com/cecamilo00/posts/blob/circleci-project-setup/terraformAWS/init1.png?raw=true)


2.**terraform validate**: Valida que la plantilla no tenga errores de sintaxis o de lógica.

![Validate]
(https://github.com/cecamilo00/posts/blob/circleci-project-setup/terraformAWS/validate.png?raw=true)

3.**terraform plan**: Nos muestra una descripción de los recursos que van a ser desplegados, valida si hay cambios en nuestros recursos (en caso de actualización) y valida que nuestra plantilla no tenga errores. 

![Plan]
(https://github.com/cecamilo00/posts/blob/circleci-project-setup/terraformAWS/plan.png?raw=true)

4.**terraform apply**: Nos va a mostrar la infraestructura que se va a desplegar y nos va a preguntar que sí estamos de acuerdo por lo que debemos escribir _yes_ para que inicie el despliegue de nuestros servicios.

![Terraform apply]
(https://github.com/cecamilo00/posts/blob/circleci-project-setup/terraformAWS/terraform%20apply.png?raw=true)

Al final nos va a mostrar la cantidad de recursos que se crearon y los outputs que tenemos definidos en la plantilla.

![Terraform apply]
(https://github.com/cecamilo00/posts/blob/circleci-project-setup/terraformAWS/terraform%20apply%201.png?raw=true)

Terraform al desplegar nuestra infraestructura crea el archivo
_terraform.tfstate_ donde se guarda la información de los recursos desplegados. 
Podemos ver qué recursos se desplegaron con el comando: **_terraform state list_**  y podemos ver la información del recurso con el comando **_terraform state show  nombre_del_recurso_**.  

Una vez que se han creado todos nuestros recursos, vamos a verlos en la consola de AWS. Para esto buscamos resources groups y le damos click.

![Resources Groups]
(https://github.com/cecamilo00/posts/blob/circleci-project-setup/terraformAWS/resorces%20groups.png?raw=true)

Ya en la pantalla principal damos click en _Tag Editor_, _seleccionamos todas las regiones_, _todos los recursos_,  en los tags escribimos _despliegue_ - _Terraform AWS_ y damos click en le botón _Search Resources_. Esto nos va a traer todos los recursos que tiene este tag.

![Tag Editor]
(https://github.com/cecamilo00/posts/blob/circleci-project-setup/terraformAWS/tag%20editor.png?raw=true)

El resultado de la búsqueda nos va a mostrar los recursos que cumplen las condiciones anteriores.

![Search Tag Editor]
(https://github.com/cecamilo00/posts/blob/circleci-project-setup/terraformAWS/search%20tag%20editor.png?raw=true)

Este resultado lo obtuvimos ya que a todos los recursos de la plantilla les asignamos el tag: _Name: despliegue - Value: Terraform AWS_,  en el siguiente fragmento de la  plantilla:

```
provider "aws" {
    region = "${var.aws_regions[0]}"
    #Se definen los tags comunes a todos los resources
    default_tags {
        tags = {
            despliegue = "Terraform AWS"
        }
    }
}
```
Ahora vamos a validar el funcionamiento de nuestra aplicación. En la pantalla de resultados damos click en la instancia de EC2 que creamos.

![Click Instance]
(https://github.com/cecamilo00/posts/blob/circleci-project-setup/terraformAWS/click%20instance.png?raw=true)

Los que nos lleva a la pantalla de EC2 y copiamos la Ip publica de la instancia y la colocamos en una nueva pestaña de nuestro navegador.

![Click Instance]
(https://github.com/cecamilo00/posts/blob/circleci-project-setup/terraformAWS/ip%20instance.png?raw=true)

lo que nos va a mostrar lo siguiente:

![Click Instance]
(https://github.com/cecamilo00/posts/blob/circleci-project-setup/terraformAWS/page.png?raw=true)

Este resultado lo obtuvimos ya que en nuestra definición de la instancia dentro dentro de la propiedad _user data_ le pedimos que instalara apache y creara una pagina web.

```
resource "aws_instance" "instancia" {
    ami= "${var.amiinstance}"
    instance_type = "${var.aws_instance_size.small}"
    subnet_id = "${aws_subnet.publicsubnet1.id}"
    vpc_security_group_ids = [ "${aws_security_group.sg_instance.id}" ]
    tags = {
        Name = "instancia_${var.tag}"
    }
    iam_instance_profile = "${aws_iam_instance_profile.instance_profile.id}"
    user_data = <<EOF
    #!/bin/bash
    yum update -y
    yum install httpd -y
    service httpd start
    chkconfig httpd on
    cd /var/www/html
    echo "<html><body><h1>Terraform Instancia: $(hostname -f)</h1></body></html>" > index.html
    yum install mysql
    EOF

}
```

De acuerdo a la anterior definición de nuestra instancia, no  definimos un access key para poder ingresar a ella desde la terminal.
Para acceder a la instancia y validar nuestra base de datos vamos a buscar el servicio System Manager.

![System Manager]
(https://github.com/cecamilo00/posts/blob/circleci-project-setup/terraformAWS/System%20manager.png?raw=true)

En la pagina principal damos click _Session Manager_, después en _Start Session_.

![Session Manager]
(https://github.com/cecamilo00/posts/blob/circleci-project-setup/terraformAWS/sesion%20manager.png?raw=true)

Ahora seleccionamos la instancia que creamos y damos click en el botón _Start Session_ 

![Start Session]
(https://github.com/cecamilo00/posts/blob/circleci-project-setup/terraformAWS/start%20session.png?raw=true)

Lo que nos va abrir una terminal como la siguiente.

![Terminal]
(https://github.com/cecamilo00/posts/blob/circleci-project-setup/terraformAWS/terminal.png?raw=true)

Donde podemos ejecutar los comandos para acceder a la base de datos creada con la plantilla:

Instalar mysql (para usar como cliente):
```
sudo su
sudo yum install mysql
```
Conectarse a la base de datos:
```
mysql -h reemplezar_por_endpoint_basededatos -u admin -p database
```

Para lograr abrir la terminal desde System Manager se crearon los siguientes recursos en la plantilla:

```
aws_iam_role
aws_iam_policy
aws_iam_role_policy_attachment
aws_iam_instance_profile
```
para asociar el recurso _aws_iam_instance_profile_ a nuestra instancia.

  

5.**terraform destroy**: al ejecutar el comando nos muestra los recursos que se van a eliminar, después nos solicita que autoricemos la eliminación por lo que debemos escribir _yes_.

![Destroy]
(https://github.com/cecamilo00/posts/blob/circleci-project-setup/terraformAWS/destroy.png?raw=true)


Con la ejecución de estos comando terminamos con el despliegue y la liberación de los recursos desplegados con Terraform.

Terraform me parece practico ya que su sintaxis se me asemeja al desarrollo (he trabajado como desarrollador). Una de las cosas que creo mas importantes para trabajar con herramientas IAC es la documentación la cual me pareció sencilla pero practica.

Para mejorar la plantilla pueden crear archivos separados para las variables, para los outputs y realizar la misma ejecución de comandos, esto con el fin que nuestra plantilla quede más organizada y legible.


[Camilo Cabrales](https://www.linkedin.com/in/camilo-cabrales-31a0bb53/)


# Referencias

[Documentación](https://registry.terraform.io/providers/hashicorp/aws/latest/docs)

[Instalación](https://learn.hashicorp.com/tutorials/terraform/install-cli)

[Comando Init](https://www.terraform.io/cli/commands/init)

[Comando Plan](https://www.terraform.io/cli/commands/plan)

[Comando Destroy](https://www.terraform.io/cli/commands/destroy)