Terraform: Best practices and lessons learned

Terraform es casi con total seguridad, la herramienta estrella de la actualizad para configurar y construir la infraestructura en la nube, ya sea en AWS, Azure o GCP.

Su tendida curva de aprendizaje hace que comenzar a trabajar con él sea francamente fácil y puede llevarte a pensar que levantar toda la infraestructura en la nube será una empresa sencilla. Pero la realidad es que cuando el proyecto comienza a crecer, la cosa se complica.

Es por lo que en el presente articulo se pretenden detallar una serie de buenas practicas y lecciones aprendidas que siempre deberían tenerse en cuenta antes de comenzar a trabajar con Terraform, independientemente del tamaño y complejidad del proyecto.

Naming convention

Definir una convención de nombres estándar para los recursos a generar es fundamental para la viabilidad del proyecto a largo plazo, especialmente cuando se trabaja en un equipo multidisciplinar y/o desgeolocalizado.

En el siguiente enlace podéis encontrar uno de los mejores artículos de convenciones recomendadas de nomenclatura y etiquetado, por Microsoft.

Remote Backend

Cada vez que se ejecuta una operación “plan”, apply” o “destroy” con Terraform se genera o en caso de existir se lee y actualiza el estado de la infraestructura en un fichero denominado tftstate.

El “backend” determina precisamente cómo se carga este estado antes de ejecutar las citadas operaciones y aunque por defecto se toma de local, es prácticamente obligatorio almacenarlo en un servidor remoto, tanto por seguridad (backup) como para facilitar el trabajo en equipo.

Las principales nubes permiten almacenar el estado en sus sistemas de almacenamiento:

Eso si, Terraform no permite hacer uso de variables interpoladas a la hora de definir la configuración de backend, por lo que se requiere construir un script separado por entorno.

State Locking

Si el backend seleccionado lo permite, Terraform podrá bloquear el estado (tfstate) limitando la concurrencia a una única ejecución simultánea. Esto evita que otros procesos puedan adquirir el bloqueo y corromper potencialmente el estado.

Comentar que todos los backends descritos anteriormente soportan esta característica, como bien se describe en los enlaces proporcionados.

Variable files

Al igual que en tantos otros productos, Terraform permite hacer uso de variables con los que parametrizar la configuración en función del proyecto o entorno.

Estas variables pueden ser descritas en uno o varios ficheros “.tfvars” o mediante las tradicionales variables de entorno.

# Variable
terraform apply -var="image_id=ami-abc123"

# Variable file
terraform apply -var-file="testing.tfvars"

# Multiple variables files
terraform apply -var-file="testing-1.tfvars" -var-file="testing-1.tfvars"

# Environment variable
export TF_VAR_image_id=ami-abc123

Reusable modules

Tal y como señala la documentación oficial de Terraform, un módulo es un contenedor para la creación de múltiples recursos que se utilizan juntos, permitiendo crear abstracciones que describen la infraestructura en términos de la arquitectura, en lugar dé en términos de objetos físicos.

Por ejemplo, suponiendo una arquitectura de microservicios diseñada con el patrón domain-driven design (DDD), es posible definir un modulo de “dominio” encargado de crear las subnets, bases de datos o namespaces de Kubernetes necesarios, entre otros.

Las ventajas son evidentes: el código se ubica en un único punto centralizado, normalmente en un repositorio de git aislado y versionado, lo que permite realizar modificaciones o corregir los bugs detectados de forma sencilla y organizada.

De igual manera es posible instanciar un modulo en múltiples ocasiones con distintas configuraciones o emplearlo únicamente en aquellos entornos en los que se requiere.

Por tanto, se recomienda encarecidamente estudiar con detenimiento si merece o no la pena implementar esta aproximación, si bien para proyectos de tamaño medio o grande se vuelve prácticamente imprescindible.

Count vs for_each

Probablemente la primera vez que te encuentras ante la necesidad de realizar un bucle no te paras a analizarlo en detenimiento y optas por la primera solución que encuentras. Por desgracia, esta decisión puede ser muy perjudicial a corto medio plazo.

Antes de la versión 0.12.6 de Terraform, la sentencia count era la única forma de realizar un bucle para crear múltiples recursos del mismo tipo de forma simultánea.

resource "aws_instance" "ubuntu" {
  # This will create 3 instances
  count = 3

  ami = data.aws_ami.ubuntu.id
  instance_type = "t2.micro"
  associate_public_ip_address = true
  availability_zone = data.aws_availability_zones.available.names[count.index]
}

Ahora bien, el count es sensible a cualquier cambio en el orden de la lista, por lo que en caso de que se altere, Terraform volverá regenerar todos los recursos cuyo índice haya cambiado. Es decir, primero destruye los recursos y después los vuelve a generar con la nueva posición del índice. Un desastre.

Es por lo que se recomienda encarecidamente hacer uso del for_each, el cual se apoya en el uso de los mapas y utiliza usa la clave de este como índice de instancias del recurso creado.

variable "zones" {
  description = "AWS availability zones"
  type = map
  default = {
    a = "us-east-1a"
    b = "us-east-1b"
    c = "us-east-1c"
  }
}

resource "aws_instance" "ubuntu" {
  for_each = var.zones
  ami = data.aws_ami.ubuntu.id
  instance_type = "t2.micro"
  availability_zone = each.value
}

Interpolation-only expressions 

Otras de las novedades incluidas en la versión v0.12.0 de Terraform es que las denominadas “interpolation-only expressions” están obsoletas, permitiendo ahora acceder directamente a las variables.

Antes de Terraform v0.12.0:

resource "aws_instance" "ubuntu" {
  ami = ${data.aws_ami.ubuntu.id}
  instance_type = ${var.instance_type}
  availability_zone = ${var.availability_zone}
}

Desde Terraform v0.12.0:

resource "aws_instance" "ubuntu" {
  ami = data.aws_ami.ubuntu.id
  instance_type = var.instance_type
  availability_zone = var.availability_zone
}

Sin lugar a duda, el código se vuelve mas limpio y legible ahora.

Boolean evaluation

Aunque Terraform permite definir variables de tipo Boolean, no funcionan exactamente igual que en el resto de los lenguajes. Cuando se declara una variable como Boolean, Terraform transforma los valores literales true y false en “1” y “0” respectivamente. Es decir, los valores pasan a ser Strings.

Por tanto, los siguientes valores pasan a ser equivalentes:

true == true   # "1" == "1"
false == false # "0" == "0"
"1" == true    # "1" == "1"
"0" == false   # "0" == "0"

Claro que, a su vez, los siguientes valores dejan de ser equivalentes:

true != "true"   # "1" != "true"
false != "false" # "0" != "false"

Es por ello que se recomienda hacer uso de variables de tipo String en lugar de Booleans a la hora evaluar condicionales.

Log verbosity

Cuando las cosas se complican nada mejor que afinar el nivel de las trazas para tratar de comprender que es exactamente lo que Terraform esta ejecutando.

Para ello, basta con dar uno de los siguientes valores a la variable de entorno TF_LOG: TRACEDEBUGINFOWARN o ERROR

TF_LOG=DEBUG terraform <command>

Format and style

Terraform provee del comando “fmt” para reescribir los archivos de configuración a un formato y estilo canónicos. Algo especialmente interesante si se combina con algún hook del SCM al realizar un commit.

terraform fmt -rescursive

Referencias

Se recomienda encarecidamente leer los siguientes artículos que han servido de base para el escrito:

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google photo

You are commenting using your Google account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s