Armadilhas do Terraform

Armadilhas do Terraform
Vamos destacar algumas armadilhas, incluindo aquelas relacionadas a loops, instruções if e técnicas de implantação, bem como problemas mais gerais que afetam o Terraform em geral:

  • os parâmetros count e for_each têm limitações;
  • limitar implantações com tempo de inatividade zero;
  • até mesmo um bom plano pode falhar;
  • a refatoração pode ter suas armadilhas;
  • a coerência diferida é consistente... com o diferimento.

Os parâmetros count e for_each têm limitações

Os exemplos neste capítulo fazem uso extensivo do parâmetro count e da expressão for_each em loops e lógica condicional. Eles funcionam bem, mas têm duas limitações importantes das quais você precisa estar ciente.

  • Count e for_each não podem fazer referência a nenhuma variável de saída de recurso.
  • count e for_each não podem ser usados ​​na configuração do módulo.

count e for_each não podem fazer referência a nenhuma variável de saída de recurso

Imagine que você precisa implantar vários servidores EC2 e por algum motivo não deseja usar o ASG. Seu código poderia ser assim:

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

Vamos examiná-los um por um.

Como o parâmetro count está definido como um valor estático, este código funcionará sem problemas: quando você executar o comando apply, ele criará três servidores EC2. Mas e se você quisesse implantar um servidor em cada zona de disponibilidade (AZ) da sua região atual da AWS? Você pode fazer com que seu código carregue uma lista de zonas da fonte de dados aws_availability_zones e, em seguida, percorra cada uma delas e crie um servidor EC2 nela usando o parâmetro de contagem e o acesso ao índice de array:

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 também funcionará bem, já que o parâmetro count pode fazer referência a fontes de dados sem problemas. Mas o que acontece se o número de servidores que você precisa criar depende da saída de algum recurso? Para demonstrar isso, a maneira mais fácil é utilizar o recurso random_integer, que, como o nome sugere, retorna um número inteiro aleatório:

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

Este código gera um número aleatório entre 1 e 3. Vamos ver o que acontece se tentarmos usar 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 você executar o terraform plan neste código, receberá 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.

O Terraform exige que count e for_each sejam calculados durante a fase de planejamento, antes de qualquer recurso ser criado ou modificado. Isso significa que count e for_each podem se referir a literais, variáveis, fontes de dados e até listas de recursos (desde que seu comprimento possa ser determinado no momento do agendamento), mas não a variáveis ​​de saída de recursos computadas.

count e for_each não podem ser usados ​​na configuração do módulo

Algum dia você pode ficar tentado a adicionar um parâmetro de contagem à configuração do seu 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 de um módulo para criar três cópias do recurso webserver-cluster. Ou você pode querer tornar a conexão de um módulo opcional com base em alguma condição booleana, definindo seu parâmetro de contagem como 0. Isso pode parecer um código razoável, mas você receberá este erro ao executar o plano 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.

