Overview #
Kubernetes Setup #
In this tutorial I’m using the following Kubernetes cluster, deployed with Kubeadm:
# Kubernetes cluster:
NAME STATUS ROLES AGE VERSION INTERNAL-IP EXTERNAL-IP OS-IMAGE KERNEL-VERSION CONTAINER-RUNTIME
ubuntu1 Ready control-plane 28m v1.29.11 192.168.30.10 <none> Ubuntu 24.04.1 LTS 6.8.0-49-generic containerd://1.7.23
ubuntu2 Ready worker 22m v1.29.11 192.168.30.11 <none> Ubuntu 24.04.1 LTS 6.8.0-49-generic containerd://1.7.23
ubuntu3 Ready worker 21m v1.29.11 192.168.30.12 <none> Ubuntu 24.04.1 LTS 6.8.0-49-generic containerd://1.7.23
ubuntu4 Ready worker 20m v1.29.11 192.168.30.13 <none> Ubuntu 24.04.1 LTS 6.8.0-49-generic containerd://1.7.23
192.168.30.14 # Ubuntu 24 based deployment server with Docker platform installed
Longhorn CSI StorageClass #
I’m using the following Longhorn CSI based StorageClass named longhorn-rwo-retain
:
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
name: longhorn-rwo-retain
provisioner: driver.longhorn.io
allowVolumeExpansion: true
parameters:
numberOfReplicas: "2"
staleReplicaTimeout: "2880" # 48 hours in minutes
fromBackup: ""
fsType: "ext4"
volumeBindingMode: Immediate
reclaimPolicy: Retain
# List StorageClasses
kubectl get sc
# Shell output:
NAME PROVISIONER RECLAIMPOLICY VOLUMEBINDINGMODE ALLOWVOLUMEEXPANSION AGE
longhorn (default) driver.longhorn.io Delete Immediate true 32m
longhorn-rwo-retain driver.longhorn.io Retain Immediate true 22m
longhorn-static driver.longhorn.io Delete Immediate true 32m
GitLab Helm Repository #
Add Helm Repository #
# Add GitLab Helm repository
helm repo add gitlab https://charts.gitlab.io/ &&
helm repo update
List Available Helm Charts #
# Optional: List the available Helm Charts
helm search repo gitlab
# Shell output:
NAME CHART VERSION APP VERSION DESCRIPTION
gitlab/gitlab 8.6.0 v17.6.0 GitLab is the most comprehensive AI-powered Dev...
gitlab/gitlab-agent 2.9.0 v17.6.0 GitLab Agent for Kubernetes is a way to integra...
gitlab/gitlab-omnibus 0.1.37 GitLab Omnibus all-in-one bundle
gitlab/gitlab-operator 1.7.0 1.7.0 The GitLab operator aims to manage the full lif...
gitlab/gitlab-runner 0.71.0 17.6.0 GitLab Runner
gitlab/gitlab-zoekt 1.4.2 0.8.5 A Helm chart for deploying Zoekt as a search en...
gitlab/kubernetes-gitlab-demo 0.1.29 GitLab running on Kubernetes suitable for demos
gitlab/apparmor 0.2.0 0.1.0 AppArmor profile loader for Kubernetes
gitlab/auto-deploy-app 0.8.1 GitLab's Auto-deploy Helm Chart
gitlab/elastic-stack 3.0.0 7.6.2 A Helm chart for Elastic Stack
gitlab/fluentd-elasticsearch 6.2.8 2.8.0 A Fluentd Helm chart for Kubernetes with Elasti...
gitlab/knative 0.10.0 0.9.0 A Helm chart for Knative
gitlab/plantuml 0.1.17 1.0 PlantUML server
Helm Chart Master Values #
The Helm chart master values look like this:
# GitLab vlaues
https://gitlab.com/gitlab-org/charts/gitlab/-/blob/master/values.yaml?ref_type=heads
# GitLab Runner values
https://gitlab.com/gitlab-org/charts/gitlab-runner/-/blob/main/values.yaml?ref_type=heads
GitLab Installation #
Create Namespace #
# Create a namespace with the name "gitlab"
kubectl create ns gitlab
Kubernetes TLS Certificate Secret #
-
In this setup I’m using a Let’s Encrypt wildcard certificate.
-
The same certificate is used for GitLab, GitLab Pages & the GitLab Shell, it includes the follwoign domain names
*.gitlab-pages.jklug.work
and*.jklug.work
.
# Create a Kubernetes secret for the TLS certificate
kubectl create secret tls gitlab-tls --cert=./fullchain.pem --key=./privkey.pem -n gitlab
Adapt Helm Chart Values #
# Create a configuration for the Helm chart values
vi gitlab-values.yaml
global:
edition: ce # GitLab Cummunity Edition
hosts:
domain: jklug.work
https: true
externalIP: 192.168.30.200 # Nginx Ingress / MetalLB virtual IP
gitlab:
name: gitlab.jklug.work
https: true
ssh:
name: 192.168.30.201 # Set the SSH LoadBalancer IP
port: 22 # Default SSH port
registry:
name: gitlab-registry.jklug.work
https: true
pages:
name: gitlab-pages.jklug.work
https: true
externalHttps: 443
localStore:
enabled: true
ssh: gitlab-shell.jklug.work
pages:
enabled: true # Enable GitLab Pages
tls:
enabled: true
secretName: gitlab-tls # Kubernetes TLS Certificate Secret
# Nginx Ingress Configuration
ingress:
configureCertmanager: false
useNewIngressForCerts: false
provider: nginx
class: "nginx"
annotations: {}
enabled: true
tls:
enabled: true
secretName: gitlab-tls # Kubernetes TLS Certificate Secret
path: /
pathType: Prefix
# Enable MinIO
minio:
enabled: true
certmanager:
install: false
nginx-ingress:
enabled: false # If an NGINX Ingress is already installed in the cluster
redis:
install: true
master:
persistence:
storageClass: longhorn-rwo-retain
size: 2Gi
prometheus:
install: false
postgresql:
install: true
persistence:
storageClass: longhorn-rwo-retain
size: 2Gi
gitlab-runner:
install: false
registry:
enable: true
minio:
persistence:
storageClass: longhorn-rwo-retain
size: 2Gi
gitlab:
gitaly:
persistence:
storageClass: longhorn-rwo-retain
size: 10Gi
gitlab-shell:
service:
type: LoadBalancer # LoadBalancer
externalPort: 22 # Expose on port 22
Install GitLab #
# Dry-run
helm install --dry-run --debug gitlab gitlab/gitlab \
--namespace gitlab \
-f gitlab-values.yaml
# Install GitLab
helm upgrade --install gitlab gitlab/gitlab \
--namespace gitlab \
--timeout 400s \
-f gitlab-values.yaml
--timeout
If no value is specified, Helm uses a default timeout of 5 minutes (300s).
# Shell output
NAME: gitlab
LAST DEPLOYED: Sat Nov 23 18:31:27 2024
NAMESPACE: gitlab
STATUS: deployed
REVISION: 1
NOTES:
=== CRITICAL
The following charts are included for evaluation purposes only. They will not be supported by GitLab Support
for production workloads. Use Cloud Native Hybrid deployments for production. For more information visit
https://docs.gitlab.com/charts/installation/index.html#use-the-reference-architectures.
- PostgreSQL
- Redis
- Gitaly
- MinIO
=== NOTICE
The minimum required version of PostgreSQL is now 14. See https://docs.gitlab.com/charts/installation/upgrade.html for more details.
Help us improve the installation experience, let us know how we did with a 1 minute survey:https://gitlab.fra1.qualtrics.com/jfe/form/SV_6kVqZANThUQ1bZb?installation=helm&release=17-6
Verify Installation #
Pods & Services #
Wait around 10 min will all the pods are running, the first initialization takes a while:
# List pods in "gitlab" namespace
kubectl get pod -n gitlab
# Shell output:
NAME READY STATUS RESTARTS AGE
gitlab-gitaly-0 1/1 Running 0 8m32s
gitlab-gitlab-exporter-69975567f7-4576l 1/1 Running 0 8m32s
gitlab-gitlab-shell-846d8778b8-hf5vs 1/1 Running 0 8m17s
gitlab-gitlab-shell-846d8778b8-qvdd7 1/1 Running 0 8m32s
gitlab-kas-87dc5588b-4xgsk 1/1 Running 0 8m17s
gitlab-kas-87dc5588b-n2xm4 1/1 Running 3 (6m58s ago) 8m32s
gitlab-migrations-5184639-xsshc 0/1 Completed 0 8m32s
gitlab-minio-56dd57d56c-kvp7m 1/1 Running 0 8m32s
gitlab-minio-create-buckets-ad38ebb-4pc24 0/1 Completed 0 8m32s
gitlab-postgresql-0 2/2 Running 0 8m32s
gitlab-redis-master-0 2/2 Running 0 8m32s
gitlab-registry-54647d9684-f8ctn 1/1 Running 1 (7m4s ago) 8m32s
gitlab-registry-54647d9684-l9224 1/1 Running 0 8m17s
gitlab-sidekiq-all-in-1-v2-77dd6c77dc-kj2c2 1/1 Running 0 8m32s
gitlab-toolbox-6f44fffb5b-x9fxh 1/1 Running 0 8m32s
gitlab-webservice-default-6b4f5977cf-hlrz6 2/2 Running 1 (90s ago) 8m17s
gitlab-webservice-default-6b4f5977cf-jv7fv 2/2 Running 1 (93s ago) 8m32s
# List services in "gitlab" namespace
kubectl get svc -n gitlab
# Shell output:
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
gitlab-gitaly ClusterIP None <none> 8075/TCP,9236/TCP 10m
gitlab-gitlab-exporter ClusterIP 10.108.71.200 <none> 9168/TCP 10m
gitlab-gitlab-pages ClusterIP 10.107.120.19 <none> 8090/TCP 10m
gitlab-gitlab-pages-metrics ClusterIP 10.100.108.254 <none> 9235/TCP 10m
gitlab-gitlab-shell LoadBalancer 10.108.202.37 192.168.30.201 22:32137/TCP 10m
gitlab-kas ClusterIP 10.109.238.202 <none> 8150/TCP,8153/TCP,8154/TCP,8151/TCP 10m
gitlab-minio-svc ClusterIP 10.111.1.146 <none> 9000/TCP 10m
gitlab-postgresql ClusterIP 10.111.94.138 <none> 5432/TCP 10m
gitlab-postgresql-hl ClusterIP None <none> 5432/TCP 10m
gitlab-postgresql-metrics ClusterIP 10.104.19.244 <none> 9187/TCP 10m
gitlab-redis-headless ClusterIP None <none> 6379/TCP 10m
gitlab-redis-master ClusterIP 10.108.27.163 <none> 6379/TCP 10m
gitlab-redis-metrics ClusterIP 10.111.198.178 <none> 9121/TCP 10m
gitlab-registry ClusterIP 10.111.220.128 <none> 5000/TCP 10m
gitlab-webservice-default ClusterIP 10.98.28.130 <none> 8080/TCP,8181/TCP,8083/TCP 10m
# List Ingress in "gitlab" namespace
kubectl get ing -n gitlab
# Shell output:
NAME CLASS HOSTS ADDRESS PORTS AGE
gitlab-kas nginx kas.jklug.work 192.168.30.200 80, 443 10m
gitlab-minio nginx minio.jklug.work 192.168.30.200 80, 443 10m
gitlab-registry nginx gitlab-registry.jklug.work 192.168.30.200 80, 443 10m
gitlab-webservice-default nginx gitlab.jklug.work 192.168.30.200 80, 443 10m
PVC & PV #
# List PersistentVolumeClaims
kubectl get pvc -n gitlab
# Shell output:
NAME STATUS VOLUME CAPACITY ACCESS MODES STORAGECLASS VOLUMEATTRIBUTESCLASS AGE
data-gitlab-postgresql-0 Bound pvc-28b8b127-993a-4831-91b9-fdc54eede401 8Gi RWO longhorn <unset> 104s
gitlab-minio Bound pvc-4c758be0-314b-4f3c-8946-afa8a3a4007b 2Gi RWO longhorn-rwo-retain <unset> 104s
redis-data-gitlab-redis-master-0 Bound pvc-d4985dcd-da7d-46b5-b94c-b14c6e799b49 2Gi RWO longhorn-rwo-retain <unset> 104s
repo-data-gitlab-gitaly-0 Bound pvc-62ec7432-7132-429a-8088-81f49c83b4da 10Gi RWO longhorn-rwo-retain <unset> 104s
# List PersistentVolumes
kubectl get pv
# Shell output:
NAME CAPACITY ACCESS MODES RECLAIM POLICY STATUS CLAIM STORAGECLASS VOLUMEATTRIBUTESCLASS REASON AGE
pvc-28b8b127-993a-4831-91b9-fdc54eede401 8Gi RWO Delete Bound gitlab/data-gitlab-postgresql-0 longhorn <unset> 113s
pvc-4c758be0-314b-4f3c-8946-afa8a3a4007b 2Gi RWO Retain Bound gitlab/gitlab-minio longhorn-rwo-retain <unset> 113s
pvc-62ec7432-7132-429a-8088-81f49c83b4da 10Gi RWO Retain Bound gitlab/repo-data-gitlab-gitaly-0 longhorn-rwo-retain <unset> 113s
pvc-d4985dcd-da7d-46b5-b94c-b14c6e799b49 2Gi RWO Retain Bound gitlab/redis-data-gitlab-redis-master-0 longhorn-rwo-retain <unset> 113s
GitLab Webinterface & Shell #
DNS Entries for GitLab & GitLab Shell #
# Add DNS entries for GitLab and the GitLab Shell
192.168.30.200 gitlab.jklug.work
192.168.30.201 gitlab-shell.jklug.work
Fetch Root Password #
# Output the random generated GitLab root password
kubectl get secret gitlab-gitlab-initial-root-password -n gitlab -o jsonpath="{.data.password}" | base64 --decode ; echo
# Shell output:
lk68RxYgb3hoplbQLmOSSaewZrurIih53z9KRJ9oA7ino09D3mpCvbnmNcox2OXo
Open Webinterface #
# Open the GitLab webinterface
https://gitlab.jklug.work/users/sign_in
# Default user:
root
# Password:
lk68RxYgb3hoplbQLmOSSaewZrurIih53z9KRJ9oA7ino09D3mpCvbnmNcox2OXo
Clone Repository via GitLab Shell #
Use the GitLab Shell domain name to clone GitLab repositories:
# Example: Clone a GitLab repository
git clone git@gitlab-shell.jklug.work:root/example-k8s-deployment.git
CoreDNS DNS Entry #
Edit CoreDNS ConfigMap #
# Adopt the CoreDNS ConfigMap / configuration
kubectl edit cm coredns -n kube-system
Original
# Please edit the object below. Lines beginning with a '#' will be ignored,
# and an empty file will abort the edit. If an error occurs while saving this file will be
# reopened with the relevant failures.
#
apiVersion: v1
data:
Corefile: |
.:53 {
errors
health {
lameduck 5s
}
ready
kubernetes cluster.local in-addr.arpa ip6.arpa {
pods insecure
fallthrough in-addr.arpa ip6.arpa
ttl 30
}
prometheus :9153
forward . /etc/resolv.conf {
max_concurrent 1000
}
cache 30
loop
reload
loadbalance
}
kind: ConfigMap
metadata:
creationTimestamp: "2024-11-23T11:51:59Z"
name: coredns
namespace: kube-system
resourceVersion: "253"
uid: 7780f5a2-09c9-418c-b139-43231097afe7
Add DNS entries for GitLab and the GitLab Registry:
# Please edit the object below. Lines beginning with a '#' will be ignored,
# and an empty file will abort the edit. If an error occurs while saving this file will be
# reopened with the relevant failures.
#
apiVersion: v1
data:
Corefile: |
.:53 {
errors
health {
lameduck 5s
}
ready
kubernetes cluster.local in-addr.arpa ip6.arpa {
pods insecure
fallthrough in-addr.arpa ip6.arpa
ttl 30
}
hosts {
192.168.30.200 gitlab.jklug.work # Add hosts entry
192.168.30.200 gitlab-registry.jklug.work # Add hosts entry
fallthrough
}
prometheus :9153
forward . /etc/resolv.conf {
max_concurrent 1000
}
cache 30
loop
reload
loadbalance
}
kind: ConfigMap
metadata:
creationTimestamp: "2024-11-23T11:51:59Z"
name: coredns
namespace: kube-system
resourceVersion: "253"
uid: 7780f5a2-09c9-418c-b139-43231097afe7
Test DNS Resolution #
# Run a busybox pod and test the GitLab DNS resolution
kubectl run -i --tty --rm debug --image=busybox --restart=Never -- nslookup gitlab.jklug.work
# Shell output:
Server: 10.96.0.10
Address: 10.96.0.10:53
Name: gitlab.jklug.work
Address: 192.168.30.200
pod "debug" deleted
GitLab Runner Installation #
Namespace #
# Create a "gitlab-runner" namespace for the GitLab Runner
kubectl create ns gitlab-runner
Create Runner Token #
-
Go to: (Admin area) “CI/CD” > Runners
-
Click “New instance runner”
-
Flag the “Run untagged jobs” option
-
Add a runner description like
K8s-Runner-01
-
Click “Create runner”
-
Copy the token
glrt-t1_JsLwvj7nzFbAczz9CMjf
Helm Chart Values #
# Create a configuration for the GitLab Runner values
vi runner-values.yaml
# Define GitLab URL
gitlabUrl: https://gitlab.jklug.work/
runnerRegistrationToken: glrt-t1_JsLwvj7nzFbAczz9CMjf # Paste token
replicas: 2
runners:
config: |
[[runners]]
name = "shared-runner"
executor = "kubernetes"
[runners.kubernetes]
namespace = "gitlab-runner" # Define the GitLab Runner namespace
privileged = true
# RBAC Settings
rbac: # Create RBAC for the GitLab Runner
create: true
serviceAccount: # Create a ServiceAccount
create: true
name: gitlab-runner
Install GitLab Runner #
# Dry-run
helm install --dry-run --debug gitlab-runner gitlab/gitlab-runner \
--namespace gitlab-runner \
-f runner-values.yaml
# Install GitLab Runner
helm install gitlab-runner gitlab/gitlab-runner \
--namespace gitlab-runner \
-f runner-values.yaml
# Shell output:
NAME: gitlab-runner
LAST DEPLOYED: Sat Nov 23 20:56:34 2024
NAMESPACE: gitlab-runner
STATUS: deployed
REVISION: 1
TEST SUITE: None
NOTES:
Your GitLab Runner should now be registered against the GitLab instance reachable at: "https://gitlab.jklug.work/"
Runner namespace "gitlab-runner" # Define GitLab Runner namespace was found in runners.config template.
Verify Installation #
List Runner Pods #
# List GitLab Runner pod
kubectl get pods -n gitlab-runner
# Shell output: (Wait till ready)
NAME READY STATUS RESTARTS AGE
gitlab-runner-66c7845bd8-2bcbv 1/1 Running 0 3m2s
gitlab-runner-66c7845bd8-bcrhm 1/1 Running 0 3m2s
# Optional, verify the RBAC service account
kubectl describe pod gitlab-runner-66c7845bd8-2bcbv -n gitlab-runner | grep "Service Account"
# Shell output:
Service Account: gitlab-runner
RBAC Role & RoleBinding #
# Describe the created RBAC Role
kubectl describe role gitlab-runner -n gitlab-runner
# Shell output:
Service Account: gitlab-runner
ubuntu@ubuntu1:~$ kubectl describe role gitlab-runner -n gitlab-runner
Name: gitlab-runner
Labels: app=gitlab-runner
app.kubernetes.io/managed-by=Helm
chart=gitlab-runner-0.71.0
heritage=Helm
release=gitlab-runner
Annotations: meta.helm.sh/release-name: gitlab-runner
meta.helm.sh/release-namespace: gitlab-runner
PolicyRule:
Resources Non-Resource URLs Resource Names Verbs
--------- ----------------- -------------- -----
* [] [] [*]
# List RoleBinging
kubectl get rolebinding -n gitlab-runner
# Shell output:
gitlab-runner Role/gitlab-runner 3m37s
# Describe the RoleBinding
kubectl describe rolebinding gitlab-runner -n gitlab-runner
# Shell output:
Name: gitlab-runner
Labels: app=gitlab-runner
app.kubernetes.io/managed-by=Helm
chart=gitlab-runner-0.71.0
heritage=Helm
release=gitlab-runner
Annotations: meta.helm.sh/release-name: gitlab-runner
meta.helm.sh/release-namespace: gitlab-runner
Role:
Kind: Role
Name: gitlab-runner
Subjects:
Kind Name Namespace
---- ---- ---------
ServiceAccount gitlab-runner gitlab-runner
GitLab Webinterface #
Verify the GitLab Runner is online:
Example Deployment Pipeline #
This GitLab repository demonstrates the deployment of an Nginx container to an Ubuntu-based Linux server with Docker installed at IP address 192.168.30.14
The setup serves as a simple test to ensure that both GitLab and its Runner are functioning as expected.
Deployment Server #
Create a User for the Container Deployment #
On the deployment servers, create a new user “gitlab-deployment” for the GitLab CI pipeline:
# Create user for the GitLab deployment
sudo adduser gitlab-deployment
# Add the user to the Docker group
sudo usermod -aG docker gitlab-deployment
GitLab Registry DNS Entry #
Make sure the deployment server is able to resolv the domain name of the GitLab registry:
# Add the following DNS entry
192.168.30.200 gitlab.jklug.work gitlab-registry.jklug.work
Controller Node #
Create SSH Key Pair #
Create a SSH key pair on the controller node:
# Create SSH key pair
ssh-keygen -t rsa -b 2048
Copy SSH Key #
Copy the public SSH key from the previously created SSH key pair to the authorized_keys
file of the gitlab-deployment
user on the deployment server:
# Copy the public SSH key: Specific key
ssh-copy-id -i ~/.ssh/id_rsa.pub gitlab-deployment@192.168.30.14
SSH into the deployment servers to add the fingerprint:
# SSH into deployment server
ssh gitlab-deployment@192.168.30.14
# Exit the SSH session
exit
GitLab Repository #
File & Folder Structure #
The file and folder structure of the GitLab repository looks like this:
GitLab-Repository
├── Dockerfile
├── .gitlab-ci.yml
├── README.md
└── website
└── index.html
CI/CD Variable #
Add the private SSH key from the previously created SSH key pair as variable to the GitLab repository:
-
Go to: (Project) “Settings” > “CI/CD”
-
Expand the “Variables” section
-
Click “Add variable”
-
Select “Type”: “File”
-
Unflag “Protect variable”
-
Key:
ID_RSA
-
Past the private SSH key into the “Value” box (Add empty paragraph after the key)
-
Click “Add variable”
CI Pipeline Manifest #
- .gitlab-ci.yml
stages:
- build
- deploy
### Build Job
build:
stage: build
image: docker:20.10.16
services:
- name: docker:20.10.16-dind
command:
- "--tls=false" # Disable TLS for Docker-in-Docker
variables:
DOCKER_HOST: tcp://localhost:2375
DOCKER_TLS_CERTDIR: ""
script:
- docker info
- docker build -t $CI_REGISTRY_IMAGE:latest .
- docker tag $CI_REGISTRY_IMAGE:latest $CI_REGISTRY_IMAGE:$CI_COMMIT_REF_NAME
- docker login -u gitlab-ci-token -p $CI_JOB_TOKEN $CI_REGISTRY
- docker push $CI_REGISTRY_IMAGE:$CI_COMMIT_REF_NAME
### Deploy Job
deploy_production:
stage: deploy
image: alpine:latest
variables:
IP: "192.168.30.14"
USER: "gitlab-deployment"
before_script:
- apk update && apk add openssh-client
- if [ -f "$ID_RSA" ]; then chmod og= $ID_RSA; fi
script:
- ssh -i $ID_RSA -o StrictHostKeyChecking=no $USER@$IP "docker login -u gitlab-ci-token -p $CI_JOB_TOKEN $CI_REGISTRY"
- ssh -i $ID_RSA -o StrictHostKeyChecking=no $USER@$IP "docker pull $CI_REGISTRY_IMAGE:$CI_COMMIT_REF_NAME"
- ssh -i $ID_RSA -o StrictHostKeyChecking=no $USER@$IP "docker container rm -f website-preview || true"
- ssh -i $ID_RSA -o StrictHostKeyChecking=no $USER@$IP "docker run -d -p 80:80 --restart=unless-stopped --name website-preview $CI_REGISTRY_IMAGE:$CI_COMMIT_REF_NAME"
only:
- main
Dockerfile #
FROM nginx:1.18
COPY website/index.html /usr/share/nginx/html
HTML File #
- website/index.html
<!DOCTYPE html>
<html>
<head>
<title>jklug.work</title>
</head>
<body>
<h1>Deploy from K8s based GitLab</h1>
<p>Nginx Container</p>
</body>
</html>