DEV Community

Mehmet Samet Duman
Mehmet Samet Duman

Posted on

How to install GitLab EE Ultimate with K8s Helm Chart

This document was prepared by Mehmet Samet Duman under the auspices of Project Tick HQ and is a documented account of a real-life event. This document is licensed under CC-BY-SA 4.0.

Hi everyone! Today I'm going to show you how to set up GitLab with K8s
Helm Chart. Let's get started. We'll start by dumping the default values
​​and then deploy PostgreSQL with the first CNPG and Redis with Bitnami.

Firstly, init your first repository and create namespace from GitLab

mkdir gitlabhq-production && cd gitlabhq-production
git init
kubectl create namespace gitlab
Enter fullscreen mode Exit fullscreen mode

Then, add helm repositories and dump Redis values with this commands;

helm repo add bitnami https://charts.bitnami.com/bitnami
helm repo update
helm show values bitnami/redis > redis-all-values.yaml
Enter fullscreen mode Exit fullscreen mode

And we'll copy the values ​​so you can edit them and reference the defaults.

cp redis-all-values.yaml redis-values.yaml
Enter fullscreen mode Exit fullscreen mode

Afterwards, you can edit it as you wish using any editing tool you prefer.
This is what my values ​​file looks like now.

# redis-values.yaml
architecture: replication
auth:
  existingSecret: gitlab-redis
  existingSecretPasswordKey: redis-password
fullnameOverride: gitlab-redis
master:
  nodeSelector:
    kubernetes.io/hostname: mail.projecttick.org
  persistence:
    size: 8Gi
replica:
  nodeSelector:
    kubernetes.io/hostname: mail.projecttick.org
  persistence:
    size: 8Gi
  replicaCount: 3
sentinel:
  enabled: true
  masterSet: gitlab-redis
  nodeSelector:
    kubernetes.io/hostname: mail.projecttick.org
Enter fullscreen mode Exit fullscreen mode

But before deploying it, we need to create its secret. For this, I prefer YAML. The following YAML file will help me define my secret.

# gitlab-redis-secret.yaml
apiVersion: v1
kind: Secret
metadata:
  name: gitlab-redis
  namespace: gitlab
type: Opaque
stringData:
  redis-password: <YOURREDISPASSWORD>
Enter fullscreen mode Exit fullscreen mode

And install with this command.

kubectl apply -f gitlab-redis-secret.yaml
Enter fullscreen mode Exit fullscreen mode

If you saved the password, please delete this YAML file, otherwise your secret and passwords will be corrupted in your Git commit.

rm gitlab-redis-secret.yaml
Enter fullscreen mode Exit fullscreen mode

And we install it with Helm install using the following command:

helm upgrade gitlab-redis bitnami/redis -f ./redis-values.yaml -n gitlab --timeout 600s --install
Enter fullscreen mode Exit fullscreen mode

Now we will deploy PostgreSQL with CNPG. Because GitLab itself created these dependencies, it fell to us to deploy them.
The operator will not create the secret. Therefore, we will create the secret ourselves. Creating it with YAML is easier,
but remember to save these secrets in hidden locations. Otherwise, your data will disappear and you won't be able to access it.

For CNPG, we'll be deploying manually instead of using an operator and helm chart. I'm using this and it's working perfectly for me.

# gitlab-prod-cnpg.yaml
apiVersion: postgresql.cnpg.io/v1
kind: Cluster
metadata:
  name: gitlab-prod-cnpg
  namespace: gitlab
spec:
  postgresql:
    parameters:
      max_connections: "200"
  instances: 1
  imageName: ghcr.io/cloudnative-pg/postgresql:17.6
  primaryUpdateStrategy: unsupervised
  enableSuperuserAccess: true
  bootstrap:
    initdb:
      database: gitlabhq_production
      owner: gitlab
      secret:
        name: gitlab-prod-cnpg-app
  storage:
    size: 150Gi
    storageClass: local-path
  affinity:
    nodeSelector:
      kubernetes.io/hostname: mail.projecttick.org
  resources:
    requests:
      cpu: 500m
      memory: 1Gi
    limits:
      cpu: "2"
      memory: 4Gi
Enter fullscreen mode Exit fullscreen mode
# gitlab-prod-cnpg-app-secret.yaml
apiVersion: v1
kind: Secret
metadata:
  name: gitlab-prod-cnpg-app
  namespace: gitlab
type: kubernetes.io/basic-auth
stringData:
  username: gitlab
  password: <YOURPGSQLPASSWORD>
Enter fullscreen mode Exit fullscreen mode

You can deploy this with the following command.

