Retour aux articles

Automatiser le déploiement de Jenkins sur AWS avec Terraform, EC2, Docker et Cloudflare Tunnel

Dans cet article, je documente la mise en œuvre d'un déploiement Jenkins sur AWS à l'aide de Terraform, Docker et Cloudflare Tunnel.

Level Sony
Jenkins AWS Terraform Docker Cloudflare DevOps CI/CD IaC
Automatiser le déploiement de Jenkins sur AWS avec Terraform, EC2, Docker et Cloudflare Tunnel
Table des matières

Note : Dans le cadre de ce projet, mon objectif n’est pas uniquement de déployer Jenkins sur AWS, mais aussi de poser les bases d’une infrastructure DevOps automatisée. Je souhaite mettre en place une infrastructure capable d’intégrer les outils indispensables au cycle de vie d’une application, depuis les phases de développement jusqu’à sa mise en production. Mon intention est de construire un socle technique reproductible, maintenable et évolutif, capable d’automatiser le provisionnement de l’infrastructure, le déploiement des services nécessaires et, à terme, l’orchestration des processus de CI/CD. Ce projet représente également pour moi une opportunité d’apprentissage concrète : expérimenter, tester, me tromper, corriger et progresser à travers une mise en pratique réelle des concepts DevOps.


Problématique

Le besoin initial était simple en apparence : disposer d’une instance Jenkins fonctionnelle sur AWS. Mais en pratique, je ne voulais pas d’une installation manuelle, fragile et difficile à reproduire.

Je voulais une solution capable de :

  • créer l’infrastructure automatiquement ;
  • configurer la machine au démarrage ;
  • déployer Jenkins dans un environnement isolé ;
  • sécuriser l’accès externe sans exposer directement le port natif de Jenkins ;
  • servir de base à d’autres composants DevOps dans le futur.

Autrement dit, le vrai sujet n’était pas “installer Jenkins”, mais construire les premières briques d’une infrastructure DevOps automatisée.


Choix techniques

Pour cette implémentation, j’ai retenu les composants suivants :

OutilRôle
AWSHébergement de l’infrastructure
TerraformProvisionnement déclaratif
EC2Machine d’hébergement
DockerExécution de Jenkins et des services
JenkinsPremière brique CI/CD
Cloudflare TunnelExposition sécurisée sans port ouvert
IAMLimitation des privilèges, sans compte root

Ce choix me permet de garder une architecture relativement simple, tout en restant aligné avec une logique d’automatisation, de modularité et de sécurité.


Architecture cible

L’architecture repose sur une chaîne d’exécution claire.

Depuis ma machine locale, j’exécute Terraform. Terraform communique avec AWS pour créer l’instance EC2 et la configuration réseau minimale. Lors du premier démarrage, l’instance exécute un script de bootstrap via user_data. Ce script installe Docker, prépare la configuration des services et déploie Jenkins ainsi que Cloudflare Tunnel.

Jenkins tourne dans un conteneur Docker et conserve ses données grâce à un volume persistant. Cloudflare Tunnel établit une connexion sortante vers Cloudflare, ce qui permet d’accéder à Jenkins via un hostname dédié sans exposer publiquement le port 8080.

Architecture haut niveau

Cette architecture combine automatisation du déploiement, isolation des services et réduction de l’exposition réseau directe.


Organisation du projet

Pour garder le code maintenable, j’ai organisé le projet Terraform selon une logique modulaire.

.
├── LICENSE
├── README.md
└── terraform
    ├── environments
    │   └── dev
    │       ├── main.tf
    │       ├── outputs.tf
    │       ├── terraform.tfvars
    │       └── variables.tf
    ├── main.tf
    ├── modules
    │   └── ec2_jenkins
    │       ├── main.tf
    │       ├── outputs.tf
    │       ├── README.md
    │       └── variables.tf
    └── provider.tf

Cette structure sépare :

  • la logique réutilisable du module ec2_jenkins ;
  • la configuration spécifique à l’environnement dev ;
  • la configuration globale du provider.

Elle est particulièrement utile si je souhaite plus tard ajouter un environnement staging ou prod, ou réutiliser le module pour d’autres services.


Mise en œuvre pas à pas

1. Configuration du provider AWS

La première étape a consisté à configurer Terraform pour communiquer avec AWS.

provider "aws" {
  region = var.aws_region
}

Je ne configure pas le provider directement dans les modules — je préfère garder le pilotage au niveau du root module. Une variable dédiée à la région est définie :

variable "aws_region" {
  description = "AWS region"
  type        = string
  default     = "eu-west-3"
}

Et dans terraform.tfvars :

