Kubernetes Agent, Cluster Access, GitOps-Oriented Delivery, Kubernetes Deployments, and Environment Integration: Guided Hands-On Workflow and Core Operations
Build a disposable local cluster workflow with namespace-scoped RBAC, an immutable image reference, rollout evidence, GitLab environment metadata, and exact cleanup—then map the same state to the GitLab Agent.
Learning objectives
- Create a disposable local Kubernetes target and prove its starting state.
- Create a namespace-scoped ServiceAccount/Role/RoleBinding and test allowed and denied actions.
- Resolve a human-readable image tag to a recorded digest and deploy by the digest.
- Preserve rollout, runtime imageID, manifest and environment-mapping evidence.
- Map the local workflow to the GitLab Agent without requiring a real Agent, cloud account or paid feature.
1. Scenario and safety boundary
You own a disposable project named glci-ch28-k8s-lab.
The local cluster is named glci-ch28; the namespace is
glci-ch28; the workload is glci-ch28-web.
No production cluster, real cloud identity, shared runner or
proprietary image is used.
2. Preflight: record tools, source and current cluster identity
set -eu
printf 'git_sha=%s\n' "$(git rev-parse HEAD)"
kubectl version --client --output=yaml
kind version || true
docker --version || true
kubectl config current-context || true
kubectl config get-contexts -o name || true
In a real GitLab job, add CI_PIPELINE_SOURCE,
CI_COMMIT_SHA, CI_PIPELINE_ID and
CI_JOB_ID to the evidence packet. This local preflight
substitutes the repository HEAD because no GitLab pipeline exists
yet.
3. Create or select only the disposable cluster
If you already have a disposable kind/k3d/minikube cluster, use it and record its context. The following path uses kind:
LAB_CLUSTER=glci-ch28
if ! kind get clusters | grep -qx "$LAB_CLUSTER"; then
kind create cluster --name "$LAB_CLUSTER"
fi
EXPECTED_CONTEXT="kind-$LAB_CLUSTER"
ACTUAL_CONTEXT="$(kubectl config current-context)"
test "$ACTUAL_CONTEXT" = "$EXPECTED_CONTEXT" || {
echo "Refusing to continue: unexpected context" >&2
exit 2
}
kubectl cluster-info
kubectl get nodes -o wide
Creating a local cluster changes only local Docker/Kubernetes resources. It does not create a GitLab environment or Agent connection.
4. Bootstrap namespace and a least-privilege identity
Use the administrator context only to create the security boundary. The deployment itself will later use a short-lived ServiceAccount token stored in a temporary kubeconfig file.
apiVersion: v1
kind: Namespace
metadata:
name: glci-ch28
---
apiVersion: v1
kind: ServiceAccount
metadata:
name: gitlab-ci-sim
namespace: glci-ch28
---
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
name: workload-deployer
namespace: glci-ch28
rules:
- apiGroups: ["apps"]
resources: ["deployments"]
verbs: ["get", "list", "watch", "create", "update", "patch", "delete"]
- apiGroups: [""]
resources: ["pods", "services"]
verbs: ["get", "list", "watch", "create", "update", "patch", "delete"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: workload-deployer
namespace: glci-ch28
subjects:
- kind: ServiceAccount
name: gitlab-ci-sim
namespace: glci-ch28
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: Role
name: workload-deployer
Save that as lab/rbac.yaml, then:
kubectl apply -f lab/rbac.yaml
kubectl auth can-i create deployments.apps -n glci-ch28 --as=system:serviceaccount:glci-ch28:gitlab-ci-sim
kubectl auth can-i create clusterroles.rbac.authorization.k8s.io --as=system:serviceaccount:glci-ch28:gitlab-ci-sim
kubectl auth can-i get secrets -n glci-ch28 --as=system:serviceaccount:glci-ch28:gitlab-ci-sim
Expected: deployment creation is yes; cluster-role
creation and Secret reads are no. Capture all three
answers.
5. Create a short-lived local kubeconfig without displaying its token
This is a faithful identity simulation, not the GitLab Agent itself. The ServiceAccount token is generated through the Kubernetes TokenRequest API, written to a temporary file, embedded into another temporary kubeconfig, and never printed.
mkdir -p .lab-private evidence
chmod 700 .lab-private
TOKEN_FILE=.lab-private/token
KCFG=.lab-private/kubeconfig
kubectl -n glci-ch28 create token gitlab-ci-sim --duration=15m > "$TOKEN_FILE"
chmod 600 "$TOKEN_FILE"
python3 - "$TOKEN_FILE" "$KCFG" <<'PY2'
import json, pathlib, subprocess, sys
source = json.loads(subprocess.check_output([
'kubectl','config','view','--raw','--minify','-o','json'
], text=True))
cluster = source['clusters'][0]['cluster']
token = pathlib.Path(sys.argv[1]).read_text().strip()
out = {
'apiVersion': 'v1', 'kind': 'Config',
'clusters': [{'name':'glci-ch28','cluster':cluster}],
'users': [{'name':'gitlab-ci-sim','user':{'token':token}}],
'contexts': [{'name':'glci-ch28-sim','context':{
'cluster':'glci-ch28','user':'gitlab-ci-sim','namespace':'glci-ch28'}}],
'current-context':'glci-ch28-sim'
}
pathlib.Path(sys.argv[2]).write_text(json.dumps(out), encoding='utf-8')
PY2
chmod 600 "$KCFG"
KUBECONFIG="$KCFG" kubectl config current-context
KUBECONFIG="$KCFG" kubectl auth can-i create deployments.apps -n glci-ch28
KUBECONFIG="$KCFG" kubectl auth can-i get secrets -n glci-ch28
Do not archive .lab-private. It contains temporary
authentication material. The evidence packet records only context
name, RBAC results and token expiry intent—not the bearer value.
6. Resolve an upstream tag once, then deploy by digest
For a disposable lab, a versioned tag can be used only as a discovery input. Resolve it, capture its digest, and use the digest thereafter. For stricter reproducibility, start from a previously reviewed digest instead.
SOURCE_IMAGE=nginx:1.27.5-alpine
docker pull "$SOURCE_IMAGE"
IMAGE_REF="$(docker inspect --format='{{index .RepoDigests 0}}' "$SOURCE_IMAGE")"
case "$IMAGE_REF" in
*@sha256:*) ;;
*) echo "No immutable RepoDigest resolved" >&2; exit 3 ;;
esac
printf '%s\n' "$IMAGE_REF" > evidence/image-ref.txt
sha256sum evidence/image-ref.txt > evidence/image-ref.txt.sha256
The evidence file is safe: an image digest is identity metadata, not a secret.
7. Generate the exact manifest from the immutable reference
KCFG=.lab-private/kubeconfig
IMAGE_REF="$(cat evidence/image-ref.txt)"
KUBECONFIG="$KCFG" kubectl -n glci-ch28 create deployment glci-ch28-web --image="$IMAGE_REF" --replicas=1 --dry-run=client -o yaml > evidence/deployment.yaml
KUBECONFIG="$KCFG" kubectl -n glci-ch28 create service clusterip glci-ch28-web --tcp=80:80 --dry-run=client -o yaml > evidence/service.yaml
sha256sum evidence/deployment.yaml evidence/service.yaml > evidence/manifest-digests.txt
Review the generated YAML before applying it. The manifest hash becomes desired-state evidence.
8. Apply through the narrow identity and preserve the first result
KCFG=.lab-private/kubeconfig
KUBECONFIG="$KCFG" kubectl apply -f evidence/deployment.yaml | tee evidence/apply-deployment.txt
KUBECONFIG="$KCFG" kubectl apply -f evidence/service.yaml | tee evidence/apply-service.txt
KUBECONFIG="$KCFG" kubectl -n glci-ch28 rollout status deployment/glci-ch28-web --timeout=180s | tee evidence/rollout.txt
The apply output proves API acceptance.
rollout status proves the Deployment controller reached
its rollout condition. Neither alone proves HTTP behavior.
9. Verify runtime image identity and workload state
KCFG=.lab-private/kubeconfig
KUBECONFIG="$KCFG" kubectl -n glci-ch28 get deployment/glci-ch28-web -o json > evidence/deployment-after.json
KUBECONFIG="$KCFG" kubectl -n glci-ch28 get pods -l app=glci-ch28-web -o json > evidence/pods-after.json
KUBECONFIG="$KCFG" kubectl -n glci-ch28 get service/glci-ch28-web -o json > evidence/service-after.json
python3 - <<'PY2'
import json
pods=json.load(open('evidence/pods-after.json', encoding='utf-8'))
for p in pods['items']:
for s in p.get('status',{}).get('containerStatuses',[]):
print(p['metadata']['name'], s.get('ready'), s.get('imageID'))
PY2
Compare each runtime imageID with
evidence/image-ref.txt. A digest mismatch is a
deployment-integrity problem even if the Pod is Ready.
10. Verify external behavior separately
For the local simulation, start a port-forward in a second terminal:
KUBECONFIG=.lab-private/kubeconfig kubectl -n glci-ch28 port-forward service/glci-ch28-web 18080:80
Then verify from the first terminal:
curl --fail --silent --show-error http://127.0.0.1:18080/ > evidence/http-body.html
wc -c evidence/http-body.html
sha256sum evidence/http-body.html > evidence/http-body.html.sha256
The local URL is a simulation only. Do not register it as a remotely usable GitLab environment URL.
11. Map the lab to a real Agent-enabled GitLab job
If you own an authorized disposable Agent, the equivalent pipeline selects the Agent context explicitly and declares environment metadata:
deploy_staging:
stage: deploy
variables:
KUBE_CONTEXT: platform/cluster-agents:staging-agent
before_script:
- kubectl config get-contexts -o name
- kubectl config use-context "$KUBE_CONTEXT"
- kubectl auth can-i patch deployments.apps -n app-staging
script:
- kubectl apply -f k8s/staging.yaml
- kubectl rollout status deployment/example -n app-staging --timeout=180s
environment:
name: staging
url: https://staging.example.test
kubernetes:
agent: platform/cluster-agents:staging-agent
dashboard:
namespace: app-staging
The real job additionally records CI_PIPELINE_SOURCE,
CI_COMMIT_SHA, pipeline/job IDs, selected Agent
context, manifest digest and rollout evidence. Never upload the
kubeconfig as an artifact.
12. Map the same release to GitOps
In a production GitOps design, the pipeline does not need permission to patch the Deployment directly. Instead it updates reviewed desired state—or publishes a versioned OCI manifest artifact—and Flux reconciles that exact revision. The evidence chain becomes source/build digest → desired-state commit/OCI digest → Flux reconciliation → Kubernetes runtime digest → GitLab environment/dashboard/health evidence.
13. Challenge: which layer should you change?
The deployment job sees the expected Agent context, but
kubectl auth can-i patch deployments -n app-staging
returns no. Should you change rules, retry
the runner, broaden the project’s Agent authorization, or modify
Kubernetes RBAC?
Answer: first preserve the context and denial evidence, then fix the Kubernetes authorization layer if the identity is intended to deploy. Pipeline rules and Agent project authorization already succeeded. Do not grant cluster-admin as a shortcut.
14. Exact cleanup
set -eu
LAB_CLUSTER=glci-ch28
NS=glci-ch28
test "$(kubectl config current-context)" = "kind-$LAB_CLUSTER"
test "$NS" = "glci-ch28"
KUBECONFIG=.lab-private/kubeconfig kubectl -n "$NS" delete -f evidence/service.yaml -f evidence/deployment.yaml --ignore-not-found
kubectl delete -f lab/rbac.yaml --ignore-not-found
rm -f .lab-private/token .lab-private/kubeconfig
rmdir .lab-private 2>/dev/null || true
kubectl get namespace "$NS" --ignore-not-found
# Optional only when the entire kind cluster was created for this chapter:
# kind delete cluster --name glci-ch28
If lab/rbac.yaml includes the Namespace object,
deleting it removes only this guarded disposable namespace. Keep the
evidence directory, but never keep temporary authentication files.
Knowledge check
Why does the lab create a temporary ServiceAccount kubeconfig instead of using the administrator context for the deployment?
It makes Kubernetes authorization observable and proves that the workload change can be performed by a namespace-scoped identity rather than relying on implicit cluster-admin power.
Why resolve the image tag before generating the manifest?
The deployment manifest should name immutable bytes. Resolving once produces an image@sha256 reference that can be recorded and compared with runtime imageID.
What evidence differentiates API acceptance from rollout health?
The apply response proves the API accepted desired state; rollout status, Deployment/Pod state and runtime imageID prove controller/runtime progress.
In a real Agent job, what should be recorded instead of the kubeconfig contents?
The selected context/Agent identity, authorization results, pipeline/job identity and namespace—not bearer credentials or kubeconfig contents.
Why is Flux only mapped conceptually in the mandatory lab?
The chapter must stay free/local/disposable and not require a GitLab Agent or external GitOps bootstrap. The local lab proves the same state boundaries without claiming to exercise GitLab’s managed connectivity.
Version and compatibility note
GitLab and GitLab Runner evolve continuously. Treat version-sensitive YAML, runner/executor behavior, APIs, security features, policy controls, tiers, and deprecations as assumptions to verify against the current official GitLab documentation before production use. Preserve the exact project/ref/SHA, compiled configuration, pipeline/job IDs, runner/tool versions, and external target evidence used for any reproducible lab or incident record.
Official references and version notes
Documentation verification date: 2026-09-12.
Current GitLab documentation lists the Agent CI/CD workflow as Free,
Premium, and Ultimate on GitLab.com, Self-Managed, and Dedicated.
Authorized CI/CD jobs receive a KUBECONFIG containing
contexts for authorized agent connections; the context identifies
the agent configuration project and agent name.
ci_access can authorize projects/groups and can
restrict access to named environment patterns. The current per-agent
authorization limit is 500 projects and 500 groups. CI-job
impersonation with access_as: ci_job is
Premium/Ultimate; the mandatory lab therefore demonstrates true
Kubernetes namespace-scoped ServiceAccount RBAC locally instead of
pretending that paid impersonation is Free. GitLab recommends Flux
for GitOps; the Agent’s legacy built-in pull-based GitOps
functionality was removed in GitLab 17.0. Certificate-based cluster
integration was deprecated in GitLab 14.5. The Kubernetes dashboard
is currently Beta and available on all tiers. For
environment:kubernetes, dashboard namespace/Flux
resource settings belong under dashboard:; the older
direct namespace/flux_resource_path form
is deprecated. The local ServiceAccount token is intentionally
short-lived and stored only in a private temporary file. Real Agent
jobs should use the Agent-provided kubeconfig and must not replace
it with a long-lived production kubeconfig variable.
- Using GitLab CI/CD with a Kubernetes cluster — official reference.
- GitLab Agent for Kubernetes — official reference.
- Get started connecting a Kubernetes cluster — official reference.
- Install the Agent for Kubernetes — official reference.
- Migrate legacy Agent GitOps to Flux — official reference.
- Dashboard for Kubernetes — official reference.
- CI/CD YAML syntax — environment:kubernetes — official reference.
- Deprecated CI/CD keywords — official reference.
- Migrate from certificate-based Kubernetes integration — official reference.
- GitLab-managed Kubernetes resources — official reference.
- Kubernetes RBAC authorization — official reference.
- Kubernetes Deployments — official reference.
- kind quick start — official reference.
- Flux documentation — official reference.
Current assumptions used in this chapter: Version-sensitive YAML, Runner/executor behavior, APIs, security features, policy controls, tiers, and deprecations must be verified against the exact GitLab, GitLab Runner, tool, and external-system versions used in production. Preserve the exact project/ref/SHA, compiled configuration, pipeline/job IDs, runner/tool versions, and external target evidence used for reproducible work.
Keep the academy open
Support free, practical DevOps education.
Every lesson is designed to remain readable in a browser, downloadable from GitHub, and usable without a paid learning platform. Contributions help expand and maintain the curriculum.
0x716c4Ab160C4B66F31a28AE2448BfF68fc3a2ef0
Send only Ethereum/ERC-20 compatible assets to this
address.