Déployer Jenkins sur AWS Fargate

Déploiement et configuration automatisés de Jenkins sur AWS Fargate avec Terraform et Jenkins Configuration as Code.

Table des matières
La dernière version de ce projet (v3.0.0) a été simplifiée en n’utilisant plus un network load balancer pour la communication agents -> contrôleur. Le service discovery avec Cloudmap est utilisé à la place. L’objectif est le même : fournir un endpoint pour que les agents puissent se connecter au contrôleur.

Dans cet article, nous verrons comment déployer automatiquement Jenkins sur AWS Fargate avec Terraform et le plugin Jenkins Configuration as Code. Cette stack Terraform pourra servir de base pour ceux voulant aller plus loin dans le déploiement et l’administration d’un Jenkins prod-ready sur AWS.

Le déploiement de Jenkins sur Fargate est désormais possible grâce à la mise à jour récente de Fargate qui passe de la version 1.3.0 à la version 1.4.0. Cette mise à jour apporte le support d’Amazon Elastic File System (EFS) ainsi que d’autres changements. Le support EFS était l’un des plus demandés par la communauté. Sans EFS sur Fargate, il était impossible d’avoir un stockage persistant sur Fargate et donc stocker de manière durable la configuration de Jenkins.

À la fin de cet article, vous aurez :

  • Un Jenkins Contrôleur (anciennement appelé Master) et sa configuration sur un système de fichier EFS.
  • Un agent éphémère configuré sur Fargate et un job d’exemple utilisant cet agent.
  • Un accès sécurisé au Controller en HTTPs (à condition d’avoir une zone publique sur Route53).
  • Un process automatisé de mise à jour de la configuration du Contrôleur.
  • Les logs du Contrôleur et des agents dans CloudWatch.
  • Des alertes configurées pour surveiller l’EFS et le Contrôleur.
  • Et le tout entièrement automatisé…
Le code source complet se trouve sur GitHub. Au moment où j’écris ces lignes, nous utilisons la version v2.1.0 du dépôt. Des modifications seront sans doute apportées après l’écriture de cet article. Veillez donc à utiliser le tag v2.1.0 pour suivre cet article.

N’hésitez pas à mettre en commentaires vos remarques et suggestions ou à contribuer sur GitHub :-)

Infrastructure

Communication Contrôleur <-> Agent

Nous allons déployer Jenkins en mode Contrôleur/Agent avec des agents éphémères. Un agent éphémère est une machine ou un conteneur dont le seul rôle est d’exécuter un ou plusieurs jobs puis s’éteindre par la suite, immédiatement ou après un certain temps. Un agent permanent quant à lui reste actif en attente d’instructions de la part du Contrôleur pour exécuter un ou plusieurs jobs. Dans les deux cas, les informations de l’exécution du job (logs, temps d’exécution, artifacts…) sont remontées au Contrôleur pour être stockées de manière permanente sous forme de fichiers XML principalement.

Dans cette architecture, aucun job ne s’exécute sur le Contrôleur qui se chargera uniquement d’orchestrer l’exécution des différents jobs et fournir une interface graphique aux utilisateurs. Elle présente plusieurs avantages dans la mesure où le plus gros du travail est effectué par les agents. Ainsi, on peut avoir un Contrôleur relativement petit en termes de CPU/RAM tout en disposant d’une grande ferme d’agents.

Jenkins supporte différents protocoles pour assurer la communication Contrôleur <-> Agent. Parmi ces protocoles, nous avons :

  • SSH : Avec le protocole SSH, la communication est initiée par le Contrôleur qui se connecte au moment voulu à l’agent en lui envoyant les instructions à lancer. Il doit donc avoir accès aux agents (flux réseaux et clé publique SSH du Contrôleur déployé sur les agents) qui devront avoir Java installé au moins plus d’autres outils nécessaires au build. Ce protocole est surtout utilisé pour des agents permanents à l’aide du plugin “SSH Build Agents” ou le plugin “Amazon EC2” qui lance des instances EC2 à la demande.
  • Java Web Start (Java Networking Launching Protocol ou JNLP) : Java Web Start permet de lancer des applications Java à partir d’un navigateur Web en cliquant sur un lien. Jenkins implémente ce protocole pour permettre aux agents JNLP de se connecter au Contrôleur : dans ce cas, la communication est initiée par l’agent et non par le Contrôleur. Le Contrôleur envoie son point d’accès (une URL ou une adresse IP) et un secret permettant à l’agent d’initier la communication qui peut se faire en headless (sans une interface graphique) ou avec une interface graphique. C’est ce dernier protocole que nous allons utiliser en headless avec le plugin Amazon ECS et des agents dans des conteneurs sur Fargate.

