Cero tiempo de inactividad Jenkins Continuous Deployment con Terraform en AWS
Publicado: 2022-03-11En el mundo actual de Internet, donde literalmente todo debe estar activo las 24 horas del día, los 7 días de la semana, la confiabilidad es clave. Esto se traduce en un tiempo de inactividad cercano a cero para sus sitios web, esquivando la temida página de error "No encontrado: 404" u otras interrupciones del servicio mientras implementa su versión más reciente.
Suponga que ha creado una nueva aplicación para su cliente, o tal vez para usted mismo, y ha logrado obtener una buena base de usuarios a la que le gusta su aplicación. Recopiló comentarios de sus usuarios y acudió a sus desarrolladores para pedirles que creen nuevas características y preparen la aplicación para su implementación. Con eso listo, puede detener toda la aplicación e implementar la nueva versión o crear una canalización de implementación de CI/CD sin tiempo de inactividad que haría todo el tedioso trabajo de enviar una nueva versión a los usuarios sin intervención manual.
En este artículo, hablaremos exactamente sobre esto último, cómo podemos tener una canalización de implementación continua de una aplicación web de tres niveles creada en Node.js en la nube de AWS utilizando Terraform como orquestador de infraestructura. Usaremos Jenkins para la parte de implementación continua y Bitbucket para alojar nuestra base de código.
Repositorio de código
Usaremos una aplicación web de demostración de tres niveles para la cual puede encontrar el código aquí.
El repositorio contiene código tanto para la web como para la capa API. Es una aplicación simple en la que el módulo web llama a uno de los puntos finales en la capa API que internamente obtiene información sobre la hora actual de la base de datos y regresa a la capa web.
La estructura del repositorio es la siguiente:
- API: código para la capa API
- Web: código para la capa web
- Terraform: Código para orquestación de infraestructura usando Terraform
- Jenkins: código para el orquestador de infraestructura para el servidor Jenkins utilizado para la canalización de CI/CD.
Ahora que entendemos lo que necesitamos implementar, analicemos las cosas que debemos hacer para implementar esta aplicación en AWS y luego hablaremos sobre cómo hacer que eso sea parte de la canalización de CI/CD.
Hornear Imágenes
Dado que usamos Terraform para el orquestador de infraestructura, tiene más sentido tener imágenes preparadas para cada nivel o aplicación que desee implementar. Y para eso, estaríamos usando otro producto de Hashicorp, es decir, Packer.
Packer es una herramienta de código abierto que ayuda a crear una imagen de máquina de Amazon o AMI, que se utilizará para la implementación en AWS. Se puede usar para crear imágenes para diferentes plataformas como EC2, VirtualBox, VMware y otras.
Este es un fragmento de cómo se utiliza el archivo de configuración de Packer ( terraform/packer-ami-api.json
) para crear una AMI para la capa 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" } ] }
Y debe ejecutar el siguiente comando para crear la AMI:
packer build -machine-readable packer-ami-api.json
Ejecutaremos este comando desde la compilación de Jenkins más adelante en este artículo. De manera similar, también usaremos el archivo de configuración de Packer ( terraform/packer-ami-web.json
) para la capa web.
Repasemos el archivo de configuración de Packer anterior y entendamos qué está tratando de hacer.
- Como se mencionó anteriormente, Packer se puede usar para crear imágenes para muchas plataformas y, dado que estamos implementando nuestra aplicación en AWS, usaríamos el constructor "amazon-ebs", ya que es el constructor más fácil para comenzar.
- La segunda parte de la configuración toma una lista de aprovisionadores que son más como scripts o bloques de código que puede usar para configurar su imagen.
- El paso 1 ejecuta un aprovisionador de shell para crear una carpeta API e instalar Node.js en la imagen mediante la propiedad
inline
, que es un conjunto de comandos que desea ejecutar. - El paso 2 ejecuta un aprovisionador de archivos para copiar nuestro código fuente de la carpeta API a la instancia.
- El paso 3 nuevamente ejecuta un aprovisionador de shell, pero esta vez usa una propiedad de secuencia de comandos para especificar un archivo (terraform/scripts/install_api_software.sh) con los comandos que deben ejecutarse.
- El paso 4 copia un archivo de configuración en la instancia que se necesita para Cloudwatch, que se instala en el siguiente paso.
- El paso 5 ejecuta un aprovisionador de shell para instalar el agente de AWS Cloudwatch. La entrada a este comando sería el archivo de configuración copiado en el paso anterior. Hablaremos de Cloudwatch en detalle más adelante en el artículo.
- El paso 1 ejecuta un aprovisionador de shell para crear una carpeta API e instalar Node.js en la imagen mediante la propiedad
Entonces, en esencia, la configuración de Packer contiene información sobre qué constructor desea y luego un conjunto de aprovisionadores que puede definir en cualquier orden según cómo desee configurar su imagen.
Configuración de una implementación continua de Jenkins
A continuación, analizaremos la configuración de un servidor Jenkins que se utilizará para nuestra canalización de CI/CD. Usaremos Terraform y AWS para configurar esto también.
El código de Terraform para configurar Jenkins está dentro de la carpeta jenkins/setup
. Repasemos algunas de las cosas interesantes sobre esta configuración.
- Credenciales de AWS: puede proporcionar el ID de la clave de acceso de AWS y la clave de acceso secreta al proveedor de Terraform AWS (
instance.tf
) o puede proporcionar la ubicación del archivo de credenciales a la propiedadshared_credentials_file
en el proveedor de AWS. - Rol de IAM: dado que ejecutaremos Packer y Terraform desde el servidor Jenkins, accederán a S3, EC2, RDS, IAM, equilibrio de carga y servicios de escalado automático en AWS. Por lo tanto, proporcionamos nuestras credenciales en Jenkins para que Packer & Terraform acceda a estos servicios o podemos crear un perfil de IAM (
iam.tf
), mediante el cual crearíamos una instancia de Jenkins. - Estado de Terraform: Terraform tiene que mantener el estado de la infraestructura en algún lugar de un archivo y, con S3 (
backend.tf
), puede mantenerlo allí, para que pueda colaborar con otros compañeros de trabajo, y cualquiera puede cambiar e implementar desde el estado. se mantiene en una ubicación remota. - Par de claves públicas/privadas: deberá cargar la clave pública de su par de claves junto con la instancia para que pueda acceder a la instancia de Jenkins una vez que esté activa. Hemos definido un recurso
aws_key_pair
(key.tf
) en el que especifica la ubicación de su clave pública mediante variables de Terraform.
Pasos para configurar Jenkins:
Paso 1: para mantener el estado remoto de Terraform, debe crear manualmente un depósito en S3 que Terraform pueda usar. Este sería el único paso realizado fuera de Terraform. Asegúrese de ejecutar AWS configure
antes de ejecutar el siguiente comando para especificar sus credenciales de AWS.
aws s3api create-bucket --bucket node-aws-jenkins-terraform --region eu-west-1 --create-bucket-configuration LocationConstraint=eu-west-1
Paso 2: Ejecute terraform init
. Esto inicializará el estado y lo configurará para que se almacene en S3 y descargue el complemento del proveedor de AWS.
Paso 3: Ejecute terraform apply
. Esto verificará todo el código de Terraform y creará un plan y mostrará cuántos recursos se crearán una vez finalizado este paso.
Paso 4: Escriba yes
, y luego el paso anterior comenzará a crear todos los recursos. Una vez que finalice el comando, obtendrá la dirección IP pública del servidor Jenkins.
Paso 5: Ssh en el servidor Jenkins, usando su clave privada. ubuntu
es el nombre de usuario predeterminado para las instancias respaldadas por AWS EBS. Utilice la dirección IP devuelta por el comando de terraform apply
.
ssh -i mykey [email protected]
Paso 6: Inicie la interfaz de usuario web de Jenkins yendo a http://34.245.4.73:8080
. La contraseña se puede encontrar en /var/lib/jenkins/secrets/initialAdminPassword
.
Paso 7: elija "Instalar complementos sugeridos" y cree un usuario administrador para Jenkins.
Configuración de la canalización de CI entre Jenkins y Bitbucket
- Para esto, necesitamos instalar el complemento Bitbucket en Jenkins. Vaya a Administrar Jenkins → Administrar complementos y, desde Complementos disponibles , instale el complemento de Bitbucket.
- En el lado del repositorio de Bitbucket, vaya a Configuración → Webhooks , agregue un nuevo webhook. Este enlace enviará todos los cambios en el repositorio a Jenkins y eso activará las canalizaciones.
Jenkins Pipeline para hornear/construir imágenes
- El siguiente paso será crear pipelines en Jenkins.
- La primera tubería será un proyecto de estilo libre que se usaría para construir la AMI de la aplicación usando Packer.
- Debe especificar las credenciales y la URL de su repositorio de Bitbucket.
- Especifique el activador de compilación.
- Agregue dos pasos de compilación, uno para compilar la AMI para el módulo de la aplicación y otros para compilar la AMI para el módulo web.
- Una vez hecho esto, puede guardar el proyecto de Jenkins y ahora, cuando envíe algo a su repositorio de Bitbucket, activará una nueva compilación en Jenkins que crearía la AMI y enviaría un archivo de Terraform que contiene el número de AMI de esa imagen al Depósito de S3 que puede ver en las dos últimas líneas del paso de compilación.
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
Canalización de Jenkins para activar el script de Terraform
Ahora que tenemos las AMI para la API y los módulos web, activaremos una compilación para ejecutar el código de Terraform para configurar toda la aplicación y luego pasaremos por los componentes en el código de Terraform, lo que hace que esta canalización implemente los cambios sin tiempo de inactividad del servicio.

