← Back to list

Pipeline CI/CD pour une architecture microservices : API Flask -passerelle Node.js-

Déployer une application à la main, c’est lent, répétitif et truffé d’occasions de se tromper. Un oubli, une mauvaise version copiée, et le…

Manuel FOUDA · 2026-06-14 18:17 · 0 claps · 7.4 min read
#devsecops-services #cicd #gitlab #nodejs #api
Open on Medium ↗
Wiki topics: 🌐 · Web Development ☁️ · DevOps & Cloud 🔓 · Open Source 🏛️ · Architecture

Pipeline CI/CD pour une architecture microservices : API Flask -passerelle Node.js- Reverse proxy Nginx

Architecture complète

Architecture complète

Déployer une application à la main, c’est lent, répétitif et truffé d’occasions de se tromper. Un oubli, une mauvaise version copiée, et le serveur tombe. J’ai voulu supprimer ce risque en automatisant tout : à chaque git push, le code est construit, testé, publié puis déployé, sans que je touche au serveur.

Dans cet article, je détaille l’architecture que j’ai choisie, le pipeline GitLab CI qui se charge d’automatiser, et surtout les trois bugs bien réels sur lesquels je me suis cogné — parce que c’est en les résolvant que j’ai le plus appris.

L’architecture en un coup d’œil

Architecture

Architecture

L’application est volontairement simple (une mini-API de citations), car l’intérêt du projet n’est pas l’appli en elle-même mais la chaîne qui la déploie. Elle est composée de trois services :

  • Flask — l’API métier en Python, qui sert les données.
  • Node.js (Express) — une passerelle qui sert la page et appelle Flask.
  • Nginx — un reverse proxy, point d’entrée unique de tout le système.

Pourquoi un reverse proxy ?

Sans Nginx, il faudrait exposer les ports de Node et de Flask vers l’extérieur : deux portes d’entrée à surveiller. Avec Nginx devant, un seul port est ouvert sur la machine : le 80. Node et Flask deviennent invisibles depuis Internet ; ils ne sont joignables que par Nginx, sur le réseau interne de Docker.

Conteneuriser chaque service avec Docker

Chaque service a son propre Dockerfile. Pour Flask, on copie les dépendances avant le code, afin de profiter du cache de couches Docker (les dépendances ne sont réinstallées que si requirements.txt change) :

FROM python:3.12-slim

WORKDIR /app

COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

COPY app.py .

EXPOSE 5000

CMD ["gunicorn", "--bind", "0.0.0.0:5000", "--workers", "2", "app:app"]

Le Dockerfile de Node ensuite. On copie d’abord lepackage*.json, puis le code, en utilisant la version d’image alpine, beaucoup plus légère.

FROM node:20-alpine

WORKDIR /app

COPY package*.json ./
RUN npm install

# Code + tests
COPY src ./src
COPY tests ./tests

EXPOSE 3000

CMD ["npm", "start"]

Pour Nginx, pas besoin de créer son Dockerfile car on n’a pas à reconstruire son image, on va juste la télécharger depuis DockerHub durant chaque tache où on en aura besoin.

Orchestrer avec Docker-compose

Le docker-compose.yml décrit les trois conteneurs et le réseau qui les relie.

services:
  nginx:
    image: nginx:alpine
    ports:
      - "80:80"                 # Le seul port qui sera expose dans l'hote EC2
    volumes:
      - /etc/nginx/:/etc/nginx/
    depends_on: [node, flask]
    networks: [app-net]
    restart: unless-stopped

  node:
    image: manuelkevin4real/node-api:latest
    environment:
      - FLASK_URL=http://flask:5000
    expose: ["3000"]            # interne seulement 
    depends_on: [flask]
    networks: [app-net]
    restart: unless-stopped

  flask:
    image: manuelkevin4real/api-flask:latest
    expose: ["5000"]            # interne seulement
    networks: [app-net]
    restart: unless-stopped

networks:
  app-net:
    driver: bridge

Les services Flask et Node ne sont exposés qu’en interne, contrairement au service Nginx qui sera exposé dans l’hôte EC2 (mapping 80:80).

Les 3 services communiquent entre eux grâce au type de réseau Bridge dans Docker (communication entre conteneurs).

Côté Nginx, le routage par URL tient en quelques lignes. Un aperçu du fichier nginx.conf :

events {}

http {
  # Cible les services Flask et Node 
  upstream node_gateway { server node:3000; }
  upstream flask_api    { server flask:5000; }

  server {

    listen 80;  # Nginx ecoute sur le port 80
      # Tout ce qui commence par /api/ passe part Flask 
    location /api/ {
      proxy_pass http://flask_api/;
      proxy_set_header Host       $host;
      proxy_set_header X-Real-IP  $remote_addr;
    }

    # tout le reste va vers Node
    location / {
      proxy_pass http://node_gateway;
      proxy_set_header Host       $host;
      proxy_set_header X-Real-IP  $remote_addr;
    }
  }
}