Architecture

L’architecture que l’on va mettre en place se présente comme suit :

Schéma d'architecture simplifié du Contrôleur et des agents
Schéma d’architecture simplifié du Contrôleur et des agents

Nous avons un ALB en frontal constituant le point d’entrée des utilisateurs vers le Jenkins Contrôleur. Il sera déployé dans les sous réseaux publics et écoutera sur les ports HTTP et HTTPs avec une redirection permanente vers HTTPs si vous disposez d’une zone publique sur Route53 (voir plus bas pour les détails).

Un NLB est utilisé pour la communication des agents vers le contrôleur. Ce NLB écoutera sur deux ports : un port pour le protocole JNLP (50000) et un port pour accéder au Contrôleur en HTTP (80 -> 8080). Le port JNLP est configuré au niveau du Contrôleur dans “Manage Jenkins > Configure Global Security > Agents > TCP port inbound agents”.

On peut techniquement se passer d’un NLB en utilisant l’adresse IP du Contrôleur comme point d’accès pour les agents. Cette adresse changeant à chaque démarrage d’un nouveau conteneur, il faudra mettre en place un mécanisme de mise à jour automatique de la configuration du plugin Amazon ECS avec la nouvelle adresse IP du Contrôleur (à l’aide d’un script groovy dans le dossier $JENKINS_HOME/init.groovy.d/ par exemple).
Note : le redémarrage de Jenkins n’implique pas le démarrage d’un nouveau conteneur Contrôleur dans ECS.

Un service ECS jenkins-controller est déployé avec un seul conteneur et attaché à 3 targets groupes : 1 pour l’ALB en HTTP et 2 pour le NLB en HTTP et JNLP. Le groupe de sécurité du service n’autorise que l’ALB et le NLB en entrée. L’ALB est autorisée grâce à son groupe de sécurité. Pour le NLB, les IPs de ses interfaces réseaux sont utilisées en ingress sachant qu’un NLB ne peut avoir de groupe de sécurité. Le lancement des jobs sera désactivé sur le Contrôleur (numExecutors: 0). Les agents JNLP ainsi que le Contrôleur seront dans le même cluster ECS pour des raisons de simplicité.

Un système de fichier EFS (a.k.a NFS) stocke la configuration du Contrôleur dans le dossier /var/jenkins_home. Il a un point de montage dans chaque sous-réseau privé grâce auquel le conteneur se connecte à travers le port 2049. L’EFS aura un mode de débit en rafales (mode burst) et un mode de performance à usage général. Dans ce mode de débit, le système de fichier est mis à l’échelle en fonction de sa taille avec un débit alloué de 50 Kio/s par Gio. Dit autrement, plus la taille de la configuration du Contrôleur augmentera, plus les performances de l’EFS seront meilleures. En mode rafale, des pics de consommation allant au-delà du debit alloué sont autorisés pendant un certain temps par jour. Ce temps est également déterminé par la taille du système de fichiers. Plus d’informations dans la documentation AWS.

Une attention toute particulière est à porter à l’EFS notamment à la métrique BurstCreditBalance. Cette métrique correspond au nombre de crédits de burst que le système de fichiers possède à un instant t. À la création, le système de fichier dispose de 2,1To de crédit. Ce crédit diminuera au fil du temps si le débit utilisé est supérieur au débit alloué pendant un certain temps. Une fois ce crédit à 0, les performances de l’EFS seront grandement dégradées et le Contrôleur deviendra inutilisable.
Une solution est de créer de gros fichiers à coup de dd dans l’EFS pour gagner en performances ou changer de mode de débit en débit alloué (coûte relativement plus cher). Plus d’informations sur le monitoring EFS sur cette page.

