Environments, Deployments, Review Apps, Protected Environments, and Deployment Approvals: Guided Hands-On Workflow and Core Operations
Create and inspect a tiny synthetic Review App deployment, correlate commit/job/environment state, exercise stop behavior, and clean up without cloud spend.
Learning objectives
- Inspect existing environment and deployment state before creating anything.
- Create a deterministic build artifact and a Free-compatible synthetic deployment record.
-
Create a dynamic review environment with an
example.invalidURL and no external side effect. -
Exercise
on_stop/auto_stop_inand verify environment state transitions. - Capture pipeline, job, commit, artifact, environment, and deployment evidence independently.
environment:on_stop,
environment:auto_stop_in, environment/deployment APIs,
deployment rollback/redeploy, and project-level environment-scoped
CI/CD variables are available on GitLab Free/Premium/Ultimate across
GitLab.com, Self-Managed, and Dedicated. Protected environments and
deployment approvals are Premium/Ultimate. Group-variable environment
scoping is also Premium/Ultimate, so the mandatory chapter path uses
only a disposable project and synthetic project-level values. No cloud
account, Kubernetes cluster, production endpoint, real credential, or
paid approval feature is required.
1. Scenario: a deployment record without cloud spend
Create or use a disposable GitLab Free project/branch named
ch17/environment-lab. The pipeline creates a tiny text
artifact, verifies its checksum, and runs a deployment job that
changes no external system. The environment URL uses
example.invalid, a reserved domain that cannot
accidentally become a real service. This lets you study GitLab’s
deployment state machine safely.
| Preflight item | Required state | Reason |
|---|---|---|
| Project/ref |
Disposable project or ch17/environment-lab.
|
Environment/deployment history is intentionally synthetic. |
| Role | Developer is enough for branch/pipeline lab; use Maintainer only for optional settings/cleanup requiring it. | Least privilege. |
| Runner | Any tiny eligible runner, or CI Lint + supplied fixture if hosted compute is unavailable. | No quota dependency. |
| External target | None. example.invalid is metadata only. |
No cloud spend or accidental deployment. |
| Secrets | None. Optional environment-scoped variable is fake data only. | Secret safety. |
2. Inspect before state
Record whether the target environment already exists. In a shared disposable project, never assume an environment name is unused.
PROJECT_ID="12345678"
glab api "projects/$PROJECT_ID/environments?name=review/ch17-environment-lab" \
--jq '.[] | {id,name,state,external_url,tier,last_deployment:(.last_deployment.id // null)}'
glab api "projects/$PROJECT_ID/deployments?environment=review/ch17-environment-lab" \
--jq '.[] | {id,status,sha,ref,finished_at}'
If real data appears for that name, choose another disposable name. Do not stop/delete an environment just because a lab expected it to be empty.
3. Produce a deterministic artifact first
The deploy job should consume an identified output rather than silently rebuild. Add this minimal build job:
stages: [build, deploy]
build_payload:
stage: build
script:
- mkdir -p out
- printf 'sha=%s\npipeline=%s\n' "$CI_COMMIT_SHA" "$CI_PIPELINE_ID" > out/payload.txt
- sha256sum out/payload.txt > out/payload.txt.sha256
artifacts:
paths:
- out/payload.txt
- out/payload.txt.sha256
expire_in: 2 days
access: developer
This artifact is the lab’s “release identity.” It is synthetic, tiny, and checksum-addressable.
4. Add a synthetic dynamic deployment
deploy_review:
stage: deploy
needs:
- job: build_payload
artifacts: true
resource_group: review-$CI_COMMIT_REF_SLUG
script:
- sha256sum -c out/payload.txt.sha256
- echo "deployment_target=synthetic"
- echo "artifact_verified=true"
environment:
name: review/$CI_COMMIT_REF_SLUG
url: https://$CI_ENVIRONMENT_SLUG.example.invalid
deployment_tier: development
on_stop: stop_review
auto_stop_in: 1 day
stop_review:
stage: deploy
resource_group: review-$CI_COMMIT_REF_SLUG
script:
- echo "synthetic_cleanup=true"
environment:
name: review/$CI_COMMIT_REF_SLUG
action: stop
when: manual
Every line has a scope: needs reads the build artifact,
resource_group serializes operations for this synthetic
resource, environment:name creates/updates the GitLab
environment, and the URL is metadata. The script has no credential
and no external deployment command.
5. Validate before triggering
Use the Pipeline Editor/CI Lint to verify configuration expansion.
Confirm the environment name and stop job resolve from the same
branch variables. Do not use a production-like name such as
production in a shared project merely for realism.
6. Commit on the disposable branch
git switch -c ch17/environment-lab
git add .gitlab-ci.yml
git diff --cached --check
git diff --cached
git commit -m "ch17: synthetic environment lab"
LAB_SHA="$(git rev-parse HEAD)"
printf 'lab_sha=%s\n' "$LAB_SHA"
git push -u origin ch17/environment-lab
Before pushing, predict: one build artifact, one deployment job, one
dynamic environment named from the ref slug, one deployment record
tied to LAB_SHA, and an available stop action after
deployment succeeds.
7. Capture pipeline and job identity
Once the tiny pipeline runs, record the pipeline ID, source, SHA, and jobs:
PROJECT_ID="12345678"
PIPELINE_ID="123456789"
glab api "projects/$PROJECT_ID/pipelines/$PIPELINE_ID" \
--jq '{id,sha,ref,source,status,web_url}'
glab api "projects/$PROJECT_ID/pipelines/$PIPELINE_ID/jobs" --paginate \
--jq '.[] | {id,name,status,stage,runner:(.runner.description // null)}'
Do not copy runner authentication material or full environment dumps into evidence.
8. Inspect the environment record
Open Operate → Environments and select the review environment. Independently query:
ENV_NAME="review/ch17-environment-lab"
glab api "projects/$PROJECT_ID/environments?name=$ENV_NAME" \
--jq '.[] | {id,name,state,external_url,tier,updated_at,last_deployment:(.last_deployment.id // null)}'
Expected state after a successful deploy: available.
The external URL resolves to .invalid, proving that
environment availability in GitLab is not an external health probe.
9. Inspect deployment identity
glab api "projects/$PROJECT_ID/deployments?environment=$ENV_NAME&order_by=finished_at&sort=desc" \
--jq '.[] | {id,status,sha,ref,deployable_job_id:.deployable.id,environment:.environment.name,finished_at}'
Verify the newest successful deployment SHA equals
LAB_SHA and the deployable job is
deploy_review. That correlation is the causal proof
that GitLab recorded this job/commit as a deployment.
10. Verify artifact identity without treating the environment as the artifact
The deployment record identifies a commit/job, while the build
artifact has its own producer and checksum. Inspect artifact
metadata from the build job and confirm the deploy log ran
sha256sum -c. Do not claim the deployment record stores
your release payload.
11. Optional Free exercise: project variable scoped to
review/*
Create a synthetic project variable such as
CH17_SCOPE_MARKER=synthetic-only with environment scope
review/*. Add a job-time presence check:
script:
- test -n "$CH17_SCOPE_MARKER"
- echo "scoped_marker_present=true"
Do not print the value. Run the same presence test in a non-environment job and predict absence. Delete the variable afterward. Group-variable environment scoping is Premium/Ultimate; this exercise is deliberately project-level.
12. Stop the review environment and verify two planes
Trigger stop_review from the pipeline or environment
UI. Because this is a simulation, the external cleanup is a harmless
echo. Verify both:
-
GitLab plane: environment state becomes
stopped. - External plane: the stop job log says the synthetic cleanup path executed. In real infrastructure you would separately verify the target was removed.
ENV_ID="123456"
glab api "projects/$PROJECT_ID/environments/$ENV_ID" \
--jq '{id,name,state,external_url,updated_at}'
13. Observe auto_stop_in without waiting a day
On the environment page, inspect the scheduled stop/expiry information. Do not wait for the background worker. The learning goal is to understand that the deployment sets a lifecycle deadline and that later deployments reset it.
14. Premium/Ultimate read-only fixture: protection and approval
Do not enable paid governance for the mandatory lab. Instead, reason about this synthetic state:
{
"environment": "production",
"protected": true,
"allowed_to_deploy": ["release-operators"],
"required_approvals": 1,
"approval_state": "blocked"
}
Predict: a Developer outside release-operators cannot
deploy; an eligible approver can approve according to configured
rules; approval and deployment execution remain distinct events. The
actual target IAM must still reject unauthorized credentials
independently.
15. Challenge: choose the correct GitLab surface
You need three outcomes: “reviewers see a URL for each branch,” “the environment disappears from active inventory after the change is done,” and “only production deploy jobs receive the production credential.” Which controls fit?
Answer before revealing: dynamic review/* environment +
URL, explicit on_stop/auto_stop_in, and an
environment-scoped project variable for production.
Protected environments/approvals are separate governance controls
and not required to express these three mechanics.
16. Cleanup
Ensure the environment is stopped. Remove the synthetic project variable if created. Restore/remove the lab CI config, verify the exact remote branch, then delete only that disposable ref:
git switch main
git ls-remote --heads origin refs/heads/ch17/environment-lab
# Delete only after the exact ref is confirmed:
git push origin --delete ch17/environment-lab
git branch -D ch17/environment-lab
Do not delete the entire project unless it was created solely for this chapter and contains no valuable data.
Knowledge check
Why does the lab use example.invalid?
It is a reserved non-real domain, so GitLab can record a URL without accidentally deploying to or advertising a real endpoint.
What independently proves the deployment record belongs to the lab commit?
Compare deployment sha, deployable job ID, pipeline
SHA, and local LAB_SHA.
Why use the same resource group for deploy and stop?
It serializes operations on the synthetic resource and supports the current UI stop-action requirements.
Should the scoped variable value be echoed to prove it exists?
No. Test presence and print a boolean/synthetic status; real secret values should never be printed.
What does a stopped environment prove about real infrastructure?
Only GitLab state plus stop-job execution; independent external verification is still required in production.
Summary
You created a real GitLab environment/deployment record without changing a real external target. You bound it to a commit, job, and verified artifact, created a dynamic review URL, exercised stop state and lifecycle metadata, and kept paid protection/approvals as explicit fixtures. That is enough to reason about production CD without renting infrastructure.
Official references
- GitLab Docs — Environments
- GitLab Docs — Deployments
- GitLab Docs — Review apps
- GitLab Docs — Deployment safety
- GitLab Docs — Protected environments
- GitLab Docs — Deployment approvals
- GitLab Docs — Environments API
- GitLab Docs — Deployments API
- GitLab Docs — Protected environments API
- GitLab Docs — CI/CD variables
- GitLab Docs — CI/CD YAML syntax reference
- GitLab Docs — Predefined CI/CD variables
- GitLab Docs — Where variables can be used
- GitLab Docs — Resource groups
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.
0x716c4Ab160C4B66F31a28AE2448BfF68fc3a2ef0Send only Ethereum/ERC-20 compatible assets to this
address.