Implantação contínua do Jenkins com tempo de inatividade zero com Terraform na AWS

Publicados: 2022-03-11

No mundo atual da internet, onde literalmente tudo precisa estar funcionando 24 horas por dia, 7 dias por semana, a confiabilidade é fundamental. Isso se traduz em quase zero tempo de inatividade para seus sites, evitando a temida página de erro “Não encontrado: 404” ou outras interrupções de serviço enquanto você lança sua versão mais recente.

Suponha que você criou um novo aplicativo para seu cliente, ou talvez para si mesmo, e conseguiu obter uma boa base de usuários que gosta do seu aplicativo. Você coletou feedback de seus usuários e vai até seus desenvolvedores e pede que eles criem novos recursos e preparem o aplicativo para implantação. Com isso pronto, você pode parar o aplicativo inteiro e implantar a nova versão ou criar um pipeline de implantação de CI/CD com tempo de inatividade zero que faria todo o trabalho tedioso de enviar uma nova versão aos usuários sem intervenção manual.

Neste artigo, falaremos exatamente sobre o último, como podemos ter um pipeline de implantação contínua de um aplicativo web de três camadas construído em Node.js na Nuvem AWS usando o Terraform como um orquestrador de infraestrutura. Usaremos o Jenkins para a parte de implantação contínua e o Bitbucket para hospedar nossa base de código.

Repositório de código

Usaremos um aplicativo Web de demonstração de três camadas para o qual você pode encontrar o código aqui.

O repositório contém código para a camada da Web e da API. É um aplicativo simples em que o módulo da web chama um dos endpoints na camada da API que busca internamente informações sobre a hora atual do banco de dados e retorna para a camada da web.

A estrutura do repositório é a seguinte:

  • API: código para a camada de API
  • Web: código para a camada da web
  • Terraform: código para orquestração de infraestrutura usando o Terraform
  • Jenkins: código para orquestrador de infraestrutura para servidor Jenkins usado para o pipeline de CI/CD.

Agora que entendemos o que precisamos implantar, vamos discutir o que precisamos fazer para implantar esse aplicativo na AWS e, em seguida, falaremos sobre como fazer parte do pipeline de CI/CD.

Imagens de panificação

Como estamos usando o Terraform para orquestrador de infraestrutura, faz mais sentido ter imagens pré-configuradas para cada camada ou aplicativo que você deseja implantar. E para isso, usaríamos outro produto da Hashicorp, ou seja, Packer.

Packer é uma ferramenta de código aberto que ajuda a construir uma Amazon Machine Image ou AMI, que será usada para implantação na AWS. Ele pode ser usado para construir imagens para diferentes plataformas como EC2, VirtualBox, VMware e outras.

Aqui está um trecho de como o arquivo de configuração do Packer ( terraform/packer-ami-api.json ) é usado para criar uma AMI para a camada de API.

 { "builders": [{ "type": "amazon-ebs", "region": "eu-west-1", "source_ami": "ami-844e0bf7", "instance_type": "t2.micro", "ssh_username": "ubuntu", "ami_name": "api-instance {{timestamp}}" }], "provisioners": [ { "type": "shell", "inline": ["mkdir api", "sudo apt-get update", "sudo apt-get -y install npm nodejs-legacy"], "pause_before": "10s" }, { "type": "file", "source" : "../api/", "destination" : "api" }, { "type": "shell", "inline": ["cd api", "npm install"], "pause_before": "10s" } ] }

E você precisa executar o seguinte comando para criar a AMI:

 packer build -machine-readable packer-ami-api.json

Estaremos executando este comando da compilação do Jenkins posteriormente neste artigo. De maneira semelhante, usaremos o arquivo de configuração do Packer ( terraform/packer-ami-web.json ) para a camada da web também.

Vamos analisar o arquivo de configuração do Packer acima e entender o que ele está tentando fazer.

  1. Como mencionado anteriormente, o Packer pode ser usado para criar imagens para muitas plataformas e, como estamos implantando nosso aplicativo na AWS, usaríamos o construtor “amazon-ebs”, pois é o construtor mais fácil de começar.
  2. A segunda parte da configuração leva uma lista de provisionadores que são mais como scripts ou blocos de código que você pode usar para configurar sua imagem.
    • A etapa 1 executa um provisionador de shell para criar uma pasta de API e instalar o Node.js na imagem usando a propriedade inline , que é um conjunto de comandos que você deseja executar.
    • A etapa 2 executa um provisionador de arquivos para copiar nosso código-fonte da pasta da API para a instância.
    • A etapa 3 executa novamente um provisionador de shell, mas desta vez usa uma propriedade de script para especificar um arquivo (terraform/scripts/install_api_software.sh) com os comandos que precisam ser executados.
    • A etapa 4 copia um arquivo de configuração para a instância necessária para o Cloudwatch, que é instalada na próxima etapa.
    • A etapa 5 executa um provisionador de shell para instalar o agente do AWS Cloudwatch. A entrada para este comando seria o arquivo de configuração copiado na etapa anterior. Falaremos sobre o Cloudwatch em detalhes mais adiante no artigo.