aws_region = "eu-west-3"

2. Gestion des credentials AWS

Avant d’exécuter Terraform, j’ai configuré mes credentials AWS localement. Sans cette étape, la chaîne est bloquée dès le départ.

Au démarrage, j’ai rencontré l’erreur classique :

No valid credential sources found

Cette erreur signifie simplement que Terraform ne trouve aucun identifiant AWS valide sur la machine locale.

Pour stocker et récupérer les secrets de façon sécurisée, j’ai utilisé AWS Secrets Manager. Les secrets (Access Key ID, Secret Access Key) y sont stockés, puis récupérés via la CLI au moment de la configuration :

 aws secretsmanager get-secret-value --secret-id <nom-du-secret> --query SecretString --output text

Sécurité : remarquez l’espace en début de commande. Sur bash et zsh, préfixer une commande d’un espace empêche son enregistrement dans l’historique du shell (~/.bash_history / ~/.zsh_history). C’est une précaution simple mais utile pour toute commande manipulant des secrets ou des tokens.

Une fois les valeurs récupérées, on configure le profil local :

 aws configure

Pour vérifier que l’identité est bien reconnue :

aws sts get-caller-identity

3. Création du module EC2 pour Jenkins

Une fois le provider prêt, j’ai construit le module principal chargé de créer l’instance EC2. Ce module porte la logique principale : récupération de l’AMI, création de l’instance, configuration du security group, exécution du bootstrap via user_data.

resource "aws_instance" "jenkins" {
  ami           = data.aws_ami.amazon_linux_2023.id
  instance_type = var.instance_type
  key_name      = var.key_name

  vpc_security_group_ids = [aws_security_group.jenkins_sg.id]

  user_data = templatefile("${path.module}/user_data.sh.tpl", {
    cloudflare_tunnel_token = var.cloudflare_tunnel_token
  })

  tags = {
    Name = "jenkins-ec2"
  }
}

Cette ressource ne se contente pas de créer une machine virtuelle : elle déclenche aussi tout le processus d’initialisation.


4. Choix du type d’instance

Au moment du premier déploiement, j’ai rencontré une erreur liée au type d’instance :

InvalidParameterCombination: The specified instance type is not eligible for Free Tier

J’ai corrigé cela en basculant sur un type d’instance plus adapté :

variable "instance_type" {
  description = "EC2 instance type"
  type        = string
  default     = "t3.micro"
}

Cette étape illustre une réalité concrète : il faut souvent ajuster la théorie à la réalité du compte, de la région et des quotas disponibles.


5. Configuration réseau minimale

Je ne voulais pas exposer Jenkins publiquement sur son port natif 8080. J’ai donc configuré un security group minimal, en n’autorisant que ce qui était réellement nécessaire.

resource "aws_security_group" "jenkins_sg" {
  name = "jenkins-sg"

  ingress {
    description = "SSH access"
    from_port   = 22
    to_port     = 22
    protocol    = "tcp"
    cidr_blocks = [var.allowed_ssh_cidr]
  }

  egress {
    description = "Allow outbound traffic"
    from_port   = 0
    to_port     = 0
    protocol    = "-1"
    cidr_blocks = ["0.0.0.0/0"]
  }
}

Le port SSH reste limité à une plage d’adresses autorisée. Jenkins n’a pas besoin d’être publiquement accessible puisqu’il sera servi via Cloudflare Tunnel. Cette décision réduit directement la surface d’exposition réseau.


6. Bootstrap automatique de l’instance

L’un des points les plus importants de ce projet est l’automatisation du bootstrap. Je voulais éviter toute connexion manuelle à l’instance pour installer Docker ou lancer les services. J’ai utilisé user_data pour exécuter un script shell au premier démarrage.

Ce script fait tout en une seule passe : installer Docker depuis le dépôt officiel Debian, écrire les fichiers de configuration, et démarrer les services.

#!/bin/bash

# =============================================================================
# Install Docker and start Jenkins with Cloudflare Tunnel
#
# This script is executed by Terraform when the EC2 instance is created.
# It installs Docker, starts Jenkins with Cloudflare Tunnel, and configures
# the necessary environment variables.
#
# author: Sony level
# =============================================================================

set -euxo pipefail

LOG=/var/log/jenkins-setup.log

echo "[$(date '+%Y-%m-%d %H:%M:%S')] INFO  Starting Jenkins setup" >> "$LOG"

# Update system and install prerequisites
echo "[$(date '+%Y-%m-%d %H:%M:%S')] INFO  Updating system packages" >> "$LOG"
apt-get update -y
apt-get install -y ca-certificates curl gnupg
echo "[$(date '+%Y-%m-%d %H:%M:%S')] INFO  Prerequisites installed" >> "$LOG"

