Chapter 20Lesson 01~160 minutes

Review Apps, Dynamic Environments, Ephemeral Test Stacks, Route Maps, and Environment Cleanup: Concepts, Architecture, and Mental Model

Model review apps as bounded dynamic environments whose source ref, environment identity, URL, route mapping, credentials, stop policy, resource IDs, and cleanup evidence are independently traceable.

Review appsDynamic environmentsSlugsRoute mapsCleanup

Learning objectives

  • Explain why a review app is a dynamic GitLab environment plus an external ephemeral resource lifecycle, not merely a URL in a merge request.
  • Trace merge-request/source identity through deterministic environment naming, CI_ENVIRONMENT_SLUG, external resource identity, environment URL, route maps, review activity, stop/auto-stop, and cleanup proof.
  • Distinguish GitLab environment state from the existence and health of the external preview stack.
  • Explain current slug, on_stop, auto_stop_in, route-map, stop-job, and variable-scope semantics that affect safe review-app design.
  • Identify the minimum evidence needed to prove a review app was created for the intended revision and later removed without touching unrelated resources.

1. The practical problem: previews create infrastructure debt if identity and cleanup are vague

A review app lets reviewers see a change before it merges. That sounds simple until the pipeline creates something outside the repository: a container, namespace, directory, VM, database schema, object-store prefix, temporary hostname, or other resource that can outlive the job that created it. Chapter 19 separated a deployment job from GitLab's deployment record and from real target health. Chapter 20 adds a lifecycle question: which temporary target belongs to this merge request, and how do we prove that exact target was removed?

The safe mental model is not “branch name → deploy somewhere.” It is “trusted source identity → deterministic bounded review identity → explicit resource manifest → review URL/navigation → stop request → resource-specific deletion → independent absence proof.” Every step produces evidence. A green stop job is not cleanup proof any more than a green deploy job is health proof.

Safety boundary: never let untrusted branch text become a shell path, namespace, cloud account selector, cluster context, database name, or broad deletion prefix without normalization and an allowlisted prefix. This chapter uses synthetic MR identifiers such as mr-101 and a disposable local root.

2. Terms before YAML

Term Meaning in this chapter Do not confuse it with
Review app A temporary preview deployment associated with a branch or merge request. The merge request itself or the pipeline UI.
Dynamic environment A GitLab environment whose name is generated from pipeline variables, commonly one per branch/MR. An automatically healthy or automatically deleted external resource.
Environment name Stable GitLab identity such as review/mr-101. The external resource ID or DNS hostname.
Environment slug GitLab-generated normalized value suitable for URLs/labels, exposed as CI_ENVIRONMENT_SLUG. A globally collision-proof infrastructure identifier.
Route map Repository mapping from changed source files to public paths in the review app. A router, DNS record, ingress rule, or deployment mechanism.
Stop job CI job annotated with environment: action: stop that performs teardown and marks lifecycle state. Proof that every provider-side resource is gone.
Auto-stop GitLab scheduling of an environment stop after inactivity/lifetime. A real-time hard TTL enforced by your cloud provider.

3. State model: keep GitLab metadata and external resources separate

Before changing anything, write down the state you are about to affect. The same merge request can simultaneously have a source SHA, compiled pipeline config, a deployment job, a GitLab environment, and external resources. These states are related but not interchangeable.

Layer Read-only evidence Mutation in this chapter
Source/revision MR IID, source/target refs, CI_COMMIT_SHA, pipeline source. None in conceptual inspection.
Compiled configuration Merged YAML, job inclusion, effective rules, environment expression. Adding review deploy/stop jobs changes future pipelines only.
Job/runner Pipeline/job IDs, runner/executor/image, job status. Run bounded synthetic deploy/stop work.
Identity/trust MR origin, branch protection, available non-secret inputs, variable scope. No production credential is introduced.
GitLab environment Environment name, slug, URL, state, deployment history, on-stop link. Create/stop one review environment.
External target Synthetic resource ID, manifest, directory/digest/health marker. Create/delete only a guarded sandbox resource.
Governance/cost Owner, expiration, allowed resource prefix, cleanup evidence. Record lifecycle/cleanup proof.

