Skip to content

06 — GITLAB CE SELF-HOSTED + RUNNER SUR AWS

Objectif

A la fin de ce fichier :

  • GitLab CE self-hosted tourne sur une instance EC2 dediee dans notre VPC
  • Un GitLab Runner auto-heberge tourne sur des instances EC2 Spot separees
  • Les jobs CI/CD s'executent via Docker executor avec cache S3 partage
  • Le runner est dimensionne pour 3 developpeurs et 11 microservices
  • L'infra est provisionnee par Terraform, coherente avec les modules existants

Pre-requis : fichiers 01_infra_base.md et 02_cicd_et_services.md completes, cluster EKS operationnel.


Contexte

Migration depuis GitLab SaaS (gitlab.com) vers GitLab CE self-hosted (gitlab.paywithnex.com) realisee en mars 2026. Motivations :

  • Cout Premium GitLab SaaS (29 USD/user/mois) remplace par une instance EC2 (~88 USD/mois fixe)
  • Controle total sur les donnees, la configuration, et les runners
  • Pas de quota de minutes CI
  • Cache persistant entre pipelines via S3
  • Registry Docker integre sur notre infra

Architecture deployee

                    Internet / Cloudflare (zone paywithnex.com)
                         |
         +---------------+----------------+
         |               |                |
   gitlab.paywithnex.com  registry.paywithnex.com  ssh-gitlab.paywithnex.com
   (proxied, HTTPS)       (proxied, HTTPS)          (DNS only, port 22)
         |               |                |
         +-------+-------+                |
                 |                         |
                 v                         v
    +---------------------------+
    |  EC2: GitLab CE           |   Subnet public (10.0.1.0/24)
    |  i-0e9ac36ca2e71331c      |   Elastic IP: 51.44.133.121
    |  t3a.xlarge (4 vCPU/16GB) |   200 GB gp3 EBS
    |  Ubuntu 22.04 + Omnibus   |
    +---------------------------+
         |          |         |
         v          v         v
    +--------+  +-------+  +------------------+
    | RDS PG |  | Redis |  | S3               |
    | (ext.) |  | (bun.)|  | registry bucket  |
    +--------+  +-------+  | backups bucket   |
                            +------------------+

    +---------------------------+
    |  EC2: GitLab Runner       |   Subnets prives (10.0.10.0/24, 10.0.20.0/24)
    |  ASG: nex-staging-runner  |   Pas d'IP publique
    |  t3a.large Spot (2v/8GB)  |   Sortie via NAT Gateway
    |  Amazon Linux 2023        |
    |  Docker executor          |
    +---------------------------+
         |
         +--> S3 cache bucket (nex-staging-gitlab-runner-cache)
         +--> gitlab.paywithnex.com (polling HTTPS, git clone)
         +--> registry.paywithnex.com (push/pull images Docker)
         +--> npm registry (pnpm install)

Composants principaux

ComposantInstanceTypeSubnetDetails
GitLab CEi-0e9ac36ca2e71331ct3a.xlargePublicOmnibus, EIP 51.44.133.121
GitLab RunnerASG nex-staging-gitlab-runnert3a.large SpotPriveDocker executor, S3 cache
RDS PostgreSQLExternedb.t3.smallDataBase gitlab sur RDS staging
RedisBundled OmnibusSession/cache GitLab
Container RegistryS3-backedBucket nex-staging-gitlab-registry
BackupsS3Bucket nex-staging-backups/gitlab/, cron 02:00 UTC

DNS & TLS (Cloudflare)

RecordCibleProxyUsage
gitlab.paywithnex.com51.44.133.121ProxiedUI web + API GitLab
registry.paywithnex.com51.44.133.121ProxiedContainer Registry
ssh-gitlab.paywithnex.com51.44.133.121DNS onlyGit SSH (port 22)
  • SSL mode : Full (strict) — Cloudflare Origin CA (wildcard *.paywithnex.com, 15 ans)
  • TLS cert/key : AWS Secrets Manager
  • SMTP : Brevo relay (smtp-relay.brevo.com:587)

Mirror depuis GitLab SaaS

Le repo a ete mirror depuis gitlab.com/nxpay/nex le 2026-03-18 :

  • 33 branches poussees
  • 5 MR ouvertes recrees manuellement via l'API
  • Remote self-hosted configure en local (git@ssh-gitlab.paywithnex.com:nxpay/nex.git)
  • L'ancien remote origin (gitlab.com) est conserve pendant la transition