Lors du démarrage du Contrôleur, la configuration est récupérée depuis un bucket S3 versionné puis placée dans /var/jenkins_home/jenkins.yaml pour être lue automatiquement par le plugin Jenkins Configuration as Code (JCasC). Ce plugin permet de configurer Jenkins de manière déclarative à partir d’un ou plusieurs fichiers YAML. Terraform générera ce fichier YAML pour le mettre dans le bucket S3. Le template est situé dans templates/jcasc.template.yml. D’autres exemples de fichiers de configuration sont disponibles sur GitHub.

Dans notre cas, nous utilisons ce plugin pour :

  • Créer un utilisateur admin avec un mot de passe aléatoire et désactiver les inscriptions des utilisateurs.
  • Paramétrer le plugin Amazon ECS pour utiliser notre cluster ECS. Un template ECS ainsi qu’un label example-agent sont aussi créés.
  • Créer un job d’exemple utilisant le label configuré et qui affiche l’identité AWS du build ainsi que les variables d’environnement du build.
  • Activer le protocole JNLP sur le port 50000 et désactiver le serveur SSH du Contrôleur.
  • Désactiver de l’exécution des jobs sur le Contrôleur.

Deux logs groupes CloudWatch sont utilisés : un pour le jenkins Contrôleur (/jenkins/controller) et un pour les logs des agents (/jenkins/agents). Les logs des agents correspondent aux logs lors de la communication Agents -> Contrôleur. Chaque nouvel agent aura un nouveau log stream dédié.

Rôles IAM

4 rôles IAM sont utilisées dans cette stack dont 2 associés au Contrôleur et 2 à l’agent :

  • jenkins-controller-ecs-execution : Il s’agit du rôle utilisé par ECS pour envoyer les logs du Contrôleur dans CloudWatch. On utilise la stratégie gérée par AWS AmazonECSTaskExecutionRolePolicy qui donne aussi accès à Amazon ECR pour récupérer des images Docker.
  • jenkins-controller-ecs-task : Ce rôle est utilisé par le Contrôleur pour récupérer sa configuration depuis S3, lancer et stopper des tâches sur le cluster ECS. Il permet également au Contrôleur de passer les deux rôles plus bas aux agents. C’est ce rôle qu’il faudra modifier si le Contrôleur doit effectuer d’autres actions sur les services AWS (récupération de secrets ou de paramètres depuis SSM par exemple).
  • jenkins-agents-ecs-execution : Ce rôle est utilisé par ECS pour envoyer les logs des agents dans CloudWatch. La stratégie gérée par AWS AmazonECSTaskExecutionRolePolicy lui est aussi attachée.
  • jenkins-agents-ecs-task : Un rôle exemple attaché à notre agent. Si les agents doivent accéder à des ressources AWS pour builder une AMI ou lancer Terraform, c’est ce rôle qu’il faudra modifier. Il n’a aucune permission dans notre stack.

Images docker

Deux images Docker Alpine sont utilisées :

  • Une pour le Contrôleur basée sur l’image officielle avec des plugins pré-installés tels que les plugins Amazon ECS et Jenkins Configuration as Code. Un script est utilisé en entrypoint pour récupérer la configuration depuis le bucket S3 avant le démarrage de Jenkins.
  • Une pour les agents JNLP également basée sur l’image officielle des agents Jenkins.

Ces deux images pourront servir d’exemple pour ceux voulant aller plus loin dans la personnalisation du Contrôleur ainsi que des agents. Voir le dossier docker/.

Déploiement

Maintenant qu’on a planté le décor, passons aux choses sérieuses.

Prérequis

