Como migrar de versión de Terraform y no morir en el intento

Uno de los puntos más críticos en cualquier plataforma es como ir subiendo las versiones de los productos, la finalidad es clara el estar en la última versión siempre nos proporciona una serie de ventajas. Sin embargo, también nos lleva una serie de retos, cuando lo abordamos en desarrollo puede ser algo más sencillo porque podemos tener los dos proyectos de forma simultánea…. pero con la infraestructura que puede ocurrir? Sin infraestructura nuestra aplicación no funciona, por lo tanto, toda atención que se le puede prestar siempre es poco. En este artículo vamos a ver la forma en la que lo hemos planteado y que problemas/soluciones hemos aplicado para ir subiendo de la versión de Terraform 0.12 a la versión 1.0.4.

Introducción

Antes de empezar la faena vamos a intentar poner un poco de contexto a este artículo con la finalidad de intentar que el lector entienda el contexto y le pueda servir de utilidad. Empezaremos por la base, hasta que lleguemos al caso de uso que estamos hablando. Espero no enrollarme mucho e ir directo al grano (aunque muchas veces no se puede).

¿Qué es Terraform?

Es un programa para gestionar IaC (Infraestructura como código). Define una estructura y sintaxis generales para el código y lo ejecuta para alcanzar el estado deseado.

Por decirlo de alguna forma declaramos como queremos tener la infraestructura, Terraform se encarga de chequear la infraestructura y si existe alguna modificación aplica estos cambios, en caso de que se lo indiquemos. Por este motivo hay que tener muy presente cual es el ciclo de vida que tenemos en Terraform. ciclo vida terraform

  1. Terraform Init => Inicializa el directorio de trabajo que consta de todos los archivos de configuración
  2. Plan => El plan se utiliza para crear un plan de ejecución para alcanzar el estado deseado de la infraestructura. Los cambios en los archivos de configuración se realizarán para lograr el estado realizado.
  3. Apply => Realiza los cambios en la infraestructura tal y como se define en el plan y la infraestructura llega al estado deseado.
  4. Destroy => Se utiliza para eliminar los recursos de infraestructura antiguos.

¿Como funciona Terraform?

como funciona Terraform ¿Qué es cada cosa? En los ficheros con extensión “.TF” son la fuente de entrada en la que se define lo que se debe de crear o aprovisionare. Los ficheros con extensión “.tfstate” es donde está el estado en el que se encuentra el estado actual de la infraestructura. Lo que hace Terraform es comprobar el “estado” en el que estaba junto con los ficheros de configuración, junto con el acceso al proveedor donde queremos ejecutar estos cambios. Si al realizar esta comprobación existe algo que ha cambiado en la infraestructura en el plan se nos notificará de este cambio. Cuando pasamos un Plan hay que observar todos los cambios que se aplican porque cualquier cambio que se produzca va a provocar cambios en la infraestructura. ¿Esto implica que una vez pase un Terraform no puedo tocar nada desde el portal de Azure? La respuesta es que NO, se puede modificar cualquier valor desde el Portal, eso sí, si ese cambio posteriormente no lo bajamos al terraform la siguiente vez que pasemos el Plan volverá al último estado anterior.

¿Qué nos beneficia Terraform respecto a utilizar el lenguaje nativo de cada Cloud?

Mucha gente pone como principal ventaja que Terraform nos proporciona una capa que es agnóstica al Cloud en el que lo estamos ejecutando. Si mañana llega y migramos toda nuestra infraestructura de Azure a AWS, Google o el que sea no tendríamos que hacer ningún cambio… sin embargo, ¿cuántas veces hacemos esto? ¿Cuántos clientes cambian de proveedor de Cloud? Al final, esto es un ejemplo de cuando nos planteamos usar un ORM porque si en un futuro cambiamos de Base de Datos que no tendremos que realizar ninguna modificación y será trasparente… y cuantas veces cambiamos de BD en un proyecto. Pues con Terraform y esta ventaja para mí esto no es una ventaja ni algo por lo que tengamos que decidir usar Terraform en lugar de ARM por ejemplo. Entonces, ¿que nos ofrece Terraform que nos ofrece Azure Resource Manager? Al final desde mi punto de vista para mí la opción de utilizar Terraform es que disponemos de un lenguaje declarativo en el que podemos fabricar nuestros módulos, para establecer nuestras propiedades y que estas las podamos aplicar a todos nuestros desarrollos y vincular estas propiedades al ciclo de vida. De forma que la creación de estos módulos nos simplifique mucho la vida a la hora de crear nuevos entornos y también tener una serie de tags/tallas preestablecidas para tener unificado todo. De esta forma tenemos un control de la infraestructura de la misma forma que hacemos con el código fuente de nuestro desarrollo, para mi esta es la mejor opción.

Este último punto es muy importante a la hora de utilizar Terraform pero el objetivo principal de este articulo NO nos olvidemos que es como migrar de una versión a otro de Terraform sin morir en el intento.

Como migramos de versión de Terraform