4. Mental model: MR to bounded resource to verified deletion

A merge request or branch event first supplies source identity. The pipeline compiles a dynamic environment name such as review/mr-$CI_MERGE_REQUEST_IID. GitLab derives an environment slug and records the environment URL metadata. Your deployment code creates an external resource using a separately validated resource ID. Reviewers then navigate either to the environment root or, when useful, through route-map links. Cleanup is a second side effect: a stop job or scheduled stop invokes a resource-specific teardown, after which external inventory must independently confirm that the resource no longer exists.

The arrows matter: a route map depends on a working review URL; a stopped GitLab environment does not delete a cloud resource by magic; and a provider deletion does not automatically prove that the correct MR was targeted unless the manifest ties resource identity back to source SHA and MR identity.

Review-app lifecycle and evidence flow
            flowchart TD
              A[MR / ref / exact SHA] --> B[Compile dynamic env name]
              B --> C[GitLab environment + slug + URL]
              C --> D[Create bounded external resource]
              D --> E[Record resource ID + source evidence]
              E --> F[Review root URL / route-map links]
              F --> G[Stop request or auto-stop due]
              G --> H[Resource-specific teardown]
              H --> I[Independent absence check]
              I --> J[Cleanup evidence packet]
          

5. Deterministic identity: MR IID is usually safer than raw branch text

Human branch names are expressive; infrastructure identifiers need narrow syntax and predictable length. CI_COMMIT_REF_SLUG is safer than CI_COMMIT_REF_NAME for URL-like values because GitLab normalizes it and limits it to 63 bytes. For merge-request review apps, an even smaller identity such as mr-$CI_MERGE_REQUEST_IID is often easier to audit and to guard in cleanup code.

GitLab separately derives CI_ENVIRONMENT_SLUG from the environment name. Current documentation limits that slug to 24 characters and adds a random suffix when the environment name contains uppercase characters. That means you should inspect the actual slug rather than predicting it by hand, especially if another system has tighter naming rules.

Design rule: keep the GitLab environment name human/auditable, keep the provider resource ID narrow and machine-safe, and record the mapping between them. Do not assume that one slug is suitable for every cloud, Kubernetes, DNS, database, and filesystem naming constraint.

6. URL and route maps are navigation metadata, not lifecycle control

environment:url tells GitLab where reviewers should navigate. The URL can contain supported CI/CD variables, but it does not provision DNS, a certificate, an ingress, or an application. The target must already exist and be reachable through infrastructure you control.

A route map adds source-aware navigation. GitLab reads .gitlab/route-map.yml, evaluates mappings in order, and uses the first matching source entry to compute a public path. A useful route map might connect docs/guide.md to /guide/. It still depends on a functioning review-app base URL.

# .gitlab/route-map.yml
- source: 'docs/index.md'
  public: '/'
- source: /docs\/(.+)\.md/
  public: '/\1/'

7. Stop and expiry: two controls, neither is cleanup proof by itself

environment:on_stop links a deployment job to a teardown job. The teardown job declares the same environment name and action: stop. For reliable lifecycle behavior, deploy and stop jobs need compatible inclusion rules; the stop job must also be runnable when the branch/MR lifecycle triggers cleanup. GitLab documentation recommends avoiding stage/needs arrangements that strand the stop job behind failed work.

environment:auto_stop_in adds a time-based backstop. It accepts human-readable durations and can be reset by later environment activity. Current GitLab runs environment-expiration background work roughly hourly, so “1 hour” means eligible for stop processing after the period—not a provider-side hard TTL at exactly 60 minutes.

GitLab also automatically stops environments when associated branches are merged/deleted under current default behavior, but external cleanup still depends on your teardown path. Keep provider TTLs or separate stale-resource reconciliation where cost/risk warrants defense in depth.

8. Trust boundary: review code is often less trusted than production code

Review apps commonly execute code that has not merged. Treat that as a lower-trust context. A review job should not receive production credentials simply because it deploys something. Prefer fake data, scoped test credentials, isolated accounts/namespaces, least-privilege short-lived identity, and environment-specific variables only where necessary.