kubectl apply -f gitlab-prod-cnpg-app-secret.yaml
kubectl apply -f gitlab-prod-cnpg.yaml
Enter fullscreen mode Exit fullscreen mode

I'm saying this again and again: if you saved the password, please delete this YAML file, otherwise your secret and passwords will get mixed up in your Git commit.

rm gitlab-prod-cnpg-app-secret.yaml
Enter fullscreen mode Exit fullscreen mode

Now, if you don't have an ingress provider in the cluster, you need to set one up. I recommend Nginx Ingress. If you don't have a provider, let's set one up immediately. But we need to import our TLS certificates as secrets, otherwise recognition might not work, and you'll end up with a stub certificate that won't be able to pass through anywhere. I used to use omnibus gitlab, but I generated my certificates with certbot. First, let's check the certificate directory with certbot. This is the result I got:

[root@mail k8s-migration]# certbot certificates
Saving debug log to /var/log/letsencrypt/letsencrypt.log

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Found the following certs:
  Certificate Name: projecttick.org
    Serial Number: ************************
    Key Type: ECDSA
    Identifiers: projecttick.org *.pages.projecttick.net *.projecttick.com *.projecttick.net *.projecttick.org projecttick.com projecttick.net
    Expiry Date: 2026-09-12 14:19:06+00:00 (VALID: 82 days)
    Certificate Path: /etc/letsencrypt/live/projecttick.org/fullchain.pem
    Private Key Path: /etc/letsencrypt/live/projecttick.org/privkey.pem
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
[root@mail k8s-migration]# 
Enter fullscreen mode Exit fullscreen mode

Now, we import our TLS certificate using this command.

kubectl -n gitlab create secret tls gitlab-wildcard-tls --cert="/etc/letsencrypt/live/projecttick.org/fullchain.pem" --key="/etc/letsencrypt/live/projecttick.org/privkey.pem" --dry-run=client -o yaml | kubectl apply -f -
Enter fullscreen mode Exit fullscreen mode

Now let's add the ingress-nginx repository, update the repositories, and dump all the values.

helm repo add ingress-nginx https://kubernetes.github.io/ingress-nginx
helm repo update
helm show values ingress-nginx/ingress-nginx > ingress-nginx-all-values.yaml
Enter fullscreen mode Exit fullscreen mode

And we'll copy the values again ​​so you can edit them and reference the defaults.

cp ingress-nginx-all-values.yaml ingress-nginx-values.yaml
Enter fullscreen mode Exit fullscreen mode

I'll install it after I've edited it, but first let me show you mine as a real usage reference.

# ingress-nginx-values.yaml
controller:
  ingressClass: gitlab-nginx
  ingressClassByName: true
  ingressClassResource:
    name: gitlab-nginx
    enabled: true
    controllerValue: k8s.io/ingress-nginx
    default: false

  watchIngressWithoutClass: false

  nodeSelector:
    kubernetes.io/hostname: mail.projecttick.org

  replicaCount: 1

  hostPort:
    enabled: true
    ports:
      http: 80
      https: 443

  containerPort:
    http: 80
    https: 443

  updateStrategy:
    type: Recreate

  service:
    enabled: false

  extraArgs:
    default-ssl-certificate: gitlab/gitlab-wildcard-tls
    ingress-class-by-name: "true"

  admissionWebhooks:
    enabled: false

  allowSnippetAnnotations: true

  config:
    annotations-risk-level: Critical
Enter fullscreen mode Exit fullscreen mode

As you may have noticed, I've assigned the same certificate we just added as the default. Now we can proceed with the installation.

helm upgrade ingress-nginx ingress-nginx/ingress-nginx --version 4.15.1 --namespace ingress-nginx -f ./ingress-nginx-values.yaml --timeout 600s --install --create-namespace
Enter fullscreen mode Exit fullscreen mode

If you're wondering why we put this in a separate namespace, it's so that it's easier if you ever want to delete GitLab and install another service. And we don't need to carry a secret here because we're reading directly from the GitLab namespace.

Now, I have a question for you. If you want to use advanced search, you should install Elasticsearch or Opensearch. If you don't want to install them, please skip this part. I prefer Elasticsearch. Therefore, we'll first add the Elasticsearch repository, then take a dump and edit it. The process is the same. After that, we'll install MinIO and finally install GitLab to finish.

helm repo add elastic https://helm.elastic.co
helm repo update
helm show values elastic/elasticsearch > elasticsearch-all-values.yaml
cp elasticsearch-all-values.yaml elasticsearch-values.yaml
Enter fullscreen mode Exit fullscreen mode

This is what my values ​​file looks like:

# elasticsearch-values.yaml
createCert: false
esConfig:
  elasticsearch.yml: 'xpack.security.enabled: false'