Pour lancer cette stack, il faut :

  • Un VPC avec des sous-réseaux publics et privés. On partira du principe qu’ils sont bien configurés : table de routage, nat gateway, internet gateway, communication entre les sous réseaux publics et privés, etc.
  • Un utilisateur IAM pour lancer Terraform. Cet utilisateur devra au moins avoir des droits étendus sur les services EC2, ECS, IAM, S3, Cloudwatch, EFS, Route53 et ACM.
  • Terraform 0.12.X pour déployer l’infrastructure (j’utilise la version 0.12.25). Un backend local est utilisé par défaut pour des raisons de simplicité. Dans un environnement de production, il faut obligatoirement un autre backend tel que S3 avec le versioning activé.
  • [Optionnel] Un domaine sur Route53 pour accéder à Jenkins en HTTPs avec une URL “user-friendly”.

Le déploiement se fera par défaut en Ireland. Modifiez la variable Terraform aws_region pour déployer dans une autre région. Voir le fichier variables.tf pour la liste de toutes les variables disponibles.

Lancement

Avant de lancer les commandes Terraform, il faut soit exporter le profile AWS (AWS_PROFILE) pointant vers votre utilisateur IAM soit exporter les variables d’environnement AWS_SECRET_ACCESS_KEY et AWS_ACCESS_KEY_ID associées à votre utilisateur IAM. Ensuite :

# Clonage du dépot depuis github
git clone https://github.com/haidaraM/terraform-jenkins-aws-fargate
cd terraform-jenkins-aws-fargate && git checkout v2.1.0

# Export des variables obligatoires
export TF_VAR_vpc_id="vpc-123456789"
export TF_VAR_private_subnets='["private-subnet-a", "private-subnet-b", "private-subnet-c"]'
export TF_VAR_public_subnets='["public-subnet-a", "public-subnet-b", "public-subnet-c"]'

# Lancement de Terraform
terraform init
terraform apply # 'yes' une fois le plan terminé
Les variables TF_VAR_vpc_id, TF_VAR_private_subnets et TF_VAR_public_subnets sont à remplacer par celles de votre VPC.
Si vous disposez d’une zone sur Route53, exportez également la variable TF_VAR_route53_zone_name="mondomaine.com". Jenkins sera alors accessible sur https://jenkins.mondomaine.com.

Une fois la commande apply terminée, attendez quelques minutes le temps que le Contrôleur démarre et s’enregistre auprès des deux load balancers. Nous aurons une sortie qui ressemble à ça :

agents_log_group = "/jenkins/agents"
controller_config_on_s3 = "s3://jenkins-jcasc-12345678910/jenkins-conf.yml"
controller_log_group = "/jenkins/controller"
jenkins_credentials = <sensitive>
jenkins_public_url = "http://alb-jenkins-controller-450011780.eu-west-1.elb.amazonaws.com"

Et voilà : vous avez un Jenkins déployé et configuré automatiquement 😄. Il sera accessible sur l’URL dans jenkins_public_url avec les credentials contenus dans jenkins_credentials. Pour voir les credentials:

terraform output jenkins_credentials

Si après quelques minutes le Contrôleur n’est toujours pas disponible, consultez le log groupe /jenkins/controller et l’état du service dans la console AWS ECS.

Si tout s’est bien passé, une fois connecté vous devriez voir le job example dans la page d’accueil :

Page d'accueil Jenkins avec le job `example`.
Page d’accueil Jenkins avec le job example.

Nous allons lancer le job example pour vérifier la communication Agent -> Contrôleur : “example > Build now”. Ce job affiche l’identité AWS du build ainsi que les variables d’environnement disponibles dans le contexte d’exécution du build. Ces variables comprennent les variables fournies par Jenkins ainsi que les variables fournies par AWS aux conteneurs sur Fargate : AWS_EXECUTION_ENV=AWS_ECS_FARGATE nous indique donc que le job s’est exécuté sur Fargate.

Page d'accueil Jenkins avec le job `example`.
Page d’accueil Jenkins avec le job example.
Si le job ne démarre après quelques minutes, consultez les logs du Contrôleur ainsi que ceux des agents dans Cloudwatch.

Pour voir la configuration du plugin ECS : “Manage Jenkins > Manage Nodes and Clouds > Configure Clouds” ou sur /configureClouds:

Configuration du plugin ECS
Configuration du plugin ECS

