Terragrunt, un paso más allá de Terraform

0
8232
Fila de servidores con discos duros en un centro de datos para almacenamiento en la nube.
Infraestructura de servidores utilizada para almacenamiento en la nube.

Índice

1. Introducción

Terraform es un lenguaje de infrastructure as code (IAC) muy potente. Su principal virtud es que permite trabajar en base al resultado final que deseamos, abstrayéndonos de las acciones concretas que son necesarias para conseguirlo. Sin embargo, presenta algunos problemas a la hora de mantener nuestro código bajo el principio DRY.

Aunque trabajar con módulos de Terraform puede solventar en parte, algunos de estos problemas, todavía tenemos que lidiar con otros. En este tutorial vamos a echar un vistazo a estos inconvenientes y cómo Terragrunt, nos puede ayudar a solucionarlos.

2. Motivación

En primer lugar, vamos a ver algunas razones por las que es necesario ir un paso más allá de Terraform si queremos mantener nuestro código lo más mantenible posible.

2.1. Aislamiento de la infraestructura

Ya vimos cómo los módulos podían ayudarnos a reutilizar nuestro código de forma sencilla en un tutorial anterior. Sin embargo, aunque esta forma de trabajar separa la infraestructura en diferentes ficheros, su aplicación se realiza de forma conjunta al estar toda en el mismo directorio. Es decir, ejecutar ‘terraform apply’ evalúa de nuevo todos los ficheros y aplica los cambios necesarios.

Esta no es una manera idónea de trabajar. En primer lugar, porque podemos disponer de código que afecte a diferentes entornos y nos gustaría trabajar en cada uno de ellos sin afectar al resto. Además, un cambio en un apartado de la infraestructura podría afectar a cualquier otro área si cometemos un error. También nos vemos obligados a aprovisionar la infraestructura en conjunto, cuando quizá todavía no necesitemos parte de ella. No queremos pagar por algo que no necesitamos, ¿verdad?

Separar el uso de los módulos por entornos y por los propios módulos soluciona en parte este problema. Podríamos ejecutar ‘terraform apply’ en cada directorio de forma aislada y que los cambios sólo afectasen a esa parte de la infraestructura. No obstante, eso genera un nuevo inconveniente: la repetición de código. Puesto que ahora cada fichero está en su propio directorio, las configuraciones compartidas ya no se aplican.

Además, surge otra cuestión: tampoco hay una manera sencilla de poder aplicar todos los cambios de nuestra infraestructura de golpe. Sí, hay algunos métodos que pueden ayudarnos a mitigar todas estas cuestiones, pero el esfuerzo es demasiado grande y nos gustaría no tener que utilizar demasiado boilerplate.

2.2. Gestión del estado

Otra cuestión importante es la gestión del estado. Cada vez que llamamos a ‘terraform apply’, se genera un fichero que contiene el estado de la infraestructura. Este fichero es importante, ya que es el que Terraform utiliza para saber los cambios que debe aplicar. Esto quiere decir que si llamamos a ‘terraform apply’ sobre una infraestructura existente sin contar con la información de este fichero, el resultado será impredecible y acabará con casi toda seguridad en error.

Este fichero no es conveniente compartirlo mediante el repositorio de código. En primer lugar, debemos recordar siempre sincronizarlo antes de realizar cualquier cambio en la infraestructura. Además, debería bloquearse para prevenir el lanzamiento de cambios de manera simultánea por parte de varios usuarios.

Lo más típico es utilizar almacenamiento en la nube como un bucket de S3, apoyado en una base de datos como Dynamo. Esto genera un proceso de dos pasos, ya que debemos crear primero algo de infraestructura en la nube para poder gestionar el estado de Terraform, y luego configurar el backend de Terraform para que utilice dicha infraestructura como soporte.

Además, si pretendemos aislar nuestra infraestructura como comentábamos en el apartado anterior, el estado también se particionará. Nos gustaría que, aunque cada parte de la infraestructura tenga su propio estado, podamos gestionarlo todo de una manera global y sin mucho enredo.

3. Terragrunt al rescate

Terragrunt es un wraper de Terraform que nos permite resolver los problemas que hemos mencionado de una forma muy sencilla. Además, el lenguaje de sus ficheros es el mismo que el de Terraform, HCL, de forma que se hace más llevadero el cambio.

Podríamos entender que los módulos que generamos con Terragrunt son consumidores de módulos de Terraform. Esto quiere decir que la intención es reutilizar módulos de Terraform, no crear módulos nuevos o recursos asociados.