Infelizmente, a partir do Terraform 0.12.6, o uso de count ou for_each em um recurso de módulo não é suportado. De acordo com as notas de lançamento do Terraform 0.12 (http://bit.ly/3257bv4), a HashiCorp planeja adicionar esse recurso no futuro, portanto, dependendo de quando você ler este livro, ele já poderá estar disponível. Para saber com certeza, leia o changelog do Terraform aqui.

Limitações de implantações com tempo de inatividade zero

Usar o bloco create_before_destroy em combinação com ASG é uma ótima solução para criar implantações com tempo de inatividade zero, exceto por uma ressalva: regras de escalonamento automático não são suportadas. Ou, para ser mais preciso, isso redefine o tamanho do ASG para min_size em cada implantação, o que poderia ser um problema se você estivesse usando regras de escalonamento automático para aumentar o número de servidores em execução.

Por exemplo, o módulo webserver-cluster contém um par de recursos aws_autoscaling_schedule, que às 9h aumenta o número de servidores no cluster de dois para dez. Se você implantar, digamos, às 11h, o novo ASG será inicializado com apenas dois servidores em vez de dez e permanecerá assim até as 9h do dia seguinte.

Esta limitação pode ser contornada de diversas maneiras.

  • Altere o parâmetro de recorrência em aws_autoscaling_schedule de 0 9 * * * (“executar às 9h”) para algo como 0-59 9-17 * * * (“executar a cada minuto das 9h às 5h”). Se o ASG já tiver dez servidores, executar novamente essa regra de escalonamento automático não mudará nada, que é o que queremos. Mas se o ASG tiver sido implantado recentemente, esta regra garantirá que em no máximo um minuto o número de seus servidores chegará a dez. Esta não é uma abordagem totalmente elegante, e grandes saltos de dez para dois servidores e vice-versa também podem causar problemas para os usuários.
  • Crie um script personalizado que use a API da AWS para determinar o número de servidores ativos no ASG, chame-o usando uma fonte de dados externa (consulte "Fonte de dados externa" na página 249) e defina o parâmetro desejado_capacity do ASG como o valor retornado por o roteiro. Dessa forma, cada nova instância do ASG sempre será executada com a mesma capacidade do código Terraform existente e dificultará sua manutenção.

É claro que o ideal seria que o Terraform tivesse suporte integrado para implantações com tempo de inatividade zero, mas em maio de 2019, a equipe da HashiCorp não tinha planos de adicionar essa funcionalidade (detalhes - aqui).

O plano correto pode ser implementado sem sucesso

Às vezes, o comando plan produz um plano de implantação perfeitamente correto, mas o comando apply retorna um erro. Tente, por exemplo, adicionar o recurso aws_iam_user com o mesmo nome usado para o usuário IAM criado anteriormente no Capítulo 2:

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

Agora, se você executar o comando plan, o Terraform produzirá um plano de implantação aparentemente razoável:

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 você executar o comando apply, obterá 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, claro, é que já existe um usuário IAM com esse nome. E isso pode acontecer não apenas com usuários IAM, mas com quase todos os recursos. É possível que alguém tenha criado este recurso manualmente ou usando a linha de comando, mas de qualquer forma, a correspondência de IDs leva a conflitos. Existem muitas variações desse erro que muitas vezes pegam de surpresa os recém-chegados ao Terraform.

O ponto principal é que o comando terraform plan leva em consideração apenas os recursos especificados no arquivo de estado do Terraform. Se os recursos forem criados de alguma outra forma (por exemplo, manualmente clicando no console AWS), eles não irão parar no arquivo de estado e, portanto, o Terraform não os levará em consideração ao executar o comando plan. Como resultado, um plano que à primeira vista parece correto não terá sucesso.

Há duas lições a serem aprendidas com isso.

  • Se você já começou a trabalhar com o Terraform, não use mais nada. Se parte da sua infraestrutura for gerenciada usando o Terraform, você não poderá mais modificá-la manualmente. Caso contrário, você não apenas corre o risco de erros estranhos do Terraform, mas também nega muitos dos benefícios do IaC, já que o código não será mais uma representação precisa de sua infraestrutura.
  • Se você já possui alguma infraestrutura, use o comando import. Se você estiver começando a usar o Terraform com infraestrutura existente, poderá adicioná-lo ao arquivo de estado usando o comando terraform import. Desta forma, o Terraform saberá qual infraestrutura precisa ser gerenciada. O comando de importação leva dois argumentos. O primeiro é o endereço do recurso nos seus arquivos de configuração. A sintaxe aqui é a mesma dos links de recursos: _. (como aws_iam_user.existente_user). O segundo argumento é o ID do recurso a ser importado. Digamos que o ID do recurso aws_iam_user seja o nome do usuário (por exemplo, yevgeniy.brikman) e o ID do recurso aws_instance seja o ID do servidor EC2 (como i-190e22e5). A forma de importar um recurso geralmente está indicada na documentação no final da página.

    Abaixo está um comando de importação que sincroniza o recurso aws_iam_user que você adicionou à configuração do Terraform junto com o usuário IAM no Capítulo 2 (substituindo seu nome por yevgeniy.brikman, é claro):

    $ terraform import aws_iam_user.existing_user yevgeniy.brikman

    O Terraform chamará a API da AWS para localizar seu usuário IAM e criar uma associação de arquivo de estado entre ele e o recurso aws_iam_user.existent_user em sua configuração do Terraform. A partir de agora, ao executar o comando plan, o Terraform saberá que o usuário IAM já existe e não tentará criá-lo novamente.

    É importante notar que se você já possui muitos recursos que deseja importar para o Terraform, escrever o código manualmente e importar cada um de cada vez pode ser um incômodo. Portanto, vale a pena procurar uma ferramenta como o Terraforming (http://terraforming.dtan4.net/), que pode importar automaticamente código e estado da sua conta AWS.

    A refatoração pode ter suas armadilhas

    Reestruturação é uma prática comum em programação onde você altera a estrutura interna do código enquanto deixa o comportamento externo inalterado. Isso torna o código mais claro, organizado e fácil de manter. A refatoração é uma técnica indispensável que deve ser usada regularmente. Mas quando se trata de Terraform ou qualquer outra ferramenta IaC, você deve ser extremamente cuidadoso com o que entende por “comportamento externo” de um trecho de código, caso contrário surgirão problemas inesperados.

    Por exemplo, um tipo comum de refatoração é substituir os nomes de variáveis ​​ou funções por outros mais compreensíveis. Muitos IDEs possuem suporte integrado para refatoração e podem renomear automaticamente variáveis ​​e funções ao longo do projeto. Em linguagens de programação de uso geral, este é um procedimento trivial no qual você pode não pensar, mas no Terraform você deve ter muito cuidado com isso, caso contrário poderá sofrer interrupções.

    Por exemplo, o módulo webserver-cluster possui uma variável de entrada cluster_name:

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

    Imagine que você começou a usar este módulo para implantar um microsserviço chamado foo. Mais tarde, você deseja renomear seu serviço para bar. Esta mudança pode parecer trivial, mas na realidade pode causar interrupções no serviço.

    O fato é que o módulo webserver-cluster utiliza a variável cluster_name em vários recursos, incluindo o parâmetro name de dois grupos de segurança 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 você alterar o parâmetro name em um recurso, o Terraform excluirá a versão antiga desse recurso e criará uma nova em seu lugar. Mas se esse recurso for um ALB, entre excluí-lo e baixar uma nova versão, você não terá um mecanismo para redirecionar o tráfego para o seu servidor web. Da mesma forma, se um grupo de segurança for excluído, seus servidores começarão a rejeitar qualquer tráfego de rede até que um novo grupo seja criado.

    Outro tipo de refatoração em que você pode estar interessado é alterar o ID do Terraform. Vamos pegar o recurso aws_security_group no módulo webserver-cluster como exemplo:

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

    O identificador deste recurso é denominado instância. Imagine que durante a refatoração você decidiu alterá-lo para um nome mais compreensível (na sua opinião) cluster_instance:

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

    O que acontecerá no final? Isso mesmo: uma disrupção.

    O Terraform associa cada ID de recurso ao ID do provedor de nuvem. Por exemplo, iam_user está associado ao ID do usuário AWS IAM e aws_instance está associado ao ID do servidor AWS EC2. Se você alterar o ID do recurso (digamos, de instância para cluster_instance, como é o caso de aws_security_group), para o Terraform parecerá que você excluiu o recurso antigo e adicionou um novo. Se você aplicar essas alterações, o Terraform excluirá o grupo de segurança antigo e criará um novo, enquanto seus servidores começarão a rejeitar qualquer tráfego de rede.

    Aqui estão quatro lições principais que você deve tirar dessa discussão.

    • Sempre use o comando plan. Pode revelar todos esses obstáculos. Revise seu resultado com cuidado e preste atenção às situações em que o Terraform planeja excluir recursos que provavelmente não deveriam ser excluídos.
    • Crie antes de excluir. Se você quiser substituir um recurso, pense cuidadosamente se precisa criar um substituto antes de excluir o original. Se a resposta for sim, create_before_destroy pode ajudar. O mesmo resultado pode ser alcançado manualmente executando duas etapas: primeiro adicione um novo recurso à configuração e execute o comando apply e, em seguida, remova o recurso antigo da configuração e use o comando apply novamente.
    • A alteração de identificadores requer alteração de estado. Se quiser alterar o ID associado a um recurso (por exemplo, renomear aws_security_group de instância para cluster_instance) sem excluir o recurso e criar uma nova versão dele, você deverá atualizar o arquivo de estado do Terraform adequadamente. Nunca faça isso manualmente - em vez disso, use o comando terraform state. Ao renomear identificadores, você deve executar o comando terraform state mv, que possui a seguinte sintaxe:
      terraform state mv <ORIGINAL_REFERENCE> <NEW_REFERENCE>

      ORIGINAL_REFERENCE é uma expressão que se refere ao recurso em sua forma atual e NEW_REFERENCE é para onde você deseja movê-lo. Por exemplo, ao renomear o grupo aws_security_group de instance para cluster_instance, você precisa executar o seguinte comando:

      $ terraform state mv 
         aws_security_group.instance 
         aws_security_group.cluster_instance

      Isso informa ao Terraform que o estado que estava anteriormente associado a aws_security_group.instance agora deve ser associado a aws_security_group.cluster_instance. Se depois de renomear e executar este comando o terraform plan não mostrar nenhuma alteração, então você fez tudo corretamente.

    • Algumas configurações não podem ser alteradas. Os parâmetros de muitos recursos são imutáveis. Se você tentar alterá-los, o Terraform excluirá o recurso antigo e criará um novo em seu lugar. Cada página de recursos geralmente indica o que acontece quando você altera uma configuração específica, portanto, verifique a documentação. Sempre use o comando plan e considere usar a estratégia create_before_destroy.

    A consistência diferida é consistente... com o adiamento

    As APIs de alguns provedores de nuvem, como AWS, são assíncronas e apresentam consistência atrasada. Assincronia significa que a interface pode retornar imediatamente uma resposta sem esperar que a ação solicitada seja concluída. A consistência atrasada significa que as alterações podem levar algum tempo para se propagarem por todo o sistema; enquanto isso acontece, suas respostas podem ser inconsistentes e depender de qual réplica da fonte de dados está respondendo às suas chamadas de API.

    Imagine, por exemplo, que você faça uma chamada de API para a AWS solicitando a criação de um servidor EC2. A API retornará uma resposta “bem-sucedida” (201 Criado) quase instantaneamente, sem esperar que o próprio servidor seja criado. Se você tentar conectar-se a ele imediatamente, é quase certo que falhará porque nesse ponto a AWS ainda está inicializando recursos ou, alternativamente, o servidor ainda não foi inicializado. Além disso, se você fizer outra chamada para obter informações sobre este servidor, poderá receber um erro (404 Not Found). O problema é que as informações sobre este servidor EC2 ainda podem ser propagadas por toda a AWS antes de ficarem disponíveis em todos os lugares, você terá que esperar alguns segundos.

    Sempre que você usar uma API assíncrona com consistência lenta, deverá repetir periodicamente sua solicitação até que a ação seja concluída e propagada pelo sistema. Infelizmente, o AWS SDK não fornece boas ferramentas para isso, e o projeto Terraform costumava sofrer muitos bugs 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

    Em outras palavras, você cria um recurso (como uma sub-rede) e tenta obter algumas informações sobre ele (como o ID da sub-rede recém-criada), e o Terraform não consegue encontrá-lo. A maioria desses bugs (incluindo 6813) foram corrigidos, mas ainda surgem de vez em quando, especialmente quando o Terraform adiciona suporte para um novo tipo de recurso. Isso é irritante, mas na maioria dos casos não causa nenhum dano. Ao executar o terraform apply novamente, tudo deverá funcionar, pois a essa altura as informações já estarão espalhadas por todo o sistema.

    Este trecho é apresentado do livro de Evgeniy Brikman "Terraform: infraestrutura em nível de código".

Fonte: habr.com

Adicionar um comentário