Andrew Mercer
on this page

GitLab Certification Study Guide with Hands-On Labs

This guide covers three targets:

  1. GitLab Certified Git Associate (retired) — Git fundamentals and GitLab navigation. Still worth doing: every other GitLab exam assumes it, and its content lives on in the "GitLab with Git Essentials" course.
  2. GitLab Certified CI/CD Associate — pipelines, runners, variables, artifacts/cache, environments, registry, releases, basic security scanning.
  3. GitLab Certified Professional Services Engineer (PSE) learning path — historically a bundle of six certifications: Git Associate, Project Management Associate, CI/CD Associate, Security Specialist, Implementation Services Specialist, and Migration Services Specialist.

How the exams work

The associate-level exams share a format: a short written multiple-choice exam (15 questions, 80% to pass, unlimited retakes) plus a hands-on project submission that a GitLab engineer grades. No reference material is allowed during the written portion. The hands-on part is where people actually fail — they know the theory but submit a project that doesn't meet every stated requirement. Every lab below ends with a "Verify" checklist for exactly that reason: practise reading requirements and proving each one is satisfied.

Caveat: GitLab University pages are JavaScript-rendered and topic lists change between GitLab releases. Before booking each exam, log in and read the current "Topics Covered" list on the exam page, then map it against the sections here. The Implementation and Migration Services specialist certifications have historically been restricted to GitLab team members and authorised partners — confirm you can enrol in every component of the PSE path before paying for any part of it.

Suggested schedule (6 weeks, ~1–2 h/day)

Week Focus Labs
1 Lab environment + Git/GitLab fundamentals 0–6
2 CI/CD core: pipelines, rules, needs, variables 7–12
3 CI/CD delivery: artifacts, registry, environments, releases, components 13–19
4 Project management + security scanning 20–26
5 Implementation: install, admin, runners, backup/restore, upgrade 27–33
6 Migration + capstone mock exams 34–39

Given your background (managed on-prem GitLab at Sandvine, GitLab CI daily, CKA), weeks 1–2 will go fast. Don't skip the "Verify" steps though — the exams test GitLab's vocabulary and defaults, which day-to-day familiarity tends to blur.


Lab 0 — Build the lab environment

You want two GitLab targets:

  • gitlab.com (Free account, plus a 30-day Ultimate trial on a group you create when you reach the security and project-management sections — epics, roadmaps, scoped labels, scan policies and the vulnerability report need Premium/Ultimate).
  • A self-managed instance in a KVM VM on your homelab host — required for the Implementation and Migration material, and handy for testing the runner and admin labs without touching gitlab.com quotas.

0.1 Self-managed GitLab VM

Minimum sensible sizing for a lab: 4 vCPU, 8 GB RAM, 40 GB disk. Debian 12 or Ubuntu 24.04.

# On the KVM host
virt-install \
  --name gitlab-lab \
  --vcpus 4 --memory 8192 \
  --disk size=40 \
  --os-variant ubuntu24.04 \
  --network network=default \
  --cdrom /var/lib/libvirt/images/ubuntu-24.04-live-server-amd64.iso

Then inside the VM:

sudo apt-get update
sudo apt-get install -y curl openssh-server ca-certificates tzdata perl postfix
# Use the "Internet Site" postfix option, or skip postfix and point GitLab at your relay later.

curl -fsSL https://packages.gitlab.com/install/repositories/gitlab/gitlab-ee/script.deb.sh | sudo bash

# EE without a licence behaves as CE; it lets you activate a trial licence later.
sudo EXTERNAL_URL="http://gitlab.lab.example" apt-get install -y gitlab-ee

sudo cat /etc/gitlab/initial_root_password   # valid for 24 h — change it immediately

Put gitlab.lab.example (substitute your own name) in your LAN DNS or /etc/hosts. If you front it with your existing nginx reverse proxy, set nginx['listen_https'] = false and external_url 'https://…' and configure trusted_proxies — but for the exam material it's simpler to let Omnibus own TLS (Lab 28).

Snapshot the VM now. You will break it deliberately in later labs.

virsh snapshot-create-as gitlab-lab clean-install "Fresh Omnibus install"

0.2 A runner host

Use either a second small VM or your k3s node. You'll do both executor styles in Lab 9.

0.3 Tooling on your workstation

git --version            # 2.40+
glab --version           # GitLab CLI — increasingly used for releases and API work

Create a personal access token (scopes api, read_repository, write_repository) on both instances and store it in pass:

pass insert gitlab/com/pat
pass insert gitlab/lab/pat

Verify: you can log in to both instances, ssh -T [email protected] greets you by username, and the VM snapshot exists.


Part A — Git and GitLab Fundamentals (Git Associate)

The retired Git Associate exam covered: GitLab overview and terminology, Git basics, basic code creation in GitLab, an introduction to CI/CD, package/release basics, and an introduction to security scanning. Parts B and C go deeper on the last three; Part A focuses on Git itself and navigating GitLab.

Key concepts to be able to explain

  • The three trees: working directory, staging area (index), repository (HEAD). Know which command moves content between which.
  • Commits are snapshots, identified by SHA; branches and tags are just refs pointing at commits; HEAD points at the current branch (or a commit when detached).
  • Merge vs rebase vs squash, fast-forward vs merge commit, and what GitLab's project merge method setting does (Merge commit / Merge commit with semi-linear history / Fast-forward merge).
  • GitLab hierarchy: instance → groups → subgroups → projects. Permissions inherit downwards. Know the roles in order: Guest, Planner, Reporter, Developer, Maintainer, Owner (Planner is the newer role between Guest and Reporter — check the current docs, as role names come up in questions).
  • Visibility levels: Private, Internal (self-managed only in practice; disabled for new projects on gitlab.com), Public.
  • Issue → branch → MR → review → merge is "the GitLab flow". Expect questions on it.
  • Forking workflow vs shared-repository workflow, and when each is appropriate.

Lab 1 — Local Git fundamentals

mkdir git-lab && cd git-lab
git init -b main
git config user.name  "Andrew"
git config user.email "[email protected]"

echo "# Git Lab" > README.md
git status
git add README.md
git commit -m "Initial commit"

echo "line two" >> README.md
git diff            # working dir vs index
git add README.md
git diff --staged   # index vs HEAD
git commit -m "Add line two"

git log --oneline --graph --decorate
git show HEAD~1

Undo operations — know these cold, they are classic exam questions:

git restore README.md            # discard working-dir changes
git restore --staged README.md   # unstage, keep changes
git commit --amend               # rewrite the last commit (never on shared history)
git revert <sha>                 # new commit that inverts <sha> — safe on shared branches
git reset --soft  HEAD~1         # move branch; keep index + working dir
git reset --mixed HEAD~1         # move branch; keep working dir (default)
git reset --hard  HEAD~1         # move branch; discard everything
git reflog                       # how you recover from the above

