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)
Volume50 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
  4. Docker cleanup : systemd timer toutes les 6h (prune images/volumes)

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  = 50
}

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) + 50 GB (runner) gp3~5 USD
S3Registry + cache + backups~1 USD
NAT GatewayPartage avec EKS0 USD (deja paye)
CloudWatch Logs~1 GB/mois< 1 USD
Total~125 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)

NxPay — Plateforme fintech CEMAC