eadl:bloc4:fm4:td4

Ceci est une ancienne révision du document !


TD4 – Déploiement d'un backend Spring Boot avec Ansible et GitHub Actions

  • Déployer une application Spring Boot sur une instance EC2
  • Utiliser Ansible pour automatiser la configuration
  • Connecter l'application à une base RDS PostgreSQL existante
  • Récupérer un secret depuis AWS Secrets Manager
  • Mettre en place un pipeline CI/CD avec GitHub Actions
  • Comprendre les limites de SSH et introduire AWS Systems Manager

Vous travaillez dans une startup en cours de croissance.

L'infrastructure AWS a été sécurisée lors des TD précédents :

  • VPC avec sous-réseaux publics et privés (TD2)
  • Application Load Balancer (TD2)
  • RDS PostgreSQL en sous-réseau privé (TD3)
  • Secrets Manager pour les mots de passe (TD3)
  • CloudTrail actif pour la traçabilité (TD3)

Un backend Spring Boot doit maintenant être déployé automatiquement sur cette infrastructure.

Objectifs métier :

  • déploiement fiable et reproductible
  • aucun secret exposé dans le code
  • traçabilité complète des déploiements
  • Infrastructure TD2 et TD3 opérationnelle
  • Instance EC2 accessible via SSH ou Session Manager
  • Dépôt GitHub disponible avec les droits d'administration
  • Java 17 et Maven installés sur le poste de travail

Rappel sur les environnements d'exécution :

  • Java et Maven sont nécessaires sur le poste de travail pour compiler l'application
  • Java est nécessaire sur l'EC2 pour exécuter le jar
  • Maven n'a pas sa place sur l'EC2 : on y dépose uniquement le jar déjà compilé

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

Questions de compréhension :

  • Pourquoi le mot de passe en clair dans ce fichier pose-t-il un problème de sécurité ?
  • Que se passe-t-il si ce fichier est commité dans Git ?
  • Quel service AWS permet de corriger cette situation ?

Le build se fait sur le poste de travail. L'EC2 ne reçoit que le jar final.

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/ sur le poste de travail :

cd app
mvn clean package -DskipTests

Vérifier que le fichier app/target/demo-1.0.jar est bien généré.

Pourquoi utilise-t-on -DskipTests ici ?

Dans quel cas ne faudrait-il pas ignorer les tests ?

Avant d'automatiser, on effectue un déploiement manuel pour valider que l'application fonctionne.

Le jar est compilé sur le poste de travail. Avant de le copier sur l'EC2, il faut s'assurer que Java est disponible sur l'instance.

Étape 1 : Vérifier que Java est installé sur l'instance EC2.

ssh -i ~/.ssh/ma-cle.pem ec2-user@EC2_IP

# Sur l'instance EC2 – vérifier Java
java -version

Si Java n'est pas installé :

# Sur l'instance EC2
sudo yum install -y java-17-amazon-corretto

# Vérifier l'installation
java -version

# Quitter la session
exit

Étape 2 : Copier le jar sur l'instance EC2 depuis le poste de travail :

scp -i ~/.ssh/ma-cle.pem \
  app/target/demo-1.0.jar \
  ec2-user@EC2_IP:/home/ec2-user/demo.jar

Étape 3 : 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

Questions d'analyse :

  • Quels sont les problèmes concrets de cette méthode de déploiement ?
  • Que se passe-t-il si l'instance EC2 redémarre ?
  • Pourquoi passer le mot de passe en argument de ligne de commande est-il risqué ?

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.

Analysez l'erreur :

  • Pourquoi l'application ne peut-elle pas se connecter à DB_HOST ?
  • Quelle valeur devrait remplacer DB_HOST ?
  • Comment vérifier que l'instance EC2 peut joindre le RDS ?

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.

Ansible se connecte à l'EC2 via SSH depuis le poste de travail. Il installe Java si nécessaire, dépose le jar et prépare le service.

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/

Ansible installe Java automatiquement via la tâche dédiée. Ce comportement est idempotent.

  • Que se passe-t-il si on rejoue le playbook alors que Java est déjà installé ?
  • Pourquoi l'idempotence est-elle importante dans un contexte d'automatisation ?

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 :

  • Quel élément manque dans le playbook actuel ?
  • Comment Ansible peut-il récupérer un secret depuis AWS Secrets Manager ?
  • Pourquoi ne doit-on pas mettre le mot de passe directement dans le playbook ?

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

