CVE Watch
Cybersécuritédevsecops5 min de lecture

CI/CD avec GitLab : déployer un backend et un frontend en architecture microservices

MB
Massioudath Bankole
13 mai 2026 · 28 vues

Prérequis

Avant de commencer, tu as besoin de :

  • Un compte GitLab
  • Un serveur Linux (Ubuntu/Debian) avec Docker installé
  • Un projet à déployer
  • Un nom de domaine (optionnel mais recommandé)

1. L'architecture

Dans ce guide, on déploie deux microservices indépendants :

  • Un backend Python/FastAPI qui expose une API sur le port 8000
  • Un frontend Next.js qui consomme cette API sur le port 3000

Les deux communiquent via Nginx qui joue le rôle de reverse proxy. Chaque microservice a son propre pipeline CI/CD indépendant dans GitLab.

2. Installer le GitLab Runner

Sur ton serveur :

curl -L https://packages.gitlab.com/install/repositories/runner/gitlab-runner/script.deb.sh | sudo bash
sudo apt-get install gitlab-runner

Si l'installation via apt échoue, télécharge directement le binaire :

sudo curl -L --output /usr/local/bin/gitlab-runner \
  "https://gitlab-runner-downloads.s3.amazonaws.com/latest/binaries/gitlab-runner-linux-amd64"
sudo chmod +x /usr/local/bin/gitlab-runner
sudo useradd --comment 'GitLab Runner' --create-home gitlab-runner --shell /bin/bash
sudo gitlab-runner install --user=gitlab-runner --working-directory=/home/gitlab-runner
sudo gitlab-runner start

3. Enregistrer le Runner sur GitLab

Dans GitLab, va dans Project → Settings → CI/CD → Runners → Create project runner. Donne un tag à ton runner, puis sur le serveur :

sudo gitlab-runner register

Réponds aux questions :

QuestionRéponse
GitLab instance URLhttps://gitlab.com/
Registration tokenglrt-xxxx (depuis GitLab)
Executordocker
Default imagedocker:latest

Choisis toujours docker comme executor. Avec shell, les images spécifiées dans le yml sont ignorées et les jobs s'exécutent directement sur la machine hôte sans isolation.

4. Configurer le Runner pour Docker

Après l'enregistrement, modifie /etc/gitlab-runner/config.toml :

sudo nano /etc/gitlab-runner/config.toml

Dans la section [runners.docker] :

privileged = true
volumes = ["/var/run/docker.sock:/var/run/docker.sock", "/cache"]

privileged = true permet au container d'exécuter des commandes Docker. Le volume docker.sock monte le socket Docker de l'hôte dans le container pour éviter de démarrer un daemon supplémentaire.

sudo gitlab-runner restart

5. Configurer les variables CI/CD

Dans GitLab → Settings → CI/CD → Variables, ajoute les mêmes variables dans chaque projet :

VariableDescriptionOptions
SSH_PRIVATE_KEYClé privée SSH encodée en base64Protected
SERVER_HOSTIP du serveurProtected + Masked
SERVER_USERUtilisateur SSHProtected

Génère et encode la clé SSH sur le serveur :

ssh-keygen -t ed25519 -C "gitlab-ci" -f ~/.ssh/gitlab-ci
cat ~/.ssh/gitlab-ci.pub >> ~/.ssh/authorized_keys
cat ~/.ssh/gitlab-ci | base64 -w 0

L'encodage base64 est indispensable car une clé SSH contient des sauts de ligne qui cassent la variable GitLab si elle est copiée directement.

6. Le pipeline CI/CD du backend Python/FastAPI

variables:
  IMAGE_NAME: $CI_REGISTRY_IMAGE/backend
  IMAGE_TAG: $CI_COMMIT_REF_SLUG

stages:
  - test
  - build
  - deploy

test:
  stage: test
  tags:
    - mon-runner
  image: python:3.11-slim
  script:
    - pip install --no-cache-dir -r requirements.txt
    - python -m py_compile app/main.py
    - echo "Tests OK"
  rules:
    - if: '$CI_PIPELINE_SOURCE == "push"'

build:
  stage: build
  tags:
    - mon-runner
  image: docker:24
  variables:
    DOCKER_HOST: unix:///var/run/docker.sock
    DOCKER_TLS_CERTDIR: ""
  script:
    - docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY
    - docker build -t $IMAGE_NAME:$IMAGE_TAG .
    - |
      if [ "$CI_COMMIT_REF_NAME" == "main" ]; then
        docker tag $IMAGE_NAME:$IMAGE_TAG $IMAGE_NAME:latest
        docker push $IMAGE_NAME:latest
      fi
    - docker push $IMAGE_NAME:$IMAGE_TAG
  needs: ["test"]
  rules:
    - if: '$CI_PIPELINE_SOURCE == "push"'

deploy:
  stage: deploy
  tags:
    - mon-runner
  image: alpine:latest
  script:
    - apk add --no-cache openssh-client
    - mkdir -p ~/.ssh
    - echo "$SSH_PRIVATE_KEY" | base64 -d > ~/.ssh/id_rsa
    - chmod 600 ~/.ssh/id_rsa
    - ssh-keyscan -H $SERVER_HOST >> ~/.ssh/known_hosts
    - |
      ssh $SERVER_USER@$SERVER_HOST "
        docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY &&
        docker pull $IMAGE_NAME:latest &&
        docker stop mon-backend || true ;
        docker rm mon-backend || true ;
        docker run -d \
          --name mon-backend \
          --env-file /home/app/.env \
          -p 8000:8000 \
          --restart always \
          $IMAGE_NAME:latest &&
        sleep 5 &&
        curl -f http://localhost:8000/
      "
  needs: ["build"]
  rules:
    - if: '$CI_COMMIT_REF_NAME == "main"'