minimumMasterNodes: 1
protocol: http
replicas: 1
volumeClaimTemplate:
  resources:
    requests:
      storage: 50Gi
Enter fullscreen mode Exit fullscreen mode

Now let's set this up with Helm.

helm upgrade elasticsearch elastic/elasticsearch -n gitlab -f ./elasticsearch-values.yaml --timeout 600s --install
Enter fullscreen mode Exit fullscreen mode

Now we can move on to the main event. We will deploy MinIO, but we won't be using Helm; instead, we'll deploy it with a YAML file using kubectl. However, for this, we first need to create a namespace called "storage", write a YAML file, and deploy it using apply. I chose the minio/minio:latest image from quay.io. We will also write environment values ​​and create a secret when deploying this.

I wrote a YAML file. It contains a statefulset, a service, and a secret. We'll also write a YAML file for the ingress layer, but that'll come later.

# minio.yaml
apiVersion: v1
kind: Namespace
metadata:
  name: storage

---
apiVersion: v1
kind: Secret
metadata:
  name: minio-secret
  namespace: storage
type: Opaque
stringData:
  root-user: "YOURUSER"
  root-password: "YOURPASSWORD"

---
apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: minio
  namespace: storage
spec:
  serviceName: minio
  replicas: 1
  selector:
    matchLabels:
      app: minio
  template:
    metadata:
      labels:
        app: minio
    spec:
      containers:
      - name: minio
        image: quay.io/minio/minio:latest
        args:
          - server
          - /data
          - --console-address
          - ":9001"
        env:
        - name: MINIO_ROOT_USER
          valueFrom:
            secretKeyRef:
              name: minio-secret
              key: root-user
        - name: MINIO_ROOT_PASSWORD
          valueFrom:
            secretKeyRef:
              name: minio-secret
              key: root-password
        - name: MINIO_SERVER_URL
          value: https://s3.projecttick.net

        - name: MINIO_BROWSER_REDIRECT_URL
          value: https://minio.projecttick.net

        - name: MINIO_DOMAIN
          value: s3.projecttick.net

        ports:
        - containerPort: 9000
          name: s3
        - containerPort: 9001
          name: console
        volumeMounts:
        - name: data
          mountPath: /data
  volumeClaimTemplates:
  - metadata:
      name: data
    spec:
      storageClassName: local-path
      accessModes:
        - ReadWriteOnce
      resources:
        requests:
          storage: 120Gi

---
apiVersion: v1
kind: Service
metadata:
  name: minio
  namespace: storage
spec:
  type: NodePort
  selector:
    app: minio
  ports:
  - name: s3
    port: 9000
    targetPort: 9000
    nodePort: 30900

  - name: console
    port: 9001
    targetPort: 9001
    nodePort: 30901

Enter fullscreen mode Exit fullscreen mode

CAUTION!
Do not assign https:// to the MINIO_DOMAIN value! Otherwise, you will get a 403 error and the S3 system will be corrupted for external use.

Let's deploy it now.

kubectl create namespace storage
kubectl apply -f minio.yaml
Enter fullscreen mode Exit fullscreen mode

Now we'll write the ingress layer for this. If you're wondering why I've given it a separate entry, it's so you can have a reference for subsequent ingress YAML files.

# minio-ingress.yaml
# === s3.projecttick.net -> minio 9000 (S3 API) ===
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: minio-s3
  namespace: storage
  annotations:
    nginx.ingress.kubernetes.io/proxy-body-size: "0"
    nginx.ingress.kubernetes.io/proxy-request-buffering: "off"
    nginx.ingress.kubernetes.io/proxy-buffering: "off"
spec:
  ingressClassName: gitlab-nginx
  tls:
    - hosts: [s3.projecttick.net]
      secretName: gitlab-wildcard-tls
  rules:
    - host: s3.projecttick.net
      http:
        paths:
          - path: /
            pathType: Prefix
            backend: {service: {name: minio, port: {number: 9000}}}
---
# === minio.projecttick.net -> minio 9001 (Console, websocket) ===
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: minio-console
  namespace: storage
  annotations:
    nginx.ingress.kubernetes.io/proxy-body-size: "0"
spec:
  ingressClassName: gitlab-nginx
  tls:
    - hosts: [minio.projecttick.net]
      secretName: gitlab-wildcard-tls
  rules:
    - host: minio.projecttick.net
      http:
        paths:
          - path: /
            pathType: Prefix
            backend: {service: {name: minio, port: {number: 9001}}}
Enter fullscreen mode Exit fullscreen mode

But before we deploy this, we need to move our TLS to the storage namespace. We deploy it directly after transporting it.