Portanto, em essência, a configuração do Packer contém informações sobre qual construtor você deseja e, em seguida, um conjunto de provisionadores que você pode definir em qualquer ordem, dependendo de como deseja configurar sua imagem.

Configurando uma implantação contínua do Jenkins

Em seguida, examinaremos a configuração de um servidor Jenkins que será usado para nosso pipeline de CI/CD. Usaremos o Terraform e a AWS para configurar isso também.

O código do Terraform para configurar o Jenkins está dentro da pasta jenkins/setup . Vamos passar por algumas das coisas interessantes sobre essa configuração.

  1. Credenciais da AWS: você pode fornecer o ID da chave de acesso da AWS e a chave de acesso secreta ao provedor Terraform AWS ( instance.tf ) ou pode fornecer a localização do arquivo de credenciais para a propriedade shared_credentials_file no provedor da AWS.
  2. Função do IAM: como executaremos o Packer e o Terraform a partir do servidor Jenkins, eles acessarão os serviços S3, EC2, RDS, IAM, balanceamento de carga e escalonamento automático na AWS. Portanto, ou fornecemos nossas credenciais no Jenkins para Packer & Terraform para acessar esses serviços ou podemos criar um perfil do IAM ( iam.tf ), usando o qual criaríamos uma instância do Jenkins.
  3. Estado do Terraform: o Terraform precisa manter o estado da infraestrutura em algum lugar em um arquivo e, com o S3 ( backend.tf ), você pode apenas mantê-lo lá, para poder colaborar com outros colegas de trabalho, e qualquer pessoa pode alterar e implantar desde o estado é mantido em um local remoto.
  4. Par de chaves pública/privada: você precisará fazer upload da chave pública do seu par de chaves junto com a instância para que você possa ssh na instância do Jenkins assim que ela estiver ativa. Definimos um recurso aws_key_pair ( key.tf ) no qual você especifica a localização de sua chave pública usando variáveis ​​do Terraform.

Etapas para configurar o Jenkins:

Etapa 1: Para manter o estado remoto do Terraform, você precisaria criar manualmente um bucket no S3 que pode ser usado pelo Terraform. Este seria o único passo feito fora do Terraform. Certifique-se de executar a AWS configure antes de executar o comando abaixo para especificar suas credenciais da AWS.

 aws s3api create-bucket --bucket node-aws-jenkins-terraform --region eu-west-1 --create-bucket-configuration LocationConstraint=eu-west-1

Etapa 2: execute terraform init . Isso inicializará o estado e o configurará para ser armazenado no S3 e fará o download do plug-in do provedor da AWS.

Etapa 3: execute terraform apply . Isso verificará todo o código do Terraform e criará um plano e mostrará quantos recursos serão criados após a conclusão desta etapa.

Etapa 4: digite yes e a etapa anterior começará a criar todos os recursos. Após a conclusão do comando, você obterá o endereço IP público do servidor Jenkins.

Etapa 5: Ssh no servidor Jenkins, usando sua chave privada. ubuntu é o nome de usuário padrão para instâncias com suporte do AWS EBS. Use o endereço IP retornado pelo comando terraform apply .

 ssh -i mykey [email protected]

Etapa 6: inicie a interface do usuário da Web do Jenkins acessando http://34.245.4.73:8080 . A senha pode ser encontrada em /var/lib/jenkins/secrets/initialAdminPassword .

Etapa 7: escolha “Instalar plug-ins sugeridos” e crie um usuário administrador para o Jenkins.

Configurando o pipeline de CI entre Jenkins e Bitbucket

  1. Para isso, precisamos instalar o plugin Bitbucket no Jenkins. Vá para Manage Jenkins → Manage Plugins e em Available plugins instale o plugin Bitbucket.
  2. No lado do repositório do Bitbucket, vá para Configurações → Webhooks , adicione um novo webhook. Este gancho enviará todas as alterações no repositório para o Jenkins e isso acionará os pipelines.
    Adicionando um webhook à implantação contínua do Jenkins via Bitbucker

