Trampas de Terraform

Trampas de Terraform
Destaquemos algunhas trampas, incluíndo as relacionadas con bucles, declaracións if e técnicas de despregamento, así como cuestións máis xerais que afectan a Terraform en xeral:

  • os parámetros count e for_each teñen limitacións;
  • limitar cero despregamentos de tempo de inactividade;
  • mesmo un bo plan pode fallar;
  • a refactorización pode ter as súas trampas;
  • a coherencia aprazada é consistente... co aprazamento.

Os parámetros count e for_each teñen limitacións

Os exemplos deste capítulo fan un uso extensivo do parámetro count e da expresión for_each en bucles e lóxica condicional. Funcionan ben, pero teñen dúas limitacións importantes que debes ter en conta.

  • Count e for_each non poden facer referencia a ningunha variable de saída de recurso.
  • count e for_each non se poden usar na configuración do módulo.

count e for_each non poden facer referencia a ningunha variable de saída de recurso

Imaxina que necesitas implementar varios servidores EC2 e por algún motivo non queres usar ASG. O teu código podería ser así:

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

Mirámolos un por un.

Dado que o parámetro count está configurado nun valor estático, este código funcionará sen problemas: cando execute o comando apply, creará tres servidores EC2. Pero que pasa se quixese implantar un servidor en cada zona de dispoñibilidade (AZ) na súa rexión de AWS actual? Podes facer que o teu código cargue unha lista de zonas desde a fonte de datos aws_availability_zones e, a continuación, recorra cada unha e cree un servidor EC2 nel usando o parámetro de conta e o acceso ao í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 tamén funcionará ben, xa que o parámetro count pode facer referencia a fontes de datos sen ningún problema. Pero que pasa se o número de servidores que necesitas crear depende da saída dalgún recurso? Para demostrar isto, o xeito máis sinxelo é usar o recurso random_integer, que, como o nome indica, devolve un enteiro aleatorio:

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

Este código xera un número aleatorio entre 1 e 3. Vexamos que pasa se tentamos utilizar a saída deste recurso no parámetro count do recurso aws_instance:

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

Se executas terraform plan neste código, obterás o seguinte erro:

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 require que count e for_each se calculen durante a fase de planificación, antes de crear ou modificar recursos. Isto significa que count e for_each poden referirse a literais, variables, fontes de datos e incluso listas de recursos (sempre que a súa lonxitude se poida determinar no momento da programación), pero non ás variables de saída de recursos calculadas.

count e for_each non se poden usar na configuración do módulo