kubectl -n storage create secret tls gitlab-wildcard-tls --cert="/etc/letsencrypt/live/projecttick.org/fullchain.pem" --key="/etc/letsencrypt/live/projecttick.org/privkey.pem" --dry-run=client -o yaml | kubectl apply -f -
kubectl apply -f minio-ingress.yaml
Enter fullscreen mode Exit fullscreen mode

MinIO has now been deployed. Now let's get to the final boss, GitLab. While its values ​​aren't overly complex, I'd say they're extremely detailed.

helm repo add gitlab https://charts.gitlab.io/
helm repo update
helm show values gitlab/gitlab > gitlab-all-values.yaml
cp gitlab-all-values.yaml gitlab-values.yaml
Enter fullscreen mode Exit fullscreen mode

My values ​​file looks like this. But remember, almost everything is open in it. And don't forget, the GitLab example you're currently reading this document from is running with this YAML values ​​file.

global:
  edition: ee
  hosts:
    ssh: git.projecttick.org
    domain: projecttick.org
    https: true
    pages:
      name: pages.projecttick.net
    gitlab:
      name: git.projecttick.org
    registry:
      name: registry.projecttick.org
    kas:
      name: kas.projecttick.org
    minio:
      name: minio.projecttick.org

  shell:
    port: 22

  gatewayApi:
    enabled: false
    installEnvoy: false
    configureCertmanager: false

  ingress:
    enabled: true
    provider: nginx
    class: gitlab-nginx
    configureCertmanager: false
    tls:
      enabled: true
      secretName: gitlab-wildcard-tls

  keda:
    enabled: true

  monitoring:
    enabled: true

  minio:
    enabled: false

  praefect:
    enabled: false

  gitaly:
    enabled: true
    internal:
      names: [default]
    service:
      name: gitaly
      type: ClusterIP
      externalPort: 8075
      internalPort: 8075
      tls:
        externalPort: 8076
        internalPort: 8076
    client:
      maxAttempts: 4
      initialBackoff: '0.4s'
      maxBackoff: '1.4s'

  appConfig:
    omniauth:
      enabled: true
      allowSingleSignOn: ['openid_connect','github','gitlab','google_oauth2','azure_activedirectory_v2']
      blockAutoCreatedUsers: true
      autoLinkUser: ['openid_connect','github','gitlab','google_oauth2','azure_activedirectory_v2']
      providers:
        - secret: gitlab-omniauth-github
          key: provider
        - secret: gitlab-omniauth-gitlab
          key: provider
        - secret: gitlab-omniauth-google
          key: provider
        - secret: gitlab-omniauth-azure
          key: provider
        - secret: gitlab-omniauth-openid
          key: provider
        - secret: gitlab-omniauth-group
          key: provider
    enableUsagePing: false
    enableSeatLink: false
    usernameChangingEnabled: true
    incomingEmail:
      enabled: true
      address: "pt-gitlab-incoming+%{key}@projecttick.org"
      host: "mail.projecttick.org"
      port: 993
      ssl: true
      startTls: false
      user: "pt-gitlab-incoming@projecttick.org"
      password:
        secret: gitlab-incoming-email-password
        key: password
      mailbox: "inbox"
      inboxMethod: "imap"
      deliveryMethod: webhook
    serviceDeskEmail:
      enabled: true
      address: "desk+%{key}@projecttick.org"
      host: "mail.projecttick.org"
      port: 993
      ssl: true
      startTls: false
      user: "desk@projecttick.org"
      password:
        secret: gitlab-service-desk-email-password
        key: password
      mailbox: "inbox"
      inboxMethod: "imap"
      deliveryMethod: webhook
    defaultTheme: 4
    defaultColorMode: 1
    issueClosingPattern: "\\b((?:[Cc]los(?:e[sd]?|ing)|\\b[Ff]ix(?:e[sd]|ing)?|\\b[Rr]esolv(?:e[sd]?|ing)|\\b[Ii]mplement(?:s|ed|ing)?)(:?) +(?:(?:issues? +)?%{issue_ref}(?:(?:, *| +and +)?)|([A-Z][A-Z0-9_]+-\\d+))+)"
    enableImpersonation: false
    defaultProjectsFeatures:
      issues: true
      mergeRequests: true
      wiki: true
      snippets: true
      builds: true
    extra:
      googleAnalyticsId: "G-91XWT2Q4LY"
    ldap:
      enabled: true
      preventSignin: false
      servers:
        main:
          label: 'Project Tick LDAP'
          host: 'ldap.projecttick.net'
          port: 636
          uid: 'uid'
          base: 'ou=people,dc=projecttick,dc=net'
          encryption: 'simple_tls'
          verify_certificates: true
          bind_dn: 'cn=Directory Manager'
          password:
            secret: gitlab-ldap-password
            key: password
          user_filter: '(objectClass=nsPerson)'
          active_directory: false
    object_store:
      enabled: true
      connection:
        secret: gitlab-object-storage
        key: connection
    lfs:
      enabled: true
      proxy_download: true
      bucket: gitlab-lfs
    artifacts:
      enabled: true
      proxy_download: true
      bucket: gitlab-artifacts
    uploads:
      enabled: true
      proxy_download: true
      bucket: gitlab-uploads
    packages:
      enabled: true
      proxy_download: true
      bucket: gitlab-packages
    externalDiffs:
      enabled: true
      proxy_download: true
      bucket: gitlab-mr-diffs
    terraformState:
      enabled: true
      bucket: gitlab-terraform
    ciSecureFiles:
      enabled: true
      bucket: gitlab-secure-files
    dependencyProxy:
      enabled: true
      bucket: gitlab-dependency-proxy
    backups:
      bucket: gitlab-backups
      tmpBucket: tmp

  smtp:
    enabled: true
    address: "152.53.231.231"
    port: 587
    authentication: "login"
    domain: "projecttick.org"
    starttls_auto: true
    openssl_verify_mode: "none"
    password:
      secret: gitlab-smtp-password
      key: password
    user_name: "noreply@projecttick.org"

  pages:
    enabled: true
    accessControl: false
    artifactsServer: true
    objectStore:
      enabled: true
      bucket: gitlab-pages
      connection:
        secret: gitlab-pages-storage
        key: connection
      namespaceInPath: false

  email:
    from: "noreply@projecttick.org"
    display_name: "Project Tick GitLab"
    reply_to: "support@projecttick.org"

  psql:
    host: gitlab-prod-cnpg-rw.gitlab.svc.cluster.local
    port: 5432
    username: gitlab
    database: gitlabhq_production
    password:
      secret: gitlab-prod-cnpg-app
      key: password
  redis:
    host: gitlab-redis
    sentinels:
      - host: gitlab-redis-node-0.gitlab-redis-headless.gitlab.svc.cluster.local
        port: 26379
      - host: gitlab-redis-node-1.gitlab-redis-headless.gitlab.svc.cluster.local
        port: 26379
      - host: gitlab-redis-node-2.gitlab-redis-headless.gitlab.svc.cluster.local
        port: 26379
    sentinelAuth:
      enabled: true
      secret: gitlab-redis
      key: redis-password
    auth:
      enabled: true
      secret: gitlab-redis
      key: redis-password

  time_zone: UTC

  serviceAccount:
    enabled: true
    create: true
    automountServiceAccountToken: true