La pipeline CI/CD : le cœur du projet

Tout se joue dans le .gitlab-ci.yml, organisé en trois stages ( grandes étapes ) qui s'enchaînent comme suit :

  • Build-test : Ce stage est constitué de 2 jobs, qui sont en faite des blocs de taches qui s’exécuteront en parallèle tout au long du stage. Ici, chaque job va construire l’image de chaque service ( Flask et Node ), lancer chacun un conteneur à partir de ces images afin d’y effectuer à l’intérieur leurs tests respectifs.
  • Push : Etant donné que les stages sont exécutés de manière séquentielle, si les tests de l’étape Build-test sont bons, alors nous allons charger les images construites déjà, les tagguer, puis les pousser vers DockerHub. Ces étapes seront également divisées en 2 jobs.
  • Deploy : Ici, nous utiliserons un seul job. Contrairement aux autres étapes, on commence avec le before-script qui est en fait un script qui s’exécute avant même de passer au script du stage. On va installer un client ssh (Openssh en occurrence) dans le runner gitlab (la machine qui effectue le job), on charge la clé SSH dans le runner et on se connecte à l’instance EC2 grâce à elle. Dans le script lui même, on exécute les taches ci-après : — Copier les fichiers nginx.conf et docker-compose.yml dans EC2 grâce au protocole SCP ; — Déplacer ces fichiers dans les répertoires appropriés et y configurant les permissions nécessaires pour les exécuter ; — Se connecter à Dockerhub pour télécharger les images des services Flask et Node ; —Lancer simultanément tous les services avec les commandes de Docker compose.

Un aperçu du fichier .gitlab-ci.yml :

stages:
  - build-test
  - push
  - deploy

variables:
    IMAGE_NODE: "image-node:$CI_COMMIT_SHORT_SHA"
    IMAGE_FLK: "image-flk:$CI_COMMIT_SHORT_SHA"

build-flask:
    stage: build-test
    image: docker:27
    services:
    - docker:27-dind
    variables:
        DOCKER_TLS_CERTDIR: "/certs"
    script:
        - docker build -t "$IMAGE_FLK" ./flask-api
        - docker run --rm "$IMAGE_FLK" pytest -q 
        - docker save "$IMAGE_FLK" -o image-flask.tar
    artifacts:
        paths:
            - image-flask.tar
        when: on_success
        expire_in: 30 days

build-node:
    stage: build-test
    image: docker:27
    services:
    - docker:27-dind
    variables:
        DOCKER_TLS_CERTDIR: "/certs"
    script:
        - docker build -t "$IMAGE_NODE" ./node-gateway
        - docker run --rm "$IMAGE_NODE" npm test
        - docker save "$IMAGE_NODE" -o image-node.tar
    artifacts:
        paths:
            - image-node.tar
        when: on_success
        expire_in: 30 days

push-flask:
    stage: push
    image: docker:27
    services:
    - docker:27-dind
    variables:
      DOCKER_TLS_CERTDIR: "/certs"
    script:
        - docker load -i image-flask.tar
        - docker login -u "$DEPLOY_TOKEN_USER" -p "$DEPLOY_TOKEN_PASSWORD"
        - docker tag "$IMAGE_FLK" "manuelkevin4real/api-flask:latest"
        - docker push manuelkevin4real/api-flask:latest
    only:
    - main

push-node:
    stage: push
    image: docker:27
    services:
    - docker:27-dind
    variables:
      DOCKER_TLS_CERTDIR: "/certs"
    script:
        - docker load -i image-node.tar
        - docker login -u "$DEPLOY_TOKEN_USER" -p "$DEPLOY_TOKEN_PASSWORD"
        - docker tag "$IMAGE_NODE" "manuelkevin4real/node-api:latest"
        - docker push manuelkevin4real/node-api:latest
    only:
    - main

deploy-ec2:
    stage: deploy
    image: alpine:3.19
    when: manual
    before_script:
        - apk add --no-cache openssh-client # installer ssh
        - eval $(ssh-agent -s) #demarrer l'agent ssh
        - echo "$SSH_PRIVATE_KEY" | tr -d '\r' | ssh-add - #charge la cle privee dans l'agent
        - mkdir -p ~/.ssh && chmod 700 ~/.ssh #cree le dossier .ssh et restreint les permissions 
        # Ajouter l'empreinte de l'hôte pour éviter le prompt de confirmation
        - ssh-keyscan "$EC2_HOST" >> ~/.ssh/known_hosts 2>/dev/null # Ajouter l'empreinte de l'hôte 
    script:
        - |
            ssh "ubuntu@$EC2_HOST" "
              set -e

              "
        - scp nginx/nginx.conf "ubuntu@$EC2_HOST:/home/ubuntu/"
        - scp docker-compose.yml "ubuntu@$EC2_HOST:/home/ubuntu/"
        - |
            ssh "ubuntu@$EC2_HOST" "
            set -e
            sudo mv /home/ubuntu/nginx.conf /etc/nginx/
            sudo mv /home/ubuntu/docker-compose.yml /etc/
            sudo chown root:root /etc/nginx/nginx.conf
            sudo chown root:root /etc/docker-compose.yml
            cd /etc/
            echo pwd
            docker login -u '$DEPLOY_TOKEN_USER' -p '$DEPLOY_TOKEN_PASSWORD'
            docker pull manuelkevin4real/node-api:latest
            docker pull manuelkevin4real/api-flask:latest
            docker compose down || true 
            docker compose pull && docker compose up -d
            " 

    only:
        - main