# Install Docker from official Debian repository
echo "[$(date '+%Y-%m-%d %H:%M:%S')] INFO  Installing Docker" >> "$LOG"
install -m 0755 -d /etc/apt/keyrings
curl -fsSL https://download.docker.com/linux/debian/gpg \
  | gpg --dearmor -o /etc/apt/keyrings/docker.gpg
chmod a+r /etc/apt/keyrings/docker.gpg

echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] \
  https://download.docker.com/linux/debian \
  $(. /etc/os-release && echo "$VERSION_CODENAME") stable" \
  | tee /etc/apt/sources.list.d/docker.list > /dev/null

apt-get update -y
apt-get install -y docker-ce docker-ce-cli containerd.io \
  docker-buildx-plugin docker-compose-plugin

systemctl enable --now docker
echo "[$(date '+%Y-%m-%d %H:%M:%S')] INFO  Docker installed and started" >> "$LOG"

# Create application directory
mkdir -p /opt/jenkins
echo "[$(date '+%Y-%m-%d %H:%M:%S')] INFO  Created /opt/jenkins" >> "$LOG"

# Write .env file with sensitive values (not stored in docker-compose.yml)
cat > /opt/jenkins/.env <<ENVEOF
TUNNEL_TOKEN=${cloudflare_tunnel_token}
ENVEOF
chmod 600 /opt/jenkins/.env
echo "[$(date '+%Y-%m-%d %H:%M:%S')] INFO  Written /opt/jenkins/.env" >> "$LOG"

# Write compose.yaml (rendered by Terraform at provision time)
cat > /opt/jenkins/compose.yaml <<'COMPOSEEOF'
${compose_content}
COMPOSEEOF
echo "[$(date '+%Y-%m-%d %H:%M:%S')] INFO  Written /opt/jenkins/compose.yaml" >> "$LOG"

# Start services
echo "[$(date '+%Y-%m-%d %H:%M:%S')] INFO  Starting Jenkins and Cloudflare Tunnel" >> "$LOG"
cd /opt/jenkins
docker compose up -d

echo "[$(date '+%Y-%m-%d %H:%M:%S')] INFO  Setup complete" >> "$LOG"

Quelques points importants dans ce script :

  • Docker est installé depuis le dépôt officiel Debian avec vérification GPG — pas via snap ou un paquet système ;
  • le fichier .env est créé avec les permissions 600 pour protéger le token du tunnel ;
  • le compose.yaml est injecté par Terraform via la variable ${compose_content}, ce qui évite de le dupliquer dans le script ;
  • chaque étape est tracée dans /var/log/jenkins-setup.log pour faciliter le débogage en cas d’échec au démarrage.

7. Déploiement de Jenkins et Cloudflare Tunnel avec Docker Compose

Pour le déploiement applicatif, j’ai regroupé Jenkins et Cloudflare Tunnel dans un seul fichier compose.yaml. Cette approche permet de tout démarrer en une seule commande, tout en gardant les deux services cohérents et couplés.

Au départ, j’avais envisagé un reverse proxy classique pour exposer Jenkins, mais j’ai finalement choisi Cloudflare Tunnel. Ce choix évite d’ouvrir le moindre port entrant : le tunnel établit une connexion sortante vers Cloudflare, qui redirige ensuite le trafic vers Jenkins en interne.

services:
  jenkins:
    image: jenkins/jenkins:latest
    restart: unless-stopped
    ports:
      - '127.0.0.1:8080:8080'
    environment:
      - SERVICE_URL_JENKINS_8080
    volumes:
      - jenkins-home:/var/jenkins_home
      - /var/run/docker.sock:/var/run/docker.sock
    healthcheck:
      test: ['CMD', 'curl', '-f', 'http://localhost:8080/login']
      interval: 30s
      timeout: 10s
      retries: 3
      start_period: 40s

  cloudflared:
    container_name: cloudflare-tunnel
    image: cloudflare/cloudflared:latest
    restart: unless-stopped
    network_mode: host
    command: tunnel --no-autoupdate run --token ${TUNNEL_TOKEN}
    env_file: .env
    healthcheck:
      test: ['CMD', 'cloudflared', '--version']
      interval: 5s
      timeout: 20s
      retries: 10

volumes:
  jenkins-home:

Quelques choix importants dans cette configuration :

  • le port 8080 est lié à 127.0.0.1 uniquement — Jenkins n’est pas accessible depuis l’extérieur directement ;
  • jenkins_home est persisté dans un volume dédié pour conserver la configuration, les plugins et les jobs après redémarrage ;
  • docker.sock est monté pour permettre à Jenkins de lancer des conteneurs, ce qui est pratique mais implique des considérations de sécurité ;
  • cloudflared utilise network_mode: host pour accéder directement à Jenkins sur localhost:8080 ;
  • le token du tunnel est injecté via un fichier .env et non écrit en dur dans la configuration ;
  • les deux services disposent d’un healthcheck pour surveiller leur état de démarrage.

8. Validation du déploiement

Une fois le déploiement terminé, j’ai vérifié plusieurs points.

Côté Terraform :

terraform init
terraform validate
terraform plan
terraform apply

Côté instance :

docker ps
docker logs jenkins
docker logs cloudflare-tunnel

Pour récupérer le mot de passe administrateur initial de Jenkins :

docker exec jenkins cat /var/jenkins_home/secrets/initialAdminPassword

Cela m’a permis de terminer la configuration initiale de Jenkins via le hostname configuré dans Cloudflare.


Problèmes rencontrés et corrections

Comme souvent dans un projet d’infrastructure, tout ne s’est pas passé du premier coup. Ces erreurs ont été utiles : elles m’ont obligé à mieux comprendre le comportement réel des outils.

Absence de credentials AWS valides

Erreur :

No valid credential sources found

Cause : aucun credential AWS valide n’était configuré localement.

Correction : configuration d’un profil AWS CLI et vérification avec aws sts get-caller-identity.


Type d’instance non éligible Free Tier

Cause : type EC2 non compatible avec les contraintes de mon compte ou de la région.

Correction : remplacement par t3.micro.


Exposition réseau de Jenkins

Problème initial : exposer Jenkins directement via le port 8080 était possible, mais peu satisfaisant d’un point de vue sécurité.

Correction : intégration de Cloudflare Tunnel pour éviter toute exposition directe.


Privilèges Docker

Le montage de /var/run/docker.sock simplifie l’usage de Docker depuis Jenkins, mais il augmente le niveau de contrôle du conteneur Jenkins sur l’hôte.

Approche actuelle : conservation de cette approche pour la simplicité initiale, avec l’intention de la durcir dans une version ultérieure via des agents dédiés ou une architecture plus isolée.


Considérations de sécurité

La sécurité n’a pas été traitée comme une réflexion secondaire, mais comme une contrainte présente dès les premiers choix :

  • ne pas utiliser le compte root AWS ;
  • limiter l’accès SSH à une plage d’adresses autorisée ;
  • ne pas exposer directement le port 8080 ;
  • utiliser Cloudflare Tunnel comme couche d’accès externe ;
  • isoler Jenkins dans un conteneur ;
  • ne pas stocker les secrets dans le code.

Cette implémentation reste une première base fonctionnelle. Le montage du socket Docker, par exemple, devra être reconsidéré dans une version plus mature.


Résultat obtenu

À l’issue de cette mise en œuvre, j’ai obtenu une première brique d’infrastructure DevOps automatisée capable de :

  • provisionner automatiquement une instance AWS avec Terraform ;
  • configurer l’instance au démarrage sans intervention manuelle ;
  • installer Docker et déployer Jenkins en conteneur ;
  • conserver les données Jenkins entre les redémarrages ;
  • exposer le service via un tunnel sécurisé ;
  • éviter l’exposition directe du port natif de Jenkins.

Au-delà du simple déploiement d’un outil, cette implémentation fournit un point de départ crédible pour faire évoluer l’environnement vers un véritable socle DevOps plus complet.


Limites actuelles

Même si le résultat est satisfaisant pour une première implémentation, plusieurs limites subsistent :

  • Jenkins reste hébergé sur une seule instance EC2, sans haute disponibilité ;
  • la persistance repose sur un volume local Docker — suffisant pour un premier niveau, mais une approche plus robuste s’appuierait sur un stockage externe ;
  • la sécurité liée à l’usage de docker.sock devra être améliorée ;
  • l’observabilité, les sauvegardes et la supervision ne sont pas encore intégrées.

Perspectives d’évolution

Cette base ouvre plusieurs pistes :

  • gestion plus avancée des permissions IAM ;
  • stockage plus robuste pour la persistance des données Jenkins ;
  • sauvegardes automatisées ;
  • monitoring et logs centralisés ;
  • agents Jenkins dédiés pour les builds ;
  • pipelines CI/CD complets ;
  • ajout d’autres outils DevOps au sein de la même infrastructure.

Jenkins n’est ici que la première brique d’un écosystème plus large à construire progressivement.

Commentaires