Verify: you can state, without looking, the difference between revert and reset and which one is appropriate on a protected branch.

Lab 2 — Branching and merging, including conflicts

git switch -c feature/greeting
echo "Hello" > greet.txt && git add . && git commit -m "Add greeting"

git switch main
echo "Hi" > greet.txt && git add . && git commit -m "Add different greeting"

git merge feature/greeting       # CONFLICT
git status                        # shows "both added"
# Edit greet.txt, remove markers, choose content
git add greet.txt
git commit                        # completes the merge

git log --oneline --graph --all

Now do it again with rebase:

git switch -c feature/rebase-me main~1
echo "rebase" > r.txt && git add . && git commit -m "Rebase work"
git rebase main
git switch main && git merge --ff-only feature/rebase-me

Also practise: git cherry-pick, git stash push -m, git stash pop, git tag -a v0.1.0 -m "…", git push --tags, and an interactive rebase that squashes three commits into one (git rebase -i HEAD~3).

Verify: you can explain why --ff-only succeeded after rebasing, and what a merge commit's two parents are.

Lab 3 — Remotes and GitLab basics

  1. On gitlab.com, create a group cert-lab-<you> and a blank project git-basics inside it without a README.
  2. Push your local repo:
git remote add origin [email protected]:cert-lab-<you>/git-basics.git
git push -u origin main
git push origin --tags
  1. Explore in the UI and be able to find each of these quickly — navigation questions are cheap marks: - Repository → Files, Commits, Branches, Tags, Graph, Compare - Code → Merge requests; Plan → Issues, Issue boards, Milestones - Build → Pipelines, Jobs, Pipeline editor - Deploy → Releases, Package registry, Container registry - Settings → General, Members, Repository (protected branches/tags), CI/CD, Merge requests
  2. Use the Web IDE (press . on a project) to make a commit, and the single-file editor to make another.
git fetch origin
git pull --rebase origin main
git branch -vv

Verify: you know the difference between fetch and pull, and where in GitLab to change the default branch.

