Cómo usar Terraform y Workspaces en AWS y GCloud

Todos los que hayan usado alguna vez Terraform con algún proveedor web, tienen claro cómo configurar un provider y el backend, pero ¿todos usan workspaces para distinguir los estados y configuraciones de cada entorno?

En este artículo veremos una breve guía para ayudar a entender y configurar estos dos procesos. Atent@s, ¡que empezamos!

¿Provider?

Un proveedor, como ya se ha comentado en varias ocasiones en el blog, es el IaaS, SaaS o PaaS en el que queremos desplegar nuestros recursos con Terraform. La configuración para un proveedor u otro varía considerablemente e incluso dentro de un mismo proveedor se pueden usar distintas configuraciones.

Sin ir más lejos, en el caso de AWS podemos trabajar con Usuarios o Roles y dentro de esa distinción, podemos usar la configuración local que tengamos (perfiles) o directamente las credenciales.

En el caso del usuario sin perfil (y este es el ejemplo más sencillo y menos recomendable de todos) usamos directamente variables que referencian al access_key y secret del mismo.

provider "aws" {
  access_key = "${var.aws_access_key}"
  secret_key = "${var.aws_secret_key}"
  region     = "us-east-1"
}

Como acabo de comentar, este método no debería usarse nunca porque requiere un fichero en el que guardemos dichas variables, y esto genera un problema a nivel de seguridad, porque están por escrito, en claro, en un fichero. Para resolverlo se recomienda usar variables de entorno y de perfiles.

Cuando accedemos a muchas cuentas del proveedor, la mejores prácticas de AWS recomiendan el uso de perfiles en la configuración del cliente de AWS (y con Terraform).

En el caso de ser cuentas de tu misma empresa (por ejemplo para acceder a los distintos entornos/cuentas de un mismo proyecto), se recomienda a su vez el uso de roles de “Intercuentas” de esta forma, se crea una única vez el usuario y se utiliza este rol para cambiar a otra cuenta.

La configuración en este caso de nuestros archivos de configuración de AWS serían:

~/.aws/credentials

[miusuario]
region=eu-west-1
aws_access_key_id = [...]
aws_secret_access_key = [...]

~/.aws/config

[default]
region = eu-west-1

[profile cuenta1]
region=eu-west-1
role_arn = arn:aws:iam::[id_cuenta1]:role/[nombre_rol]
source_profile = [miusuario]

[profile cuenta2]
region=eu-west-1
role_arn = arn:aws:iam::[id_cuenta2]:role/[nombre_rol]
source_profile = [miusuario]

[profile cuenta3]
region=eu-west-1
role_arn = arn:aws:iam::[id_cuenta3]:role/[nombre_rol]
source_profile = [miusuario]

Y nuestros archivos de configuración en Terraform (sí, necesitamos claramente uno por cuenta) tendrían lo siguiente:

shared_credentials_file = "[path_to_credentials_file]"
location = "eu-west-1"
profile = "[cuenta1 / cuenta2 / cuenta3]"

El caso de Google es bastante similar (tenemos un proyecto en vez de una cuenta y un archivo de credenciales .json):

provider "google" {
  region = "europe-west1"
  project = "id_proyecto"
  credentials = "${file("path_to_credential_json_file")}"
}

¿Qué es un backend? ¿Y el tfstate?

El tfstate es el fichero donde se guardan los datos con los recursos que hemos ido creando con nuestro código en Terraform. El backend es donde decidimos guardar ese archivo/s.

En el equipo de Sistemas de Paradigma, tras un arduo debate, decidimos usar los sistemas de almacenamiento de objetos del proveedor para guardar dicho fichero.

Esta decisión se basó en varios puntos:

  • La disponibilidad, accesibilidad y fiabilidad que da este tipo de almacenamiento. Es fundamental que esté disponible para todo el equipo y no se pierda. De hecho, es muy recomendable recurrir al versionado y backups que suelen venir por defecto con este tipo de almacenamientos. Por favor, no olvidemos versionar y replicar de forma síncrona (si es posible) el bucket que contiene los tfstate. ¡El tfstate es crítico para la gestión de los recursos con terraform y si borramos el bucket perderíamos todos los tfstate de todos nuestros entornos!
  • Las opciones de securización y cifrado que tienen dichas soluciones. No olvidemos que en este archivo se guardan datos importantes (y en claro) de nuestra infraestructura. En definitiva, siempre deberíamos guardar el tfstate en algún tipo de almacenamiento con cifrado “at rest”.

La siguiente discusión que tuvimos es si tener todos los recursos en el mismo directorio dentro del bucket correspondiente, o tener cada recurso en un prefijo y archivo distintos. Es decir:

