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 :
| Outil | Rôle |
|---|---|
| AWS | Hébergement de l’infrastructure |
| Terraform | Provisionnement déclaratif |
| EC2 | Machine d’hébergement |
| Docker | Exécution de Jenkins et des services |
| Jenkins | Première brique CI/CD |
| Cloudflare Tunnel | Exposition sécurisée sans port ouvert |
| IAM | Limitation 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.

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
bashetzsh, 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
snapou un paquet système ; - le fichier
.envest créé avec les permissions600pour protéger le token du tunnel ; - le
compose.yamlest 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.logpour 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
8080est lié à127.0.0.1uniquement — Jenkins n’est pas accessible depuis l’extérieur directement ; jenkins_homeest persisté dans un volume dédié pour conserver la configuration, les plugins et les jobs après redémarrage ;docker.sockest monté pour permettre à Jenkins de lancer des conteneurs, ce qui est pratique mais implique des considérations de sécurité ;cloudflaredutilisenetwork_mode: hostpour accéder directement à Jenkins surlocalhost:8080;- le token du tunnel est injecté via un fichier
.envet non écrit en dur dans la configuration ; - les deux services disposent d’un
healthcheckpour 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.sockdevra ê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.
