Destacaremos algunos inconvenientes, incluidos los relacionados con los bucles, las expresiones if y las metodologías de despliegue, así como problemas más generales que afectan a Terraform en su conjunto:
- los parámetros count y for_each tienen limitaciones;
- limitaciones en despliegues con tiempo de inactividad cero;
- incluso un buen plan puede resultar fallido;
- el refactorizado puede tener sus trampas;
- la consistencia eventual se alinea... con la dilación.
Los parámetros count y for_each tienen limitaciones
En los ejemplos de este capítulo, el parámetro count y la expresión for_each se aplican activamente en bucles y lógica condicional. Funcionan bien, pero hay dos limitaciones importantes que es necesario conocer.
- No se pueden referir a ninguna variable de salida del recurso en count y for_each.
- count y for_each no se pueden usar en la configuración de módulos.
No se pueden referir a ninguna variable de salida del recurso en count y for_each.
Imagina que necesitas desplegar varios servidores EC2 y, por alguna razón, no deseas usar ASG. Tu código podría ser el siguiente:
resource "aws_instance" "example_1" {
count = 3
ami = "ami-0c55b159cbfafe1f0"
instance_type = "t2.micro"
}
Consideremos cada uno por separado.
Dado que al parámetro count se le asigna un valor estático, este código funcionará sin problemas: cuando ejecutes el comando apply, creará tres servidores EC2. Pero, ¿qué pasa si deseas desplegar un servidor en cada zona de disponibilidad (Availability Zone o AZ) dentro de la región actual de AWS? Puedes hacer que tu código cargue la lista de zonas desde la fuente de datos aws_availability_zones y luego recorrer cada una de ellas de forma "cíclica" y crear un servidor EC2 en cada una, utilizando el parámetro count y accediendo al array por índice:
resource "aws_instance" "example_2" {
count = length(data.aws_availability_zones.all.names)
availability_zone = data.aws_availability_zones.all.names[count.index]
ami = "ami-0c55b159cbfafe1f0"
instance_type = "t2.micro"
}
data "aws_availability_zones" "all" {}Este código también funcionará perfectamente, ya que el parámetro count puede referirse sin problemas a las fuentes de datos. Pero, ¿qué ocurrirá si la cantidad de servidores que necesitas crear depende de la salida de algún recurso? Para demostrarlo, lo más sencillo es usar el recurso random_integer, que, como su nombre sugiere, devuelve un número entero aleatorio:
resource "random_integer" "num_instances" {
min = 1
max = 3
}Este código genera un número aleatorio del 1 al 3. Veamos qué sucede si intentamos usar la salida result de este recurso en el parámetro count del recurso aws_instance:
resource "aws_instance" "example_3" {
count = random_integer.num_instances.result
ami = "ami-0c55b159cbfafe1f0"
instance_type = "t2.micro"
}Si ejecutas este código terraform plan, obtendrás el siguiente error:
Error: Argumento count no válido
en main.tf línea 30, en resource "aws_instance" "example_3":
30: count = random_integer.num_instances.result
El valor de "count" depende de atributos de recurso que no se pueden determinar hasta aplicar, por lo que Terraform no puede predecir cuántas instancias se crearán. Para solucionar esto, usa el argumento -target para aplicar primero solo los recursos de los cuales depende el count.Terraform requiere que count y for_each se evalúen en la fase de planificación, antes de crear o modificar cualquier recurso. Esto significa que count y for_each pueden referirse a literales, variables, fuentes de datos e incluso listas de recursos (siempre que su longitud se pueda determinar en el momento de la planificación), pero no a variables de salida calculadas de recursos.
count y for_each no se pueden utilizar en la configuración de un módulo
Alguna vez podrías tener la tentación de agregar el parámetro count en las configuraciones de un módulo:
module "count_example" {
source = "..\/..\/..\/..\/modules\/services\/webserver-cluster"
count = 3
cluster_name = "terraform-up-and-running-example"
server_port = 8080
instance_type = "t2.micro"
}Este código intenta utilizar count dentro de un módulo para crear tres copias del recurso webserver-cluster. O tal vez desees hacer que la conexión del módulo sea opcional dependiendo de alguna condición booleana, asignándole al parámetro count un valor de 0. Este código podría parecer bastante razonable, sin embargo, al ejecutar terraform plan recibirás el siguiente error:
Error: Nombre de argumento reservado en el bloque de módulo
en main.tf línea 13, en module "count_example":
13: count = 3
El nombre "count" está reservado para su uso en una versión futura de Terraform.Desafortunadamente, en el momento del lanzamiento de Terraform 0.12.6, el uso de count o for_each en el recurso del módulo no está soportado. Según las notas de lanzamiento de Terraform 0.12 (http:\/\/bit.ly\/3257bv4), HashiCorp planea agregar esta funcionalidad en el futuro, por lo que, dependiendo de cuándo leas este libro, podría ya estar disponible. Para saberlo con certeza, .
Limitaciones en implementaciones con tiempo de inactividad cero
El uso del bloque create_before_destroy junto con ASG es una excelente solución para organizar despliegues con un tiempo de inactividad nulo, salvo por un pequeño matiz: las reglas de autoescalado no son compatibles en este caso. O, para ser más precisos, esto restablece el tamaño del ASG a min_size en cada despliegue, lo que puede convertirse en un problema si has utilizado reglas de autoescalado para aumentar el número de servidores en ejecución.
Por ejemplo, el módulo webserver-cluster contiene un par de recursos aws_autoscaling_schedule, que a las 9 de la mañana aumentan el número de servidores en el clúster de dos a diez. Si realizas un despliegue, digamos, a las 11 de la mañana, el nuevo grupo de ASG se iniciará no con diez, sino con solo dos servidores, y permanecerá en ese estado hasta las 9 de la mañana del día siguiente.
Esta limitación se puede sortear de varias maneras.
- Cambiar el parámetro recurrence en aws_autoscaling_schedule de 0 9 * * * («iniciar a las 9 de la mañana») a algo como 0-59 9-17 * * * («iniciar cada minuto de 9 de la mañana a 5 de la tarde»). Si ya hay diez servidores en el ASG, volver a ejecutar esta regla de autoescalado no cambiará nada, que es lo que necesitamos. Pero si el grupo de ASG se implementó recientemente, esta regla garantiza que a más tardar en un minuto el número de sus servidores alcanzará diez. Este enfoque no es del todo elegante, y grandes saltos de diez a dos servidores y viceversa también pueden causar problemas a los usuarios.
- Crear un script personalizado que utilice la API de AWS para determinar cuántos servidores activos hay en el ASG, invocarlo mediante una fuente de datos externa (ver «Fuente de datos externa» en la pág. 249) y asignar al parámetro desired_capacity del grupo ASG el valor devuelto por este script. De este modo, cada nueva instancia de ASG siempre se iniciará con la misma capacidad que nuestro código Terraform, lo que complica su mantenimiento.
Por supuesto, idealmente, Terraform debería tener soporte integrado para despliegues con un tiempo de inactividad nulo, pero a partir de mayo de 2019, el equipo de HashiCorp no planeaba añadir esta funcionalidad ().
Un plan correcto puede verse mal implementado
A veces, al ejecutar el comando plan, se obtiene un plan de implementación bastante correcto, sin embargo, el comando apply devuelve un error. Intente, por ejemplo, agregar el recurso aws_iam_user con el mismo nombre que utilizó para el usuario IAM que creó anteriormente en el capítulo 2:
resource "aws_iam_user" "existing_user" {
# Sustituya aquí el nombre del usuario IAM existente,
# para practicar el uso del comando terraform import
name = "yevgeniy.brikman"
}Ahora, si ejecuta el comando plan, Terraform mostrará a primera vista un plan de implementación bastante razonable:
Terraform realizará las siguientes acciones:
# aws_iam_user.existing_user será creado
+ resource "aws_iam_user" "existing_user" {
+ arn = (conocido después de aplicar)
+ force_destroy = false
+ id = (conocido después de aplicar)
+ name = "yevgeniy.brikman"
+ path = "\/"
+ unique_id = (conocido después de aplicar)
}
Plan: 1 a agregar, 0 a cambiar, 0 a destruir.Si ejecuta el comando apply, obtendrá el siguiente error:
Error: Error al crear el usuario IAM yevgeniy.brikman: EntityAlreadyExists:
El usuario con el nombre yevgeniy.brikman ya existe.
en main.tf línea 10, en resource "aws_iam_user" "existing_user":
10: resource "aws_iam_user" "existing_user" {El problema, por supuesto, es que ya existe un usuario IAM con ese nombre. Y esto puede suceder no solo con los usuarios IAM, sino también con casi cualquier recurso. Puede que alguien haya creado este recurso manualmente o mediante la línea de comandos, pero de cualquier manera, la coincidencia de identificadores provoca conflictos. Esta error tiene muchas variantes que a menudo sorprenden a los principiantes en Terraform.
El punto clave es que el comando terraform plan solo considera los recursos que están en el archivo de estado de Terraform. Si los recursos se crean de otra manera (por ejemplo, manualmente, haciendo clic en la consola de AWS), no aparecerán en el archivo de estado y, por lo tanto, Terraform no los tendrá en cuenta al ejecutar el comando plan. Como resultado, un plan que parece correcto a primera vista terminará siendo fallido.
De esto se pueden extraer dos lecciones.
- Si ya ha comenzado a trabajar con Terraform, no use nada más. Si parte de su infraestructura se gestiona con Terraform, no se puede modificar manualmente. De lo contrario, no solo corre el riesgo de obtener errores extraños de Terraform, sino que también anula muchas de las ventajas de IaC, ya que el código ya no será una representación precisa de su infraestructura.
- Si ya tiene alguna infraestructura, utilice el comando import. Si está comenzando a usar Terraform con infraestructura existente, se puede agregar al estado utilizando el comando terraform import. Así, Terraform sabrá qué infraestructura debe gestionar. El comando import acepta dos argumentos. El primero es la dirección del recurso en sus archivos de configuración. Aquí el mismo sintaxis que en los enlaces a recursos: _. (como aws_iam_user.existing_user). El segundo argumento es la identificación del recurso que se debe importar. Por ejemplo, como ID del recurso aws_iam_user se utiliza el nombre de usuario (por ejemplo, yevgeniy.brikman), y el ID del recurso aws_instance es el identificador del servidor EC2 (como i-190e22e5). La forma de importar el recurso generalmente se indica en la documentación en la parte inferior de su página.
A continuación se muestra el comando import que permite sincronizar el recurso aws_iam_user que ha añadido a su configuración de Terraform junto con el usuario IAM en el capítulo 2 (por supuesto, en lugar de yevgeniy.brikman debe sustituir su nombre):
$ terraform import aws_iam_user.existing_user yevgeniy.brikmanTerraform se conectará a la API de AWS para encontrar su usuario IAM y crear en el archivo de estado una relación entre él y el recurso aws_iam_user.existing_user en su configuración de Terraform. A partir de ese momento, al ejecutar el comando plan, Terraform sabrá que el usuario IAM ya existe y no intentará crearlo nuevamente.
Cabe señalar que, si ya tiene muchos recursos que desea importar en Terraform, escribir código manualmente e importar cada uno de ellos uno por uno puede resultar una tarea tediosa. Por lo tanto, vale la pena considerar una herramienta como Terraforming (http://terraforming.dtan4.net/), que puede importar automáticamente el código y el estado de la cuenta de AWS.
El refactorizado puede tener sus trampas
Refactorización — es una práctica común en programación cuando cambia la estructura interna del código, dejando el comportamiento externo sin cambios. Esto es necesario para que el código sea más comprensible, ordenado y fácil de mantener. La refactorización es una técnica indispensable que debe aplicarse regularmente. Pero, cuando se trata de Terraform o cualquier otra herramienta de IaC, se debe tener mucho cuidado con lo que se entiende por "comportamiento externo" de una sección de código, ya que de lo contrario pueden surgir problemas imprevistos.
Por ejemplo, una forma común de refactorización es reemplazar los nombres de variables o funciones por otros más comprensibles. Muchas IDE tienen soporte de refactorización integrado y pueden renombrar automáticamente variables y funciones en todo el proyecto. En lenguajes de programación de propósito general, este es un procedimiento trivial que no requiere mucho pensamiento, sin embargo, en Terraform hay que tener mucho cuidado con esto, de lo contrario se pueden enfrentar a interrupciones en el funcionamiento.
Por ejemplo, el módulo webserver-cluster tiene una variable de entrada cluster_name:
variable "cluster_name" { description = "El nombre a utilizar para todos los recursos del clúster" type = string }Imagina que comenzaste a usar este módulo para desplegar un microservicio llamado foo. Más tarde quisiste renombrar tu servicio a bar. Este cambio puede parecer trivial, pero en realidad puede causar interrupciones en el funcionamiento.
La cuestión es que el módulo webserver-cluster utiliza la variable cluster_name en una serie de recursos, incluyendo el parámetro name de dos grupos de seguridad y ALB:
resource "aws_lb" "example" { name = var.cluster_name load_balancer_type = "application" subnets = data.aws_subnet_ids.default.ids security_groups = [aws_security_group.alb.id] }Si cambias el parámetro name en algún recurso, Terraform eliminará la versión antigua de ese recurso y creará una nueva en su lugar. Pero si ese recurso es un ALB, durante el período entre su eliminación y la carga de la nueva versión, no tendrás un mecanismo para redirigir el tráfico a tu servidor web. De igual manera, si se elimina un grupo de seguridad, tus servidores comenzarán a rechazar cualquier tráfico de red hasta que se cree un nuevo grupo.
Otro tipo de refactorización que puede interesarte es el cambio de identificador de Terraform. Tomemos como ejemplo el recurso aws_security_group en el módulo webserver-cluster:
resource "aws_security_group" "instance" { # (...) }El identificador de este recurso se llama instance. Imagina que durante la refactorización decides cambiarlo a un nombre más comprensible (en tu opinión) cluster_instance:
resource "aws_security_group" "cluster_instance" { # (...) }¿Qué sucederá al final? Correcto: una interrupción en el funcionamiento.
Terraform vincula la ID de cada recurso con el identificador del proveedor de la nube. Por ejemplo, iam_user se asocia con el ID de usuario IAM en AWS, y aws_instance se vincula a la ID del servidor AWS EC2. Si se cambia el identificador del recurso (por ejemplo, de instance a cluster_instance, como en el caso de aws_security_group), para Terraform parecerá que has eliminado el recurso antiguo y añadido uno nuevo. Si aplicas estos cambios, Terraform eliminará el antiguo grupo de seguridad y creará otro, mientras tanto, tus servidores comenzarán a rechazar cualquier tráfico de red.
Aquí hay cuatro lecciones clave que debes aprender de esta discusión.
- Siempre utiliza el comando plan. Ayuda a identificar todos estos inconvenientes. Revisa cuidadosamente su salida y presta atención a las situaciones en las que Terraform planea eliminar recursos que es probable que no deban ser eliminados.
- Crea antes de eliminar. Si deseas reemplazar un recurso, piénsalo bien, ¿necesitas crear la sustitución antes de eliminar el original? Si la respuesta es afirmativa, esto puede ayudar: create_before_destroy. También puedes lograr el mismo resultado manualmente realizando dos pasos: primero, añade el nuevo recurso a la configuración y ejecuta el comando apply, y luego elimina el recurso antiguo de la configuración y utiliza el comando apply una vez más.
- Cambiar identificadores requiere modificar el estado. Si deseas cambiar el identificador asociado a un recurso (por ejemplo, renombrar aws_security_group de instance a cluster_instance), evitando así eliminar el recurso y crear una nueva versión, necesitas actualizar adecuadamente el archivo de estado de Terraform. Nunca hagas esto manualmente; utiliza el comando terraform state en su lugar. Al renombrar identificadores, se debe ejecutar el comando terraform state mv, que tiene la siguiente sintaxis:
terraform state mvORIGINAL_REFERENCE es una expresión que se refiere al recurso en su forma actual, y NEW_REFERENCE es el lugar al que deseas moverlo. Por ejemplo, al renombrar el grupo aws_security_group de instance a cluster_instance, debes ejecutar el siguiente comando:
$ terraform state mv aws_security_group.instance aws_security_group.cluster_instanceAsí le indicarás a Terraform que el estado que anteriormente pertenecía a aws_security_group.instance ahora debe estar asociado a aws_security_group.cluster_instance. Si después de renombrarlo y ejecutar este comando terraform plan no muestra ningún cambio, significa que lo hiciste correctamente.
- Algunos parámetros no se pueden cambiar. Los parámetros de muchos recursos son inmutables. Si intentas cambiarlos, Terraform eliminará el recurso antiguo y creará uno nuevo en su lugar. En la página de cada recurso, generalmente se indica lo que sucede al cambiar uno u otro parámetro, así que no olvides consultar la documentación. Siempre usa el comando plan y considera la viabilidad de aplicar la estrategia create_before_destroy.
La consistencia eventual se basa en... en la deliberación
Las API de algunos proveedores de la nube, como AWS, son asincrónicas y tienen consistencia eventual. La asincronía significa que la interfaz puede devolver una respuesta de inmediato sin esperar a que se complete la acción solicitada. La consistencia eventual significa que puede llevar tiempo propagar los cambios en todo el sistema; mientras esto ocurre, tus respuestas pueden ser inconsistentes y depender de qué réplica de la fuente de datos responde a tus llamadas API.
Imagina, por ejemplo, que haces una llamada API a AWS pidiendo crear un servidor EC2. La API devolverá una respuesta "exitosa" (201 Created) casi instantáneamente, sin esperar a que se cree el servidor. Si intentas conectarte inmediatamente, casi seguramente no funcionará, ya que en ese momento AWS aún está inicializando los recursos o, como alternativa, el servidor aún no ha arrancado. Además, si haces otra llamada para obtener información sobre este servidor, puede que recibas un error (404 Not Found). El hecho es que la información sobre este servidor EC2 aún puede estar propagándose por AWS, y se necesitarán unos segundos para que esté disponible en todas partes.
Cada vez que uses una API asincrónica con consistencia eventual, debes repetir tu solicitud periódicamente hasta que la acción se complete y se propague por el sistema. Desafortunadamente, el SDK de AWS no proporciona buenas herramientas para esto, y el proyecto Terraform anteriormente sufrió muchos errores como el 6813 (https://github.com/hashicorp/terraform/issues/6813):
$ terraform apply aws_subnet.private-persistence.2: InvalidSubnetID.NotFound: El ID de subred 'subnet-xxxxxxx' no existeEn otras palabras, estás creando un recurso (como una subred) y luego intentas obtener información sobre él (como el ID de la subred recién creada), pero Terraform no puede encontrarlos. La mayoría de estos errores (incluido el 6813) ya han sido corregidos, pero de vez en cuando todavía aparecen, especialmente cuando se añade soporte para un nuevo tipo de recurso en Terraform. Es frustrante, pero en la mayoría de los casos no causa ningún daño. Al volver a ejecutar terraform apply, todo debería funcionar, ya que para ese momento la información ya se habrá propagado por el sistema.
Este fragmento se extrae del libro de Evgeny Brikman .
Fuente: habr.com
