Terraform Essentials V: Cómo importar infraestructura existente a Terraform

· 6 min de lectura
Terraform Essentials V: Cómo importar infraestructura existente a Terraform


En este artículo, que es el último de la serie Terraform Essentials, veremos como importar infraestructura existente a Terraform.

Bienvenidos al último capítulo de esta serie que llamé Terraform Essentials. Si te perdiste los artículos anteriores, te los dejo aquí:

Terraform Essentials I: Cómo conectarlo con Docker y desplegar un Hello World.
Este es el primer artículo de la serie Terraform Essentials, donde aprenderemos a trabajar con esta herramienta de Infrastructure as Code.
Terraform Essentials II: Cómo desplegar Traefik y Wordpress en Docker con soporte para Lets Encrypt [Entendiendo Argumentos]
En este artículo te mostraré una receta de Terraform que nos servirá para desplegar Traefik, NGINX, Wordpress y MariaDB con soporte para Lets Encrypt.
Terraform Essentials III: Cómo hacer nuestra Infraestructura escalable y reproducible gracias a las variables.
En este caso, les voy a hablar de las variables y de como estás pueden ayudarnos a que nuestro código e infraestructura sea escalable y reproducible.
Terraform Essentials IV: Cómo gestionar “secretos” en Terraform.
En este artículo, veremos cómo gestionar passwords y tokens cuando trabajamos con Terraform.

Arrancamos

"Nacho, muy lindo todo esto de Terraform, aprendí a desplegar Infraestructura, ningún problema, todo divino, pero, yo ya tengo mi infra desplegada, ¿cómo puedo importarla a Terraform?

Esta es una buena pregunta Mike...

En serio, es una excelente pregunta. Porque esta bien cuando desde un principio montamos nuestra infra desde cero y con Terraform, ya que desde ahí, modificar, romper o reducir, es sumamente sencillo, el desafío viene cuando tenemos recursos ya desplegados de manera manual o usando otras herramientas.

La importación de recursos en Terraform, aún es un tanto limitada, porque la misma permite solamente escribir un "tfstate" y replicar eso a otro ambiente o re deployar sobre el mismo, pero, lo que aún no hace es generar archivos "tf" en base a eso, por ahora, lo debemos hacer nosotros... a mano!.

Para este ejemplo, voy a importar la configuración de un contenedor muy sencillo que tengo corriendo en Docker. Algo a tener en cuenta es que ese contenedor puede estar corriendo o no, para Terraform, es indiferente.

Vamos a generar un archivo que se llamado container.tf que va a tener el siguiente contenido:

# Contenedor
resource "docker_container" "nginx" {
  name  = "nginx"
  image = "nginx:latest"
}

Luego, veamos el contenedor que tengo corriendo.

2c6ede4ffa8a        nginx:latest        "nginx -g 'daemon of…"   7 seconds ago       Up 6 seconds        80/tcp              nginx

Ahora, lo que debemos ejecutar es el comando "terraform import", en el mismo le vamos a indicar que recurso vamos a importar, en este caso, un contenedor y vamos a especificar el nombre del mismo, este dato, debe coincidir con el del contenedor:

terraform import docker_container.nginx $(docker inspect -f {{.ID}} nginx)

Terraform nos responderá con algo como esto:

docker_container.nginx: Importing from ID "2c6ede4ffa8ab824e5f9b2df44eaf2b29620c91c37aea5cae6e8bc367c917586"...
docker_container.nginx: Import prepared!
  Prepared docker_container for import
docker_container.nginx: Refreshing state... [id=2c6ede4ffa8ab824e5f9b2df44eaf2b29620c91c37aea5cae6e8bc367c917586]

Import successful!

The resources that were imported are shown above. These resources are now in
your Terraform state and will henceforth be managed by Terraform.

Y si inspeccionamos, nuestro archivo terraform.tfstate, se verá así:

{
  "version": 4,
  "terraform_version": "0.12.24",
  "serial": 1,
  "lineage": "8b83edb5-1c52-da0a-c243-871eb8eeb337",
  "outputs": {},
  "resources": [
    {
      "mode": "managed",
      "type": "docker_container",
      "name": "nginx",
      "provider": "provider.docker",
      "instances": [
        {
          "schema_version": 2,
          "attributes": {
            "attach": null,
            "bridge": "",
            "capabilities": [],
            "command": [
              "nginx",
              "-g",
              "daemon off;"
            ],
            "container_logs": null,
            "cpu_set": "",
            "cpu_shares": 0,
            "destroy_grace_seconds": null,
            "devices": [],
            "dns": [],
            "dns_opts": [],
            "dns_search": [],
            "domainname": "",
            "entrypoint": [],
            "env": [
              "NGINX_VERSION=1.17.10",
              "NJS_VERSION=0.3.9",
              "PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin",
              "PKG_RELEASE=1~buster"
            ],
            "exit_code": null,
            "gateway": "172.17.0.1",
            "group_add": [],
            "healthcheck": [],
            "host": [],
            "hostname": "2c6ede4ffa8a",
            "id": "2c6ede4ffa8ab824e5f9b2df44eaf2b29620c91c37aea5cae6e8bc367c917586",
            "image": "sha256:602e111c06b6934013578ad80554a074049c59441d9bcd963cb4a7feccede7a5",
            "ip_address": "172.17.0.2",
            "ip_prefix_length": 16,
            "ipc_mode": "private",
            "labels": [
              {
                "label": "maintainer",
                "value": "NGINX Docker Maintainers \u003cdocker-maint@nginx.com\u003e"
              }
            ],
            "links": [],
            "log_driver": "json-file",
            "log_opts": {},
            "logs": null,
            "max_retry_count": 0,
            "memory": 0,
            "memory_swap": 0,
            "mounts": [],
            "must_run": null,
            "name": "nginx",
            "network_alias": null,
            "network_data": [
              {
                "gateway": "172.17.0.1",
                "ip_address": "172.17.0.2",
                "ip_prefix_length": 16,
                "network_name": "bridge"
              }
            ],
            "network_mode": "default",
            "networks": null,
            "networks_advanced": [],
            "pid_mode": "",
            "ports": [],
            "privileged": false,
            "publish_all_ports": false,
            "read_only": false,
            "restart": "no",
            "rm": false,
            "shm_size": 64,
            "start": null,
            "sysctls": {},
            "tmpfs": {},
            "ulimit": [],
            "upload": [],
            "user": "",
            "userns_mode": "",
            "volumes": [],
            "working_dir": ""
          },
          "private": "eyJzY2hlbWFfdmVyc2lvbiI6IjIifQ=="
        }
      ]
    }
  ]
}

A partir de acá, como comenté más arriba, podríamos re-deployar o hacerlo en otro ambiente con esa configuración, aunque también, podríamos editar el archivo container.tf y por ejemplo, exponerle el puerto 80 a ese contenedor.

# Contenedor
resource "docker_container" "nginx" {
  name  = "nginx"
  image = "nginx:latest"

  ports {
    internal = "80"
    external = "80"
 }
}

Ejecutamos...

terraform apply

Nos devuelve lo siguiente:

docker_container.nginx: Refreshing state... [id=2c6ede4ffa8ab824e5f9b2df44eaf2b29620c91c37aea5cae6e8bc367c917586]

An execution plan has been generated and is shown below.
Resource actions are indicated with the following symbols:
-/+ destroy and then create replacement

Terraform will perform the following actions:

  # docker_container.nginx must be replaced