Las características principales que nos ofrece son:

  • Mantener nuestro código de Terraform DRY.
  • Mantener nuestra configuración de backend DRY.
  • Mantener los argumentos CLI de Terraform DRY.
  • Ejecutar comandos de Terraform en múltiples módulos.
  • Hooks before y after.

3. Ejemplo de uso con Terragrunt

Ahora que sabemos lo que Terragrunt puede hacer por nosotros, vamos a tomar como base el ejemplo de uso de módulos con Terraform que ya hicimos en otro tutorial. Podéis encontrar ese código en este repositorio, bajo la tag V2.0.

3.1. Instalar Terragrunt

Antes de nada, tenemos que instalar Terragrunt. La manera más sencilla es utilizando Homebrew:

brew install terragrunt

Si ejecutamos ahora por consola el comando ‘terragrunt’, veremos los comandos que podemos utilizar. Son prácticamente iguales que los de Terraform, así que parece un entorno bastante acogedor.

3.2. Configuración general

En primer lugar, vamos a establecer la configuración general del proyecto. Esto incluye la gestión del estado remoto y el proveedor de AWS. Creamos un archivo ‘terragrunt.hcl’ en la raíz del repositorio. Su contenido será el siguiente:

# /terragrunt.hcl

remote_state {
  backend = "s3"

  generate = {
    path = "backend.tf"
    if_exists = "overwrite_terragrunt"
  }

  config = {
    encrypt = true
    bucket = local.remote_bucket
    key = "${path_relative_to_include()}/terraform.tfstate"
    region = "eu-west-1"
    dynamodb_table = "my-modular-infrastructure-terraform-locks"
    profile = local.profile
    skip_bucket_versioning = false
    s3_bucket_tags = local.common_tags
    dynamodb_table_tags = local.common_tags
  }
}

locals {
  remote_bucket = format("javier-estrada-tutorial-%s-terraform-state", get_env("env", "dev"))
  profile = "tutorial"
  aws_region = "eu-west-1"
  common_tags = {
    Terraform : "true"
    Environment : get_env("env", "dev")
  }
}

inputs = {
  aws_region = local.aws_region
}

terraform {
  extra_arguments "aws_profile" {
    commands = get_terraform_commands_that_need_vars()
    env_vars = {
      AWS_PROFILE = local.profile,
      AWS_REGION = local.aws_region
    }
  }
}

generate "provider" {
  path      = "provider.tf"
  if_exists = "overwrite"
  contents = <<EOF
provider "aws" {
  shared_credentials_file = "~/.aws/credentials"
  region = "eu-west-1"
}
EOF
}
  • Los bloques generate permiten crear archivos temporales de Terraform.
  • En primer lugar, configuramos el uso de un bucket de S3 como estado remoto.
    • Recuerda que el nombre de un bucket es único, así que cámbialo.
    • Terragrunt nos permite utilizar variables en la definición.
    • Los buckets se crean de forma independiente conforme al entorno (dev por defecto).
    • El estado de cada módulo se almacena en el bucket con la misma ruta que tendremos en el repositorio.
    • Aunque no hemos aprovisionado el bucket ni la base de datos Dynamo, Terragrunt lo hará por nosotros.
  • Configuramos el proveedor de AWS, de modo que no necesitaremos hacerlo en cada módulo.

3.3. Migración de los ficheros

Lo primero que tendremos que hacer es migrar los ficheros de Terraform. Para ello, vamos a crear la siguiente estructura de directorios en el repositorio:

  • /dev
    • /vpc
    • /ec2
    • /bucket
    • /security
      • /ssh-security-group
  • /pro
      • /vpc
      • /ec2
      • /bucket
      • /security
        • /ssh-security-group

Como vemos, hemos aislado la infraestructura por entornos y a su vez por módulos. Dentro de cada directorio de entorno, vamos a definir un fichero con variables comunes para todos nuestros módulos. Lo llamaremos ‘common_vars.yaml’.

# /dev/common_vars.yaml
        env: dev
        instance_type: t2.micro
        instance_number: 2
# /pro/common_vars.yaml
        env: pro
        instance_type: t2.micro
        instance_number: 3

Podemos configurar cualquier aspecto de los parámetros que debemos introducir en los módulos. Esto nos facilita bastante la labor, al ser sencillo modificar los parámetros según el entorno y no tener que modificar directamente el fichero de Terragrunt.

Dentro de cada subdirectorio de módulo, vamos a crear el archivo ‘terragrunt.hcl’. Aquí es donde vamos a escribir el código que llama a los módulos de Terraform. Como nota, Terragrunt tiene problemas para utilizar los módulos directamente del registro de Terraform, así que es necesario referenciar a los repositorios directamente. La sintaxis cambia un poco, aunque veréis que es bastante sencilla también. Como ejemplo, vamos a ver el fichero para las instancias EC2:

# /dev/ec2/terragrunt.hcl

terraform {
  source = "git@github.com:terraform-aws-modules/terraform-aws-ec2-instance.git?ref=v2.16.0"
    }

include {
  path = find_in_parent_folders()
}

dependency "vpc" {
    config_path = "../vpc"
    mock_outputs = {
      private_subnets = ["subnet-10.0.1.0", "subnet-10.0.2.0"]
    }
}

dependency "ssh" {
    config_path = "../security/ssh-security-group"
    mock_outputs = {
      this_security_group_id = "security-group-mock"
    }
}

locals {
common_vars = yamldecode(file("${find_in_parent_folders("common_vars.yaml")}"))
name = "cluster-${local.common_vars.env}"
}

inputs = {
    name                   = local.name
  instance_count         = local.common_vars.instance_number

  ami                    = "ami-ebd02392"
  instance_type          = local.common_vars.instance_type
  vpc_security_group_ids = [dependency.ssh.outputs.this_security_group_id]
  subnet_ids = dependency.vpc.outputs.private_subnets

  tags = {
    Terraform   = "true"
    Environment = local.common_vars.env
}
}
  • El bloque include indica que se incluya la configuración definida en otro fichero. En este caso, Terragrunt busca en los directorios padre hasta encontrar uno.
  • El bloque dependency permite establecer dependencias con otros módulos.
    • Establece un orden para la aplicación de los módulos.
    • Podemos utilizar outputs de estos módulos.
    • Si queremos utilizar los comandos plan o validate, es necesario mockear los outputs de las dependencias o darán error si no están ya desplegados en la nube.
  • En locals, cargamos nuestras common_vars para utilizarlas como inputs, además de poder preparar otros valores.
  • El resto es bastante similar a lo que ya conocíamos de Terraform, aunque con una sintaxis ligeramente diferente.

El ejemplo completo podéis encontrarlo bajo la tag v3.0 del repositorio. Cada módulo ha sido trasladado a su directorio correspondiente en ambos entornos, y los archivos .tf originales se han eliminado.

3.4. Aplicando la infraestructura

Como hemos dicho, la ventaja de dividir la infraestructura en módulos en vez de tenerla en un solo directorio es que podemos aplicarla por partes, únicamente aquellas que nos interesan. Así pues, podemos entrar en los directorios de cada módulo y aplicar los cambios. Eso sí, deberemos estar seguros de respetar la relación de dependencias. Por ejemplo, no podremos desplegar las instancias EC2 sin tener previamente la VPC y el security group.

export AWS_PROFILE=tutorial
export env=dev
cd dev/vpc
terragrunt apply

Sin embargo, también podemos hacer lo mismo, pero con toda la infraestructura o parte de ella. Terragrunt cuenta con los comandos -all, que se encargan de buscar todos los módulos disponibles a partir de un directorio.

export AWS_PROFILE=tutorial
export env=dev
cd dev
terragrunt apply-all

Esta secuencia de comandos aplicará la infraestructura contenida en los módulos que se encuentran dentro del directorio dev. El resultado será el mismo que teníamos con los ficheros de Terraform en un mismo directorio, aunque se nos mostrarán los recursos divididos por módulos.

En ambos casos, si no existe el bucket donde vamos a almacenar el estado, Terragrunt nos preguntará si deseamos crearlo.

Para terminar, podemos deshacernos de todo con la misma facilidad.

export AWS_PROFILE=tutorial
export env=dev
cd dev
terragrunt destroy-all

Ten en cuenta que los bucket donde se gestiona el estado de Terraform no se eliminarán en este proceso. Esto debe hacerse manualmente.

4. Conclusión

Terragrunt nos permite profundizar en el aislamiento de la infraestructura gracias a que nos ofrece algunas características importantes para conseguir un código DRY. Además, también nos facilita la gestión del estado, ya que no debemos crear una infraestructura previa para poder almacenarlo en remoto si utilizamos AWS o Google Cloud.

Aún así, no hay que entender Terragrunt como un sustituto de Terraform, sino como un complemento. La creación de módulos es algo que sólo se puede llevar a cabo con Terraform. Una vez creados, sí que es recomendable consumirlos con Terragrunt para mejorar la mantenibilidad del código.

DEJA UNA RESPUESTA

Por favor ingrese su comentario!

He leído y acepto la política de privacidad

Por favor ingrese su nombre aquí

Información básica acerca de la protección de datos

  • Responsable:
  • Finalidad:
  • Legitimación:
  • Destinatarios:
  • Derechos:
  • Más información: Puedes ampliar información acerca de la protección de datos en el siguiente enlace:política de privacidad