certmanager-issuer:
  install: false
  email: projecttick@projecttick.org

registry:
  storage:
    secret: gitlab-registry
    key: config

installCertmanager: false

nginx-ingress:
  enabled: false

certmanager:
  installCRDs: false

redis:
  install: false

postgresql:
  install: false

minio:
  install: false

prometheus:
  install: true
  rbac:
    create: true
  alertmanager:
    enabled: true
  kube-state-metrics:
    enabled: true
  prometheus-node-exporter:
    enabled: true
  prometheus-pushgateway:
    enabled: true
  scrapeConfigs: null
  server:
    retention: 15d
    strategy:
      type: Recreate
    containerSecurityContext:
      runAsUser: 1000
      allowPrivilegeEscalation: false
      runAsNonRoot: true
      capabilities:
        drop: [ "ALL" ]
      seccompProfile:
        type: "RuntimeDefault"
  podSecurityPolicy:
    enabled: false
  configmapReload:
    prometheus:
      containerSecurityContext:
        runAsUser: 1000
        allowPrivilegeEscalation: false
        runAsNonRoot: true
        capabilities:
          drop: [ "ALL" ]
        seccompProfile:
          type: "RuntimeDefault"
  serverFiles:
    prometheus.yml:
      scrape_configs:
        - job_name: prometheus
          static_configs:
            - targets:
                - localhost:9090
        - job_name: kubernetes-apiservers
          kubernetes_sd_configs:
            - role: endpoints
          scheme: https
          tls_config:
            ca_file: /var/run/secrets/kubernetes.io/serviceaccount/ca.crt
            insecure_skip_verify: true
          bearer_token_file: /var/run/secrets/kubernetes.io/serviceaccount/token
          relabel_configs:
            - source_labels:
                [
                  __meta_kubernetes_namespace,
                  __meta_kubernetes_service_name,
                  __meta_kubernetes_endpoint_port_name,
                ]
              action: keep
              regex: default;kubernetes;https
        - job_name: kubernetes-pods
          kubernetes_sd_configs:
            - role: pod
          relabel_configs:
            - source_labels:
                [__meta_kubernetes_pod_annotation_gitlab_com_prometheus_scrape]
              action: keep
              regex: true
            - source_labels:
                [__meta_kubernetes_pod_annotation_gitlab_com_prometheus_scheme]
              action: replace
              regex: (https?)
              target_label: __scheme__
            - source_labels:
                [__meta_kubernetes_pod_annotation_gitlab_com_prometheus_path]
              action: replace
              target_label: __metrics_path__
              regex: (.+)
            - source_labels:
                [
                  __address__,
                  __meta_kubernetes_pod_annotation_gitlab_com_prometheus_port,
                ]
              action: replace
              regex: ([^:]+)(?::\d+)?;(\d+)
              replacement: $1:$2
              target_label: __address__
            - action: labelmap
              regex: __meta_kubernetes_pod_label_(.+)
            - source_labels: [__meta_kubernetes_namespace]
              action: replace
              target_label: kubernetes_namespace
            - source_labels: [__meta_kubernetes_pod_name]
              action: replace
              target_label: kubernetes_pod_name
        - job_name: kubernetes-service-endpoints
          kubernetes_sd_configs:
            - role: endpoints
          relabel_configs:
            - action: keep
              regex: true
              source_labels:
                - __meta_kubernetes_service_annotation_gitlab_com_prometheus_scrape
            - action: replace
              regex: (https?)
              source_labels:
                - __meta_kubernetes_service_annotation_gitlab_com_prometheus_scheme
              target_label: __scheme__
            - action: replace
              regex: (.+)
              source_labels:
                - __meta_kubernetes_service_annotation_gitlab_com_prometheus_path
              target_label: __metrics_path__
            - action: replace
              regex: ([^:]+)(?::\d+)?;(\d+)
              replacement: $1:$2
              source_labels:
                - __address__
                - __meta_kubernetes_service_annotation_gitlab_com_prometheus_port
              target_label: __address__
            - action: labelmap
              regex: __meta_kubernetes_service_label_(.+)
            - action: replace
              source_labels:
                - __meta_kubernetes_namespace
              target_label: kubernetes_namespace
            - action: replace
              source_labels:
                - __meta_kubernetes_service_name
              target_label: kubernetes_name
            - action: replace
              source_labels:
                - __meta_kubernetes_pod_node_name
              target_label: kubernetes_node
        - job_name: kubernetes-services
          metrics_path: /probe
          params:
            module: [http_2xx]
          kubernetes_sd_configs:
            - role: service
          relabel_configs:
            - source_labels:
                [
                  __meta_kubernetes_service_annotation_gitlab_com_prometheus_probe,
                ]
              action: keep
              regex: true
            - source_labels: [__address__]
              target_label: __param_target
            - target_label: __address__
              replacement: blackbox
            - source_labels: [__param_target]
              target_label: instance
            - action: labelmap
              regex: __meta_kubernetes_service_label_(.+)
            - source_labels: [__meta_kubernetes_namespace]
              target_label: kubernetes_namespace
            - source_labels: [__meta_kubernetes_service_name]
              target_label: kubernetes_name