[bucket]/terraform.tfstate

VS

[bucket]/vpc/terraform.tfstate
[bucket]/subnets/terraform.tfstate
…

La primera opción (todo en el mismo archivo) permite la gestión de los recursos desde un mismo archivo (sólo un main.tf, un backend.tf y un config/provider.tf), lo cual facilita la legibilidad así como la gestión de los mismos, cuando solo trabajan una o dos personas sobre la misma plataforma.

Esta opción tiene los inconvenientes de que si desaparece el tfstate único del proyecto tenemos un buen problema, y cuando tocan varias personas el mismo código se generan interbloqueos.

La segunda opción requiere la división del código de Terraform en directorios y, por lo tanto, complica el desarrollo y despliegue de los mismos (hay que entrar en el directorio de cada recurso para hacer el plan y el apply, así como multiplicar el número de archivos de configuración de Terraform).

Además, para cada directorio de recurso el archivo de configuración del “backend” o “remote state” debería ser algo así (en este caso el ejemplo es para Google Cloud):

terraform {
  backend "[nombre_backend]" {
    bucket  = "[nombre_bucket]"
    credentials = "[path_to_credentials_json_file]"
    prefix    = "[nombre_recurso]"
    project = "[nombre_proyecto]"
  }
}

Es fundamental la variable con el prefijo para que se distribuyan los archivos de estado en las distintos directorios dentro del bucket.

La estructura del código de Terraform para cada recurso sería algo parecido a esto:

Esta opción es aparentemente más segura en el sentido de que cada recursos tiene su propio tfstate independiente, que podría estar incluso en buckets diferentes y reduce el nivel de interbloqueos cuando varias personas del equipo trabajan en el mismo código.

En definitiva, la división correcta no es trivial y depende mucho de los niveles de seguridad requeridos así como de la forma de trabajar del equipo.

¿Workspaces o un directorio por entorno?

Es muy habitual que en un proyecto haya que desplegar distintos entornos (desarrollo, preproducción, producción…).

Y también lo es que estos entornos sean iguales. De no ser así (si tenemos distintos recursos por entorno), lo habitual es hacer una división de directorios en nuestro repositorio, para distinguir los recursos de cada entorno.

También se puede desarrollar algún tipo de script lleno de condicionales según el entorno que queramos desplegar, que despliega los recursos de una forma específica según el entorno… las soluciones en este caso son muy variadas.

Pero, si nuestros recursos son idénticos para todos los entornos y únicamente cambian variables del tipo “nombre del vpc”, “id de la cuenta”, “proyecto”… ¿por qué no usar workspaces?

Desde la versión 0.10 de Terraform, existe el concepto de Workspace, que permite, mediante el uso del comando “terraform workspace”, crear distintos espacios de trabajo que apuntan de forma automática a distintos archivos de estado de Terraform.

Se usa fundamentalmente ejecutando los siguientes comandos desde el directorio donde reside nuestro fichero de recursos (dependiendo de la estructura de directorios que hayamos elegido más arriba, tendremos que ejecutar estos comandos una vez o tantas como directorios con ficheros de recursos tengamos):

terraform workspace new [nombre_workspace]  -> para crear un workspace nuevo
terraform workspace select [nombre_workspace]  -> para trabajar en ese workspace

A la hora de hacer “plan” y “apply” se pueden usar distintos archivos de variables, así como distintos ficheros de salida por “workspace” con las opciones –var-file y -out:

terraform plan -var-file='../../dev/dev.tfvars' -out='../../dev/terraform-dev.tfplan'
terraform apply -out='../../dev/terraform-dev.tfplan'

Así tendríamos en nuestro repositorio un archivo de variables y un archivo con el output del comando “terraform plan” para cada entorno, pero la misma definición de recursos para todos.

Otra opción, si queremos evitar mantener distintos archivos de variables por entorno, consiste en usar la variable “local.env” en nuestro archivo de definición de variables. Esta variable se inicializa cuando ejecutamos el comando “terraform select workspace”.

Así por ejemplo, si ejecutamos “terraform select workspace dev”, el valor de local.env es “dev”. De esta forma podemos usar los siguientes “mapas locales” de variables para definir, desde un único fichero de variables, las distintas casuísticas de nuestros entornos:

locals {
  instances = {
    "dev" = "m4.large"
    "pre"    = "m4.large"
    "pro"    = "m5.large"
  }

  instance_type = "${lookup(local.instances,local.env)}"
}

Esta opción es bastante más elegante, pues desde un mismo fichero se pueden ver de forma rápida e intuitiva las distintas configuraciones de los distintos entornos. También es más sencilla pues no es necesario incluir en los comandos de plan y apply ninguna referencia al entorno o al fichero concreto de variables a utilizar.

