Errores de Terraform

Errores de Terraform
Resaltemos algunos errores, incluidos los relacionados con bucles, declaraciones if y técnicas de implementación, así como problemas más generales que afectan a Terraform en general:

  • los parámetros count y for_each tienen limitaciones;
  • limitar las implementaciones con tiempo de inactividad cero;
  • incluso un buen plan puede fracasar;
  • la refactorización puede tener sus riesgos;
  • la coherencia diferida es consistente... con el diferimiento.

Los parámetros count y for_each tienen limitaciones

Los ejemplos de este capítulo hacen un uso extensivo del parámetro count y la expresión for_each en bucles y lógica condicional. Funcionan bien, pero tienen dos limitaciones importantes que debes tener en cuenta.

  • Count y for_each no pueden hacer referencia a ninguna variable de salida de recursos.
  • count y for_each no se pueden utilizar en la configuración del módulo.

count y for_each no pueden hacer referencia a ninguna variable de salida de recursos

Imagine que necesita implementar varios servidores EC2 y por alguna razón no desea utilizar ASG. Tu código podría ser así:

resource "aws_instance" "example_1" {
   count             = 3
   ami                = "ami-0c55b159cbfafe1f0"
   instance_type = "t2.micro"
}

Veámoslos uno por uno.

Dado que el parámetro de recuento está establecido en un valor estático, este código funcionará sin problemas: cuando ejecute el comando aplicar, creará tres servidores EC2. Pero, ¿qué pasaría si quisiera implementar un servidor en cada zona de disponibilidad (AZ) dentro de su región actual de AWS? Puede hacer que su código cargue una lista de zonas de la fuente de datos aws_availability_zones y luego recorrer cada una y crear un servidor EC2 en ella utilizando el parámetro count y el acceso al índice de matriz:

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á bien, ya que el parámetro de recuento puede hacer referencia a fuentes de datos sin ningún problema. Pero, ¿qué sucede si la cantidad de servidores que necesita crear depende de la producción de algún recurso? Para demostrar esto, la forma más sencilla es utilizar el recurso random_integer, que, como sugiere el nombre, devuelve un número entero aleatorio:

resource "random_integer" "num_instances" {
  min = 1
  max = 3
}

Este código genera un número aleatorio entre 1 y 3. Veamos qué sucede si intentamos utilizar la salida de este recurso en el parámetro de recuento del recurso aws_instance:

resource "aws_instance" "example_3" {
   count             = random_integer.num_instances.result
   ami                = "ami-0c55b159cbfafe1f0"
   instance_type = "t2.micro"
}

Si ejecuta terraform plan en este código, obtendrá el siguiente error:

Error: Invalid count argument

   on main.tf line 30, in resource "aws_instance" "example_3":
   30: count = random_integer.num_instances.result

The "count" value depends on resource attributes that cannot be determined until apply, so Terraform cannot predict how many instances will be created. To work around this, use the -target argument to first apply only the resources that the count depends on.

Terraform requiere que count y for_each se calculen durante la fase de planificación, antes de crear o modificar cualquier recurso. Esto significa que count y for_each pueden hacer referencia a literales, variables, fuentes de datos e incluso listas de recursos (siempre que su longitud pueda determinarse en el momento de la programación), pero no a variables de salida de recursos calculadas.

count y for_each no se pueden usar en la configuración del módulo

Algún día podrías sentirte tentado a agregar un parámetro de conteo a la configuración de tu 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 el recuento dentro de un módulo para crear tres copias del recurso del clúster del servidor web. O quizás quieras hacer que la conexión de un módulo sea opcional dependiendo de alguna condición booleana estableciendo su parámetro de recuento en 0. Esto puede parecer un código razonable, pero obtendrás este error al ejecutar el plan terraform:

Error: Reserved argument name in module block

   on main.tf line 13, in module "count_example":
   13: count = 3

The name "count" is reserved for use in a future version of Terraform.