Questions de compréhension :

  • Pourquoi utilise-t-on no_log: true sur la tâche de récupération du secret ?
  • Pourquoi le fichier application.properties a-t-il les permissions 0600 ?
  • Pourquoi ne jamais stocker ce secret dans le dépôt Git, même dans un fichier de variables Ansible ?

Comparez les deux approches :

# Approche nohup – à ne pas utiliser en production
nohup java -jar demo.jar &

  • Que se passe-t-il avec nohup si l'instance redémarre ?
  • Que se passe-t-il avec nohup si l'application plante ?
  • Comment systemd résout-il ces deux problèmes ?
  • Quelle option du service systemd gère le redémarrage automatique ?

On automatise maintenant le build et le déploiement via GitHub Actions.

Le runner GitHub Actions joue le même rôle que le poste de travail : il compile le jar avec Maven et Java, puis lance Ansible pour déployer sur l'EC2.

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 :

  • Quels secrets GitHub doivent être configurés pour que le pipeline fonctionne ?
  • Pourquoi le runner GitHub Actions ne peut-il pas se connecter à EC2 par défaut ?
  • Quel fichier Ansible permet d'éviter l'erreur de vérification de la clé SSH hôte en CI ?
  • Pourquoi la valeur EC2_IP dans inventory.ini doit-elle être remplacée par une vraie adresse ?

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 AWS
  • AWS_SECRET_ACCESS_KEY : clé secrète correspondante
  • EC2_SSH_KEY : contenu complet du fichier .pem de la clé SSH
  • RDS_HOST : endpoint du RDS récupéré depuis la console AWS

Mettre à 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

Questions sur la sécurité des secrets CI/CD :

  • Pourquoi ne faut-il jamais mettre une clé AWS dans un fichier commité dans Git ?
  • Quelle est la différence entre un secret GitHub et une variable GitHub Actions ?
  • Comment s'assurer que les logs du pipeline n'affichent pas les valeurs des secrets ?

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

  • Que doit retourner la commande systemctl status demo si le déploiement est réussi ?
  • Quelle différence y a-t-il entre accéder à l'application via l'IP EC2 et via l'ALB ?
  • Pourquoi vaut-il mieux exposer uniquement l'ALB et non l'IP de l'instance ?

Objectif de cette section :

  • supprimer le port 22 du Security Group
  • éviter la gestion des clés SSH
  • améliorer la traçabilité des connexions

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.

Comparez les deux méthodes de connexion :

  • Quelle différence y a-t-il entre une session SSH et une session SSM du point de vue réseau ?
  • Où sont enregistrées les sessions SSM ? Comment cela améliore-t-il la traçabilité ?
  • Quel service AWS peut être utilisé pour stocker les logs de sessions SSM ?

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

Réflexion sur la limite de l'architecture actuelle :

  • Le playbook Ansible utilise encore SSH pour se connecter à l'instance. Pourquoi ?
  • Ansible dispose d'un plugin de connexion SSM. Quels seraient les avantages de l'utiliser ?
  • Pourquoi le passage complet à SSM pour Ansible est-il plus complexe qu'un simple changement de configuration ?
  • Dans une architecture sans SSH, quelle alternative à Ansible pourrait être envisagée pour la configuration des instances ?

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 :

  • Quels sont les points faibles restants dans cette architecture ?
  • Si l'instance EC2 est remplacée par un conteneur ECS, quels éléments du pipeline doivent changer ?
  • Comment garantir qu'un déploiement raté ne met pas l'application hors service ?

Mettre en place un déploiement sans interruption de service :

Modifier le playbook Ansible pour :

  • déployer le nouveau jar dans un répertoire horodaté
  • basculer un lien symbolique vers la nouvelle version
  • redémarrer le service uniquement si le jar a changé

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

  • Pourquoi utiliser un lien symbolique facilite-t-il un retour arrière (rollback) ?
  • Comment modifier ce playbook pour conserver uniquement les 3 dernières releases ?
  • Quelle est la différence entre ce déploiement et un vrai Blue/Green sur AWS ?
  • eadl/bloc4/fm4/td4.1782347781.txt.gz
  • Dernière modification : il y a 5 semaines
  • de jcheron