upgradeCheck:
  enabled: true
  securityContext:
    runAsUser: 65534
    fsGroup: 65534
    seccompProfile:
      type: "RuntimeDefault"
  containerSecurityContext:
    runAsUser: 65534
    allowPrivilegeEscalation: false
    runAsNonRoot: true

shared-secrets:
  enabled: true
  rbac:
    create: true

ai-gateway:
  install: false
  ingress:
    enabled: false

gitlab-runner:
  install: true
  nodeSelector:
    region: us
  tolerations:
    - key: dedicated
      operator: Equal
      value: canary
      effect: NoSchedule
  replicas: 4
  runners:
    config: |
      concurrent = 40
      check_interval = 3

      [[runners]]
        [runners.kubernetes]
          namespace = "gitlab"
          image = "alpine:latest"
          memory_request = "2Gi"
          memory_limit = "2Gi"
          cpu_request = "300m"

        [runners.kubernetes.node_selector]
          "region" = "us"
          "kubernetes.io/arch" = "arm64"

        [runners.kubernetes.node_tolerations]
          "dedicated=canary" = "NoSchedule"

gitlab-zoekt:
  install: true
  gateway:
    basicAuth:
      enabled: true
      secretName: gitlab-zoekt-gateway-basic-auth
  zoekt:
    indexer:
      replicas: 1
    webserver:
      replicas: 1
  persistence:
    size: 150Gi

