Skip to content
CloudOps
All prompts
AI for GitLab CI/CD Difficulty: Intermediate ClaudeChatGPT

GitLab Review Apps Setup Prompt

Set up GitLab review apps — per-MR dynamic environments, URL routing via wildcard DNS / ingress, auto-cleanup, secret injection, cost control.

Target user
DevOps engineers building review app workflows
Difficulty
Intermediate
Tools
Claude, ChatGPT

The prompt

You are a senior DevOps engineer who has built review-app workflows that scale to dozens of concurrent MRs. You know how to do URL routing (wildcard DNS, path-based, subdomain), per-MR namespace isolation in Kubernetes, secret scoping, and auto-cleanup so you don't end up with 200 stale review envs.

I will provide:
- The target platform (Kubernetes most common, also Heroku, Docker host, serverless)
- The application (web SPA, API, full stack with DB?)
- URL routing approach (wildcard DNS subdomain, path-based, etc.)
- DB / dependency strategy (shared dev DB, per-review DB, mocked?)
- The team scale (1-10 concurrent MRs, 50+, etc.)

Your job:

1. **Design the URL strategy**:
   - **Wildcard subdomain**: `*.review.example.com` → ingress with regex; per-MR subdomain `mr-123.review.example.com`
   - **Path-based**: `https://review.example.com/mr-123/...` — simpler DNS but harder for SPA routing
   - **Per-namespace ingress**: each review in its own namespace with separate ingress
2. **Naming convention** (avoid collisions, respect K8s/DNS limits):
   - Use `$CI_MERGE_REQUEST_IID` (short, stable) or `$CI_COMMIT_REF_SLUG` (branch-derived)
   - K8s names: max 63 chars, lowercase alphanumeric + `-`
   - DNS: same constraints
   - Example: `review-mr-${CI_MERGE_REQUEST_IID}`
3. **Resource isolation per review**:
   - **Namespace per review** (preferred): full isolation, easy cleanup (delete namespace = gone)
   - **Shared namespace, label-based**: cheaper resource-wise but cleanup is harder
   - DB: per-review DB (cleanup-friendly) OR shared dev DB (cost-friendly but state pollution)
4. **Pipeline shape**:
   ```yaml
   review:
     script:
       - kubectl create namespace review-mr-$CI_MERGE_REQUEST_IID --dry-run=client -o yaml | kubectl apply -f -
       - helm upgrade --install review-mr-$CI_MERGE_REQUEST_IID ./chart \
           -n review-mr-$CI_MERGE_REQUEST_IID \
           --set image.tag=$CI_COMMIT_SHORT_SHA \
           --set ingress.host=mr-$CI_MERGE_REQUEST_IID.review.example.com
     environment:
       name: review/$CI_COMMIT_REF_SLUG
       url: https://mr-$CI_MERGE_REQUEST_IID.review.example.com
       on_stop: stop-review
       auto_stop_in: 1 week
     rules:
       - if: $CI_PIPELINE_SOURCE == "merge_request_event"
   ```
5. **Stop job** (triggered on MR close OR `auto_stop_in`):
   ```yaml
   stop-review:
     variables:
       GIT_STRATEGY: none
     script:
       - kubectl delete namespace review-mr-$CI_MERGE_REQUEST_IID --ignore-not-found
     environment:
       name: review/$CI_COMMIT_REF_SLUG
       action: stop
     rules:
       - if: $CI_PIPELINE_SOURCE == "merge_request_event"
         when: manual
   ```
6. **For secrets**:
   - Don't expose production secrets to review apps (security)
   - Use dev/staging credentials only
   - Variable scope: `review/*` to limit
   - Consider per-MR generated credentials (rotated)
7. **For cost control**:
   - `auto_stop_in: 1 week` — auto cleanup
   - Resource caps in chart (small `requests`/`limits`)
   - Scheduled CronJob to clean up orphaned namespaces nightly
   - Limit per-user concurrent review apps
8. **For monorepo + multiple services**:
   - Deploy only changed services (per-service `rules:changes:`)
   - URL convention: `service.mr-123.review.example.com`

Mark DESTRUCTIVE: stop job that uses `kubectl delete --force` (skips finalizers; orphan resources), shared DB without isolation (one review can break others), wildcard DNS for review without authentication (public internet exposure).

---

Target: [K8s / Heroku / Docker / serverless]
App type: [SPA / API / full-stack / monorepo]
URL strategy: [wildcard / path-based / per-ingress]
DB strategy: [per-review DB / shared / mocked]
Concurrent MR scale: [DESCRIBE]
Constraints: [budget / compliance / external IdP]

Why this prompt works

Review apps are a high-value GitLab feature but a sustainability challenge. Without isolation, cleanup, and cost control, teams end up with orphaned environments and surprise bills. This prompt enforces design choices that scale.

How to use it

  1. Pick URL strategy first. It dictates DNS and ingress setup.
  2. Decide on resource isolation level. Namespace-per-review is cleanest.
  3. Set up cleanup automation FROM DAY 1. Janitor + auto_stop_in.
  4. Use synthetic data only. No production state in review apps.

Useful commands

# Verify wildcard DNS
dig +short mr-test.review.example.com

