First things first: We need to login into our Gitlab account for this lab. Done already? Great!
So far, we configured our Helm chart without the need of any source code, as we were using an image from a registry provided to us. So in order to continue, let’s first clone the code of the demo app by running
git clone https://git.bnerd.com/workshop-public/todo-app.git
on your machine - and please keep in mind our focus is on infrastructure and not so much on the apps themselves 😇
You will find two folders containing the source code for our frontend and our backend. Please also create a folder called infrastructure with a subfolder “charts” and copy the helm charts from Lab 2 into the charts folder, so that it looks as follows:
Ready? Then let’s visit the Gitlab UI in the browser, navigate to groups where you can find a workshop group and create in there a blank project todo-app-<your-namespace>
. You can skip initializing it with a Readme.
In your IDE, ensure you are in the todo-app directory and change the git origin by running
git remote rename origin old-origin
and
git remote add origin https://git.bnerd.com/workshop/todo-app-<your namespace>.git
⚠️ Please do not forget to add your namespace in the origin link!
After we set the origin, you can push by running:
git add .
git commit -m "initial commit"
git push --set-upstream origin main
You will be asked to insert your GitLab username and password before being able to push.
Great, first step is done - our code is in Gitlab. So next, let’s start with our pipeline by adding a .gitlab-ci.yml
file at the root of your directory.
Please add the following lines to your pipeline file
stages:
- build
- deploy
build:
stage: build
image: docker:latest
script:
- echo "Hello World from Build stage"
deploy:
stage: deploy
image: alpine/k8s:1.29.13
script:
- echo "Hello World from Deploy stage"
For starters, this is just a very basic pipeline with two stages (build and deploy), each echoing a message. Lets push this pipeline to Gitlab to see what happens :)
Navigate to Build -> Pipelines in the sidebar and inspect our very first pipeline run by clicking on the (hopefully ;)) “passed” button. It should look similar to this:
Both stages passed? Wonderful! Then lets have a look into the build stage and check if we can find our message:
Great, there we go. Feel free to also check our deploy stage. Now let’s bring our pipeline to life :)
In order to being able to build the images later used in the deploy stage, we first need a registry where these images can be stored. Given Gitlab is providing a Container Registry out of the box, let’s use this one for the sake of simplicity.
So, first of all, lets adapt our pipeline to being able to build an image for our backend and push it to the Gitlab registry by changing our yaml file as follows:
stages:
- build
- deploy
build_backend:
stage: build
image: docker:latest
services:
- docker:20.10.7-dind
script:
- echo "$CI_REGISTRY_PASSWORD" | docker login $CI_REGISTRY -u $CI_REGISTRY_USER --password-stdin
- cd backend
- docker build -t $CI_REGISTRY/workshop/todo-app-<your-namespace>/backend .
- docker tag $CI_REGISTRY/workshop/todo-app-<your-namespace>/backend $CI_REGISTRY/workshop/todo-app-<your-namespace>/backend:latest
- docker push $CI_REGISTRY/workshop/todo-app-<your-namespace>/backend:latest
deploy:
stage: deploy
image: alpine/k8s:1.29.13
script:
- echo "Hello World from Deploy stage"
⚠️ Please add your namespace in the script (lines 13 - 15) before pushing, so that the pipeline will run through successfully.
Pipeline ran through after your push? Great, then navigate to Deploy -> Container Registry in the sidebar and there we have our image 🎉
Let’s also build the image for our frontend by adding the following in our yaml on line 16 between the build_backend and the deploy stage:
build_frontend:
stage: build
image: docker:latest
services:
- docker:20.10.7-dind
script:
- echo "$CI_REGISTRY_PASSWORD" | docker login $CI_REGISTRY -u $CI_REGISTRY_USER --password-stdin
- cd frontend
- docker build -t $CI_REGISTRY/workshop/todo-app-<your-namespace>/frontend .
- docker tag $CI_REGISTRY/workshop/todo-app-<your-namespace>/frontend $CI_REGISTRY/workshop/todo-app-<your-namespace>/frontend:latest
- docker push $CI_REGISTRY/workshop/todo-app-<your-namespace>/frontend:latest
⚠️ Again, please add your namespace in the script before pushing.
Got your image in the registry? Great!
Given the build steps are taking some time, let’s just add some minor change in our build steps for now. After the scripts of both the “build_backend” and “build_frontend” steps, add the following:
only:
changes:
- backend/*
and respectively
only:
changes:
- frontend/**/*
This ensures that only if there are changes in our source code, the pipeline will build new images. This will speed up our deployment :)
So our build steps should now look as follows:
build_backend:
stage: build
image: docker:latest
services:
- docker:20.10.7-dind
script:
- echo "$CI_REGISTRY_PASSWORD" | docker login $CI_REGISTRY -u $CI_REGISTRY_USER --password-stdin
- cd backend
- docker build -t $CI_REGISTRY/workshop/todo-app-<your-namespace>/backend .
- docker tag $CI_REGISTRY/workshop/todo-app-<your-namespace>/backend $CI_REGISTRY/workshop/todo-app-<your-namespace>/backend:latest
- docker push $CI_REGISTRY/workshop/todo-app-<your-namespace>/backend:latest
only:
changes:
- backend/*
build_frontend:
stage: build
image: docker:latest
services:
- docker:20.10.7-dind
script:
- echo "$CI_REGISTRY_PASSWORD" | docker login $CI_REGISTRY -u $CI_REGISTRY_USER --password-stdin
- cd frontend
- docker build -t $CI_REGISTRY/workshop/todo-app-<your-namespace>/frontend .
- docker tag $CI_REGISTRY/workshop/todo-app-<your-namespace>/frontend $CI_REGISTRY/workshop/todo-app-<your-namespace>/frontend:latest
- docker push $CI_REGISTRY/workshop/todo-app-<your-namespace>/frontend:latest
only:
changes:
- frontend/**/*
Now comes the exiting part - let’s deploy to our cluster!
Before really getting into updating our deploy stage in the pipeline, we need to get some ground work done. Given our cluster needs to be able to pull the images from Gitlab, we first need to generate an ImagePullSecret so that our cluster is able to authenticate against our registry.
Generating an ImagePullSecret
In the Gitlab UI, navigate to Settings -> Repository and look for Deploy tokens. Expand the section and click “Add token”.
Name it “gitlab-deploy-token”, choose the “read registry” scope and create your token. Copy the password (you have to scoll up a little to see it), as you won’t see it again :)
Now we need to create a Secret so our cluster has access to the credentials. But first, we need to create a Docker credential file. Hence create a .dockerconfigjson
file in the infrastructure folder and add the following:
{
"auths": {
"registry.noisy-thunder-9457.gl.apps.muc1.de.bnerd.io": {
"username": "NAME OF DEPLOY TOKEN",
"password": "PASSWORD OF DEPLOY TOKEN",
"email": "REGISTRY_EMAIL",
"auth": "BASE_64_BASIC_AUTH_CREDENTIALS"
}
}
}
For the credentials, use the following command
echo -n "<NAME OF DEPLOY TOKEN>:<PASSWORD OF DEPLOY TOKEN>" | base64
Copy the encoded string and replace BASE_64_BASIC_AUTH_CREDENTIALS in your dockerconfigjson. Now we only need to base64 encode the dockerconfigjson for the Kubernetes secret by running (in the infrastructure directory)
cat .dockerconfigjson | base64
Now we can finally create our secret in a registry-credentials.yaml
file within our infrastructure folder and add the following:
apiVersion: v1
kind: Secret
type: kubernetes.io/dockerconfigjson
metadata:
name: registry-credentials-<your namespace>
namespace: <your namespace>
data:
.dockerconfigjson: <the output from the cat .dockerconfigjson | base64 command>
Add the value you received by the cat command in line 8 as well as your namespace in lines 5 & 6.
Ensure you are still having access to the correct cluster by running
kubectl config current-context
and deploy your secret via
kubectl apply -f registry-credentials.yaml
Open k9s and search for your secret by running :secrets
. Got it? With d
you can have a look into how it looks like in the Kubernetes world :)
Great, we now enabled Kubernetes to pull images from Gitlab. But we also need to let Helm know. So in your values.yaml files for the backend and the frontend, add the following:
imagePullSecrets: [{ name: registry-credentials-<your namespace> }]
Please again add your namespace.
A note on credentials and Gitlab
In real-world projects, we would use tools like SOPS to encrypt sensitive files and values so they can be safely stored in Git repositories.
But for the sake of simplicity and time in this workshop, we’re skipping encryption and instead using a .gitignore file to prevent secrets from being committed.
So please create a .gitignore
file in the root of your todo-app repository and add the follwing:
.dockerconfigjson
registry-credentials.yaml
Important: This is fine for learning purposes — but never rely on .gitignore alone in production. It prevents accidental commits, but doesn’t secure the data.
Update the image repository
There is one more thing we need to change in our values file. Helm is for now still not looking in our GitLab registry for the image. Hence adapt line 10 in the values.yaml
files for both backend & frontend to
registry.noisy-thunder-9457.gl.apps.muc1.de.bnerd.io/workshop/todo-app-<your namespace>/backend
registry.noisy-thunder-9457.gl.apps.muc1.de.bnerd.io/workshop/todo-app-<your namespace>/frontend
and do not forget to add your namespace.
Add the Kubeconfig so GitLab knows where to deploy to
In production, we’d use the GitLab Kubernetes Agent for secure and robust deployments.
But for this workshop, to keep things simple and fast, we’ll use a temporary workaround: we’ll pass the Kubeconfig as an environment variable in GitLab CI.
For this, please navigate in the sidebar to Settings -> CI/CD and look out for “Variables”. Expand the section and click “Add variable”. Then:
Adding the deploy steps
Let’s start with deploying our backend to the cluster. To achieve this, replace the current deploy stage at the end of the .gitlab-ci.yml with the following
deploy_backend:
stage: deploy
image: alpine/k8s:1.29.13
script:
- echo "Hello World from Deploy stage"
- kubectl get pods -n <your namespace>
and add your namespace, to see if we are able to list all pods in our namespace.
Push your changes and check the pipeline. Got details about the pods? Wonderful!
So, let’s move ahead and add the helm deployment command by replacing the current step with the following:
deploy_backend:
stage: deploy
image: alpine/k8s:1.29.13
script:
- echo "Hello World from Deploy stage"
- helm upgrade --install backend ./infrastructure/charts/backend
--values ./infrastructure/charts/backend/values.yaml
--namespace <your namespace>
Add your namespace, push your changes and have a look at the pipeline. All there? Amazing, them we can also add the deploy step for the frontend with the following lines at the end of our pipeline yaml file:
deploy_frontend:
stage: deploy
image: alpine/k8s:1.29.13
script:
- echo "Hello World from Deploy stage"
- helm upgrade --install frontend ./infrastructure/charts/frontend
--values ./infrastructure/charts/frontend/values.yaml
--namespace <your namespace>
Push your changes again and there we go - we do have both frontend and backend deployed in our cluster by our pipeline 🎉
But here’s the thing: in real-world projects, developers push code frequently — but not every commit should trigger a deployment. Sometimes it’s just a config change. Sometimes we need to update infrastructure without building a new image.
That’s why we’re taking things a step further in our DevOps journey — and adding ArgoCD to the picture!