Desafortunadamente, a partir de Terraform 0.12.6, no se admite el uso de count o for_each en un recurso de módulo. Según las notas de la versión Terraform 0.12 (http://bit.ly/3257bv4), HashiCorp planea agregar esta capacidad en el futuro, por lo que, dependiendo de cuándo lea este libro, es posible que ya esté disponible. Para estar seguro, lea el registro de cambios de Terraform aquí.

Limitaciones de las implementaciones con tiempo de inactividad cero

Usar el bloque create_before_destroy en combinación con ASG es una excelente solución para crear implementaciones sin tiempo de inactividad, excepto por una advertencia: las reglas de escalado automático no son compatibles. O para ser más precisos, esto restablece el tamaño de ASG a min_size en cada implementación, lo que podría ser un problema si estuviera usando reglas de escalado automático para aumentar la cantidad de servidores en ejecución.

Por ejemplo, el módulo webserver-cluster contiene un par de recursos aws_autoscaling_schedule, que a las 9 am aumenta la cantidad de servidores en el clúster de dos a diez. Si realiza la implementación, digamos, a las 11 a. m., el nuevo ASG se iniciará con solo dos servidores en lugar de diez y permanecerá así hasta las 9 a. m. del día siguiente.

Esta limitación se puede eludir de varias maneras.

  • Cambie el parámetro de recurrencia en aws_autoscaling_schedule de 0 9 * * * (“ejecutar a las 9 a. m.”) a algo como 0-59 9-17 * * * (“ejecutar cada minuto de 9 a. m. a 5 p. m.”). Si ASG ya tiene diez servidores, ejecutar esta regla de escalado automático nuevamente no cambiará nada, que es lo que queremos. Pero si el ASG se ha implementado recientemente, esta regla garantizará que en un minuto como máximo el número de sus servidores llegue a diez. Este no es un enfoque del todo elegante y los grandes saltos de diez a dos servidores y viceversa también pueden causar problemas a los usuarios.
  • Cree un script personalizado que utilice la API de AWS para determinar la cantidad de servidores activos en el ASG, llámelo usando una fuente de datos externa (consulte "Fuente de datos externa" en la página 249) y establezca el parámetro capacidad_deseado del ASG en el valor devuelto por la secuencia de comandos. De esta manera, cada nueva instancia de ASG siempre se ejecutará con la misma capacidad que el código Terraform existente y lo hará más difícil de mantener.

Por supuesto, lo ideal sería que Terraform tuviera soporte integrado para implementaciones sin tiempo de inactividad, pero en mayo de 2019, el equipo de HashiCorp no tenía planes de agregar esta funcionalidad (detalles - aquí).

El plan correcto puede implementarse sin éxito.

A veces, el comando plan produce un plan de implementación perfectamente correcto, pero el comando aplicar devuelve un error. Intente, por ejemplo, agregar el recurso aws_iam_user con el mismo nombre que utilizó para el usuario de IAM que creó anteriormente en el Capítulo 2:

resource "aws_iam_user" "existing_user" {
   # Подставьте сюда имя уже существующего пользователя IAM,
   # чтобы попрактиковаться в использовании команды terraform import
   name = "yevgeniy.brikman"
}

Ahora, si ejecuta el comando de plan, Terraform generará un plan de implementación aparentemente razonable:

Terraform will perform the following actions:

   # aws_iam_user.existing_user will be created
   + resource "aws_iam_user" "existing_user" {
         + arn                  = (known after apply)
         + force_destroy   = false
         + id                    = (known after apply)
         + name               = "yevgeniy.brikman"
         + path                 = "/"
         + unique_id         = (known after apply)
      }

Plan: 1 to add, 0 to change, 0 to destroy.

Si ejecuta el comando de aplicación, obtendrá el siguiente error:

Error: Error creating IAM User yevgeniy.brikman: EntityAlreadyExists:
User with name yevgeniy.brikman already exists.

   on main.tf line 10, in resource "aws_iam_user" "existing_user":
   10: resource "aws_iam_user" "existing_user" {

El problema, por supuesto, es que ya existe un usuario de IAM con ese nombre. Y esto puede suceder no sólo a los usuarios de IAM, sino a casi cualquier recurso. Es posible que alguien haya creado este recurso manualmente o usando la línea de comando, pero de cualquier manera, la coincidencia de ID genera conflictos. Hay muchas variaciones de este error que a menudo toman por sorpresa a los recién llegados a Terraform.

El punto clave es que el comando del plan de Terraform solo tiene en cuenta aquellos recursos que se especifican en el archivo de estado de Terraform. Si los recursos se crean de alguna otra manera (por ejemplo, manualmente haciendo clic en la consola de AWS), no terminarán en el archivo de estado y, por lo tanto, Terraform no los tendrá en cuenta al ejecutar el comando del plan. Como resultado, un plan que parece correcto a primera vista resultará fallido.

Hay dos lecciones que aprender de esto.

  • Si ya has empezado a trabajar con Terraform, no utilices nada más. Si parte de su infraestructura se administra mediante Terraform, ya no podrá modificarla manualmente. De lo contrario, no sólo corre el riesgo de errores extraños de Terraform, sino que también anula muchos de los beneficios de IaC, ya que el código ya no será una representación precisa de su infraestructura.
  • Si ya tiene alguna infraestructura, use el comando de importación. Si está comenzando a utilizar Terraform con una infraestructura existente, puede agregarlo al archivo de estado usando el comando de importación de terraform. De esta forma, Terraform sabrá qué infraestructura debe gestionarse. El comando de importación toma dos argumentos. La primera es la dirección del recurso en sus archivos de configuración. La sintaxis aquí es la misma que para los enlaces de recursos: _. (como aws_iam_user.existing_user). El segundo argumento es el ID del recurso que se va a importar. Digamos que el ID del recurso aws_iam_user es el nombre de usuario (por ejemplo, yevgeniy.brikman) y el ID del recurso aws_instance es el ID del servidor EC2 (como i-190e22e5). La forma de importar un recurso generalmente se indica en la documentación al final de su página.

    A continuación se muestra un comando de importación que sincroniza el recurso aws_iam_user que agregó a su configuración de Terraform junto con el usuario de IAM en el Capítulo 2 (sustituyendo su nombre por yevgeniy.brikman, por supuesto):

    $ terraform import aws_iam_user.existing_user yevgeniy.brikman

    Terraform llamará a la API de AWS para encontrar su usuario de IAM y creará una asociación de archivo de estado entre él y el recurso aws_iam_user.existing_user en su configuración de Terraform. De ahora en adelante, cuando ejecute el comando de plan, Terraform sabrá que el usuario de IAM ya existe y no intentará crearlo nuevamente.

    Vale la pena señalar que si ya tiene muchos recursos que desea importar a Terraform, escribir el código manualmente e importar cada uno a la vez puede ser una molestia. Por lo tanto, vale la pena buscar una herramienta como Terraforming (http://terraforming.dtan4.net/), que puede importar automáticamente código y estado desde su cuenta de AWS.

    La refactorización puede tener sus riesgos

    Refactorización Es una práctica común en programación en la que se cambia la estructura interna del código sin modificar el comportamiento externo. Esto es para hacer que el código sea más claro, ordenado y fácil de mantener. La refactorización es una técnica indispensable que debe utilizarse con regularidad. Pero cuando se trata de Terraform o cualquier otra herramienta IaC, hay que tener mucho cuidado con lo que se entiende por “comportamiento externo” de un fragmento de código, de lo contrario surgirán problemas inesperados.

    Por ejemplo, un tipo común de refactorización es reemplazar los nombres de variables o funciones por otros más comprensibles. Muchos IDE tienen soporte integrado para refactorización y pueden cambiar automáticamente el nombre de variables y funciones a lo largo del proyecto. En los lenguajes de programación de propósito general, este es un procedimiento trivial en el que quizás no pienses, pero en Terraform debes tener mucho cuidado con esto, de lo contrario puedes experimentar interrupciones.

    Por ejemplo, el módulo clúster-servidor web tiene una variable de entrada nombre_clúster:

    variable "cluster_name" {
       description = "The name to use for all the cluster resources"
       type          = string
    }

    Imagine que comenzó a usar este módulo para implementar un microservicio llamado foo. Más tarde, querrás cambiar el nombre de tu servicio a bar. Este cambio puede parecer trivial, pero en realidad puede provocar interrupciones en el servicio.

    El hecho es que el módulo webserver-cluster utiliza la variable cluster_name en varios recursos, incluido el parámetro de nombre de dos grupos de seguridad y el 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 cambia el parámetro de nombre en un recurso, Terraform eliminará la versión anterior de ese recurso y creará una nueva en su lugar. Pero si ese recurso es un ALB, entre eliminarlo y descargar una nueva versión, no tendrá un mecanismo para redirigir el tráfico a su servidor web. Del mismo modo, si se elimina un grupo de seguridad, sus servidores comenzarán a rechazar cualquier tráfico de red hasta que se cree un nuevo grupo.

    Otro tipo de refactorización que podría interesarle es cambiar el ID 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 instancia. Imagine que durante la refactorización decidió cambiarlo por un nombre más comprensible (en su opinión) cluster_instance:

    resource "aws_security_group" "cluster_instance" {
       # (...)
    }

    ¿Qué pasará al final? Así es: una disrupción.

    Terraform asocia cada ID de recurso con el ID del proveedor de la nube. Por ejemplo, iam_user está asociado con el ID de usuario de AWS IAM y aws_instance está asociado con el ID del servidor AWS EC2. Si cambia el ID del recurso (por ejemplo, de instancia a cluster_instance, como es el caso de aws_security_group), en Terraform aparecerá como si hubiera eliminado el recurso anterior y hubiera agregado uno nuevo. Si aplica estos cambios, Terraform eliminará el grupo de seguridad antiguo y creará uno nuevo, mientras sus servidores comienzan a rechazar cualquier tráfico de red.

    Aquí hay cuatro lecciones clave que usted debe aprender de esta discusión.

    • Utilice siempre el comando de plan. Puede revelar todos estos inconvenientes. Revise su resultado detenidamente y preste atención a situaciones en las que Terraform planea eliminar recursos que probablemente no deberían eliminarse.
    • Crea antes de eliminar. Si desea reemplazar un recurso, piense detenidamente si necesita crear un reemplazo antes de eliminar el original. Si la respuesta es sí, create_before_destroy puede ayudar. Se puede lograr el mismo resultado manualmente realizando dos pasos: primero agregue un nuevo recurso a la configuración y ejecute el comando de aplicación, y luego elimine el recurso anterior de la configuración y use el comando de aplicación nuevamente.
    • Cambiar identificadores requiere cambiar de estado. Si desea cambiar el ID asociado con un recurso (por ejemplo, cambiar el nombre de aws_security_group de instancia a cluster_instance) sin eliminar el recurso y crear una nueva versión del mismo, debe actualizar el archivo de estado de Terraform en consecuencia. Nunca hagas esto manualmente; en su lugar, usa el comando terraform state. Al cambiar el nombre de los identificadores, debe ejecutar el comando terraform state mv, que tiene la siguiente sintaxis:
      terraform state mv <ORIGINAL_REFERENCE> <NEW_REFERENCE>

      ORIGINAL_REFERENCE es una expresión que hace referencia al recurso en su forma actual y NEW_REFERENCE es donde desea moverlo. Por ejemplo, al cambiar el nombre del grupo aws_security_group de instancia a cluster_instance, debe ejecutar el siguiente comando:

      $ terraform state mv 
         aws_security_group.instance 
         aws_security_group.cluster_instance

      Esto le indica a Terraform que el estado que anteriormente estaba asociado con aws_security_group.instance ahora debería estar asociado con aws_security_group.cluster_instance. Si después de cambiar el nombre y ejecutar este comando, terraform plan no muestra ningún cambio, entonces hiciste todo correctamente.

    • Algunas configuraciones no se pueden cambiar. Los parámetros de muchos recursos son inmutables. Si intenta cambiarlos, Terraform eliminará el recurso antiguo y creará uno nuevo en su lugar. Cada página de recursos normalmente indicará lo que sucede cuando cambia una configuración particular, así que asegúrese de consultar la documentación. Utilice siempre el comando plan y considere utilizar la estrategia create_before_destroy.

    La consistencia diferida es consistente... con el aplazamiento

    Las API de algunos proveedores de nube, como AWS, son asincrónicas y tienen una coherencia retrasada. Asincronía significa que la interfaz puede devolver inmediatamente una respuesta sin esperar a que se complete la acción solicitada. La coherencia retrasada significa que los cambios pueden tardar en propagarse por todo el sistema; Mientras esto sucede, sus respuestas pueden ser inconsistentes y depender de qué réplica de fuente de datos responde a sus llamadas API.

    Imagine, por ejemplo, que realiza una llamada API a AWS pidiéndole que cree un servidor EC2. La API devolverá una respuesta "exitosa" (201 creado) casi instantáneamente, sin esperar a que se cree el servidor. Si intenta conectarse a él de inmediato, es casi seguro que fallará porque en ese momento AWS todavía está inicializando recursos o, alternativamente, el servidor aún no se ha iniciado. Además, si realiza otra llamada para obtener información sobre este servidor, puede recibir un error (404 no encontrado). El caso es que es posible que la información sobre este servidor EC2 aún se propague por AWS antes de que esté disponible en todas partes, tendrás que esperar unos segundos.

    Siempre que utilice una API asincrónica con coherencia diferida, debe volver a intentar su 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 solía sufrir muchos errores como el 6813 (https://github.com/hashicorp/terraform/issues/6813):

    $ terraform apply
    aws_subnet.private-persistence.2: InvalidSubnetID.NotFound:
    The subnet ID 'subnet-xxxxxxx' does not exist

    En otras palabras, crea un recurso (como una subred) y luego intenta obtener información sobre él (como el ID de la subred recién creada), y Terraform no puede encontrarlo. La mayoría de estos errores (incluido el 6813) se han solucionado, pero aún surgen de vez en cuando, especialmente cuando Terraform agrega soporte para un nuevo tipo de recurso. Esto es molesto, pero en la mayoría de los casos no causa ningún daño. Cuando vuelvas a ejecutar terraform apply, todo debería funcionar, ya que en ese momento la información ya se habrá extendido por todo el sistema.

    Este extracto se presenta del libro de Evgeniy Brikman. "Terraform: infraestructura a nivel de código".

Fuente: habr.com

Añadir un comentario