Vous travaillez dans une startup en cours de croissance.
L'infrastructure AWS a été sécurisée lors des TD précédents :
Un backend Spring Boot doit maintenant être déployé automatiquement sur cette infrastructure.
Objectifs métier :
Rappel sur les environnements d'exécution :
L'application Spring Boot utilise un fichier de configuration pour se connecter à la base de données.
Fichier : app/src/main/resources/application.properties
server.port=8080 spring.datasource.url=jdbc:postgresql://DB_HOST:5432/app spring.datasource.username=app_user spring.datasource.password=CHANGE_ME spring.jpa.hibernate.ddl-auto=update spring.jpa.show-sql=false
Ce fichier sera commité dans Git avec le reste du code source.
Le fichier Maven minimal pour construire le projet :
Fichier : app/pom.xml
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.example</groupId>
<artifactId>demo</artifactId>
<version>1.0</version>
<packaging>jar</packaging>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.2.0</version>
</parent>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>org.postgresql</groupId>
<artifactId>postgresql</artifactId>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
Compiler l'application depuis le répertoire app/ :
cd app mvn clean package -DskipTests
Vérifier que le fichier app/target/demo-1.0.jar est bien généré.
Dans un pipeline CI/CD en production, ignorer les tests avec -DskipTests est une mauvaise pratique.
Avant d'automatiser, on effectue un déploiement manuel pour valider que l'application fonctionne.
Copier le jar sur l'instance EC2 :
scp -i ~/.ssh/ma-cle.pem \ app/target/demo-1.0.jar \ ec2-user@EC2_IP:/home/ec2-user/demo.jar
Se connecter à l'instance et lancer l'application :
ssh -i ~/.ssh/ma-cle.pem ec2-user@EC2_IP # Sur l'instance EC2 java -jar /home/ec2-user/demo.jar \ --spring.datasource.url=jdbc:postgresql://RDS_HOST:5432/app \ --spring.datasource.username=app_user \ --spring.datasource.password=CHANGE_ME
Sur un système Linux, les arguments passés à un processus sont visibles via ps aux.
En lançant l'application avec la configuration par défaut, vous obtenez une erreur de ce type :
org.postgresql.util.PSQLException: Connection to DB_HOST:5432 refused.
Avant de chercher la solution :
refused et non timed out. Quelle différence technique cela implique-t-il sur l'origine du problème ?Commande utile pour tester la connectivité réseau depuis l'instance EC2 :
nc -zv RDS_HOST 5432
Ne cherchez pas la solution immédiatement. Identifiez d'abord l'origine exacte du problème.
On automatise maintenant le déploiement pour qu'il soit reproductible.
Fichier : ansible/inventory.ini
[web] EC2_IP ansible_user=ec2-user ansible_ssh_private_key_file=~/.ssh/ma-cle.pem
Fichier : ansible/ansible.cfg
[defaults] host_key_checking = False stdout_callback = yaml
Fichier : ansible/deploy.yml
---
- hosts: web
become: true
vars:
app_dir: /opt/demo
app_jar: demo.jar
app_user: appuser
tasks:
- name: Créer l'utilisateur applicatif
user:
name: "{{ app_user }}"
system: true
shell: /sbin/nologin
create_home: false
- name: Créer le répertoire de l'application
file:
path: "{{ app_dir }}"
state: directory
owner: "{{ app_user }}"
mode: '0755'
- name: Installer Java 17
yum:
name: java-17-amazon-corretto
state: present
- name: Copier le jar
copy:
src: ../app/target/demo-1.0.jar
dest: "{{ app_dir }}/{{ app_jar }}"
owner: "{{ app_user }}"
mode: '0644'
Lancer le playbook :
ansible-playbook -i ansible/inventory.ini ansible/deploy.yml
Vérifier que le jar est bien présent sur l'instance :
ssh -i ~/.ssh/ma-cle.pem ec2-user@EC2_IP ls -lh /opt/demo/
Le jar est déployé mais l'application ne peut pas démarrer correctement car les variables de connexion à la base de données ne sont pas fournies.
Avant de regarder la section suivante :
On ajoute la récupération du secret et le lancement de l'application via un service systemd.
Fichier : ansible/deploy.yml (version complète)
---
- hosts: web
become: true
vars:
app_dir: /opt/demo
app_jar: demo.jar
app_user: appuser
rds_host: RDS_HOST
secret_id: td3-db-password
tasks:
- name: Créer l'utilisateur applicatif
user:
name: "{{ app_user }}"
system: true
shell: /sbin/nologin
create_home: false
- name: Créer le répertoire de l'application
file:
path: "{{ app_dir }}"
state: directory
owner: "{{ app_user }}"
mode: '0755'
- name: Installer Java 17
yum:
name: java-17-amazon-corretto
state: present
- name: Copier le jar
copy:
src: ../app/target/demo-1.0.jar
dest: "{{ app_dir }}/{{ app_jar }}"
owner: "{{ app_user }}"
mode: '0644'
- name: Récupérer le secret depuis Secrets Manager
shell: >
aws secretsmanager get-secret-value
--secret-id {{ secret_id }}
--query SecretString
--output text
register: db_password
no_log: true
- name: Créer le fichier de configuration de l'application
copy:
dest: "{{ app_dir }}/application.properties"
owner: "{{ app_user }}"
mode: '0600'
content: |
server.port=8080
spring.datasource.url=jdbc:postgresql://{{ rds_host }}:5432/app
spring.datasource.username=app_user
spring.datasource.password={{ db_password.stdout }}
spring.jpa.hibernate.ddl-auto=update
no_log: true
- name: Déployer le service systemd
copy:
dest: /etc/systemd/system/demo.service
content: |
[Unit]
Description=Demo Spring Boot Application
After=network.target
[Service]
User={{ app_user }}
WorkingDirectory={{ app_dir }}
ExecStart=/usr/bin/java -jar {{ app_dir }}/{{ app_jar }} \
--spring.config.location={{ app_dir }}/application.properties
SuccessExitStatus=143
Restart=on-failure
RestartSec=10
[Install]
WantedBy=multi-user.target
- name: Recharger systemd
systemd:
daemon_reload: true
- name: Activer et démarrer l'application
systemd:
name: demo
state: restarted
enabled: true
no_log: true masque la valeur dans les logs Ansible, mais le secret est tout de même écrit sur le disque dans application.properties. Quelles mesures complémentaires permettraient de réduire la durée d'exposition de ce secret sur le système de fichiers ?appuser qui n'a pas de shell. Quel est l'intérêt de cette contrainte par rapport à un utilisateur standard ?{“password”:“valeur”} plutôt qu'une chaîne brute, que faut-il modifier dans la tâche de récupération ?Comparez les deux approches du point de vue opérationnel :
# Approche nohup – à ne pas utiliser en production nohup java -jar demo.jar &
nohup plante silencieusement à 3h du matin. Décrivez la chaîne d'événements jusqu'à la détection du problème sans systemd, puis avec systemd.On automatise maintenant le build et le déploiement via GitHub Actions.
Fichier : .github/workflows/deploy.yml
name: Build and Deploy
on:
push:
branches: [ "main" ]
jobs:
build-and-deploy:
runs-on: ubuntu-latest
steps:
- name: Récupérer le code
uses: actions/checkout@v4
- name: Configurer Java 17
uses: actions/setup-java@v4
with:
distribution: temurin
java-version: 17
- name: Compiler l'application
run: |
cd app
mvn clean package -DskipTests
- name: Configurer les credentials AWS
uses: aws-actions/configure-aws-credentials@v4
with:
aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
aws-region: eu-west-1
- name: Préparer la clé SSH
run: |
mkdir -p ~/.ssh
echo "${{ secrets.EC2_SSH_KEY }}" > ~/.ssh/ma-cle.pem
chmod 600 ~/.ssh/ma-cle.pem
- name: Installer Ansible
run: |
sudo apt-get update -q
sudo apt-get install -y ansible
- name: Déployer avec Ansible
run: |
ansible-playbook \
-i ansible/inventory.ini \
ansible/deploy.yml \
-e "rds_host=${{ secrets.RDS_HOST }}"
En l'état, le pipeline GitHub Actions va échouer pour plusieurs raisons.
Identifiez les problèmes avant de passer à la section suivante :
EC2_IP en dur. Quelles sont les deux situations concrètes dans lesquelles cette valeur devient invalide sans que personne ne s'en aperçoive immédiatement ?Dans le dépôt GitHub, aller dans Settings > Secrets and variables > Actions.
Créer les secrets suivants :
AWS_ACCESS_KEY_ID : clé d'accès du compte AWSAWS_SECRET_ACCESS_KEY : clé secrète correspondanteEC2_SSH_KEY : contenu complet du fichier .pem de la clé SSHRDS_HOST : endpoint du RDS récupéré depuis la console AWSMettre à jour l'inventaire Ansible avec l'IP réelle de l'instance :
Fichier : ansible/inventory.ini
[web] EC2_IP_REELLE ansible_user=ec2-user ansible_ssh_private_key_file=~/.ssh/ma-cle.pem
Fichier : ansible/ansible.cfg
[defaults] host_key_checking = False stdout_callback = yaml remote_user = ec2-user
Vérifier que l'application est bien démarrée sur l'instance EC2 :
ssh -i ~/.ssh/ma-cle.pem ec2-user@EC2_IP # Vérifier le statut du service sudo systemctl status demo # Consulter les logs de l'application sudo journalctl -u demo -n 50 --no-pager
Vérifier que l'application répond via l'ALB :
curl http://ALB_DNS/actuator/health
Objectif de cette section :
Fichier : terraform/ec2_ssm.tf
resource "aws_iam_role" "ec2_ssm_role" {
name = "ec2-ssm-role"
assume_role_policy = jsonencode({
Version = "2012-10-17"
Statement = [{
Effect = "Allow"
Principal = {
Service = "ec2.amazonaws.com"
}
Action = "sts:AssumeRole"
}]
})
}
resource "aws_iam_role_policy_attachment" "ssm_core" {
role = aws_iam_role.ec2_ssm_role.name
policy_arn = "arn:aws:iam::aws:policy/AmazonSSMManagedInstanceCore"
}
resource "aws_iam_instance_profile" "ec2_ssm_profile" {
name = "ec2-ssm-profile"
role = aws_iam_role.ec2_ssm_role.name
}
Appliquer la configuration Terraform :
terraform apply
Associer le profil IAM à l'instance EC2 existante depuis la console AWS : EC2 > Instance > Actions > Security > Modify IAM role
Tester la connexion sans clé SSH :
aws ssm start-session --target INSTANCE_ID
Vérifier que la session s'ouvre correctement, puis taper exit.
Modifier le Security Group de l'instance EC2 pour supprimer la règle entrante sur le port 22.
Depuis la console AWS : EC2 > Security Groups > Sélectionner le SG de l'instance > Inbound rules > Supprimer la règle port 22
Vérifier qu'une connexion SSH directe est bien refusée :
ssh -i ~/.ssh/ma-cle.pem ec2-user@EC2_IP # Attendu : Connection refused ou timeout
Vérifier que Session Manager fonctionne toujours :
aws ssm start-session --target INSTANCE_ID
L'objectif est d'améliorer l'architecture sur les points suivants.
Point 1 : Rotation automatique du secret
Configurer Secrets Manager pour effectuer une rotation automatique du mot de passe RDS tous les 30 jours.
Point 2 : Inventaire Ansible dynamique
Remplacer le fichier inventory.ini statique par un inventaire dynamique qui récupère automatiquement l'IP de l'instance EC2 via les tags AWS.
Point 3 : Notifications de déploiement
Ajouter une étape dans le pipeline GitHub Actions pour envoyer une notification (Slack ou email) en cas de succès ou d'échec du déploiement.
Questions de synthèse :
main. Un déploiement raté laisse l'application dans un état inconnu. Quelle stratégie de déploiement permet de garantir qu'une version précédente reste disponible pendant la mise à jour ?Mettre en place un déploiement sans interruption de service.
Modifier le playbook Ansible pour :
Fichier : ansible/deploy_bluegreen.yml
---
- hosts: web
become: true
vars:
app_dir: /opt/demo
releases_dir: /opt/demo/releases
app_jar: demo.jar
app_user: appuser
timestamp: "{{ ansible_date_time.epoch }}"
tasks:
- name: Créer le répertoire de la release
file:
path: "{{ releases_dir }}/{{ timestamp }}"
state: directory
owner: "{{ app_user }}"
mode: '0755'
- name: Copier le jar dans le répertoire de la release
copy:
src: ../app/target/demo-1.0.jar
dest: "{{ releases_dir }}/{{ timestamp }}/{{ app_jar }}"
owner: "{{ app_user }}"
mode: '0644'
- name: Basculer le lien symbolique vers la nouvelle release
file:
src: "{{ releases_dir }}/{{ timestamp }}/{{ app_jar }}"
dest: "{{ app_dir }}/current.jar"
state: link
owner: "{{ app_user }}"
- name: Redémarrer le service
systemd:
name: demo
state: restarted