- Creamos otro proyecto Jenkins de estilo libre,
nodejs-terraform
, que ejecutaría el código de Terraform para implementar la aplicación. - Primero crearemos una credencial de tipo "texto secreto" en el dominio de credenciales globales, que se utilizará como entrada para el script de Terraform. Dado que no queremos codificar la contraseña para el servicio RDS dentro de Terraform y Git, pasamos esa propiedad usando las credenciales de Jenkins.
- Debe definir las credenciales y la URL de forma similar al otro proyecto.
- En la sección de activación de compilación, vincularemos este proyecto con el otro de manera que este proyecto comience cuando el anterior finalice.
- Luego, podríamos configurar las credenciales que agregamos anteriormente al proyecto mediante enlaces, para que estén disponibles en el paso de compilación.
- Ahora estamos listos para agregar un paso de compilación, que descargará los archivos de script de Terraform (
amivar_api.tf
yamivar_web.tf
) que el proyecto anterior cargó en S3 y luego ejecutará el código de Terraform para compilar la aplicación completa en AWS.
Si todo está configurado correctamente, ahora, si inserta cualquier código en su repositorio de Bitbucket, debería activar el primer proyecto de Jenkins seguido del segundo y debería tener su aplicación implementada en AWS.
Configuración de cero tiempo de inactividad de Terraform para AWS
Ahora analicemos qué hay en el código de Terraform que hace que esta canalización implemente el código sin tiempo de inactividad.
Lo primero es que Terraform proporciona estos bloques de configuración del ciclo de vida para los recursos dentro de los cuales tiene una opción create_before_destroy
como bandera, lo que literalmente significa que Terraform debe crear un nuevo recurso del mismo tipo antes de destruir el recurso actual.
Ahora explotamos esta función en los recursos aws_autoscaling_group
y aws_launch_configuration
. Entonces, aws_launch_configuration
configura qué tipo de instancia EC2 se debe aprovisionar y cómo instalamos el software en esa instancia, y el recurso aws_autoscaling_group
proporciona un grupo de escalado automático de AWS.
Una captura interesante aquí es que todos los recursos en Terraform deben tener una combinación única de nombre y tipo. Entonces, a menos que tenga un nombre diferente para el nuevo aws_autoscaling_group
y aws_launch_configuration
, no será posible destruir el actual.
Terraform maneja esta restricción proporcionando una propiedad name_prefix
al recurso aws_launch_configuration
. Una vez que se define esta propiedad, Terraform agregará un sufijo único a todos los recursos aws_launch_configuration
y luego podrá usar ese nombre único para crear un recurso aws_autoscaling_group
.
Puede consultar el código de todo lo anterior en 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 } }
Y el segundo desafío con implementaciones sin tiempo de inactividad es asegurarse de que su nueva implementación esté lista para comenzar a recibir la solicitud. Solo implementar e iniciar una nueva instancia EC2 no es suficiente en algunas situaciones.
Para resolver ese problema, aws_launch_configuration
tiene una propiedad user_data
que admite la propiedad nativa de datos de usuario de user_data
automático de AWS mediante la cual puede pasar cualquier secuencia de comandos que le gustaría ejecutar en el inicio de nuevas instancias como parte del grupo de escalado automático. En nuestro ejemplo, seguimos el registro del servidor de aplicaciones y esperamos a que aparezca el mensaje de inicio. También puede verificar el servidor HTTP y ver cuándo están activos.
until tail /var/log/syslog | grep 'node ./bin/www' > /dev/null; do sleep 5; done
Junto con eso, también puede habilitar una verificación de ELB en el nivel de recursos aws_autoscaling_group
, lo que garantizará que la nueva instancia se agregue para pasar la verificación de ELB antes de que Terraform destruya las instancias anteriores. Así es como se ve la verificación de ELB para la capa API; comprueba que el punto final /api/status
devuelva el éxito.
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" } }
Resumen y próximos pasos
Así que esto nos lleva al final de este artículo; con suerte, a estas alturas, ya tiene su aplicación implementada y ejecutándose con una canalización de CI/CD sin tiempo de inactividad utilizando una implementación de Jenkins y las mejores prácticas de Terraform o se siente un poco más cómodo explorando este territorio y haciendo que sus implementaciones necesiten la menor intervención manual posible. posible.
En este artículo, la estrategia de implementación que se utiliza se denomina implementación azul-verde en la que tenemos una instalación actual (azul) que recibe tráfico en vivo mientras implementamos y probamos la nueva versión (verde) y luego los reemplazamos una vez que la nueva versión es Listo. Aparte de esta estrategia, hay otras formas de implementar su aplicación, que se explica muy bien en este artículo, Introducción a las estrategias de implementación. Adaptar otra estrategia ahora es tan simple como configurar su tubería de Jenkins.
Además, en este artículo, asumí que todos los nuevos cambios en las capas de API, web y datos son compatibles, por lo que no tiene que preocuparse de que la nueva versión se comunique con una versión anterior. Pero, en realidad, ese podría no ser siempre el caso. Para resolver ese problema, mientras diseña su nueva versión/características, siempre piense en la capa de compatibilidad con versiones anteriores o, de lo contrario, deberá modificar sus implementaciones para manejar esa situación también.
Las pruebas de integración también son algo que falta en esta canalización de implementación. Como no desea que nada se lance al usuario final sin haber sido probado, definitivamente es algo a tener en cuenta cuando llegue el momento de aplicar estas estrategias a sus propios proyectos.
Si está interesado en obtener más información sobre cómo funciona Terraform y cómo puede implementar en AWS utilizando la tecnología, le recomiendo Terraform AWS Cloud: Sane Infrastructure Management, donde el compañero Toptaler Radoslaw Szalski explica Terraform y luego le muestra los pasos necesarios para configurar un multi -configuración de Terraform lista para el entorno y la producción para un equipo