Algún día podes ter a tentación de engadir un parámetro de conta á configuración do teu 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 tenta usar count dentro dun módulo para crear tres copias do recurso do clúster do servidor web. Ou pode querer facer a conexión dun módulo opcional dependendo dalgunha condición booleana configurando o seu parámetro de conta en 0. Isto pode parecer un código razoable, pero obterá este erro ao executar o 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, non se admite o uso de count ou for_each nun recurso de módulo. Segundo as notas de versión de Terraform 0.12 (http://bit.ly/3257bv4), HashiCorp planea engadir esta capacidade no futuro, polo que dependendo de cando leas este libro, é posible que xa estea dispoñible. Para saber con certeza, le o rexistro de cambios de Terraform aquí.

Limitacións das implementacións sen tempo de inactividade

Usar o bloque create_before_destroy en combinación con ASG é unha excelente solución para crear despregamentos sen tempo de inactividade, agás unha advertencia: as regras de escalado automático non son compatibles. Ou para ser máis precisos, isto restablece o tamaño de ASG a min_size en cada implementación, o que podería ser un problema se estivese a usar regras de escalado automático para aumentar o número de servidores en execución.

Por exemplo, o módulo webserver-cluster contén un par de recursos aws_autoscaling_schedule, que ás 9 da mañá aumentan o número de servidores do clúster de dous a dez. Se se implementa ás 11 da mañá, por exemplo, o novo ASG arrancará con só dous servidores en lugar de dez e permanecerá así ata as 9 da mañá do día seguinte.

Esta limitación pódese eludir de varias maneiras.

  • Cambia o parámetro de recorrencia en aws_autoscaling_schedule de 0 9 * * * ("executar ás 9 a.m.") a algo así como 0-59 9-17 * * * ("executar cada minuto de 9 a.m. a 5 p.m."). Se ASG xa ten dez servidores, executar de novo esta regra de escalado automático non cambiará nada, que é o que queremos. Pero se o ASG só se implantou recentemente, esta regra garantirá que nun minuto como máximo o número dos seus servidores chegue a dez. Este non é un enfoque totalmente elegante, e grandes saltos de dez a dous servidores e viceversa tamén poden causar problemas aos usuarios.
  • Cree un script personalizado que use a API de AWS para determinar o número de servidores activos no ASG, chámao mediante unha fonte de datos externa (consulte "Fonte de datos externa" na páxina 249) e estableza o parámetro de capacidade_desirada do ASG co valor devolto por o guión. Deste xeito, cada nova instancia de ASG executarase sempre coa mesma capacidade que o código Terraform existente e dificulta o seu mantemento.

Por suposto, o ideal sería que Terraform tivese soporte integrado para implementacións sen tempo de inactividade, pero a partir de maio de 2019, o equipo de HashiCorp non tiña previsto engadir esta funcionalidade (detalles - aquí).

O plan correcto pode implementarse sen éxito

Ás veces, o comando plan produce un plan de implantación perfectamente correcto, pero o comando apply devolve un erro. Proba, por exemplo, a engadir o recurso aws_iam_user co mesmo nome que utilizaches para o usuario de IAM que creaches anteriormente no Capítulo 2:

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

Agora, se executas o comando plan, Terraform mostrará un plan de implantación aparentemente razoable:

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.

Se executas o comando de aplicación, obterás o seguinte erro:

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" {

O problema, por suposto, é que xa existe un usuario de IAM con ese nome. E isto pode ocorrer non só aos usuarios de IAM, senón a case calquera recurso. É posible que alguén crease este recurso manualmente ou mediante a liña de comandos, pero de calquera xeito, a coincidencia de ID provoca conflitos. Hai moitas variacións deste erro que adoitan coller por sorpresa aos recén chegados a Terraform.

O punto clave é que o comando terraform plan só ten en conta aqueles recursos que se especifican no ficheiro de estado de Terraform. Se os recursos se crean doutro xeito (por exemplo, manualmente facendo clic na consola de AWS), non acabarán no ficheiro de estado e, polo tanto, Terraform non os terá en conta ao executar o comando do plan. Como resultado, un plan que parece correcto a primeira vista resultará infructuoso.

Hai dúas leccións que se poden aprender disto.

  • Se xa comezaches a traballar con Terraform, non uses nada máis. Se parte da túa infraestrutura se xestiona mediante Terraform, xa non poderás modificala manualmente. En caso contrario, non só corres o risco de erros estraños de Terraform, senón que tamén negas moitos dos beneficios de IaC xa que o código xa non será unha representación precisa da túa infraestrutura.
  • Se xa tes algunha infraestrutura, utiliza o comando de importación. Se comeza a usar Terraform cunha infraestrutura existente, pode engadilo ao ficheiro de estado mediante o comando de importación de terraform. Deste xeito Terraform saberá que infraestruturas hai que xestionar. O comando de importación leva dous argumentos. O primeiro é o enderezo do recurso nos ficheiros de configuración. A sintaxe aquí é a mesma que para as ligazóns de recursos: _. (como aws_iam_user.existing_user). O segundo argumento é o ID do recurso que se vai importar. Digamos que o ID do recurso aws_iam_user é o nome de usuario (por exemplo, yevgeniy.brikman) e que o ID do recurso aws_instance é o ID do servidor EC2 (como i-190e22e5). Como importar un recurso adoita indicarse na documentación situada ao final da súa páxina.

    A continuación móstrase un comando de importación que sincroniza o recurso aws_iam_user que engadiu á súa configuración de Terraform xunto co usuario de IAM no Capítulo 2 (substituíndo o seu nome por yevgeniy.brikman, por suposto):

    $ terraform import aws_iam_user.existing_user yevgeniy.brikman

    Terraform chamará á API de AWS para atopar o seu usuario de IAM e crear unha asociación de ficheiros de estado entre este e o recurso aws_iam_user.existing_user na súa configuración de Terraform. A partir de agora, cando execute o comando plan, Terraform saberá que o usuario IAM xa existe e non tentará crealo de novo.

    Paga a pena sinalar que se xa tes moitos recursos que queres importar a Terraform, escribir o código manualmente e importar cada un de cada un pode ser unha molestia. Polo tanto, paga a pena buscar unha ferramenta como Terraforming (http://terraforming.dtan4.net/), que pode importar automaticamente código e estado da súa conta de AWS.

    A refactorización pode ter as súas trampas

    Refactorización é unha práctica común na programación onde se cambia a estrutura interna do código sen modificar o comportamento externo. Isto é para facer o código máis claro, ordenado e máis fácil de manter. A refactorización é unha técnica indispensable que se debe utilizar regularmente. Pero cando se trata de Terraform ou calquera outra ferramenta de IaC, tes que ter moito coidado co que queres dicir co "comportamento externo" dun anaco de código, se non, xurdirán problemas inesperados.

    Por exemplo, un tipo común de refactorización é substituír os nomes de variables ou funcións por outros máis comprensibles. Moitos IDE teñen soporte incorporado para a refactorización e poden renomear automaticamente variables e funcións ao longo do proxecto. Nas linguaxes de programación de propósito xeral, este é un procedemento trivial no que quizais non penses, pero en Terraform tes que ter moito coidado con isto, se non, podes sufrir interrupcións.

    Por exemplo, o módulo webserver-cluster ten unha variable de entrada cluster_name:

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

    Imaxina que comezaches a usar este módulo para implementar un microservizo chamado foo. Máis tarde, queres cambiar o nome do teu servizo a barra. Este cambio pode parecer trivial, pero en realidade pode causar interrupcións no servizo.

    O feito é que o módulo webserver-cluster usa a variable cluster_name nunha serie de recursos, incluíndo o parámetro nome de dous grupos de seguridade e o 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]
    }

    Se cambias o parámetro nome dun recurso, Terraform eliminará a versión antiga dese recurso e creará unha nova no seu lugar. Pero se ese recurso é un ALB, entre borralo e descargar unha nova versión, non terás un mecanismo para redirixir o tráfico ao teu servidor web. Do mesmo xeito, se se elimina un grupo de seguranza, os seus servidores comezarán a rexeitar calquera tráfico de rede ata que se cree un novo grupo.

    Outro tipo de refactorización que pode interesar é cambiar o ID de Terraform. Tomemos como exemplo o recurso aws_security_group no módulo webserver-cluster:

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

    O identificador deste recurso chámase instancia. Imaxina que durante a refactorización decidiches cambialo por un nome máis comprensible (na túa opinión) cluster_instance:

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

    Que pasará ao final? É certo: unha interrupción.

    Terraform asocia cada ID de recurso co ID do provedor de nube. Por exemplo, iam_user está asociado co ID de usuario de AWS IAM e aws_instance está asociado co ID do servidor AWS EC2. Se cambias o ID do recurso (por exemplo, de instancia a cluster_instance, como é o caso de aws_security_group), a Terraform aparecerá como se eliminaches o recurso antigo e engadiches un novo. Se aplicas estes cambios, Terraform eliminará o antigo grupo de seguranza e creará un novo, mentres os teus servidores comezan a rexeitar calquera tráfico de rede.

    Aquí tes catro leccións fundamentais que debes sacar desta discusión.

    • Use sempre o comando plan. Pode revelar todos estes inconvenientes. Revisa coidadosamente a súa saída e presta atención ás situacións nas que Terraform planea eliminar recursos que probablemente non deberían eliminarse.
    • Crear antes de eliminar. Se queres substituír un recurso, pensa coidadosamente se necesitas crear un substituto antes de eliminar o orixinal. Se a resposta é si, create_before_destroy pode axudar. O mesmo resultado pódese conseguir manualmente realizando dous pasos: primeiro engade un novo recurso á configuración e execute o comando de aplicación e, a continuación, elimine o recurso antigo da configuración e use de novo o comando de aplicación.
    • Cambiar os identificadores require cambiar o estado. Se queres cambiar o ID asociado a un recurso (por exemplo, cambiar o nome de aws_security_group de instancia a cluster_instance) sen eliminar o recurso e crear unha nova versión do mesmo, debes actualizar o ficheiro de estado de Terraform en consecuencia. Nunca faga isto manualmente; use o comando terraform state no seu lugar. Ao cambiar o nome dos identificadores, debería executar o comando terraform state mv, que ten a seguinte sintaxe:
      terraform state mv <ORIGINAL_REFERENCE> <NEW_REFERENCE>

      ORIGINAL_REFERENCE é unha expresión que fai referencia ao recurso na súa forma actual, e NEW_REFERENCE é onde queres movelo. Por exemplo, ao cambiar o nome do grupo aws_security_group de instancia a cluster_instance, cómpre executar o seguinte comando:

      $ terraform state mv 
         aws_security_group.instance 
         aws_security_group.cluster_instance

      Isto indica a Terraform que o estado que estaba asociado anteriormente con aws_security_group.instance agora debería estar asociado con aws_security_group.cluster_instance. Se despois de renomear e executar este comando terraform plan non mostra ningún cambio, entón fixeches todo correctamente.

    • Algunhas opcións de configuración non se poden cambiar. Os parámetros de moitos recursos son inmutables. Se tentas cambialos, Terraform eliminará o recurso antigo e creará un novo no seu lugar. Cada páxina de recursos adoita indicar o que ocorre cando cambias unha configuración particular, así que asegúrate de consultar a documentación. Use sempre o comando plan e considere usar a estratexia create_before_destroy.

    A coherencia aprazada é consistente... co aprazamento

    As API dalgúns provedores de nube, como AWS, son asíncronas e teñen unha coherencia atrasada. A asincronía significa que a interface pode devolver inmediatamente unha resposta sen esperar a que se complete a acción solicitada. A coherencia atrasada significa que os cambios poden tardar tempo en propagarse por todo o sistema; mentres isto ocorre, as túas respostas poden ser inconsistentes e depender da réplica da fonte de datos que responde ás túas chamadas de API.

    Imaxina, por exemplo, que fai unha chamada de API a AWS para pedirlle que cree un servidor EC2. A API devolverá unha resposta "exitosa" (201 Creado) case ao instante, sen esperar a que se cree o propio servidor. Se tentas conectarte a el de inmediato, case seguramente fallará porque nese momento AWS aínda está inicializando recursos ou, alternativamente, o servidor aínda non iniciou. Ademais, se fai outra chamada para obter información sobre este servidor, pode recibir un erro (404 Non atopado). O caso é que a información sobre este servidor EC2 aínda se pode propagar por AWS antes de que estea dispoñible en todas partes, terás que esperar uns segundos.

    Sempre que utilices unha API asíncrona con coherencia preguiceira, debes tentar de novo a solicitude periodicamente ata que a acción se complete e se propague polo sistema. Desafortunadamente, o AWS SDK non ofrece boas ferramentas para iso, e o proxecto Terraform adoitaba sufrir moitos erros como 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

    Noutras palabras, creas un recurso (como unha subrede) e despois intentas obter información sobre el (como o ID da subrede recentemente creada) e Terraform non pode atopalo. A maioría destes erros (incluíndo 6813) foron solucionados, pero aínda aparecen de cando en vez, especialmente cando Terraform engade soporte para un novo tipo de recurso. Isto é molesto, pero na maioría dos casos non causa ningún dano. Cando executes terraform apply de novo, todo debería funcionar, xa que a estas alturas a información xa se espallará por todo o sistema.

    Este fragmento preséntase do libro de Evgeniy Brikman "Terraform: infraestrutura a nivel de código".

Fonte: www.habr.com

Engadir un comentario