Branches protegees

BrancheMergePushForce pushPipeline requis
mainMaintainersMaintainersNonOui
developMaintainersMaintainersNonOui

GitLab CE — Module Terraform (modules/gitlab-ce/)

Fichiers

infrastructure/terraform/modules/gitlab-ce/
  main.tf           # EC2, EIP, DLM snapshots
  security_group.tf # SG (SSH, HTTP, HTTPS ingress)
  iam.tf            # Role IAM (S3, CloudWatch, Secrets Manager, SSM)
  s3.tf             # Bucket registry
  userdata.sh       # Bootstrap Omnibus
  variables.tf
  outputs.tf
  README.md         # Procedures operationnelles

Configuration Omnibus (via userdata)

  • PostgreSQL : connexion externe vers RDS staging (base gitlab)
  • Redis : bundled (local Omnibus)
  • Container Registry : S3-backed (nex-staging-gitlab-registry)
  • Backups : cron quotidien vers nex-staging-backups/gitlab/
  • CloudWatch : logs rails, nginx-access, nginx-error, system
  • Snapshots EBS : DLM daily, retention 14 jours

Acces

  • UI : https://gitlab.paywithnex.com
  • SSH : git@ssh-gitlab.paywithnex.com:nxpay/nex.git
  • API : https://gitlab.paywithnex.com/api/v4/
  • SSM : aws ssm start-session --target i-0e9ac36ca2e71331c
  • Admin PAT : Doppler paywithnex/stgGITLAB_ADMIN_PAT (expire 2027-03-18)

Configuration SSH pour les developpeurs

Important : le domaine SSH est ssh-gitlab.paywithnex.com, PAS gitlab.paywithnex.com.

Cloudflare proxy (plan Free) ne supporte que les ports HTTP/HTTPS (80/443). Le port 22 (SSH) ne peut pas passer par le proxy. C'est pourquoi on a un sous-domaine dedie en mode "DNS only" (pas proxie) qui pointe directement sur l'IP de l'instance GitLab.

  • gitlab.paywithnex.com → Cloudflare proxied → HTTPS uniquement (web UI, API, git clone HTTPS)
  • ssh-gitlab.paywithnex.com → DNS only (pas proxie) → port 22 (git push/pull SSH)

Chaque developpeur doit ajouter dans ~/.ssh/config :

Host ssh-gitlab.paywithnex.com
  HostName ssh-gitlab.paywithnex.com
  User git
  IdentityFile ~/.ssh/id_ed25519   # adapter au nom de la cle

Puis cloner avec :

bash
git clone git@ssh-gitlab.paywithnex.com:nxpay/nex.git

Verification :

bash
ssh -T git@ssh-gitlab.paywithnex.com
# Attendu : "Welcome to GitLab, @username!"

GitLab Runner — Module Terraform (modules/gitlab-runner/)

Fichiers

infrastructure/terraform/modules/gitlab-runner/
  main.tf           # ASG, Launch Template, S3 cache bucket
  iam.tf            # Role IAM (S3, CloudWatch, Secrets Manager, SSM, ASG lifecycle)
  security_group.tf # SG egress-only (HTTPS, HTTP, DNS)
  userdata.sh       # Bootstrap Docker + GitLab Runner + CloudWatch
  variables.tf
  outputs.tf

Configuration deployee (staging)

ParametreValeur
ASGnex-staging-gitlab-runner
Instance typet3a.large (2 vCPU, 8 GB RAM)
Mode100% Spot (on_demand_base_capacity = 0)
Spot typest3a.large, t3.large
Min/Max/Desired1 / 3 / 1
Concurrent jobs2 par instance
AMIAmazon Linux 2023
SubnetPrives (10.0.10.0/24, 10.0.20.0/24)
Volume100 GB gp3
CacheS3 nex-staging-gitlab-runner-cache (expiration 14j)
ScalingTarget tracking CPU 60%

Note : le runner est en 100% Spot car la limite vCPU on-demand (16) est deja consommee par EKS (5x t3a.medium = 10) + GitLab CE (t3a.xlarge = 4) + headroom. En cas d'interruption Spot, le graceful shutdown desenregistre le runner et l'ASG relance automatiquement.