7. Le Dockerfile du backend

FROM python:3.11-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
EXPOSE 8000
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"]

8. Le pipeline CI/CD du frontend Next.js

variables:
  IMAGE_NAME: $CI_REGISTRY_IMAGE/frontend
  IMAGE_TAG: $CI_COMMIT_REF_SLUG

stages:
  - test
  - build
  - deploy

test:
  stage: test
  tags:
    - mon-runner
  image: node:20-alpine
  script:
    - npm install
    - npm run lint
  rules:
    - if: '$CI_PIPELINE_SOURCE == "push"'

build:
  stage: build
  tags:
    - mon-runner
  image: docker:24
  variables:
    DOCKER_HOST: unix:///var/run/docker.sock
    DOCKER_TLS_CERTDIR: ""
  script:
    - docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY
    - docker build --build-arg NEXT_PUBLIC_API_URL=https://mondomaine.com/api -t $IMAGE_NAME:$IMAGE_TAG .
    - |
      if [ "$CI_COMMIT_REF_NAME" == "main" ]; then
        docker tag $IMAGE_NAME:$IMAGE_TAG $IMAGE_NAME:latest
        docker push $IMAGE_NAME:latest
      fi
    - docker push $IMAGE_NAME:$IMAGE_TAG
  needs: ["test"]
  rules:
    - if: '$CI_PIPELINE_SOURCE == "push"'

deploy:
  stage: deploy
  tags:
    - mon-runner
  image: alpine:latest
  script:
    - apk add --no-cache openssh-client
    - mkdir -p ~/.ssh
    - echo "$SSH_PRIVATE_KEY" | base64 -d > ~/.ssh/id_rsa
    - chmod 600 ~/.ssh/id_rsa
    - ssh-keyscan -H $SERVER_HOST >> ~/.ssh/known_hosts
    - |
      ssh $SERVER_USER@$SERVER_HOST "
        docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY &&
        docker pull $IMAGE_NAME:latest &&
        docker stop mon-frontend || true ;
        docker rm mon-frontend || true ;
        docker run -d \
          --name mon-frontend \
          -p 3000:3000 \
          --restart always \
          $IMAGE_NAME:latest
      "
  needs: ["build"]
  rules:
    - if: '$CI_COMMIT_REF_NAME == "main"'

9. Le Dockerfile du frontend Next.js

FROM node:20-alpine AS deps
WORKDIR /app
COPY package*.json ./
RUN npm install

FROM node:20-alpine AS builder
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .
ARG NEXT_PUBLIC_API_URL
ENV NEXT_PUBLIC_API_URL=$NEXT_PUBLIC_API_URL
RUN npm run build

FROM node:20-alpine AS runner
WORKDIR /app
ENV NODE_ENV=production
COPY --from=builder /app/.next/standalone ./
COPY --from=builder /app/.next/static ./.next/static
COPY --from=builder /app/public ./public
EXPOSE 3000
CMD ["node", "server.js"]

Et dans next.config.ts :

const nextConfig: NextConfig = {
  output: "standalone",
};

10. Communication entre les deux microservices via Nginx

Le frontend et le backend tournent dans deux containers séparés sur le même serveur. Ils communiquent via Nginx qui joue le rôle de reverse proxy.

server {
    listen 80 default_server;
    server_name _;
    return 444;
}

server {
    listen 80;
    server_name mondomaine.com www.mondomaine.com;

    location / {
        proxy_pass http://localhost:3000;
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection 'upgrade';
        proxy_set_header Host $host;
        proxy_cache_bypass $http_upgrade;
    }

    location /api/ {
        proxy_pass http://localhost:8000/;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
    }
}

Toute requête vers mondomaine.com/ est redirigée vers le frontend. Toute requête vers mondomaine.com/api/ est redirigée vers le backend.

Du côté du frontend, la variable NEXT_PUBLIC_API_URL est définie à la compilation avec --build-arg. Les variables NEXT_PUBLIC_* dans Next.js sont intégrées au moment du build et non à l'exécution. Sans cette variable, le frontend continuerait à appeler localhost:8000 au lieu du vrai domaine en production.

11. Résultat

Une fois tout configuré, le workflow devient :

  1. Tu modifies le code d'un microservice et tu fais git push origin main
  2. GitLab déclenche le pipeline de ce microservice uniquement
  3. Le Runner exécute les tests
  4. Si les tests passent, l'image Docker est construite et poussée vers le Registry
  5. Le serveur télécharge la nouvelle image et redémarre le container
  6. Le microservice est mis à jour en production sans toucher à l'autre

Conclusion

Cette architecture permet de faire évoluer chaque microservice indépendamment. Le backend peut être mis à jour sans redéployer le frontend et vice versa. C'est l'un des principaux avantages de l'architecture microservices par rapport à une application monolithique.

devsecops
Partager cet article

Commentaires (0)

Sois le premier à commenter !

Articles similaires