Jenkins Pipeline para assar/criar imagens

  1. O próximo passo será criar pipelines no Jenkins.
  2. O primeiro pipeline será um projeto Freestyle que seria usado para construir a AMI do aplicativo usando o Packer.
  3. Você precisa especificar as credenciais e o URL do seu repositório Bitbucket.
    Adicionar credenciais ao bitbucket
  4. Especifique o gatilho Build.
    Configurando o gatilho de compilação
  5. Adicione duas etapas de compilação, uma para criar a AMI para o módulo de aplicativo e outras para criar a AMI para o módulo da Web.
    Adicionando etapas de compilação de AMI
  6. Feito isso, você pode salvar o projeto Jenkins e agora, quando você enviar qualquer coisa para o seu repositório Bitbucket, ele acionará uma nova compilação no Jenkins que criaria a AMI e enviaria um arquivo Terraform contendo o número da AMI dessa imagem para o bucket do S3 que você pode ver nas duas últimas linhas da etapa de compilação.
 echo 'variable "WEB_INSTANCE_AMI" { default = "'${AMI_ID_WEB}'" }' > amivar_web.tf aws s3 cp amivar_web.tf s3://node-aws-jenkins-terraform/amivar_web.tf

Jenkins Pipeline para acionar o script do Terraform

Agora que temos as AMIs para a API e os módulos da Web, acionaremos uma compilação para executar o código do Terraform para configurar todo o aplicativo e depois passar pelos componentes no código do Terraform que faz com que esse pipeline implante as alterações com zero tempo de inatividade do serviço.

  1. Criamos outro projeto Jenkins freestyle, nodejs-terraform , que estaria executando o código Terraform para implantar o aplicativo.
  2. Primeiro, criaremos uma credencial do tipo “texto secreto” no domínio de credenciais globais, que será usada como entrada para o script do Terraform. Como não queremos codificar a senha do serviço RDS dentro do Terraform e do Git, passamos essa propriedade usando as credenciais do Jenkins.
    criando um segredo para uso com o Terraform ci cd
  3. Você precisa definir as credenciais e a URL de forma semelhante ao outro projeto.
  4. Na seção de gatilho de compilação, vincularemos este projeto com o outro de forma que este projeto inicie quando o anterior terminar.
    Vincular projetos juntos
  5. Em seguida, poderíamos configurar as credenciais que adicionamos anteriormente ao projeto usando associações, para que estejam disponíveis na etapa de compilação.
    Como configurar vinculações
  6. Agora estamos prontos para adicionar uma etapa de compilação, que fará o download dos arquivos de script do Terraform ( amivar_api.tf e amivar_web.tf ) que foram carregados no S3 pelo projeto anterior e, em seguida, executará o código do Terraform para compilar todo o aplicativo na AWS.
    Adicionando o script de compilação

Se tudo estiver configurado corretamente, agora, se você enviar qualquer código para o seu repositório Bitbucket, ele deverá acionar o primeiro projeto Jenkins seguido pelo segundo e você deverá ter seu aplicativo implantado na AWS.

Configuração do Terraform Zero Downtime para AWS

Agora vamos discutir o que há no código do Terraform que faz com que esse pipeline implante o código com tempo de inatividade zero.

A primeira coisa é que o Terraform fornece esses blocos de configuração de ciclo de vida para recursos dentro dos quais você tem uma opção create_before_destroy como um sinalizador que literalmente significa que o Terraform deve criar um novo recurso do mesmo tipo antes de destruir o recurso atual.

Agora exploramos esse recurso nos recursos aws_autoscaling_group e aws_launch_configuration . Portanto, aws_launch_configuration configura qual tipo de instância do EC2 deve ser provisionada e como instalamos o software nessa instância, e o recurso aws_autoscaling_group fornece um grupo de escalonamento automático da AWS.

Um problema interessante aqui é que todos os recursos do Terraform devem ter uma combinação exclusiva de nome e tipo. Portanto, a menos que você tenha um nome diferente para o novo aws_autoscaling_group e aws_launch_configuration , não será possível destruir o atual.

O Terraform lida com essa restrição fornecendo uma propriedade name_prefix para o recurso aws_launch_configuration . Depois que essa propriedade for definida, o Terraform adicionará um sufixo exclusivo a todos os recursos aws_launch_configuration e você poderá usar esse nome exclusivo para criar um recurso aws_autoscaling_group .