Environment-scoped variables can reduce blast radius, for example by scoping a test credential to review/*. But GitLab warns against relying on environment-scoped variables in rules or include decisions because those values might not exist when the pipeline configuration is validated. Also remember fork/MR trust rules from Chapter 16: parent-project variables can become available when an authorized user deliberately runs a fork MR pipeline in the parent project.

Never: forward production cloud keys into ordinary review pipelines, print tokens for debugging, run untrusted review code on privileged shared runners, or let review teardown credentials delete resources outside a dedicated review prefix/account/namespace.

9. Inspect first: prove the current review state without changing it

Before redeploying or cleaning anything, capture identifiers. In a real project, start with the MR, pipeline, environment, and provider inventory pages. In a job, print only non-secret identity metadata. Do not dump the full environment or variable set.

printf 'pipeline_source=%s\n' "$CI_PIPELINE_SOURCE"
printf 'source_sha=%s\n' "$CI_COMMIT_SHA"
printf 'mr_iid=%s\n' "${CI_MERGE_REQUEST_IID:-none}"
printf 'environment_name=%s\n' "${CI_ENVIRONMENT_NAME:-none}"
printf 'environment_slug=%s\n' "${CI_ENVIRONMENT_SLUG:-none}"
printf 'environment_url=%s\n' "${CI_ENVIRONMENT_URL:-none}"

For external resources, query only the dedicated review namespace/prefix and capture IDs, labels/tags, owner/MR identity, creation time, and source digest. The key question is “what exists and which source created it?”—not “can I delete everything and retry?”

10. Minimal review-environment declaration

review_deploy:
  stage: deploy
  script:
    - ./ci/deploy-review.sh "mr-$CI_MERGE_REQUEST_IID"
  resource_group: "review-mr-$CI_MERGE_REQUEST_IID"
  environment:
    name: "review/mr-$CI_MERGE_REQUEST_IID"
    url: "https://$CI_ENVIRONMENT_SLUG.review.example.invalid/"
    on_stop: review_stop
    auto_stop_in: 2 days
  rules:
    - if: '$CI_PIPELINE_SOURCE == "merge_request_event"'

review_stop:
  stage: deploy
  script:
    - ./ci/stop-review.sh "mr-$CI_MERGE_REQUEST_IID"
  resource_group: "review-mr-$CI_MERGE_REQUEST_IID"
  environment:
    name: "review/mr-$CI_MERGE_REQUEST_IID"
    action: stop
  rules:
    - if: '$CI_PIPELINE_SOURCE == "merge_request_event"'
      when: manual
      allow_failure: true

The URL uses the reserved .invalid TLD, so this snippet is metadata-safe rather than a real deployment. The shared resource_group serializes deploy/stop side effects for the same synthetic review identity and satisfies the current UI stop/on-stop requirement. Real teardown must still guard the external resource boundary.

11. Why this matters in DevOps: ephemeral means owned lifecycle, not disposable thinking

Review apps shorten feedback only when they remain trustworthy and cheap. The operating model needs ownership, deterministic identity, bounded permissions, time-based backstops, and a reconciliation path for orphans. Treat cleanup evidence as a first-class delivery artifact: source SHA, environment name/slug, resource ID, stop job ID, deletion response or inventory snapshot, and final “not found”/empty result.

This is also a platform-design problem. A platform team can provide a reusable review-app component, but the component contract should expose explicit inputs such as review identity, TTL, resource class, and route-map behavior while hiding provider credentials and enforcing allowed prefixes.

12. Common mistakes and safer alternatives

Mistake Why it fails Safer pattern
Use raw branch name as provider ID Characters/length/path semantics differ and can make cleanup ambiguous. Use MR IID or provider-specific validated slug; record mapping.
Assume stopped = deleted GitLab lifecycle state does not prove provider resources disappeared. Query provider/local inventory after teardown.
Give review job production secret Unmerged code gains production authority. Use isolated review scope and short-lived/test identity.
Only use manual cleanup Humans forget; stale resources accumulate. Manual stop + lifecycle stop + auto_stop_in/provider TTL/reconciler.
Delete by broad prefix One bad selector can remove unrelated reviews. Delete exact resource IDs from manifest with guard checks.
Route map as deployment logic Mapping only controls navigation. Deploy first; use route map only to compute public paths.

13. Summary and evidence checklist

  • Review app = GitLab dynamic environment + external ephemeral resource lifecycle.
  • Use exact source/MR identity and deterministic bounded resource IDs.
  • Inspect CI_ENVIRONMENT_SLUG; do not hand-assume provider naming.
  • on_stop and auto_stop_in are lifecycle controls, not external deletion proof.
  • Route maps improve navigation; they do not provision or clean infrastructure.
  • Keep review credentials isolated from production and prove cleanup externally.

Knowledge check

Why is a GitLab environment in the stopped state insufficient cleanup evidence?

Why prefer mr-$CI_MERGE_REQUEST_IID over a raw branch name for resource identity?

What does a route map change?

Is auto_stop_in an exact hard TTL?

What security rule should guide review-app credentials?

Next lesson

Guided hands-on workflow and core operations

Create a bounded local review resource, model its GitLab dynamic environment, inspect identity and route metadata, then stop it and prove cleanup independently.

Version and compatibility note

GitLab and GitLab Runner evolve continuously. Treat version-sensitive YAML, runner/executor behavior, APIs, security features, policy controls, tiers, and deprecations as assumptions to verify against the current official GitLab documentation before production use. Preserve the exact project/ref/SHA, compiled configuration, pipeline/job IDs, runner/tool versions, and external target evidence used for any reproducible lab or incident record.

Official references and version notes

Documentation verification date: 2026-09-12. Review-app, environment lifecycle, variable, protected-resource, route-map, and cleanup semantics are version-sensitive. Re-check the GitLab version used by your organization before copying exact lifecycle behavior into production.

  • Review apps — dynamic review environments, merge-request workflows, stop behavior, and route maps.
  • Environments — static/dynamic environments, environment states, on_stop, auto_stop_in, stale cleanup, deletion, and environment-scoped variables.
  • CI/CD YAML syntax reference — environment:name, url, on_stop, action, auto_stop_in, resource_group, and rules.
  • Predefined CI/CD variables — CI_COMMIT_REF_SLUG, CI_ENVIRONMENT_NAME, CI_ENVIRONMENT_SLUG, CI_ENVIRONMENT_URL, and merge-request variables.
  • CI/CD variables — protection, masking/hidden behavior, fork/MR exposure, and environment scope.
  • Environments API — environment metadata, stop, and stale-environment operations when API automation is appropriate.
  • Deployment safety — protected-resource and deployment-safety boundaries that also matter for review infrastructure.

Current assumptions used in this chapter: review apps and dynamic environments are available on Free, Premium, and Ultimate across GitLab.com, Self-Managed, and Dedicated. CI_COMMIT_REF_SLUG is normalized and shortened to 63 bytes; CI_ENVIRONMENT_SLUG is derived from environment:name, is truncated to 24 characters, and uppercase environment names can receive a random suffix. Route maps live in .gitlab/route-map.yml, are evaluated in declaration order, and the first matching source rule determines the public path; the merge-request widget can surface up to five mapped pages before filtering. environment:auto_stop_in accepts human-readable durations (and variables), but environment expiration is serviced by background work that runs approximately hourly, so it is not an exact timer. Deploy and stop jobs should have compatible rules/only/except; a stop job also has to be runnable when cleanup is needed. To trigger on_stop from the Environments UI, deploy and stop jobs must share a resource_group. Protected environments are Premium/Ultimate and are not required by the mandatory lab. The mandatory path uses fake .invalid URLs and guarded local filesystem resources under a temporary sandbox—no cloud account, Kubernetes cluster, production DNS, PAT, deploy token, or real secret is required.

Keep the academy open

Support free, practical DevOps education.

Every lesson is designed to remain readable in a browser, downloadable from GitHub, and usable without a paid learning platform. Contributions help expand and maintain the curriculum.

Ethereum / ERC-20
0x716c4Ab160C4B66F31a28AE2448BfF68fc3a2ef0 Send only Ethereum/ERC-20 compatible assets to this address.