Aller au contenu

GitLab CI/CD

Version française
Back to index


Pipeline overview

push / merge request
        │
        ├── stage: test
        │     ├── test:unit               (Jest tests)
        │     ├── test:quality:sonarqube  (SonarQube quality analysis)
        │     └── test:build:docker       (partial build on MR)
        │
        ├── stage: containerize
        │     └── package:image           (build + push Docker image)
        │
        └── stage: deploy
              ├── deploy:int:ansible      (staging, automatic)
              ├── deploy:dev:ansible      (dev, automatic)
              └── deploy:prod:manual      (production, ⚠️ MANUAL)

Trigger by branch

Branch Tests Build image Deploy
main ✅ (tag main) ⚠️ manual → prod
tag v1.x.x ✅ (tag v1.x.x) ⚠️ manual → prod
develop ✅ (tag develop) Auto → staging
test-ci ✅ (tag test-ci) Auto → staging
qa ✅ (tag qa)
dev ✅ (tag dev) Auto → dev
Merge Request ✅ (partial build)
Other branch

Stages

Stage: test

test:unit

test:unit:
  image: node:20-alpine
  stage: test
  cache:
    key:
      files: [package-lock.json]
    paths: [.npm/]
  before_script:
    - npm ci --cache .npm --prefer-offline
  script:
    - npm test
  rules:
    - if: main/develop/qa/test-ci or tag
    - if: merge_request_event

Runs npm test (Jest). The npm cache speeds up subsequent runs.

test:quality:sonarqube

test:quality:sonarqube:
  image: sonarsource/sonar-scanner-cli:11
  stage: test
  script:
    - sonar-scanner -Dsonar.host.url="${SONAR_HOST_URL}"
  allow_failure: true  # Does not block the pipeline on failure
  rules:
    - if: main/develop/qa/test-ci or tag

Static code analysis. allow_failure: true: a SonarQube failure does not block deployment.

test:build:docker

test:build:docker:
  stage: test
  script:
    - docker build --target builder .
  rules:
    - if: merge_request_event  # MRs only

Partial build (stage builder) to validate that the Dockerfile is correct. Does not produce a final image.


Stage: containerize

package:image

package:image:
  stage: containerize
  needs: []  # Parallel with tests
  environment:
    name: $DEPLOY_ENV
  script:
    - docker build --pull --tag ${TARGET_IMAGE} .
    - docker push ${TARGET_IMAGE}
  rules:
    - if: develop/qa/test-ci → DEPLOY_ENV=staging
    - if: dev → DEPLOY_ENV=development
    - if: main or tag → DEPLOY_ENV=production

Image tag construction:

IMAGE_TAG = CI_COMMIT_TAG (if git tag) OR CI_COMMIT_REF_SLUG (branch name)
TARGET_IMAGE = ${DOCKER_REG_URL}/${CI_PROJECT_PATH}:${IMAGE_TAG}

Examples:

registry.id2real.net/racines/racines-app:develop
registry.id2real.net/racines/racines-app:main
registry.id2real.net/racines/racines-app:v1.2.3

No --build-arg AUDIO_CDN_URL: the CDN URL is injected at runtime via Docker environment variable. A single image serves all environments.


Stage: deploy

deploy:int:ansible (staging)

deploy:int:ansible:
  stage: deploy
  environment:
    name: staging
  needs:
    - package:image
  script:
    - chmod 400 $SSHKEY_CI
    - ansible-playbook -i ansible/inventory.yaml -l integration \
        --private-key $SSHKEY_CI ansible/deployment_playbook.yaml
  rules:
    - if: develop or test-ci → automatic

deploy:dev:ansible (dev)

deploy:dev:ansible:
  stage: deploy
  environment:
    name: development
  needs:
    - package:image
  script:
    - chmod 400 $SSHKEY_DEV
    - ansible-playbook -i ansible/inventory.yaml -l dev \
        --private-key $SSHKEY_DEV ansible/deployment_playbook.yaml
  rules:
    - if: dev → automatic

deploy:prod:manual (production)

deploy:prod:manual:
  stage: deploy
  environment:
    name: production
  when: manual  # ⚠️ Does not trigger automatically
  needs:
    - package:image
  script:
    - chmod 400 $DEPLOY_SSHKEY_FILE
    - ansible-playbook -i ansible/inventory.yaml -l prod \
        --private-key $DEPLOY_SSHKEY_FILE ansible/deployment_playbook.yaml
  rules:
    - if: main or tag → available manually

Trigger:

GitLab → CI/CD → Pipelines → [pipeline on main] → deploy:prod:manual → ▶

Ansible playbook

The ansible/deployment_playbook.yaml playbook executes on the target server:

# 1. Create the deployment directory
- file: path=/docker-apps/racines state=directory

# 2. Copy the environment's docker-compose file
- copy: src=ansible/docker-compose.{env}.yaml dest=/docker-apps/racines/docker-compose.yml

# 3. Stop the current application
- shell: docker compose -f /docker-apps/racines/docker-compose.yml down

# 4. Pull the new image and restart
- shell: |
    docker compose -f /docker-apps/racines/docker-compose.yml pull
    docker compose -f /docker-apps/racines/docker-compose.yml up -d --force-recreate

The inventory (ansible/inventory.yaml) defines the hosts: - integration → staging server (SSH port 9257, user ci_gitlab_deploy) - dev → development server - prod → production server


CI/CD variables

To define in GitLab → Settings → CI/CD → Variables:

Variable Type Description Environments
DOCKER_REG_URL Variable Docker registry URL All
DOCKER_REG_LOGIN Variable Registry login All
DOCKER_REG_PASS Masked Registry password All
SSHKEY_CI File SSH key → staging CI
SSHKEY_DEV File SSH key → dev Dev
DEPLOY_SSHKEY_FILE File SSH key → production Prod
SONAR_HOST_URL Variable SonarQube URL All

No AUDIO_CDN_URL variable in CI/CD variables — this variable is in the docker-compose.yml of each environment on the server, not in the pipeline.


Configuration files per environment

File Environment Content
ansible/docker-compose.integration.yaml Staging Compose with staging URLs
ansible/docker-compose.dev.yaml Dev Compose with dev URLs
docker-compose.yml Local dev Compose for local development

Each file contains the environment-specific variables (notably AUDIO_CDN_URL).


Rollback

Via GitLab pipeline

GitLab → CI/CD → Pipelines → [previous pipeline on main] → deploy:prod:manual → ▶

The previous pipeline is still available and its image is in the registry.

Manual on the server

cd /docker-apps/racines

# Identify the previous version
docker images | grep racines-app

# Force a previous image
docker compose pull app
# Modify docker-compose.yml to point to the old tag
docker compose up -d --force-recreate app

Typical durations

Stage Estimated duration
test:unit 1–2 min
test:quality:sonarqube 2–4 min
package:image 4–8 min (npm cache)
deploy:int:ansible 1–3 min
Total push → staging ~8–15 min

Diagnostics

test:unit job fails

# Reproduce locally
npm test
# or
npm test -- --verbose

package:image job fails on registry connection

# Check DOCKER_REG_* variables in GitLab CI/CD Settings
# Test manually:
docker login registry.id2real.net -u USER -p PASS

Ansible deployment fails on SSH

# Verify the SSH key is correctly configured in the File variables
# Test SSH access from the GitLab runner:
ssh -i $SSHKEY_CI ci_gitlab_deploy@staging-server -p 9257 "echo ok"

Deployed image is not the right version

# On the server:
docker inspect racines-app | grep Image
# Compare with the expected tag in the registry

Next steps