On retrouve notre cluster ECS ainsi qu’un template d’agent. Le Contrôleur utilise un rôle IAM pour accéder au cluster. Vous pouvez également rajouter d’autres clusters ECS. Il faudra au préalable modifier le rôle IAM “jenkins-controller-ecs-task” pour avoir accès à ces clusters.

Gestion de la configuration du Contrôleur

En utilisant le plugin Jenkins Configuration as Code comme nous l’avions fait, certaines modifications effectuées depuis l’interface graphique seront écrasées au prochain démarrage d’un nouveau conteneur du Contrôleur (voir le ticket #825 sur Github).

Cette approche peut être souhaitable si nous voulons gérer toute la configuration du Contrôleur en Infra as Code. Cependant, chaque modification du fichier YAML de configuration nécessitera le lancement d’un nouveau conteneur pour être prise en compte. Pour ce faire, la variable d’environnement JENKINS_CONF_S3_VERSION_ID est utilisée dans la task definition du Contrôleur. Cette variable correspond au numéro de version associé à la configuration du Contrôleur stockée dans le bucket S3. Elle changera à chaque modification du template YAML et entraînera la création d’une nouvelle task définition qui aura pour conséquence l’arrêt de l’ancien conteneur et le démarrage automatique d’un nouveau conteneur. Il y aura donc un temps d’indisponibilité du Contrôleur de l’ordre de quelques secondes/minutes le temps que le nouveau conteneur soit prêt.

Pour ne faire qu’une configuration initiale du Contrôleur et gérer la suite manuellement, vous pouvez supprimer la variable d’environnement JENKINS_CONF_S3_VERSION_ID de la task definition. Le point d’entrée de l’image Docker est configurée pour supprimer un éventuel fichier YAML existant si cette variable n’est pas définie pour éviter d’écraser la configuration.

ECS peut cependant aussi être paramétré pour lancer un nouveau conteneur et attendre qu’il soit à l’état RUNNING et healthy auprès du load balancer avant d’arrêter l’ancien conteneur (voir la variable Terraform controller_deployment_percentages). Mettez les valeurs min=100 et max=200 pour avoir ce comportement. Par défaut, le conteneur est arrêté avant de lancer un nouveau.

Néanmoins, la version open source de Jenkins ne supporte pas plusieurs contrôleurs. Durant le laps de temps où les deux contrôleurs seront en cours d’exécution, vous pourriez avoir des erreurs du type “No valid crumb was included in the request” depuis l’interface graphique du Contrôleur ou la perte de jobs en cours d’exécution. De manière générale, le bon fonctionnement du Contrôleur n’est pas garanti dans ces conditions.

Une autre approche est de générer une URL S3 pré-signée à chaque modification de la configuration dans le bucket S3 puis appliquer cette configuration manuellement depuis l’interface graphique dans le menu “Manage Jenkins > Configuration as Code”. Cette approche ne nécessite pas un redémarrage du Contrôleur : la configuration est modifiée à chaud par le plugin.

Page Jenkins Configuration as Code
Page Jenkins Configuration as Code

Depuis la page du plugin Configuration as Code, on peut générer une documentation du fichier template et un schéma JSON en fonction des plugins installés. Notez aussi que tous les plugins ne sont pas compatibles avec la configuration as code. On se tournera vers du groovy dans ces cas.

Monitoring

Avant de commencer à monitorer une infrastructure/application, il est important d’identifier les composants ou ressources les plus critiques. Une fois ces composants identifiés, nous regardons quelles sont leurs métriques les plus pertinentes par rapport notre besoin qui est d’être informé (à l’avance ou non) de problèmes liés à notre application. Cela peut être des problèmes de performance ou de disponibilité par exemple.

Dans notre stack, les composants à surveiller de près sont le système de fichiers EFS (comme expliqué plus haut), le Jenkins Contrôleur (CPU et RAM) ainsi que l’ALB. Nos conteneurs étant sur Fargate, il n’y a rien de particulier à monitorer à ce niveau : AWS se charge de l’infrastructure sous-jacente.

Ainsi, les alarmes suivantes sont configurées pour envoyer les évènements vers un topic SNS (voir le fichier monitoring.tf) :

  • jenkins-efs-low-burst-credits-balance : Le crédit EFS a atteint la moitié du crédit alloué par AWS. Voir plus haut pour plus de détails.
  • jenkins-alb-too-many-5xx-errors : Le Jenkins Contrôleur a renvoyé plus de 60 erreurs internes 500 à l’ALB sur une période de 5 mn. Dans le cas de cette alarme, il faut consulter les logs du Contrôleur. Des erreurs 500 peuvent également subvenir lors de la mise à jour du Contrôleur.
  • jenkins-alb-no-healthy-target: Il n’y a aucun target healthy enregistré auprès de l’ALB. Cette erreur peut être dûe au Contrôleur qui ne démarre plus ou un problème au niveau du healthcheck du target groupe.
  • jenkins-controller-high-cpu-utilization: Utilisation moyenne du CPU supérieure à 80 % sur une période de 5 mn.
  • jenkins-controller-high-memory-utilization: Utilisation moyenne de la mémoire supérieure à 75 % sur une période de 5 mn.

Si les alarmes liées au CPU et à la mémoire se déclenchent fréquemment, pensez à augmenter les ressources du Contrôleur. Voir la variable Terraform controller_cpu_memory.

Pour recevoir ces alarmes par mail, il faut configurer manuellement un abonnement au topic SNS depuis la console AWS ou par CLI. Terraform ne supporte pas l’abonnement d’une adresse email.

Limitations et inconvénients

Avoir un Jenkins entièrement sur Fargate présente quelques limitations et inconvénients :

  • Temps de démarrage des jobs : démarrer un conteneur sur Fargate prend un peu plus de temps que sur une instance EC2. Comptez ~45s en moyenne pour démarrer un job sur Fargate vs ~20 s sur ECS EC2 avec une image docker de 261MB. Ce temps correspond au temps entre le moment où Jenkins lance la tâche ECS et le moment où l’agent arrive à se connecter au Contrôleur.
    Ceci est dû en partie au temps de création d’une Elastic Network Interface (ENI) pour la tâche, chaque tâche sur Fargate ayant sa propre adresse IP avec le mode réseau awsvpc qui est le seul supporté par Fargate à l’heure actuelle. À ce temps, il faut rajouter le temps de téléchargement de l’image Docker : pas de cache possible pour les images sur Fargate.
    Avec un cluster EC2, on profite du caching des images sur les conteneurs instances et nous n’avons pas forcément besoin d’une ENI pour chaque tâche. Le conteneur peut utiliser les interfaces réseaux de l’EC2 (mode host) ou celle de Docker (mode bridge) pour communiquer.
    Mise à jour décembre 2023: Ce temps de démarrage peut être optimiser en utilisant SOCI. Voir mon article sur SOCI.
  • Pas de mode privilégié pour faire du Docker in Docker (DinD) : Lancer un conteneur en mode privilégié permet de faire du Docker dans du Docker : le conteneur Docker privilégié aura accès directement au Docker host. Ceci est généralement considéré comme une mauvaise pratique au vu de la faille de sécurité que cela implique. Néanmoins, cette fonctionnalité est indispensable si on veut construire des images Docker dans notre chaîne de CI/CD par exemple.

Sachez qu’il est possible d’avoir simultanément des agents sur Fargate et sur EC2. Ces deux inconvénients peuvent donc être contournés pour certains jobs critiques qui auraient besoin de se lancer le plus rapidement possible ou de faire du DinD.

Pour aller plus loin

  • Sauvegarde de l’EFS : pas la peine de rappeler l’importance de faire des sauvegardes régulières du Contrôleur. AWS Backup peut être utilisé pour cela.
  • Si vous souhaitez facilement parcourir la configuration du Contrôleur (par curiosité ou pour fixer des fichiers corrompus), vous pouvez utiliser Cloud Commander. Il s’agit d’un gestionnaire de fichiers dans le navigateur. Il dispose d’une CLI et d’un éditeur de texte intégré.
  • Activer les access logs de l’ALB.
Mohamed El Mouctar HAIDARA
Mohamed El Mouctar HAIDARA
Senior Platform Engineer
comments powered by Disqus