# Confirm ingress controller is set up for wildcard
kubectl get ingress -A | grep review

# Cleanup orphaned namespaces (janitor)
kubectl get namespaces -l environment-type=review --output=jsonpath='{.items[?(@.metadata.creationTimestamp<\'2026-05-01T00:00:00Z\')].metadata.name}' | xargs -n1 kubectl delete namespace

# Count current review envs
kubectl get namespaces -l environment-type=review --no-headers | wc -l

# Per-user count (limit enforcement)
kubectl get namespaces -l environment-type=review,owner=<user>

Full pipeline pattern (Kubernetes + Helm)

.review-template: &review-template
  rules:
    - if: $CI_PIPELINE_SOURCE == "merge_request_event"

review:
  <<: *review-template
  stage: review
  image: alpine/k8s:1.30.0
  script:
    - kubectl create namespace review-mr-$CI_MERGE_REQUEST_IID --dry-run=client -o yaml | kubectl apply -f -
    - kubectl label namespace review-mr-$CI_MERGE_REQUEST_IID environment-type=review owner=$GITLAB_USER_LOGIN --overwrite
    - helm upgrade --install app ./chart -n review-mr-$CI_MERGE_REQUEST_IID \
        --set image.tag=$CI_COMMIT_SHORT_SHA \
        --set ingress.host=mr-$CI_MERGE_REQUEST_IID.review.example.com \
        --set resources.requests.cpu=100m \
        --set resources.requests.memory=128Mi \
        --wait --timeout 5m
    - echo "Review URL: https://mr-$CI_MERGE_REQUEST_IID.review.example.com"
  environment:
    name: review/$CI_COMMIT_REF_SLUG
    url: https://mr-$CI_MERGE_REQUEST_IID.review.example.com
    on_stop: stop-review
    auto_stop_in: 1 week

stop-review:
  <<: *review-template
  stage: review
  image: alpine/k8s:1.30.0
  variables:
    GIT_STRATEGY: none
  script:
    - kubectl delete namespace review-mr-$CI_MERGE_REQUEST_IID --ignore-not-found --wait=false
  environment:
    name: review/$CI_COMMIT_REF_SLUG
    action: stop
  when: manual
  rules:
    - if: $CI_PIPELINE_SOURCE == "merge_request_event"
      when: manual

Wildcard ingress (NGINX)

apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: app
  namespace: review-mr-123
  annotations:
    cert-manager.io/cluster-issuer: letsencrypt
spec:
  ingressClassName: nginx
  tls:
  - hosts: ["mr-123.review.example.com"]
    secretName: app-tls
  rules:
  - host: mr-123.review.example.com
    http:
      paths:
      - path: /
        pathType: Prefix
        backend:
          service: { name: app, port: { number: 80 } }

Janitor CronJob (cleanup orphans)

apiVersion: batch/v1
kind: CronJob
metadata:
  name: review-app-janitor
  namespace: gitlab-runner
spec:
  schedule: "0 3 * * *"
  jobTemplate:
    spec:
      template:
        spec:
          serviceAccountName: janitor
          containers:
          - name: janitor
            image: bitnami/kubectl:latest
            command:
            - sh
            - -c
            - |
              CUTOFF=$(date -u -d '1 week ago' +%Y-%m-%dT%H:%M:%SZ)
              kubectl get namespaces -l environment-type=review \
                --output=json | \
                jq -r --arg cutoff "$CUTOFF" \
                '.items[] | select(.metadata.creationTimestamp < $cutoff) | .metadata.name' | \
                xargs -r -n1 kubectl delete namespace
          restartPolicy: OnFailure

Cost-friendly per-MR DB (shared instance, schema-per-MR)

review-init-db:
  stage: review-prep
  script:
    - psql $SHARED_DB_URL -c "CREATE SCHEMA IF NOT EXISTS review_$CI_MERGE_REQUEST_IID"
    - DATABASE_URL="postgres://shared/myapp?options=-c search_path=review_$CI_MERGE_REQUEST_IID" \
        alembic upgrade head
  artifacts:
    reports:
      dotenv: db.env       # exports DATABASE_URL for next job

Common findings this catches

  • Review URL 404 → wildcard DNS / ingress not configured; ingress controller doesn’t allow wildcards.
  • Cert error on review URL → cert-manager DNS-01 / HTTP-01 challenge missing for wildcard.
  • Namespace stuck Terminating → finalizer issue; check for stuck PVCs/PVs.
  • Reviews accumulating despite auto_stop_in → stop job has syntax issue or GIT_STRATEGY not set, can’t checkout.
  • Database conflicts between concurrent reviews → schema-per-MR not implemented; each review uses same DB tables.
  • Production secret leak in review → variable scope incorrect; restrict to review/* scope.

When to escalate

  • Cluster cost spike from review apps — work with finance/SRE on quota and TTL policy.
  • Security audit flagging review app data exposure — synthetic data + auth at ingress.
  • Concurrent MR explosion at scale — consider sub-pipeline triggers per service rather than per-monorepo deploy.

Related prompts

Newsletter

Get weekly AI workflows for DevOps engineers

Practical prompts, automation ideas, and tool reviews for infrastructure engineers. One email per week. No spam.