Actualmente Terraform está por la versión 1.0.9, previamente paso por la 0.1.x hasta la 0.15.x. Si vamos a la documentación oficial tenemos una serie de guías de como subir de versión. Podéis consultarlo en este enlace. Ahora bien, porque debemos/necesitamos migrar de una versión a otra, que nos aporta, que beneficios tiene. Si vemos la documentación nos dice el cómo y lo que afecta … pero otro tema es como nosotros lo podemos hacer. La primera cuestión es que para la creación de infraestructura en la implementación de una serie de módulos que comenzaron su implementación en la versión más reciente que tenía el producto en esa versión. Estos módulos se van evolucionando según las necesidades que tiene ese producto y según lo que se utilice, esto puede provocar que tengamos unos módulos en una versión de Terraform superior a otros… lo que puede provocar un caos y muchas referencias circulares como os podéis imaginar. Ahora bien, cuál es la tarea vamos a migrar todos los módulos que tenemos a la última versión estable que tiene Terraform, al final si nuestros ficheros de infraestructura no se han modificado, el estado de Terraform no debe de aplicar casi ningún cambio y mucho menos recrear infraestructura. ¿Pero cuál es la realidad? Seguro que os lo podéis imaginar, que cualquier parecido con la realidad puede ser pura coincidencia…. A la hora de migrar un módulo de Terraform de una versión a otra que nos podemos encontrar:

Todas estas casuísticas hay que ver cómo abordarlas. Las dos primeras en la gran mayoría de los casos no implican ninguna parada del servicio y el aplicarlas en cualquier momento es un trámite (o debería podemos preguntar a la banda local un par de errores nuevos). ¿Sin embargo, en el caso de que nos pida recrear la infraestructura… que debemos de hacer? En primer lugar, tirarnos de los pelos yo no he cambiado nada quiero un recurso y no estoy haciendo ningún cambio que requeria hacerlo. Una vez pasado este estado de no entender la cosa nos toca parar a pensar. ¿Porque nos pide recrear la infraestructura y por este motivo debemos de entender cómo funciona Terraform que nos está diciendo el plan? Nos está diciendo que se van a agregar nuevas funcionalidades (algo que hace solo por el hecho de subir versión). Ahora bien, ¿por qué? Voy a poner un caso que nos ha ocurrido a nosotros, en nuestro caso nosotros a la hora de aprovisionar el Redis. Que estaba pasando en el momento que nosotros creamos el módulo no existía un módulo que generase el failover y siguiendo las indicaciones que había teníamos un código similar al siguiente:

resource "null_resource" "create_failover" {
  count   = local.create_replica && local.redis_name != "" ? 1 : 0
  depends_on = [ azurerm_redis_cache.redis_caches ]
  provisioner "local-exec" {
    command = "az login --service-principal -u ${var.client_id} -p ${var.client_secret} --tenant ${var.tenant_id} && az account set --subscription=${var.subscription_id} && az redis server-link create --name ${local.redis_name} --replication-role Secondary --resource-group ${local.redis_resource_group} --server-to-link ${local.redis_link_id}"
  }
}
resource "null_resource" "delete_failover" {
  count      = local.create_replica ? 1 : 0
  depends_on = [ azurerm_redis_cache.redis_caches ]
  provisioner "local-exec" {
    when       = destroy
    command    = "az login --service-principal -u ${var.client_id} -p ${var.client_secret} --tenant ${var.tenant_id} && az account set --subscription=${var.subscription_id} & az redis server-link delete --linked-server-name ${local.redis_link_name} --name ${local.redis_name} --resource-group ${local.redis_resource_group}"
    on_failure = continue
  }
}

Para poder migrar este código de la versión 0.12 y pasarlo a la versión 0.13 tendríamos que poner este código

resource null_resource" "create_failover" {
  count   = local.create_replica && local.redis_name != "" ? 1 : 0
  depends_on = [ azurerm_redis_cache.redis_caches ]
  provisioner "local-exec" {
    command =  <<-EOT
        az login --service-principal --username ${data.azurerm_client_config.current.client_id} --password=${var.client_secret} --tenant ${data.azurerm_client_config.current.tenant_id}
        az redis server-link create --name ${local.redis_name} --replication-role Secondary --resource-group ${local.redis_resource_group} --server-to-link ${local.redis_link_id} --subscription ${data.azurerm_client_config.current.subscription_id}
    EOT
  }
}

resource "null_resource" "delete_failover" {
  count      = local.create_replica ? 1 : 0
  depends_on = [ azurerm_redis_cache.redis_caches ]
  triggers   = {
      client_id            = data.azurerm_client_config.current.client_id
      client_secret        = var.client_secret
      subscription_id      = data.azurerm_client_config.current.subscription_id
      tenant_id            = data.azurerm_client_config.current.tenant_id
      redis_name           = local.redis_name
      redis_link_name      = local.redis_link_name
      redis_resource_group = local.redis_resource_group
  }

  provisioner "local-exec" {
    when       = destroy
    command    = <<-EOT
        az login --service-principal --username ${self.triggers.client_id} --password=${self.triggers.client_secret} --tenant ${self.triggers.tenant_id}
        az redis server-link delete --linked-server-name ${self.triggers.redis_link_name} --name ${self.triggers.redis_name} --resource-group ${self.triggers.redis_resource_group} --subscription ${self.triggers.subscription_id}
    EOT

    on_failure = continue
  }
}