NB: Les variables environnement sensibles c’est-à-dire les secrets comme les clés SSH, les crédentials, les tokens, clés API sont à configurer au préalable dans GitLab CI > Settings > CI/CD > variables. Cela permet de masquer et protéger ces secrets dans les logs.

L’environnement de destination : AWS EC2

C’est ici que le projet sera déployé au final. Pour cela, il est nécessaire de le configurer en accordance avec notre application.

Pour ce projet, j’ai choisi une image Ubuntu, dans laquelle on installera Docker et Docker-compose. Deux solutions se présentent : soit les installer manuellement ( apt-get update && apt-get install Docker Docker-compose), soit utiliser un script bash user-data ( un script que vous même configurez, qui va s’exécuter lors de la création de votre instance EC2 dans AWS).

Les 2 erreurs qui m’ont le plus appris

Voici la partie que je voulais vraiment écrire. Aucun tutoriel ne montre ça, parce que tout le monde affiche un chemin parfait. Le mien ne l’était pas.

Erreur 1 — Le port 80 était déjà utilisé dans l’instance.

En testant le fonctionnement des services, je me suis rendu compte que le port 80 était déjà utilisé dans l’instance car pour des raisons de tests, j’ai installé une première fois Nginx dans cette instance. Or il faut savoir que Docker-compose up lance des conteneurs embarqués avec leur propre configuration et leurs ports. Par conséquent, je me suis retrouvé avec Nginx dans mon instance connecté au port 80 et le service Nginx de mon docker-compose qui exposait également son service au port 80 de l’instance EC2. D'où la collision des services. En ce qui vous concerne, vous ne le rencontrerez pas en faisant ce tutoriel mais juste pour vous tenir avisé du fait de se rassurer de n’avoir qu’un seul service exposé vers le port 80 de votre instance.

Erreur 2 — Le Docker compose down nettoyait la mauvaise stack

Dans l’optique d’arrêter et remplacer les images modifiées lors de chaque commit dans l’instance EC2, il s’avère que j’avais un problème de répertoire, c’est-a-dire, la commande Docker-compose up /down ne s’ exécute que dans le répertoire dans lequel est présent le fichier Docker-compose.yml or dans mon cas, j’avais ce fichier présent dans deux répertoires ( encore pour des raisons de tests mais important). Ce qui démarrait et stoppait le mauvais service. Alors ce qu’il faut faire c’est créer un répertoire unique ou sera copié le fichier Docker-compose.yml .

Dans ce cas, la solution de déploiement est plus complexe car Docker-compose ne prend pas en compte les autres types de déploiement (Canary, rolling update,…) comme on en a avec d’autres solutions plus adéquates comme Kubernetes.

Le résultat

A la fin, on obtient une architecture complète automatisée du Commit au cloud, en passant par le pipeline CI/CD. Ce projet m’a permis d’asseoir mes connaissances sur GitLab CI, les pipelines CI/CD et leur fonctionnement, le réseau grâce au reverse proxy sur Nginx, les API REST avec Node.js et Python Flask.

Interface principale

Interface principale

Interface Pipeline CI/CD dans GitLab CI

Interface Pipeline CI/CD dans GitLab CI

Le repo dans GitLab CI

Le repo dans GitLab CI

Lien du dépôt GitLab : https://gitlab.com/ManuelKevin4real/projet-node-flask

Merci pour votre attention !


메타데이터
post_id
9fb9f65d2cff
slug
pipeline-ci-cd-pour-une-architecture-microservices-api-flask-passerelle-node-js-9fb9f65d2cff
url
https://medium.com/@manuelkevin4real/pipeline-ci-cd-pour-une-architecture-microservices-api-flask-passerelle-node-js-9fb9f65d2cff
canonical_url
https://medium.com/@manuelkevin4real/pipeline-ci-cd-pour-une-architecture-microservices-api-flask-passerelle-node-js-9fb9f65d2cff
author_url
https://medium.com/@manuelkevin4real
status
ok
fetched_at
2026-06-15 20:49:13