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.
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.
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.
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.
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.
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_stopandauto_stop_inare 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?
Because the GitLab record describes lifecycle metadata; the external resource can still exist. Query the provider or bounded synthetic inventory independently.
Why prefer mr-$CI_MERGE_REQUEST_IID over a raw branch name for resource identity?
The IID is short and constrained. Raw branch text can contain characters or lengths that are unsafe or ambiguous for provider resources and cleanup.
What does a route map change?
It maps repository source paths to public paths under a review app URL. It does not deploy DNS, hosting, or application resources.
Is auto_stop_in an exact hard TTL?
No. It schedules GitLab lifecycle stop processing; current environment-expiration work runs roughly hourly, so external provider TTL/reconciliation may still be useful.
What security rule should guide review-app credentials?
Assume unmerged review code is lower trust; grant only isolated, least-privilege review authority and never ordinary production credentials.
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, andrules. -
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.
0x716c4Ab160C4B66F31a28AE2448BfF68fc3a2ef0
Send only Ethereum/ERC-20 compatible assets to this
address.