Spinning Up Per-Merge-Request Review Environments With GitLab CI Dynamic Environments

By KP  |  TZoneLabs  |  DevOps & Cloud Engineering

A dynamic environment: block in .gitlab-ci.yml turns every merge request into its own deployed, reachable review app, and turns every closed merge request into a cleanup job that tears it back down. Reviewers stop asking “can you deploy this so I can look at it” and start just opening the link GitLab already posted on the MR.

This covers the environment definition itself, deploying each one into its own namespace, tearing it down automatically, and the cleanup gaps that let review environments quietly pile up if you skip them.

The Basic Dynamic Environment Definition

deploy-review:
  stage: deploy
  script:
    - ./deploy.sh review-$CI_ENVIRONMENT_SLUG
  environment:
    name: review/$CI_COMMIT_REF_SLUG
    url: https://$CI_ENVIRONMENT_SLUG.review.example.com
  rules:
    - if: '$CI_PIPELINE_SOURCE == "merge_request_event"'

$CI_COMMIT_REF_SLUG gives each branch its own environment name, and $CI_ENVIRONMENT_SLUG is GitLab’s sanitized version of that name, safe to use in a URL or a Kubernetes resource name where the raw branch name might contain characters that aren’t valid there. GitLab posts the resulting URL directly on the merge request once the job runs, no separate notification step needed.

Deploying Each Review App to Its Own Namespace

#!/bin/bash
# deploy.sh
NAMESPACE="review-$CI_ENVIRONMENT_SLUG"

kubectl create namespace "$NAMESPACE" --dry-run=client -o yaml | kubectl apply -f -

helm upgrade --install "$NAMESPACE" ./chart \
  --namespace "$NAMESPACE" \
  --set image.tag="$CI_COMMIT_SHORT_SHA" \
  --set ingress.host="$CI_ENVIRONMENT_SLUG.review.example.com"

One namespace per environment keeps review apps fully isolated from each other, two branches with conflicting migrations or config can run at the same time without touching one another’s resources. It also makes cleanup a single command instead of hunting down which resources belonged to which review app.

Tearing It Down When the MR Closes

stop-review:
  stage: deploy
  script:
    - kubectl delete namespace "review-$CI_ENVIRONMENT_SLUG"
  environment:
    name: review/$CI_COMMIT_REF_SLUG
    action: stop
  rules:
    - if: '$CI_PIPELINE_SOURCE == "merge_request_event"'
      when: manual

The action: stop environment tells GitLab this job is the teardown for the environment defined above, so it shows up as a “Stop” button directly on the environment and gets triggered automatically when the merge request is merged or closed, without needing separate logic to detect that event yourself.

Auto-Stopping Environments Nobody Explicitly Closed

deploy-review:
  stage: deploy
  script:
    - ./deploy.sh review-$CI_ENVIRONMENT_SLUG
  environment:
    name: review/$CI_COMMIT_REF_SLUG
    url: https://$CI_ENVIRONMENT_SLUG.review.example.com
    auto_stop_in: 1 week
    on_stop: stop-review

A branch that gets abandoned without its MR ever being formally closed leaves its review environment running indefinitely if that’s the only cleanup mechanism. auto_stop_in gives GitLab a hard deadline to stop the environment on its own, independent of whether anyone remembers to close the MR.

Common Gotchas

  • No resource limits on the review namespace. A dozen review apps with no CPU or memory ceiling can quietly consume as much cluster capacity as your production workloads, especially if several stay open at once during a busy review week.
  • A wildcard TLS cert covering the review domain. Without *.review.example.com issued once up front, every new review environment needs its own certificate request, which turns a fast deploy into a slow one waiting on ACME validation.
  • Branch names that don’t slug cleanly. $CI_ENVIRONMENT_SLUG handles most of this, but very long branch names get truncated, and two branches that only differ after the truncation point can collide into the same environment.
  • Relying only on the manual stop job. If nobody clicks it and auto_stop_in isn’t set, the namespace and everything in it just sits there.

Checking What’s Actually Deployed Right Now

kubectl get namespaces -l app.kubernetes.io/managed-by=review-apps

Label every review namespace at creation time so a periodic sweep can find and report on ones GitLab’s own environment list might not perfectly match, particularly useful after any incident where a stop job failed partway through and left a namespace behind without GitLab knowing.

Key Lessons

  1. $CI_ENVIRONMENT_SLUG exists specifically for the places raw branch names break things.
    Use it for URLs, namespace names, and anything else that needs to be a valid identifier.
  2. One namespace per environment makes teardown a single command.
    Mixed resources across review apps mean cleanup has to be selective instead of just deleting a namespace.
  3. action: stop wires into GitLab’s merge/close events automatically.
    No custom webhook or detection logic needed for the common case.
  4. A manual stop job alone will eventually leave orphaned environments.
    auto_stop_in is the backstop for every branch that gets abandoned instead of properly closed.
  5. Review apps need their own resource limits, not a free pass on cluster capacity.
    Several open at once without limits can compete directly with production workloads for the same nodes.

Summary

Setting Purpose Use When
environment.name Gives each branch its own tracked environment Every merge-request pipeline
action: stop Defines the teardown job for an environment Paired with every dynamic environment
auto_stop_in Forces cleanup after a fixed period of inactivity Every review environment, no exceptions
Per-environment namespace Isolates resources and simplifies teardown Any review app with more than a couple of resources

Once the stop job and the auto-stop deadline are both in place, review environments stop being something anyone has to remember to clean up.

Read Next

If you’re running GitLab CI in production, follow along on LinkedIn for more guides like this one as they’re published.


Tags:
#GitLab   #CICD   #DevOps   #Kubernetes  
#ReviewApps   #SRE

Leave a Comment