Usar múltiples workspaces permite usar el mismo código para todos nuestros entornos y mantener de forma prácticamente transparente para nosotros distintos tfstates por entorno, dentro del mismo backend.

Es decir, suponiendo que tuviéramos un único archivo de recursos, un bucket y tres workspaces llamados desarrollo, preproducción y producción, entonces tendríamos el siguiente contenido en nuestro bucket:

│[bucket]/
          └── desarrollo.tfstate
          └── preproduccion.tfstate
          └── produccion.tfstate

Si además tuviéramos los recursos divididos en directorios independientes (como se indicó anteriormente), el backend con un prefijo según el nombre del recurso, y los tres workspaces ya mencionados, tendríamos la siguiente división en el backend:

│[bucket]/vpc
          └── desarrollo.tfstate
          └── preproduccion.tfstate
          └── produccion.tfstate
│[bucket]/subnetwork
          └── desarrollo.tfstate
          └── preproduccion.tfstate
          └── produccion.tfstate
│[bucket]/gke
          └── desarrollo.tfstate
          └── preproduccion.tfstate
          └── produccion.tfstate
[...]

En el caso de aws, además establece dentro del bucket el prefijo “env:/”${terrraform.workspace}”/”${recurso}”.

Por último, un consejo a la hora de usar las “salidas o outputs” generadas desde un directorio de recursos (con su correspondiente backend y su tfstate) desde otro directorio de recursos:

Si definimos todos nuestros recursos de terraform en el mismo directorio, al hacer referencia entre ellos, vale con referirse directamente al nombre del recurso, módulo o dato así: “recurso.nombre_recurso.campo”.

Imaginemos, sin embargo, que tenemos los recursos divididos por directorios y necesitamos referenciar, desde el directorio “/eks”, las “salidas del directorio donde gestionamos la creación del vpc” (es decir, referenciar algún parámetro de los recursos creados en el directorio “/vpc”, como por ejemplo, el identificador del vpc creado).

En ese caso, al tener esta división por carpetas, tendríamos que importar el tfstate remoto creado desde el directorio vpc, usando “terraform_remote_state” como sigue.

data "terraform_remote_state" "vpc" {
backend = "[nombre backend]"

config {
encrypt = true
bucket = "[nombre bucket]"
key = "env:/${terraform.workspace}/vpc/terraform.tfstate"
region = "eu-west-1"
}
}

Para luego poder usar el vpc_id (identificador del VPC) como sigue:

  vpc_id= "${data.terraform_remote_state.vpc.vpc_id}"

Es decir, la referencia al backend cuando creamos los recursos desde directorio original (en el ejemplo el directorio vpc), se hace indicando el prefijo que queremos que incluya según el recurso:

key   = "vpc/terraform.tfstate"

Y luego Terraform, de forma transparente, crea el path “env:/[nombre workspace]/vpc/terraform.tfstateY sin embargo, a la hora de “usar” ese tfstate remoto desde un directorio diferente al original (el de eks en el ejemplo), es necesario definir la key como indicamos arriba:

key = "env:/${terraform.workspace}/vpc/terraform.tfstate"

No es algo complicado, pero al ser poco intuitivo, puede llevar a errores.

Conclusión

El uso de workspaces con Terraform es muy ventajoso por múltiples motivos.

  • Para empezar, permite gestionar distintos entornos con el mismo código. Al no necesitar código distinto por entornos, el mantenimiento del mismo es más sencillo, menos costoso y evita errores “humanos”.
  • También permite crear nuevos entornos mucho más rápidamente. Únicamente habría que crear un archivo nuevo de variables, o añadir un campo en los mapas de variables para el nuevo entorno, crear el workspace nuevo y hacer plan-apply con terraform (como hemos indicado más arriba). Terraform se encarga de crear el/los tfstate correspondientes, así como el path en el sistema de almacenamiento que hayamos elegido, diferenciando cada entorno creado, sin necesidad de tener que configurar a mano un backend diferente. Y por supuesto, gestiona la creación de todos los nuevos recursos para el nuevo entorno.
  • Por otro lado, permite tener todos los archivos de tfstate de los entornos en el mismo bucket, de forma ordenada e idéntica para los distintos entornos y recursos del entorno.

¡En definitiva, son muchas razones como para no probarlo!

Nota: Quiero dar las gracias al equipo de Sistemas de Paradigma (¡el mejor del mundo!), y en concreto a Álvaro Linares, que me enseñó el concepto de workspaces, a Alfredo Espejel por su mejora en la gestión de variables con múltiples workspaces y a todos los demás por todas mis preguntas, atascos y dudas resueltas en mi iniciación a Terraform.

Escribe un comentario