GitLab Certification Study Guide with Hands-On Labs¶
This guide covers three targets:
- 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.
- GitLab Certified CI/CD Associate — pipelines, runners, variables, artifacts/cache, environments, registry, releases, basic security scanning.
- 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;
HEADpoints 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¶
- On gitlab.com, create a group
cert-lab-<you>and a blank projectgit-basicsinside it without a README. - 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
- 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
- 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¶
- Create issue "Add CONTRIBUTING guide". Assign it to yourself, add a label
docs, a milestonev0.2, and a due date. - From the issue, click Create merge request (this creates a branch named after the issue and a draft MR that says
Closes #1). - 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
- 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.
- 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¶
- Settings → Repository → Protected branches:
main— Allowed to merge: Maintainers; Allowed to push and merge: No one. - Try
git push origin maindirectly — it should be rejected. - Settings → Merge requests: enable Pipelines must succeed and All threads must be resolved.
- (Premium trial) add an approval rule requiring 1 approval, and add a
CODEOWNERSfile:
# .gitlab/CODEOWNERS
*.md @your-username
[Backend][1]
/src/ @your-username
- 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
needscreates 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.ymljob-level →.gitlab-ci.ymlglobal → 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.
rulesreplacedonly/except(still supported, not recommended).workflow:rulescontrols 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¶
- Project variable
APP_ENV=dev(Settings → CI/CD → Variables). - Group variable
APP_ENV=groupandGROUP_ONLY=yeson the parent group. - Masked and hidden variable
API_KEY(value must meet masking requirements: ≥ 8 chars, single line). - Protected variable
DEPLOY_TOKEN— only available on protected branches/tags. - File type variable
KUBECONFIG_FILE— the variable holds a path to a temp file. - An environment-scoped variable
DB_HOSTwith scopestagingand another with scopeproduction.
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 fromrelease-clito theglabCLI. Check the currentreleasekeyword 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¶
- 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"
- 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.
- Parent-child pipeline:
trigger-child:
stage: test
trigger:
include: ci/child.yml
strategy: depend # parent waits for and mirrors child status
- Multi-project pipeline:
trigger-downstream:
stage: deploy
trigger:
project: cert-lab-<you>/downstream-app
branch: main
- 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:
- Has stages
build,test,review,deploy. - Builds a container image and pushes it to the project registry tagged with the short SHA.
- Runs unit tests with JUnit and coverage reports surfaced in the MR.
- Caches dependencies keyed on the lock file.
- Runs only once per MR (no duplicate branch pipelines).
- Creates a review environment per MR with a manual stop job.
- Deploys to
productionmanually, only from the default branch, with aresource_group. - Uses a masked project variable and a protected variable correctly.
- Includes SAST via template.
- 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::valueare 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)¶
- In group
cert-lab-<you>, create scoped labelsworkflow::todo,workflow::doing,workflow::review,workflow::done, andtype::feature,type::bug. - Create an iteration cadence of two-week iterations.
- Create a group milestone
Release 1.0with start and due dates. - Create an epic "User authentication" with two child epics, a start/due date, and attach six issues spread across two projects.
- Give each issue a weight, assign to the iteration, add estimates with
/estimate. - Build a group issue board with lists for each
workflow::label; drag issues across and observe that scoped labels are mutually exclusive. - Mark one issue as blocked by another.
- View the roadmap, the milestone burndown, and the iteration report.
- 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¶
- Dismiss one finding as "false positive" with a comment.
- Confirm one and create an issue from it.
- Resolve one by fixing code; rerun on
mainand watch it move to "No longer detected". - Export the vulnerability report as CSV.
Lab 24 — Security policies¶
- Secure → Policies → New policy → Scan execution policy: run Secret Detection on every pipeline for all branches. Observe the security policy project GitLab creates.
- Merge request approval policy: require one approval from a specific user when an MR introduces a new Critical or High vulnerability.
- 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.jsonandgitlab.rbare not in application backups — back them up separately. Losinggitlab-secrets.jsonmakes 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 --mirrorcarries 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:
- On gitlab.com, create a PAT with
apiscope. - On the lab instance (as admin), enable Allow migrating GitLab groups and projects by direct transfer in import settings.
- New group → Import group → enter
https://gitlab.comand the PAT → selectcert-lab-<you>→ import with projects. - 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:
- Fresh install from snapshot, HTTPS with your private CA, SMTP working.
- LDAP sign-in, admin approval required for sign-up.
- Instance runner (Docker) + group runner (k8s), with tags.
- Import a GitHub repo with issues/PRs; reassign placeholders.
- Set up a group with scoped workflow labels, a milestone and a board.
- Implement the CI/CD mock project from Part B in the imported repo.
- Enable SAST + secret detection + dependency scanning.
- 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.tomlreference, 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).