Chapter 17Lesson 02~285 minutes

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.

Hands-onDynamic environmenton_stopauto_stop_inAPI evidenceCleanup

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.invalid URL and no external side effect.
  • Exercise on_stop/auto_stop_in and verify environment state transitions.
  • Capture pipeline, job, commit, artifact, environment, and deployment evidence independently.
Availability baseline (verified 2026-08-21 against current GitLab documentation). Environments, deployment records, Review Apps/dynamic environments, 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.

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?

What independently proves the deployment record belongs to the lab commit?

Why use the same resource group for deploy and stop?

Should the scoped variable value be echoed to prove it exists?

What does a stopped environment prove about real infrastructure?

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

Next lesson

Choose deployment architecture deliberately

Lesson 3 turns the mechanics into policy: static versus dynamic environments, automation versus human gates, environment-scoped secrets, and rollback by known artifact versus rebuild.

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
0x716c4Ab160C4B66F31a28AE2448BfF68fc3a2ef0Send only Ethereum/ERC-20 compatible assets to this address.