gitlab:
  webservice:
    deployment:
      strategy:
        type: RollingUpdate
        rollingUpdate:
          maxUnavailable: 1
          maxSurge: 1
    minReplicas: 2
    maxReplicas: 4
  sidekiq:
    minReplicas: 2
    maxReplicas: 4
    routingRules:
      - ["*", ""]
    pods:
      - name: all-in-1
        queues: ""
  gitlab-shell:
    enabled: true
    service:
      type: NodePort
      nodePort: 22
  toolbox:
    replicas: 1
    antiAffinityLabels:
      matchLabels:
        app: gitaly
    backups:
      objectStorage:
        config:
          secret: gitlab-backup-storage
          key: config
  migrations:
    enabled: true
Enter fullscreen mode Exit fullscreen mode

IMPORTANT!
I used a separate node selector for the runner, but please use your own selector to avoid it getting stuck in pending mode.

But we can't deploy it as is because there are no buckets or secrets yet.

First, let's create the secrets, and then we'll create the buckets. First, we create our most important secret to enable S3 to connect with GitLab, and we apply this for two separate purposes.

# gitlab-object-storage-connection.yaml
provider: AWS
region: us-east-1
aws_access_key_id: 'YOURUSER'
aws_secret_access_key: 'YOURPASSWORD'
aws_signature_version: 4
host: minio.storage.svc.cluster.local
endpoint: 'http://minio.storage.svc.cluster.local:9000'
path_style: true
Enter fullscreen mode Exit fullscreen mode
kubectl create secret generic gitlab-object-storage --from-file=connection=gitlab-object-storage-connection.yaml -n gitlab
kubectl create secret generic gitlab-pages-storage --from-file=connection=gitlab-object-storage-connection.yaml -n gitlab
Enter fullscreen mode Exit fullscreen mode
# gitlab-backup-storage.yaml
[default]
access_key = YOURUSER
secret_key = YOURPASSWORD
host_base = minio.storage.svc.cluster.local:9000
host_bucket = minio.storage.svc.cluster.local:9000
use_https = False
Enter fullscreen mode Exit fullscreen mode
kubectl create secret generic gitlab-backup-storage --from-file=config=gitlab-backup-storage.yaml -n gitlab
Enter fullscreen mode Exit fullscreen mode
# gitlab-registry.yaml
s3:
  bucket: gitlab-registry
  accesskey: YOURUSER
  secretkey: 'YOURPASSWORD'
  region: us-east-1
  regionendpoint: 'http://minio.storage.svc.cluster.local:9000'
  v4auth: true
  pathstyle: true
  checksum_disabled: true
redirect:
  disable: true
Enter fullscreen mode Exit fullscreen mode
kubectl create secret generic gitlab-registry --from-file=config=gitlab-registry.yaml -n gitlab
Enter fullscreen mode Exit fullscreen mode
# gitlab-mail-ldap-secrets.yaml
apiVersion: v1
kind: Secret
metadata:
  name: gitlab-incoming-email-password
  namespace: gitlab
type: Opaque
stringData:
  password: <INCOMING_EMAIL_PASSWORD>
---
apiVersion: v1
kind: Secret
metadata:
  name: gitlab-service-desk-email-password
  namespace: gitlab
type: Opaque
stringData:
  password: <SERVICE_DESK_PASSWORD>
---
apiVersion: v1
kind: Secret
metadata:
  name: gitlab-smtp-password
  namespace: gitlab
type: Opaque
stringData:
  password: <SMTP_PASSWORD>
---
apiVersion: v1
kind: Secret
metadata:
  name: gitlab-ldap-password
  namespace: gitlab
type: Opaque
stringData:
  password: <LDAP_BIND_PASSWORD>
Enter fullscreen mode Exit fullscreen mode
kubectl create secret generic gitlab-zoekt-gateway-basic-auth --from-literal=gitlab_username=gitlab --from-literal=gitlab_password=<ZOEKT_PASSWORD> -n gitlab
kubectl apply -f gitlab-mail-ldap-secrets.yaml
Enter fullscreen mode Exit fullscreen mode
# azure-omniauth.yaml
name: azure_activedirectory_v2
label: "Azure AD v2"
args:
  client_id: "CLIENT_ID"
  client_secret: "CLIENT_SECRET"
  tenant_id: "TENANT_ID"
Enter fullscreen mode Exit fullscreen mode
# github-omniauth.yaml
name: github
label: "GitHub"
app_id: "APP_ID"
app_secret: "APP_SECRET"
args:
  scope: 'user:email'
Enter fullscreen mode Exit fullscreen mode
# gitlab-omniauth.yaml
name: gitlab
label: "GitLab SaaS"
app_id: "APP_ID"
app_secret: "APP_SECRET"
args:
  scope: 'read_user'