Security Group

  • Ingress : aucun (le runner poll GitLab, pas l'inverse)
  • Egress : HTTPS (443), HTTP (80), DNS (53 TCP/UDP) vers 0.0.0.0/0

IAM

Role nex-staging-gitlab-runner avec :

  • gitlab-runner-s3-cache : acces au bucket cache S3
  • gitlab-runner-logs : ecriture CloudWatch /nex/gitlab-runner/*
  • gitlab-runner-secrets : lecture Secrets Manager nex/gitlab-runner/*
  • gitlab-runner-asg-lifecycle : completion des lifecycle hooks ASG
  • AmazonSSMManagedInstanceCore : acces SSM (pas de SSH)

Configuration runner (/etc/gitlab-runner/config.toml)

toml
concurrent = 2
check_interval = 0
shutdown_timeout = 0

[session_server]
  session_timeout = 1800

[[runners]]
  name = "nex-aws-runner-{hostname}"
  url = "https://gitlab.paywithnex.com"
  token = "{from-secrets-manager}"
  executor = "docker"
  [runners.docker]
    image = "node:20-alpine"
    privileged = true
    pull_policy = ["if-not-present"]
    volumes = ["/cache", "/var/run/docker.sock:/var/run/docker.sock"]
    memory = "6g"
    cpus = "1.5"
  [runners.cache]
    Type = "s3"
    Shared = true
    [runners.cache.s3]
      BucketName = "nex-staging-gitlab-runner-cache"
      BucketLocation = "eu-west-3"

Notes :

  • privileged = true necessaire pour Docker-in-Docker (build d'images dans les jobs CI)
  • Le montage de docker.sock evite d'utiliser le service docker:dind (plus rapide)
  • pull_policy: if-not-present garde les images en cache local sur l'instance

Token d'enregistrement

  • Secret : nex/gitlab-runner/registration-token dans AWS Secrets Manager
  • ARN : arn:aws:secretsmanager:eu-west-3:009001720821:secret:nex/gitlab-runner/registration-token-Hd9rhS
  • Le token est genere depuis GitLab CE (Admin > CI/CD > Runners > New instance runner)
  • Il ne doit JAMAIS etre dans le code Terraform ni dans le repo

Graceful Shutdown

  1. Lifecycle hook termination-wait : 600s timeout sur EC2_INSTANCE_TERMINATING
  2. Spot interruption handler : poll metadata endpoint toutes les 5s
  3. Quand terminaison detectee :
    • gitlab-runner stop — arrete d'accepter de nouveaux jobs
    • Attend fin des jobs en cours (max 5 min)
    • gitlab-runner unregister --all-runners
    • Signal CONTINUE au lifecycle hook

Nettoyage Docker (anti-saturation disque)

Le build des 11 services accumule images, couches et cache Docker sur le volume EBS local (le runner utilise le docker.sock de l'hote, pas DinD). Sans purge, le disque sature et tous les jobs tombent en runner_system_failure / no space left on device (incident 2026-05 : disque 50 GB sature a 100%, deploys bloques, agent SSM et cloud-init eux-memes bloques).

Deux garde-fous :

  1. Volume 100 GB (etait 50 GB) — marge pour les builds entre deux purges.
  2. Timer systemd docker-cleanup toutes les 6h : docker system prune -af --volumes --filter until=4h (le filtre until=4h protege tout ce qui a servi recemment / les jobs en cours).

Important : c'est un timer systemd, PAS un cron. Amazon Linux 2023 n'embarque pas de cron par defaut (le dossier /etc/cron.d n'existe pas). Une ancienne version du userdata.sh ecrivait dans /etc/cron.d, ce qui faisait echouer la commande et — via set -eavortait tout le bootstrap : le nettoyage n'etait jamais installe (et cloud-init status finissait en error). Toujours utiliser un timer systemd pour les taches periodiques sur AL2023.

Observabilite

Log GroupSourceRetention
/nex/gitlab-runner/runner/var/log/gitlab-runner/runner.log30 jours
/nex/gitlab-runner/system/var/log/messages14 jours

Instanciation staging (environments/staging/main.tf)

hcl
module "gitlab_runner" {
  source = "../../modules/gitlab-runner"

  environment            = local.environment
  vpc_id                 = module.vpc.vpc_id
  private_subnet_ids     = module.vpc.private_subnet_ids
  runner_token_secret_arn = "arn:aws:secretsmanager:${var.aws_region}:009001720821:secret:nex/gitlab-runner/registration-token-Hd9rhS"

  instance_type     = "t3a.large"
  spot_types        = ["t3a.large", "t3.large"]
  node_min          = 1
  node_max          = 3
  node_desired      = 1
  concurrent_jobs   = 2
  cache_expiry_days = 14
  root_volume_size  = 100
}

Pipeline CI/CD

Tags runner

Tous les jobs lourds dans infrastructure/cicd/_templates.yml portent le tag self-hosted :

  • .install-base — pnpm install
  • .test-base — unit/integration tests
  • .build-base — Docker build + push vers GitLab Registry
  • .security-base — Trivy security scanning
  • .deploy-k8s-base — kubectl deployments

Cache S3

yaml
cache:
  key:
    files: [pnpm-lock.yaml]
  paths: [.pnpm-store/]
  policy: pull-push  # pull-only pour les jobs test

Cle basee sur le contenu du lockfile — le cache est partage entre toutes les branches tant que le lockfile ne change pas.

Docker build

Le runner utilise le Docker socket de l'hote (/var/run/docker.sock) au lieu de docker:dind. Avantages :

  • L'image node:20-alpine n'est telechargee qu'une fois (cache local)
  • Les layers Docker intermediaires sont cachees sur le volume EBS
  • Plus rapide que DinD

Validation

bash
# Verifier l'instance runner
aws ec2 describe-instances \
  --filters "Name=tag:Name,Values=*gitlab-runner*" \
  --query "Reservations[].Instances[].{ID:InstanceId,State:State.Name,Type:InstanceType}" \
  --region eu-west-3

# Verifier le runner dans GitLab
# https://gitlab.paywithnex.com/admin/runners
# Le runner "nex-aws-runner-*" doit etre "online"

# Verifier le cache S3
aws s3 ls s3://nex-staging-gitlab-runner-cache/

# Verifier les logs CloudWatch
aws logs describe-log-groups --log-group-name-prefix /nex/gitlab-runner/ --region eu-west-3

# Recycler le runner (force nouveau bootstrap avec dernier userdata)
aws autoscaling update-auto-scaling-group \
  --auto-scaling-group-name nex-staging-gitlab-runner \
  --min-size 0 --desired-capacity 0 --region eu-west-3
# Attendre 10s puis :
aws autoscaling update-auto-scaling-group \
  --auto-scaling-group-name nex-staging-gitlab-runner \
  --min-size 1 --desired-capacity 1 --region eu-west-3

Estimation des couts

RessourceSpecificationCout mensuel estime
EC2 GitLab CE (on-demand)1x t3a.xlarge, 24/7~88 USD
EC2 Runner (Spot)1x t3a.large, 24/7~20 USD
EC2 Runner Spot (scale-up)0-2x t3a.large, ~30% du temps~10 USD
EBS200 GB (GitLab) + 100 GB (runner) gp3~25 USD
S3Registry + cache + backups~1 USD
NAT GatewayPartage avec EKS0 USD (deja paye)
CloudWatch Logs~1 GB/mois< 1 USD
Total~145 USD/mois

Comparaison : GitLab Premium SaaS = 29 USD/user/mois × 6 devs = 174 USD/mois, avec limites de minutes CI et pas de cache persistant.


Contraintes et decisions

DecisionJustification
GitLab CE self-hostedCout fixe vs cout par user, controle total, pas de limites CI
Docker executor, pas K8sPlus simple pour du build Node.js monorepo
GitLab Registry, pas ECRCoherent avec le pipeline, integre a GitLab CE
100% Spot pour le runnerLimite vCPU on-demand (16) deja consommee par EKS + GitLab CE
t3a.large (8 GB) pour runnerpnpm install + turbo build ~4-5 GB, marge pour Docker
t3a.xlarge (16 GB) pour GitLabOmnibus + PostgreSQL client + Registry, besoin de RAM
concurrent = 22 jobs paralleles par instance, bon ratio perf/ressources sur 8 GB
Cache S3 avec IAM authPas d'access keys, rotation automatique via role d'instance
Subnets prives (runner), pas de SSHSSM Session Manager uniquement
Cloudflare Origin CAWildcard *.paywithnex.com, 15 ans, SSL Full (strict)
RDS externe pour GitLabReutilise le RDS staging existant, backups gerees par AWS
Brevo SMTPRelay mail pour notifications GitLab (port 587)

Nex — Plateforme fintech CEMAC