Você pode verificar o código de todos os itens acima em terraform/autoscaling-api.tf

 resource "aws_launch_configuration" "api-launchconfig" { name_prefix = "api-launchconfig-" image_ instance_type = "t2.micro" security_groups = ["${aws_security_group.api-instance.id}"] user_data = "${data.template_file.api-shell-script.rendered}" iam_instance_profile = "${aws_iam_instance_profile.CloudWatchAgentServerRole-instanceprofile.name}" connection { user = "${var.INSTANCE_USERNAME}" private_key = "${file("${var.PATH_TO_PRIVATE_KEY}")}" } lifecycle { create_before_destroy = true } } resource "aws_autoscaling_group" "api-autoscaling" { name = "${aws_launch_configuration.api-launchconfig.name}-asg" vpc_zone_identifier = ["${aws_subnet.main-public-1.id}"] launch_configuration = "${aws_launch_configuration.api-launchconfig.name}" min_size = 2 max_size = 2 health_check_grace_period = 300 health_check_type = "ELB" load_balancers = ["${aws_elb.api-elb.name}"] force_delete = true lifecycle { create_before_destroy = true } tag { key = "Name" value = "api ec2 instance" propagate_at_launch = true } }

E o segundo desafio com implantações de tempo de inatividade zero é garantir que sua nova implantação esteja pronta para começar a receber a solicitação. Apenas implantar e iniciar uma nova instância do EC2 não é suficiente em algumas situações.

Para resolver esse problema, aws_launch_configuration tem uma propriedade user_data que oferece suporte à propriedade user_data de escalonamento automático da AWS nativa, com a qual você pode passar qualquer script que queira executar na inicialização de novas instâncias como parte do grupo de escalonamento automático. Em nosso exemplo, seguimos o log do servidor de aplicativos e esperamos que a mensagem de inicialização esteja lá. Você também pode verificar o servidor HTTP e ver quando eles estão ativos.

 until tail /var/log/syslog | grep 'node ./bin/www' > /dev/null; do sleep 5; done

Junto com isso, você também pode habilitar uma verificação de ELB no nível de recurso aws_autoscaling_group , que garantirá que a nova instância foi adicionada para passar na verificação de ELB antes que o Terraform destrua as instâncias antigas. É assim que a verificação do ELB para a camada de API se parece; ele verifica o ponto de extremidade /api/status para retornar com sucesso.

 resource "aws_elb" "api-elb" { name = "api-elb" subnets = ["${aws_subnet.main-public-1.id}"] security_groups = ["${aws_security_group.elb-securitygroup.id}"] listener { instance_port = "${var.API_PORT}" instance_protocol = "http" lb_port = 80 lb_protocol = "http" } health_check { healthy_threshold = 2 unhealthy_threshold = 2 timeout = 3 target = "HTTP:${var.API_PORT}/api/status" interval = 30 } cross_zone_load_balancing = true connection_draining = true connection_draining_timeout = 400 tags { Name = "my-elb" } }

Resumo e próximos passos

Então, isso nos leva ao final deste artigo; Espero que, agora, você já tenha seu aplicativo implantado e em execução com um pipeline de CI/CD com tempo de inatividade zero usando uma implantação do Jenkins e as práticas recomendadas do Terraform ou esteja um pouco mais confortável explorando esse território e fazendo com que suas implantações precisem de tão pouca intervenção manual quanto possível.

Neste artigo, a estratégia de implantação que está sendo usada é chamada de implantação Blue-Green, na qual temos uma instalação atual (Azul) que recebe tráfego ao vivo enquanto estamos implantando e testando a nova versão (Verde) e, em seguida, as substituímos assim que a nova versão for tudo pronto. Além dessa estratégia, existem outras maneiras de implantar seu aplicativo, que são explicadas muito bem neste artigo, Introdução às estratégias de implantação. Adaptar outra estratégia agora é tão simples quanto configurar seu pipeline Jenkins.

Além disso, neste artigo, presumi que todas as novas alterações na API, na Web e nas camadas de dados são compatíveis para que você não precise se preocupar com a nova versão falando com uma versão mais antiga. Mas, na realidade, pode nem sempre ser assim. Para resolver esse problema, ao projetar sua nova versão/recursos, sempre pense na camada de compatibilidade com versões anteriores, caso contrário, você precisará ajustar suas implantações para lidar com essa situação também.

O teste de integração também está faltando nesse pipeline de implantação. Como você não quer que nada seja lançado para o usuário final sem ser testado, definitivamente é algo para se ter em mente quando chegar a hora de aplicar essas estratégias em seus próprios projetos.

Se você estiver interessado em aprender mais sobre como o Terraform funciona e como você pode implantar na AWS usando a tecnologia, recomendo o Terraform AWS Cloud: Sane Infrastructure Management, onde o colega Toptaler Radoslaw Szalski explica o Terraform e mostra as etapas necessárias para configurar um -configuração do Terraform pronta para produção e ambiente para uma equipe

Relacionado: Terraform vs. CloudFormation: o guia definitivo