Enter fullscreen mode Exit fullscreen mode
# google-omniauth.yaml
name: google_oauth2
label: "Google"
app_id: "APP_ID"
app_secret: "APP_SECRET"
Enter fullscreen mode Exit fullscreen mode
# group-omniauth.yaml
name: group_saml
Enter fullscreen mode Exit fullscreen mode
# openid-omniauth.yaml
name: openid_connect
label: "Project Tick SSO"
args:
  name: openid_connect
  scope: ['openid','profile','email']
  response_type: code
  issuer: "https://id.projecttick.net/realms/projecttick"
  discovery: true
  client_auth_method: query
  uid_field: preferred_username
  pkce: true
  client_options:
    identifier: "gitlab"
    secret: "SECRET"
    redirect_uri: "https://git.projecttick.org/users/auth/openid_connect/callback"
Enter fullscreen mode Exit fullscreen mode
kubectl create secret generic gitlab-omniauth-azure  --from-file=provider=azure-omniauth.yaml  -n gitlab
kubectl create secret generic gitlab-omniauth-github --from-file=provider=github-omniauth.yaml -n gitlab
kubectl create secret generic gitlab-omniauth-gitlab --from-file=provider=gitlab-omniauth.yaml -n gitlab
kubectl create secret generic gitlab-omniauth-google --from-file=provider=google-omniauth.yaml -n gitlab
kubectl create secret generic gitlab-omniauth-group  --from-file=provider=group-omniauth.yaml  -n gitlab
kubectl create secret generic gitlab-omniauth-openid --from-file=provider=openid-omniauth.yaml -n gitlab
Enter fullscreen mode Exit fullscreen mode

Now that the secrets are ready, we need to create the buckets. GitLab won't create them for us; each bucket referenced in our values ​​must already exist, otherwise the relevant property will initially fail with access errors.

We will be using the MinIO client (mc). First, let's register an alias pointing to our MinIO instance with the root credentials we defined earlier:

IMPORTANT!
If you are creating the buckets from a different location, please enter the full IP address instead of localhost.

mc alias set direct http://127.0.0.1:30900 YOURUSER YOURPASSWORD
Enter fullscreen mode Exit fullscreen mode

Now create every bucket GitLab expects. These names come directly from our values file:

for b in gitlab-lfs gitlab-artifacts gitlab-uploads gitlab-packages \
         gitlab-mr-diffs gitlab-terraform gitlab-secure-files \
         gitlab-dependency-proxy gitlab-backups tmp \
         gitlab-pages gitlab-registry; do
  mc mb direct/$b
done
Enter fullscreen mode Exit fullscreen mode

Verify they were all created:

mc ls direct
Enter fullscreen mode Exit fullscreen mode

Yes! Everything is ready now. There are no more obstacles to deploying GitLab. You can now deploy by pinning the release.

helm upgrade gitlab gitlab/gitlab --version 10.1.0 --namespace gitlab -f ./gitlab-values.yaml --timeout 1200s --install
Enter fullscreen mode Exit fullscreen mode

The deployment may take a while, please wait patiently. You can easily track it afterwards with a command like kubectl get pods -n gitlab. Now, let's get to the main point: the first login with the root password! But remember, Helm may autonomously delete the secret containing the first root password in the next deployment or after 24 hours for security reasons. Therefore, change your password immediately after logging in. Learn your root password with the following command.

kubectl get secret gitlab-gitlab-initial-root-password \
  -n gitlab \
  -o jsonpath='{.data.password}' | base64 -d ; echo
Enter fullscreen mode Exit fullscreen mode

After logging in as root, obtain the Elasticsearch URL, username, and password from the admin panel and enter them. The Elasticsearch Helm installation instructions clearly explain how to obtain them. Then, immediately create an instance runner and use the secret you obtained to create the following secret.

# gitlab-runner-secret.yaml
apiVersion: v1
kind: Secret
metadata:
  name: gitlab-runner-secret
  namespace: gitlab
type: Opaque
stringData:
  runner-registration-token: ""
  runner-token: "YOURRUNNERSECRET"
Enter fullscreen mode Exit fullscreen mode
kubectl apply -f gitlab-runner-secret.yaml
Enter fullscreen mode Exit fullscreen mode

CAUTION!
Please remember to delete the secrets where you entered your passwords. Otherwise, you may encounter serious problems.

That's it! Your GitLab EE instance is now running on Kubernetes with external PostgreSQL (CNPG), Redis Sentinel, MinIO object storage, Elasticsearch, and Zoekt. Happy DevOps-ing!

Top comments (0)