-/+ resource "docker_container" "nginx" {
      + attach            = false
      + bridge            = (known after apply)
      ~ command           = [
          - "nginx",
          - "-g",
          - "daemon off;",
        ] -> (known after apply)
      + container_logs    = (known after apply)
      - cpu_shares        = 0 -> null
      - dns               = [] -> null
      - dns_opts          = [] -> null
      - dns_search        = [] -> null
      ~ entrypoint        = [] -> (known after apply)
      ~ env               = [
          - "NGINX_VERSION=1.17.10",
          - "NJS_VERSION=0.3.9",
          - "PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin",
          - "PKG_RELEASE=1~buster",
        ] -> (known after apply)
      + exit_code         = (known after apply)
      ~ gateway           = "172.17.0.1" -> (known after apply)
      - group_add         = [] -> null
      ~ hostname          = "2c6ede4ffa8a" -> (known after apply)
      ~ id                = "2c6ede4ffa8ab824e5f9b2df44eaf2b29620c91c37aea5cae6e8bc367c917586" -> (known after apply)
      ~ image             = "sha256:602e111c06b6934013578ad80554a074049c59441d9bcd963cb4a7feccede7a5" -> "nginx:latest" # forces replacement
      ~ ip_address        = "172.17.0.2" -> (known after apply)
      ~ ip_prefix_length  = 16 -> (known after apply)
      ~ ipc_mode          = "private" -> (known after apply)
      - links             = [] -> null
        log_driver        = "json-file"
      - log_opts          = {} -> null
      + logs              = false
      - max_retry_count   = 0 -> null
      - memory            = 0 -> null
      - memory_swap       = 0 -> null
      + must_run          = true
        name              = "nginx"
      ~ network_data      = [
          - {
              - gateway          = "172.17.0.1"
              - ip_address       = "172.17.0.2"
              - ip_prefix_length = 16
              - network_name     = "bridge"
            },
        ] -> (known after apply)
      - network_mode      = "default" -> null
      - privileged        = false -> null
      - publish_all_ports = false -> null
        read_only         = false
        restart           = "no"
        rm                = false
      ~ shm_size          = 64 -> (known after apply)
      + start             = true
      - sysctls           = {} -> null
      - tmpfs             = {} -> null

      - labels {
          - label = "maintainer" -> null
          - value = "NGINX Docker Maintainers <docker-maint@nginx.com>" -> null
        }
      + labels {
          + label = (known after apply)
          + value = (known after apply)
        }

      + ports { # forces replacement
          + external = 80 # forces replacement
          + internal = 80 # forces replacement
          + ip       = "0.0.0.0" # forces replacement
          + protocol = "tcp" # forces replacement
        }
    }

Plan: 1 to add, 0 to change, 1 to destroy.

Do you want to perform these actions?
  Terraform will perform the actions described above.
  Only 'yes' will be accepted to approve.

  Enter a value: yes

Tipeamos "yes" como está acá arriba y pum:

docker_container.nginx: Destroying... [id=2c6ede4ffa8ab824e5f9b2df44eaf2b29620c91c37aea5cae6e8bc367c917586]
docker_container.nginx: Destruction complete after 0s
docker_container.nginx: Creating...
docker_container.nginx: Creation complete after 1s [id=204f58c382d45bbb0c881aeeba53c8d437013d81d4203c61c60b2baccbefb2b5]

Apply complete! Resources: 1 added, 0 changed, 1 destroyed.

Nuestro contenedor, nuevo, con el puerto 80 expuesto:

204f58c382d4        nginx:latest        "nginx -g 'daemon of…"   20 seconds ago      Up 19 seconds       0.0.0.0:80->80/tcp   nginx

Para ir cerrando

Como siempre digo, no parece ser complicado pero en este caso puede ser tedioso, porque yo importe un solo contenedor a Terraform, imaginen tenerlo que hacerlo con cientos de VMs o Contenedores con puertos, volumenes, labels, etc. Complicado. Espero que pronto Terraform sea capaz de escribir por sí solo archivos de configuración.

También, les recuerdo el Gitlab del proyecto, donde esta el contenido de los archivos usados en los cinco artículos:

Ignacio Van Droogenbroeck / Terraform Essentials
En este proyecto, encontrarás todos los ejemplos que uso en la seríe llamada Terraform Essentials. Espero que te sea de útilidad: Puedes encontrar estos artículos en: https://cduser.com/tag/terraform/

Espero, que esta serie les haya servido de algo, si así fue, por favor, dejame un comentario en Twitter.

Estén atentos, porque más adelante, haré una serie llamada Terraform Advanced, donde veremos cosas muy concretas que están por fuera de la común pero que nos ayudarán a tener un mejor dominio de esta grandiosa herramienta.

Por ultimo, quiero invitarlos a que estén atentos al tag de Ansible, ya que en breve haré una serie llamada Ansible Essentials.


baehost