¿Qué ocurre en este caso cuando pasamos el plan? Pues que para Terraform el Failover lo que iba a realizar es recrear el recurso, eso que implica que destruye el Redis y lo vuelve a crear. Pero ¿porque Terraform funciona así? Al final lo que le indica es que se el failover se han añadido unas propiedades nuevas internas y tiene que volver a recrear el recurso. ¿Sabemos que implica esto? Que durante el tiempo que tarde esto NO tendremos el Redis. Imaginar que es la cache que tenemos en nuestro sistema en Producción con 60 millones de usuario. Lo que nos implica que bien tenemos que hacer una parada del aplicativo durante el tiempo que dure este proceso. Aproximadamente este proceso dura unos 25 minutos si todo va bien.

¿Tenemos alguna alternativa más? Imaginar hacer una parada de un sistema mínimo 30 minutos sin obtener mucha ganancia. Que alternativas veis posibles para no poder hacer una parada del servicio:

  1. Crearnos un Redis intermedio que lo podamos “pivotar” mientras nuestro Redis está en reconstrucción. El principal hándicap (más allá de un coste mientras el proceso esta ejecutado) es como pasamos los datos de la cache de un momento a otro. Esta opción en nuestro caso podría ser una alternativa porque disponemos de procesos en background que se encargan de rellenar los datos de la cache, pero podría darse el caso que alguna información que estuviera alojada en la cache se perdiera.
  2. Ver si con Terraform tenemos alguna opción de poder indicarla a nuestro sistema que ya tiene un failover y que coja ese estado como punto de partida.

Como seguro que habéis deducido la opción 2 es posible, y sería la mejor desde el punto de vista que se vea: económico, tiempo y afectación del servicio. ¿Qué tenemos que hacer? En este caso en Terraform salio la parte de soportar el failover en Redis de forma nativa. De esta forma en lugar de usar el comando de az-cli usamos la opción que nos da Terraform de forma nativa. Para soportar esto hay que realizar una modificación en el módulo, antes de subir de versión de Terraform, dejándolo de la siguiente forma:

locals {
  create_replica        = var.naming.is_high_availability && var.is_geo_replica
  redis_name            = local.create_replica ? try(azurerm_redis_cache.redis_caches["primary"].name, "") : ""
  redis_resource_group  = local.create_replica ? try(azurerm_redis_cache.redis_caches["primary"].resource_group_name, "") : ""
  redis_link_id         = local.create_replica ? try(azurerm_redis_cache.redis_caches["secondary"].id, "") : ""
  redis_link_name       = local.create_replica ? try(azurerm_redis_cache.redis_caches["secondary"].name, "") : ""
  redis_link_location   = local.create_replica ? try(azurerm_redis_cache.redis_caches["secondary"].location, "") : ""
}

resource "azurerm_redis_linked_server" "failover" {
  count                       = local.create_replica && local.redis_name != "" ? 1 : 0
  target_redis_cache_name     = local.redis_name
  resource_group_name         = local.redis_resource_group
  linked_redis_cache_id       = local.redis_link_id
  linked_redis_cache_location = local.redis_link_location
  server_role                 = "Secondary"
}

Ahora bien, si hacemos este cambio y lanzamos un plan vemos que aún nos indica que tiene dos acciones: eliminar el failover y volver a crearlo. ¿Como podemos hacer que no lo destruya? Es importante tener claro cuál es el ciclo de vida de Terraform (lo que hemos visto en el inicio del articulo). Tenemos que indicarle a Terraform cuál es el estado de la infraestructura que tenemos arriba. ¿Como se puede hacer? Para ello importamos el estado con el siguiente comando

terraform import -var-file terraform.dev.tfvars module.redis.azurerm_redis_linked_server.failover[0] /subscriptions/[guid]/resourceGroups/[name_grupo_recursos]/providers/Microsoft.Cache/Redis/[name_redis]/linkedServers/[name_redis_linked]

Una vez ya tenemos el estado importado, volvemos a lanzar el plan y en este momento el único cambio que tiene que hacer es eliminar las settings que tenía previamente. La eliminación de estas variables no implica la destrucción de ninguna infraestructura por lo que podemos aplicar los cambios sin ninguna afectación. Cuando ya tenemos este cambio realizado en nuestra versión actual de Terraform, podemos subir de versión de Terraform sin ningún problema.

Resumiendo brevemente el proceso a realizar

Al final el paso de una versión de Terraform a otra depende de muchos factores, que módulos estas utilizando, como los estas utilizando. Por lo tanto, a la hora de plantearse una migración de versión hay que tener muy claro cuál es la estrategia y que pros y contras nos podemos encontrar. Seguro que en los primeros proyectos en lo que lo abordemos podemos pensar que en que follón nos hemos metido… pero lo luego una vez vas migrando los proyecto al final te sientes cómodo y estas confiado de haber realizado la mejor opción.

Happy codding :)