Lab 4 — Issue → branch → merge request flow

  1. Create issue "Add CONTRIBUTING guide". Assign it to yourself, add a label docs, a milestone v0.2, and a due date.
  2. From the issue, click Create merge request (this creates a branch named after the issue and a draft MR that says Closes #1).
  3. Locally:
git fetch origin
git switch 1-add-contributing-guide
echo "PRs welcome" > CONTRIBUTING.md
git add . && git commit -m "Add CONTRIBUTING guide"
git push
  1. In the MR: mark it ready, leave a review comment on a line, create a suggestion and apply it, resolve threads, approve, and merge with Squash commits and Delete source branch ticked.
  2. Confirm the issue auto-closed.

Know the closing patterns (Closes #1, Fixes #1, Resolves #1) and that they only fire when the MR merges into the default branch.

Verify: issue closed automatically, source branch deleted, one squashed commit on main.

Lab 5 — Protecting branches and enforcing review

  1. Settings → Repository → Protected branches: main — Allowed to merge: Maintainers; Allowed to push and merge: No one.
  2. Try git push origin main directly — it should be rejected.
  3. Settings → Merge requests: enable Pipelines must succeed and All threads must be resolved.
  4. (Premium trial) add an approval rule requiring 1 approval, and add a CODEOWNERS file:
# .gitlab/CODEOWNERS
*.md @your-username
[Backend][1]
/src/ @your-username
  1. Protect tags matching v*.

Verify: a direct push to main fails with a "protected branch" error; an MR without a passing pipeline can't be merged.

Lab 6 — GitLab Flavored Markdown, quick actions, and snippets

Quick actions are frequent exam content. In an issue comment try:

/assign me
/label ~bug ~"priority::high"
/milestone %v0.2
/estimate 2h
/spend 30m
/due in 3 days
/relate #1
/close

Also create a snippet, a wiki page, and an issue template at .gitlab/issue_templates/Bug.md.

Verify: you can list five quick actions and say which feature tier scoped labels (priority::high) need (Premium).


Part B — GitLab CI/CD (CI/CD Associate)

The CI/CD course objectives are broadly: describe CI/CD; how runners work; set up and configure runners; scope and persist variables at different levels; scaffold a test/build/review/deploy pipeline using feature branches; release and deployment workflows; artifacts and dependency caching; build and push images to the GitLab registry; and either SAST or code-quality scanning.

Key concepts to be able to explain

  • Pipeline → stages → jobs. Jobs in a stage run in parallel; stages run in order unless needs creates a DAG.
  • Pipeline types: branch, tag, merge request (detached), merged results (Premium), merge trains (Premium), scheduled, triggered, parent-child, multi-project, downstream.
  • Runner scopes: instance (shared), group, project. Executors: shell, docker, docker-autoscaler / docker-machine (legacy), kubernetes, instance, ssh, virtualbox/parallels, custom.
  • Runner authentication tokens (glrt-…) created from the UI/API are the current registration flow; the old registration tokens are deprecated.
  • Tags route jobs to runners; "Run untagged jobs" is a runner setting.
  • Protected runners only run jobs on protected branches/tags.
  • Artifacts vs cache: artifacts pass build outputs between jobs/stages and are downloadable/expire; cache speeds up dependency downloads between pipelines and is best-effort.
  • Variable precedence (highest first, simplified): trigger/scheduled/manual pipeline variables → project → group (closest subgroup wins) → instance → .gitlab-ci.yml job-level → .gitlab-ci.yml global → deployment variables → predefined variables. Re-check the docs page "CI/CD variable precedence" before the exam — it's a favourite question.
  • Masked / masked and hidden / protected / file variable types, and expanded vs raw values.
  • rules replaced only/except (still supported, not recommended). workflow:rules controls whether the pipeline is created at all.

Lab 7 — Your first pipeline

Project: cicd-lab. Use a tiny Python or Rust app — Rust suits you, but Python keeps container images small for quick iteration. The examples below use Python.

app.py
test_app.py
requirements.txt
.gitlab-ci.yml
# app.py
def add(a, b):
    return a + b
# test_app.py
from app import add
def test_add():
    assert add(2, 3) == 5
# requirements.txt
pytest
# .gitlab-ci.yml
stages: [build, test, deploy]

default:
  image: python:3.12-slim

build-job:
  stage: build
  script:
    - echo "Building on $CI_COMMIT_BRANCH at $CI_COMMIT_SHORT_SHA"
    - python -m py_compile app.py

unit-test:
  stage: test
  script:
    - pip install -r requirements.txt
    - pytest -v

lint:
  stage: test
  script:
    - pip install ruff
    - ruff check .
  allow_failure: true

deploy-job:
  stage: deploy
  script:
    - echo "Deploying..."
  rules:
    - if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH

Push it, then use Build → Pipeline editor → Validate and Full configuration (shows the merged YAML after includes). Also run the CI Lint tool.

Verify: unit-test and lint run in parallel; deploy-job only appears on main; a failed lint shows a warning icon but doesn't fail the pipeline.

Lab 8 — Predefined variables and job logs

Add a job that prints environment context:

debug-env:
  stage: build
  script:
    - echo "Project: $CI_PROJECT_PATH"
    - echo "Pipeline source: $CI_PIPELINE_SOURCE"
    - echo "Ref: $CI_COMMIT_REF_NAME (slug $CI_COMMIT_REF_SLUG)"
    - echo "Runner: $CI_RUNNER_DESCRIPTION tags=$CI_RUNNER_TAGS"
    - echo "Job token user: $GITLAB_USER_LOGIN"

Then try collapsible log sections:

  script:
    - echo -e "\e[0Ksection_start:$(date +%s):deps[collapsed=true]\r\e[0KInstalling deps"
    - pip install -r requirements.txt
    - echo -e "\e[0Ksection_end:$(date +%s):deps\r\e[0K"

Set CI_DEBUG_TRACE: "true" once on a throwaway branch to see what debug tracing exposes — and understand why it's dangerous (it can print secrets).

Verify: you can name 10 predefined variables from memory, including CI_PIPELINE_SOURCE values (push, merge_request_event, schedule, web, api, trigger, pipeline, parent_pipeline).

Lab 9 — Install and register runners (shell, Docker, Kubernetes)

9a. Docker executor on a VM

In the project: Settings → CI/CD → Runners → New project runner. Add tags docker,lab, leave "Run untagged jobs" unticked, create it, and copy the glrt-… token.

# Runner host
curl -L "https://packages.gitlab.com/install/repositories/runner/gitlab-runner/script.deb.sh" | sudo bash
sudo apt-get install -y gitlab-runner docker.io
sudo usermod -aG docker gitlab-runner

sudo gitlab-runner register \
  --non-interactive \
  --url "https://gitlab.com" \
  --token "glrt-REDACTED" \
  --executor "docker" \
  --docker-image "alpine:3.20" \
  --description "lab-docker-runner"

sudo gitlab-runner verify
sudo gitlab-runner list
sudo cat /etc/gitlab-runner/config.toml

Study config.toml: concurrent (global) vs limit (per runner), [runners.docker] → privileged, volumes, pull_policy; [runners.cache].

9b. Shell executor

Register a second runner on the same host with --executor shell and tag shell. Notice jobs run as the gitlab-runner user with no isolation — be able to explain the security trade-off.

9c. Kubernetes executor on k3s (Helm)

Create another runner token (tag k8s), then:

helm repo add gitlab https://charts.gitlab.io
helm repo update

kubectl create namespace gitlab-runner
kubectl -n gitlab-runner create secret generic runner-token \
  --from-literal=runner-registration-token="" \
  --from-literal=runner-token="glrt-REDACTED"

cat > runner-values.yaml <<'YAML'
gitlabUrl: https://gitlab.com/
runners:
  secret: runner-token
  config: |
    [[runners]]
      [runners.kubernetes]
        namespace = "gitlab-runner"
        image = "alpine:3.20"
rbac:
  create: true
YAML

helm install gitlab-runner gitlab/gitlab-runner -n gitlab-runner -f runner-values.yaml
kubectl -n gitlab-runner logs deploy/gitlab-runner -f

9d. Route jobs with tags

on-docker:
  tags: [docker]
  script: [cat /etc/os-release]
on-shell:
  tags: [shell]
  image: null   # ignored by shell executor anyway
  script: [hostname, whoami]
on-k8s:
  tags: [k8s]
  script: [env | grep KUBERNETES]

Then disable instance runners for the project and confirm untagged jobs get stuck ("This job is stuck because…"). Pause a runner and observe the same.

Verify: each job lands on the intended runner; you can explain why a job is stuck from its message; you can explain instance vs group vs project runners and protected runners.

Lab 10 — rules, workflow, and pipeline types

workflow:
  rules:
    - if: $CI_PIPELINE_SOURCE == "merge_request_event"
    - if: $CI_COMMIT_BRANCH && $CI_OPEN_MERGE_REQUESTS
      when: never                       # avoid duplicate branch + MR pipelines
    - if: $CI_COMMIT_BRANCH
    - if: $CI_COMMIT_TAG

docs-check:
  script: echo "docs changed"
  rules:
    - changes: ["docs/**/*", "*.md"]

manual-smoke:
  script: echo "smoke test"
  rules:
    - if: $CI_PIPELINE_SOURCE == "merge_request_event"
      when: manual
      allow_failure: true

nightly:
  script: echo "nightly job"
  rules:
    - if: $CI_PIPELINE_SOURCE == "schedule"

release-only:
  script: echo "tag $CI_COMMIT_TAG"
  rules:
    - if: $CI_COMMIT_TAG =~ /^v\d+\.\d+\.\d+$/

delayed:
  script: echo "runs later"
  when: delayed
  start_in: 2 minutes
  rules:
    - if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH

Then: 1. Open an MR — observe an MR pipeline (labelled merge request) and no duplicate branch pipeline. 2. Create a pipeline schedule (Build → Pipeline schedules) and run it manually. 3. Push a tag v1.0.0. 4. Run a pipeline from the UI with a variable set.

Know the when values: on_success (default), on_failure, always, manual, delayed, never. Know the rules clauses: if, changes, exists, allow_failure, variables, when, needs.

Verify: the right jobs appear for each of the four pipeline sources, and you can explain why workflow:rules prevented duplicates.

Lab 11 — Variables at every level

  1. Project variable APP_ENV=dev (Settings → CI/CD → Variables).
  2. Group variable APP_ENV=group and GROUP_ONLY=yes on the parent group.
  3. Masked and hidden variable API_KEY (value must meet masking requirements: ≥ 8 chars, single line).
  4. Protected variable DEPLOY_TOKEN — only available on protected branches/tags.
  5. File type variable KUBECONFIG_FILE — the variable holds a path to a temp file.
  6. An environment-scoped variable DB_HOST with scope staging and another with scope production.
variables:
  APP_ENV: "yaml-global"
  GREETING: "hello"

show-vars:
  variables:
    GREETING: "job-level"
  script:
    - echo "APP_ENV=$APP_ENV"         # project setting wins over YAML
    - echo "GROUP_ONLY=$GROUP_ONLY"
    - echo "GREETING=$GREETING"       # job-level wins over global YAML
    - echo "API_KEY=$API_KEY"         # shows [MASKED]
    - echo "DEPLOY_TOKEN=${DEPLOY_TOKEN:-unset-on-unprotected-ref}"
    - cat "$KUBECONFIG_FILE" | head -1

show-env-scoped:
  environment: staging
  script: echo "DB_HOST=$DB_HOST"

Run on main (protected) and on a feature branch, then override APP_ENV from Run pipeline.

Also pass variables between jobs with a dotenv report:

compute-version:
  stage: build
  script:
    - echo "VERSION=1.2.$CI_PIPELINE_IID" > build.env
  artifacts:
    reports:
      dotenv: build.env

use-version:
  stage: test
  script: echo "Version is $VERSION"

Verify: you can predict every printed value before running, and explain why DEPLOY_TOKEN is empty on the feature branch.

Lab 12 — needs, DAGs, parallel, and reuse

stages: [build, test, deploy]

.python-base:            # hidden job used as a template
  image: python:3.12-slim
  before_script:
    - pip install -r requirements.txt

build-a:
  stage: build
  script: echo a
build-b:
  stage: build
  script: sleep 30 && echo b

test-a:
  extends: .python-base
  stage: test
  needs: [build-a]        # starts as soon as build-a finishes, not waiting for build-b
  script: pytest

test-matrix:
  extends: .python-base
  stage: test
  needs: []               # starts immediately
  parallel:
    matrix:
      - PYTHON: ["3.11", "3.12"]
        OS: ["slim", "bookworm"]
  image: python:${PYTHON}-${OS}
  script: python --version

deploy:
  stage: deploy
  needs: [test-a, test-matrix]
  script: echo deploy

Also try YAML anchors and !reference:

.setup:
  script:
    - echo "shared setup"

job-with-reference:
  script:
    - !reference [.setup, script]
    - echo "own step"

Know the difference between extends (GitLab merge, works across includes) and YAML anchors (pure YAML, same file only).

Verify: the pipeline's Needs view shows the DAG; test-a starts before build-b finishes; four matrix jobs appear.

Lab 13 — Artifacts and cache

variables:
  PIP_CACHE_DIR: "$CI_PROJECT_DIR/.cache/pip"

cache:
  key:
    files: [requirements.txt]
  paths: [.cache/pip]

build:
  stage: build
  script:
    - mkdir -p dist && echo "binary" > dist/app.bin
  artifacts:
    paths: [dist/]
    expire_in: 1 week
    name: "$CI_JOB_NAME-$CI_COMMIT_SHORT_SHA"

test:
  stage: test
  script:
    - pip install -r requirements.txt
    - pytest --junitxml=report.xml --cov=. --cov-report=xml
    - ls dist/                       # artifact from build is present
  coverage: '/TOTAL.*\s+(\d+%)$/'
  artifacts:
    when: always
    reports:
      junit: report.xml
      coverage_report:
        coverage_format: cobertura
        path: coverage.xml

no-artifacts-needed:
  stage: test
  dependencies: []                   # skip downloading artifacts
  script: echo "fast"

(Add pytest-cov to requirements.txt.)

Things to observe: - Tests tab on the pipeline (JUnit). - Coverage percentage on the job and MR, and coverage visualisation in the MR diff. - Download/browse artifacts from the job page; Keep button. - Cache hit/miss lines in the log; clear runner caches from the Pipelines page. - cache:policy: pull for jobs that only read the cache, pull-push default.

Verify: you can explain five differences between cache and artifacts and when dependencies: [] or needs: [{job: x, artifacts: false}] is useful.

Lab 14 — Build and push to the Container Registry

# Dockerfile
FROM python:3.12-slim
WORKDIR /app
COPY . .
RUN pip install -r requirements.txt
CMD ["python", "-c", "from app import add; print(add(2,3))"]

Docker-in-Docker (needs a privileged = true Docker runner):

build-image:
  stage: build
  image: docker:27
  services: [docker:27-dind]
  variables:
    DOCKER_TLS_CERTDIR: "/certs"
  tags: [docker]
  before_script:
    - echo "$CI_REGISTRY_PASSWORD" | docker login -u "$CI_REGISTRY_USER" --password-stdin "$CI_REGISTRY"
  script:
    - docker build -t "$CI_REGISTRY_IMAGE:$CI_COMMIT_SHORT_SHA" .
    - docker push "$CI_REGISTRY_IMAGE:$CI_COMMIT_SHORT_SHA"
    - |
      if [ "$CI_COMMIT_BRANCH" = "$CI_DEFAULT_BRANCH" ]; then
        docker tag "$CI_REGISTRY_IMAGE:$CI_COMMIT_SHORT_SHA" "$CI_REGISTRY_IMAGE:latest"
        docker push "$CI_REGISTRY_IMAGE:latest"
      fi

Rootless alternative that works on the k8s runner — Buildah:

build-image-buildah:
  stage: build
  image: quay.io/buildah/stable
  tags: [k8s]
  variables:
    STORAGE_DRIVER: vfs
  script:
    - buildah login -u "$CI_REGISTRY_USER" -p "$CI_REGISTRY_PASSWORD" "$CI_REGISTRY"
    - buildah bud -t "$CI_REGISTRY_IMAGE:buildah-$CI_COMMIT_SHORT_SHA" .
    - buildah push "$CI_REGISTRY_IMAGE:buildah-$CI_COMMIT_SHORT_SHA"

Then consume the image in a later job (image: $CI_REGISTRY_IMAGE:$CI_COMMIT_SHORT_SHA), set a cleanup policy (Settings → Packages and registries), and pull it locally with a deploy token.

Know: CI_REGISTRY, CI_REGISTRY_IMAGE, CI_REGISTRY_USER, CI_REGISTRY_PASSWORD, CI_JOB_TOKEN, and the difference between deploy tokens, project/group access tokens and personal access tokens.

Verify: the image appears under Deploy → Container registry with two tags on main and one on feature branches.

Lab 15 — Package registry

Publish a generic package and a Python package using CI_JOB_TOKEN:

publish-generic:
  stage: deploy
  image: curlimages/curl:latest
  rules:
    - if: $CI_COMMIT_TAG
  script:
    - echo "artifact for $CI_COMMIT_TAG" > tool.txt
    - >
      curl --fail --header "JOB-TOKEN: $CI_JOB_TOKEN"
      --upload-file tool.txt
      "$CI_API_V4_URL/projects/$CI_PROJECT_ID/packages/generic/mytool/${CI_COMMIT_TAG#v}/tool.txt"

For a Rust angle, you can also publish a release binary from one of your *-adm tools as a generic package — it maps neatly onto how you already distribute CLIs to /opt/tools.

Verify: package visible under Deploy → Package registry, downloadable with a PAT.

Lab 16 — Environments, review apps, and deployments

Use GitLab Pages-style static output or a simple container deploy to k3s. A no-infrastructure version that still exercises every feature:

stages: [build, test, review, staging, production]

deploy-review:
  stage: review
  script: echo "Deploy review app for $CI_COMMIT_REF_SLUG"
  environment:
    name: review/$CI_COMMIT_REF_SLUG
    url: https://$CI_COMMIT_REF_SLUG.review.lab.example
    on_stop: stop-review
    auto_stop_in: 2 days
  rules:
    - if: $CI_PIPELINE_SOURCE == "merge_request_event"

stop-review:
  stage: review
  script: echo "Tear down $CI_COMMIT_REF_SLUG"
  environment:
    name: review/$CI_COMMIT_REF_SLUG
    action: stop
  rules:
    - if: $CI_PIPELINE_SOURCE == "merge_request_event"
      when: manual
      allow_failure: true

deploy-staging:
  stage: staging
  script: echo "Deploying to staging"
  environment:
    name: staging
    url: https://staging.lab.example
  rules:
    - if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH

deploy-production:
  stage: production
  script: echo "Deploying to production"
  environment:
    name: production
    url: https://prod.lab.example
    deployment_tier: production
  resource_group: production        # prevents concurrent prod deploys
  rules:
    - if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
      when: manual

Then: 1. Open an MR — see the View app button and the review environment. 2. Merge — staging deploys; production waits for manual action. 3. Operate → Environments: re-deploy an older deployment (rollback), stop a review env. 4. (Premium trial) Protected environments — only Maintainers can deploy to production; add a deployment approval.

For a real deploy, put a kubectl apply in deploy-staging using the GitLab agent for Kubernetes (agentk) registered against your k3s cluster — the agent is the current recommended way and replaces certificate-based cluster integration (removed).

Verify: environment list shows review/staging/production, a rollback produces a new deployment entry, and two production jobs can't run concurrently.

Lab 17 — Releases

create-release:
  stage: production
  image: registry.gitlab.com/gitlab-org/release-cli:latest
  rules:
    - if: $CI_COMMIT_TAG =~ /^v\d+\.\d+\.\d+$/
  script:
    - echo "Creating release $CI_COMMIT_TAG"
  release:
    tag_name: $CI_COMMIT_TAG
    name: "Release $CI_COMMIT_TAG"
    description: "Automated release for $CI_COMMIT_TAG"
    assets:
      links:
        - name: "tool.txt"
          url: "$CI_API_V4_URL/projects/$CI_PROJECT_ID/packages/generic/mytool/${CI_COMMIT_TAG#v}/tool.txt"
          link_type: package

GitLab has been moving the release: keyword's implementation from release-cli to the glab CLI. Check the current release keyword docs for which image is recommended in your GitLab version — the YAML keyword itself is what the exam cares about.

Also create a release manually from Deploy → Releases, attach a milestone, and generate a changelog from commit trailers:

git commit -m "Add feature X" --trailer "Changelog: added"
glab changelog generate --version 1.1.0

Verify: release v1.1.0 shows the tag, evidence collection, linked milestone and package asset.

Lab 18 — Includes, templates, child pipelines, and CI/CD components

  1. include types:
include:
  - local: ci/lint.yml
  - project: cert-lab-<you>/ci-templates
    ref: main
    file: /templates/python.yml
  - remote: https://example.com/shared.yml      # public URL
  - template: Jobs/Code-Quality.gitlab-ci.yml
  - component: $CI_SERVER_FQDN/cert-lab-<you>/components/[email protected]
    inputs:
      python_version: "3.12"
  1. Build a CI/CD component in project components:
# templates/python-test.yml
spec:
  inputs:
    python_version:
      default: "3.12"
    stage:
      default: test
---
python-test:
  stage: $[[ inputs.stage ]]
  image: python:$[[ inputs.python_version ]]-slim
  script:
    - pip install -r requirements.txt
    - pytest

Tag it 1.0.0, add a release (components need a release to appear in the CI/CD Catalog), and mark the project as a catalog resource in its settings.

  1. Parent-child pipeline:
trigger-child:
  stage: test
  trigger:
    include: ci/child.yml
    strategy: depend          # parent waits for and mirrors child status
  1. Multi-project pipeline:
trigger-downstream:
  stage: deploy
  trigger:
    project: cert-lab-<you>/downstream-app
    branch: main
  1. Dynamic child pipeline: generate YAML in one job, save as artifact, trigger it with trigger:include:artifact.

Verify: "Full configuration" in the pipeline editor shows merged includes; child and downstream pipelines render as linked pipelines; your component appears in the catalog.

Lab 19 — Code Quality and SAST in CI (CI/CD-level coverage)

include:
  - template: Jobs/SAST.gitlab-ci.yml
  - template: Jobs/Secret-Detection.gitlab-ci.yml
  - template: Jobs/Code-Quality.gitlab-ci.yml

stages: [build, test]

Commit something deliberately bad (password = "hunter2hunter2", eval(input())) on a branch and open an MR.

On Free tier, results are downloadable JSON artifacts; the MR security widget and vulnerability report need Ultimate. Code Quality's MR widget works on Free.

Verify: SAST, secret detection and code quality jobs run automatically; you can find their report artifacts.

CI/CD hands-on mock project (do this before booking)

Give yourself 60 minutes and build, from an empty project, a pipeline that:

  1. Has stages build, test, review, deploy.
  2. Builds a container image and pushes it to the project registry tagged with the short SHA.
  3. Runs unit tests with JUnit and coverage reports surfaced in the MR.
  4. Caches dependencies keyed on the lock file.
  5. Runs only once per MR (no duplicate branch pipelines).
  6. Creates a review environment per MR with a manual stop job.
  7. Deploys to production manually, only from the default branch, with a resource_group.
  8. Uses a masked project variable and a protected variable correctly.
  9. Includes SAST via template.
  10. Creates a release when a semantic-version tag is pushed.

Then write a short README explaining how each requirement is met — graders appreciate it, and doing so catches gaps.


Part C — Professional Services Engineer path

C1. Project Management Associate

Concepts

  • Issues (and the newer work-item framework: tasks, objectives/key results, epics as work items), labels (scoped labels key::value are Premium), milestones (project or group), iterations and iteration cadences (Premium), weights (Premium), time tracking, health status (Ultimate), issue boards (multiple boards and list types beyond labels are Premium), epics and roadmaps (Premium; multi-level epics Ultimate), related/blocking issues, service desk, requirements and test cases (Ultimate).
  • How Scrum/Kanban map onto GitLab: epics ↔ features, issues ↔ user stories, tasks ↔ sub-tasks, iterations ↔ sprints, milestones ↔ releases, weights ↔ story points, boards ↔ sprint/kanban boards, burndown/burnup charts.
  • Value Stream Analytics and contribution analytics at a high level.

Lab 20 — Agile planning end to end (use the Ultimate trial group)

  1. In group cert-lab-<you>, create scoped labels workflow::todo, workflow::doing, workflow::review, workflow::done, and type::feature, type::bug.
  2. Create an iteration cadence of two-week iterations.
  3. Create a group milestone Release 1.0 with start and due dates.
  4. Create an epic "User authentication" with two child epics, a start/due date, and attach six issues spread across two projects.
  5. Give each issue a weight, assign to the iteration, add estimates with /estimate.
  6. Build a group issue board with lists for each workflow:: label; drag issues across and observe that scoped labels are mutually exclusive.
  7. Mark one issue as blocked by another.
  8. View the roadmap, the milestone burndown, and the iteration report.
  9. Enable Service Desk on a project and create an issue by email (self-managed needs incoming email configured; on gitlab.com it works out of the box).

Verify: you can explain which features need Premium vs Ultimate, and how weights roll up into burndown charts.

C2. Security Specialist

Concepts

  • Scanner types and what each needs:
  • SAST — source code; language auto-detected; analyzers mostly Semgrep-based now.
  • Secret Detection — commits/source; also secret push protection (blocks pushes) on Ultimate.
  • Dependency Scanning — lock files/manifests; produces an SBOM (CycloneDX).
  • Container Scanning — built images (Trivy-based).
  • DAST — a running application (review app or staging URL); browser-based analyser; also API security testing.
  • IaC scanning — Terraform, k8s manifests, etc.
  • Fuzz testing (coverage-guided and API), License compliance via the dependency list / license approval policies.
  • Vulnerability Report, Security Dashboard, vulnerability states (Needs triage, Confirmed, Dismissed, Resolved) and dismissal reasons.
  • Security policies (Ultimate): scan execution policies (force scans to run), merge request approval policies (formerly scan result policies — require approval when new vulnerabilities/licences appear), pipeline execution policies; stored in a linked security policy project.
  • Compliance frameworks, compliance center, audit events, separation of duties.
  • Shift-left: findings in the MR widget before merge.

Lab 21 — Enable all scanners

In an Ultimate-trial project with a deliberately vulnerable app (e.g. clone a known-vulnerable sample such as an old Flask app pinned to vulnerable dependency versions):

include:
  - template: Jobs/SAST.gitlab-ci.yml
  - template: Jobs/Secret-Detection.gitlab-ci.yml
  - template: Jobs/Dependency-Scanning.gitlab-ci.yml
  - template: Jobs/Container-Scanning.gitlab-ci.yml
  - template: Jobs/SAST-IaC.gitlab-ci.yml

stages: [build, test]

variables:
  CS_IMAGE: $CI_REGISTRY_IMAGE:$CI_COMMIT_SHORT_SHA

(Reuse the build-image job from Lab 14.) Also try Secure → Security configuration in the UI, which can generate an MR enabling scanners for you.

Verify: findings appear in the Vulnerability Report and the MR security widget.

Lab 22 — DAST against a review app

Deploy an intentionally vulnerable app (OWASP Juice Shop container works well) to your k3s cluster from Lab 16's review environment, then:

include:
  - template: DAST.gitlab-ci.yml

stages: [build, test, review, dast]

variables:
  DAST_TARGET_URL: https://$CI_COMMIT_REF_SLUG.review.lab.example

Know the difference between passive (default) and active scans, and why active scans should only target environments you own.

Verify: DAST job runs after the review deployment and reports findings.

Lab 23 — Triage vulnerabilities

  1. Dismiss one finding as "false positive" with a comment.
  2. Confirm one and create an issue from it.
  3. Resolve one by fixing code; rerun on main and watch it move to "No longer detected".
  4. Export the vulnerability report as CSV.

Lab 24 — Security policies

  1. Secure → Policies → New policy → Scan execution policy: run Secret Detection on every pipeline for all branches. Observe the security policy project GitLab creates.
  2. Merge request approval policy: require one approval from a specific user when an MR introduces a new Critical or High vulnerability.
  3. Open an MR that adds a hard-coded secret — approval is now required.

Verify: the policy project contains .gitlab/security-policies/policy.yml and the MR is blocked pending approval.

Lab 25 — Compliance and auditing

Create a compliance framework label on the group, apply it to a project, and look at audit events for the group. Set up push rules (Premium): reject unsigned commits, enforce commit message regex, prevent secrets files.

Lab 26 — Signed commits

git config --global gpg.format ssh
git config --global user.signingkey ~/.ssh/id_ed25519.pub
git commit -S -m "Signed commit"

Add the key as a signing key in GitLab; confirm the "Verified" badge.

C3. Implementation Services Specialist

Concepts

  • Install methods: Linux package (Omnibus), Helm chart (cloud native, recommended for Kubernetes but with stateful components — PostgreSQL, Redis, Gitaly — kept outside the cluster for production), GitLab Operator, Docker image, source. Also GitLab Dedicated (single-tenant SaaS) vs GitLab.com.
  • Architecture components: NGINX → GitLab Workhorse → Puma (Rails); Sidekiq (background jobs) fed by Redis; PostgreSQL (with PgBouncer, Patroni for HA); Gitaly (all Git storage access via gRPC) and Gitaly Cluster/Praefect (replication; note GitLab is moving toward Gitaly Cluster with Raft — check current status); GitLab Shell (SSH); Container Registry; GitLab Pages; KAS (agent server); Prometheus/exporters; Mattermost (optional); object storage for artifacts, LFS, uploads, packages, etc.
  • Reference architectures: sized by requests per second (RPS) and roughly by user count; standalone vs HA; cloud-native hybrid. Know that above the smallest sizes, object storage is required and NFS is not supported for Git data.
  • Geo (Premium): read-only secondary sites for DR and geographic performance; promotion of a secondary for disaster recovery.
  • Configuration: /etc/gitlab/gitlab.rb → gitlab-ctl reconfigure (Chef/Cinc under the hood).
  • Secrets: /etc/gitlab/gitlab-secrets.json and gitlab.rb are not in application backups — back them up separately. Losing gitlab-secrets.json makes encrypted data (CI variables, 2FA, runner tokens) unreadable.
  • Upgrades: follow the upgrade path with required stops; check background migrations complete before each next step; zero-downtime upgrades are only possible in multi-node setups.
  • Authentication: LDAP (Free on self-managed; group sync is Premium), SAML, OmniAuth providers, 2FA enforcement.
  • Admin area: instance settings, sign-up restrictions, rate limits, CI/CD settings (instance runners, max artifact size), users, impersonation, broadcast messages, licence.

Lab 27 — Explore the Omnibus install

On the Lab 0 VM:

sudo gitlab-ctl status
sudo gitlab-ctl tail                      # all logs; Ctrl-C to stop
sudo gitlab-ctl tail gitlab-rails/production_json.log
ls /var/log/gitlab/
sudo gitlab-rake gitlab:env:info
sudo gitlab-rake gitlab:check SANITIZE=true
sudo gitlab-ctl show-config | less
sudo gitlab-rails console                  # explore carefully; e.g. User.count
sudo gitlab-psql -c 'select count(*) from projects;'
ls /var/opt/gitlab/                        # git-data, postgresql, redis, etc.

Map each gitlab-ctl status service to the architecture list above.

Verify: you can explain the request path for an HTTPS git clone vs an SSH git clone.

Lab 28 — Configure the instance

Edit /etc/gitlab/gitlab.rb:

external_url 'https://gitlab.lab.example'

# TLS with your own CA (your tls-cli private CA is ideal here)
nginx['ssl_certificate']     = "/etc/gitlab/ssl/gitlab.lab.example.crt"
nginx['ssl_certificate_key'] = "/etc/gitlab/ssl/gitlab.lab.example.key"
letsencrypt['enable'] = false

# Email via your Postfix relay
gitlab_rails['smtp_enable']  = true
gitlab_rails['smtp_address'] = "relay.lab.example"
gitlab_rails['smtp_port']    = 25
gitlab_rails['gitlab_email_from'] = '[email protected]'

# Container registry
registry_external_url 'https://registry.gitlab.lab.example'

# Reduce memory for a lab VM
puma['worker_processes'] = 2
sidekiq['concurrency'] = 10
prometheus_monitoring['enable'] = false
sudo gitlab-ctl reconfigure
sudo gitlab-rails runner "Notify.test_email('[email protected]', 'Test', 'Hello').deliver_now"

In the Admin area: disable public sign-up, require admin approval for new users, enforce 2FA for admins, set a broadcast message, set instance-level CI/CD variable, set default branch protection.

Verify: HTTPS works with your CA trusted, test email arrives, docker login registry.gitlab.lab.example succeeds.

Lab 29 — LDAP authentication

Run an OpenLDAP container (e.g. osixia/openldap or bitnami/openldap) on the lab network with a couple of users, then:

gitlab_rails['ldap_enabled'] = true
gitlab_rails['ldap_servers'] = {
  'main' => {
    'label' => 'Lab LDAP',
    'host' => 'ldap.lab.example',
    'port' => 389,
    'uid' => 'uid',
    'encryption' => 'plain',
    'bind_dn' => 'cn=admin,dc=lab,dc=example',
    'password' => 'REPLACE_ME',
    'base' => 'ou=people,dc=lab,dc=example'
  }
}
sudo gitlab-ctl reconfigure
sudo gitlab-rake gitlab:ldap:check

Verify: an LDAP user can sign in; you can explain what happens to a user removed from LDAP (blocked on next sync).

Lab 30 — Runners for the self-managed instance

Create an instance runner in Admin → CI/CD → Runners and register it against https://gitlab.lab.example using the Docker executor. Because you're using a private CA:

sudo cp lab-ca.crt /etc/gitlab-runner/certs/gitlab.lab.example.crt
# or: gitlab-runner register --tls-ca-file /path/to/lab-ca.crt ...

Set instance-wide limits: max artifact size, default artifact expiry, CI/CD minutes quota (compute quota) on a group.

Verify: a pipeline in any project on the instance uses the instance runner.

Lab 31 — Backup and restore (high exam value)

# Back up application data
sudo gitlab-backup create STRATEGY=copy
ls -lh /var/opt/gitlab/backups/

# Back up configuration and secrets separately (NOT in the above)
sudo tar czf /root/gitlab-config-$(date +%F).tgz /etc/gitlab/gitlab.rb /etc/gitlab/gitlab-secrets.json /etc/gitlab/ssl

Configure scheduled backups and upload to object storage (point it at a MinIO container on your NAS):

gitlab_rails['backup_keep_time'] = 604800
gitlab_rails['backup_upload_connection'] = {
  'provider' => 'AWS',
  'region' => 'us-east-1',
  'aws_access_key_id' => 'REPLACE_ME',
  'aws_secret_access_key' => 'REPLACE_ME',
  'endpoint' => 'http://minio.lab.example:9000',
  'path_style' => true
}
gitlab_rails['backup_upload_remote_directory'] = 'gitlab-backups'

Now restore onto a fresh VM — this is the real test:

# On a new VM with the SAME GitLab version and edition installed
sudo cp gitlab.rb gitlab-secrets.json /etc/gitlab/
sudo gitlab-ctl reconfigure
sudo cp <timestamp>_gitlab_backup.tar /var/opt/gitlab/backups/
sudo chown git:git /var/opt/gitlab/backups/<timestamp>_gitlab_backup.tar

sudo gitlab-ctl stop puma
sudo gitlab-ctl stop sidekiq
sudo gitlab-ctl status

sudo gitlab-backup restore BACKUP=<timestamp>     # omit _gitlab_backup.tar
sudo gitlab-ctl restart
sudo gitlab-rake gitlab:check SANITIZE=true
sudo gitlab-rake gitlab:doctor:secrets             # confirms encrypted data is readable

Then repeat the restore without restoring gitlab-secrets.json and observe the breakage (CI variables, runner auth) — you'll never forget why it matters.

Verify: projects, issues, CI variables and container images (if registry included) are present after restore; you can state the "same version and edition" rule.

Lab 32 — Upgrade along the upgrade path

Revert to a snapshot, install an older version deliberately, then upgrade across at least one required stop:

apt-cache madison gitlab-ee | head -20
sudo apt-get install gitlab-ee=<older-version>-ee.0
# ... use it a bit ...
sudo gitlab-backup create
sudo apt-get install gitlab-ee=<next-required-stop>-ee.0
sudo gitlab-rake gitlab:background_migrations:status   # wait for all to finish
# Admin → Monitoring → Background migrations shows the same
sudo apt-get install gitlab-ee=<target-version>-ee.0

Use GitLab's Upgrade Path tool on docs to pick the stops.

Verify: you can explain why skipping required stops or not waiting for batched background migrations breaks upgrades.

Lab 33 — Cloud-native install (Helm) in k3s — awareness level

You don't need a production-grade install, but install the chart once with minimal resources so you recognise its components:

helm repo add gitlab https://charts.gitlab.io
helm upgrade --install gitlab gitlab/gitlab \
  --namespace gitlab --create-namespace \
  --set global.hosts.domain=k3s.lab.example \
  --set global.edition=ce \
  --set certmanager-issuer.email=[email protected] \
  --set installCertmanager=false \
  --set global.ingress.configureCertmanager=false \
  --set gitlab-runner.install=false \
  --set prometheus.install=false \
  --timeout 600s
kubectl -n gitlab get pods

It wants roughly 8 GB+ RAM on the node — tear it down when done. Identify webservice, sidekiq, gitaly, gitlab-shell, registry, toolbox (backups via backup-utility), kas, minio, postgresql, redis pods, and note the production guidance to move Gitaly, PostgreSQL and Redis out of the cluster.

Verify: you can explain the "cloud native hybrid" reference architecture.

C4. Migration Services Specialist

Concepts

  • What travels with Git vs what doesn't. A git clone --mirror carries commits, branches, tags and notes — the "Git envelope". It does not carry issues, MRs/PRs, comments, wikis (separate repo), CI variables, protected branch settings, webhooks, members/permissions, LFS objects (separately fetched), releases, packages or container images.
  • Migration methods into GitLab:
  • Direct transfer — group and project migration between GitLab instances via the API (the recommended method; replaces file-based group export/import).
  • File export/import — per project, version-compatible.
  • Importers — GitHub, Bitbucket Cloud, Bitbucket Server/Data Center, Gitea, FogBugz, manifest file, repository by URL.
  • Congregate — GitLab Professional Services' open-source migration automation tool for large, batch migrations (lists, stages, user mapping, waves); the PS certification expects you to know it.
  • Mirroring — push mirroring (Free) and pull mirroring (Premium) for gradual cut-overs.
  • User contribution mapping: newer GitLab versions import contributions against placeholder users, which an owner later reassigns to real users. Older behaviour matched on public email. Expect questions on how authorship is preserved.
  • Pre-migration discovery: repo counts and sizes, LFS usage, large binaries, CI system in use (Jenkins etc.), user counts and identity provider, integrations, required cut-over window, freeze plan.
  • Common problems: repos over size limits, LFS pointers without objects, timeouts on big imports, rate limits on the source API, missing users, mismatched GitLab versions for file imports, Sidekiq queue backlog on the target.
  • Post-migration validation: commit/branch/tag counts, MR/issue counts, spot checks, CI working, archive the source.

Lab 34 — Raw Git envelope migration

git clone --mirror https://github.com/<some-small-public-repo>.git
cd <repo>.git
git lfs fetch --all 2>/dev/null || true
git remote set-url --push origin [email protected]:migrated/<repo>.git
git push --mirror
git lfs push --all origin 2>/dev/null || true

Compare git for-each-ref | wc -l on source and target.

Verify: you can list everything that didn't come across.

Lab 35 — GitHub importer

On the self-managed instance: Admin → Settings → General → Import and export settings → enable GitHub. Then New project → Import project → GitHub, authorise with a GitHub PAT, import a repo with issues and PRs. Observe how PRs become MRs and how unmatched users become placeholders; then reassign them.

Verify: issues, PRs (as MRs), labels, milestones and comments arrived; placeholder reassignment works.

Lab 36 — Direct transfer between instances

Move a group from gitlab.com to your lab instance:

  1. On gitlab.com, create a PAT with api scope.
  2. On the lab instance (as admin), enable Allow migrating GitLab groups and projects by direct transfer in import settings.
  3. New group → Import group → enter https://gitlab.com and the PAT → select cert-lab-<you> → import with projects.
  4. Watch Admin → Monitoring → Background jobs (Sidekiq) and the import history page.

Verify: subgroups, projects, epics (if licensed), issues, MRs, labels and milestones transferred; CI/CD variables did not (they're excluded — know this).

Lab 37 — File-based project export/import

Export a project (Settings → General → Advanced → Export project), import it into the lab instance, and note the version-compatibility rules and what's excluded (e.g. CI variables, job artifacts, container registry images, webhooks).

Lab 38 — Congregate (awareness/hands-on)

Clone gitlab-org/professional-services-automation/tools/migration/congregate from gitlab.com and read its docs: configuration file, list → stage → migrate workflow, user migration and mapping, and its dry-run mode. If you can, run it in a container between gitlab.com and your lab instance for one small group. At minimum, be able to describe the phases and why PS uses it instead of hand-run importers (scale, repeatability, waves, reporting).

Verify: you can describe a migration plan for "500 Bitbucket Server repos, 300 users, Jenkins CI, one weekend cut-over" covering discovery, pilot wave, user mapping, CI conversion, freeze, cut-over, validation, rollback.


Part D — Capstone and review

Lab 39 — End-to-end capstone

On your self-managed instance, timed at 3 hours:

  1. Fresh install from snapshot, HTTPS with your private CA, SMTP working.
  2. LDAP sign-in, admin approval required for sign-up.
  3. Instance runner (Docker) + group runner (k8s), with tags.
  4. Import a GitHub repo with issues/PRs; reassign placeholders.
  5. Set up a group with scoped workflow labels, a milestone and a board.
  6. Implement the CI/CD mock project from Part B in the imported repo.
  7. Enable SAST + secret detection + dependency scanning.
  8. Take a backup, destroy the VM, restore onto a new one, and prove pipelines still run (CI variables decrypt, runners reconnect).

Self-check questions

Answer these without notes. If any take more than a few seconds, go back to the relevant lab.

Git and GitLab 1. What does git reset --mixed change, and what does it leave alone? 2. Why use git revert rather than git reset on main? 3. What happens to an issue when an MR containing Closes #12 merges into a non-default branch? 4. Order the roles from least to most privileged. 5. What's the difference between a fork and a branch, and when must a contributor fork? 6. Which merge method produces a perfectly linear history?

CI/CD 7. Where is a job's stage default if none is set? (test) 8. What's the difference between needs: [] and omitting needs? 9. How do you prevent duplicate branch and MR pipelines? 10. A job is "stuck". List three causes. 11. Rank these by precedence: a project CI/CD variable, a YAML global variable, a variable entered on "Run pipeline". 12. Why isn't a protected variable available on a feature branch? 13. Artifacts or cache for node_modules? For a compiled binary needed by a deploy job? 14. What do resource_group and environment:action: stop do? 15. How do you pass a value computed in one job to a later job? 16. What's the difference between trigger:include and trigger:project? 17. What does strategy: depend change? 18. What predefined variable holds the registry image path? 19. What's the minimum a CI/CD component needs to appear in the Catalog? 20. Name four when values.

PSE path 21. Which files must you back up in addition to gitlab-backup create output? 22. What must match between the backup source and the restore target? 23. Which component handles all Git repository access? 24. What's the role of Sidekiq and what does it depend on? 25. What does Geo provide, and which tier is it? 26. Name three things a git push --mirror migration does not carry. 27. Which migration method is recommended between two GitLab instances? 28. What's the difference between a scan execution policy and a merge request approval policy? 29. Which scanner needs a running application? 30. Scoped labels, epics, and iterations — which tier?

Official resources to pair with the labs

  • GitLab Docs: CI/CD YAML syntax reference (docs.gitlab.com/ci/yaml/) — read it front to back once.
  • GitLab Docs: CI/CD variables and variable precedence.
  • GitLab Docs: Runner executors, config.toml reference, and the new runner creation workflow.
  • GitLab Docs: Backup and restore; Upgrade paths; Reference architectures.
  • GitLab Docs: Migrate by direct transfer; Import from GitHub/Bitbucket; Placeholder user reassignment.
  • GitLab Docs: Application security (each scanner page) and security policies.
  • GitLab University: "GitLab with Git Essentials", "GitLab CI/CD", and the PSE learning path courses — complete the free self-paced courses before each exam, since the multiple-choice questions follow their wording closely.
  • The GitLab Handbook's Professional Services and Education Services sections, including the published ILT hands-on labs (install, runners, backup/restore).