# GitLab runner in Kubernetes with Werf

<https://habr.com/ru/articles/679826/>

### Интро

Сегодня хочу рассказать о связке GitLab + K8S + Werf и как с помощью него быстро собрать и задеплоить свое приложение в одну команду. Этот пост будет иметь формат мини-туториала.

Думаю большинство набредших на эту статью знают, что такое Gitlab и Kubernetes. Не знаете - гугл в помощь. В этой статье это out of scope.

Что такое Werf? Werf - это утилита, объединяющая CI/CD системы (типа Gitlab, Github Actions), docker и helm в одном флаконе и позволяющая одной командой собрать образ контейнера, запушить его в репозиторий контейнеров и задеплоить с помощью helm.

Итак, поехали.

Пост будет коротким и максимально сухим. Поделим его на две части:

1. Настройка окружения
2. Пример деплоя приложения

Приступим к первой части.

### Настройка окружения

Надеюсь у вас уже есть Gitlab. Если нет, то разверните. У меня гитлаб развернут с помощью docker-compose.

O подключении Kubernetes к Gitlab

💡 Gitlab начиная с 15 версии объявил метод подключения K8S через сертификаты как deprecated и предлагает теперь единственный способ подключения кластеров через gitlab-agent и kubernetes agent server (он же kas). [Подробнее тут](https://docs.gitlab.com/ee/user/project/clusters/add_existing_cluster.html).

Если у вас еще на подключен kubernetes agent server - подключайте так

```
environment:
        GITLAB_OMNIBUS_CONFIG: |
            ...
            gitlab_kas['enable'] = true

```

То есть в gitlab.rb это будет просто `gitlab_kas['enable'] = true`. Не забываем делать `gitlab-ctl reconfigure`.

Kubernetes кластер нам тоже понадобится. Надеюсь, что он у вас тоже есть. Если нет - советую попробовать Managed Service For Kubernetes от Yandex.Cloud. Можно выбрать компактные, недорогие и к тому же вымещаемые (самые бюджетные) инстансы.

Теперь необходимо запустить `gitlab-agent` для kubernetes. Для этого создаем репозиторий infra и теперь добавляем файл по пути:

```
# .gitlab/agents/mks-agent
ci_access:
  groups:
  - id: <group_id>
  projects:
  - id: <project_id>

```

Вместо group\_id или project\_id проставляем пути к проектам или группам, где этот kubernetes кластер будет доступен. Например для группы `infra` и проекта `my-group/my-app` это будет выглядеть так:

```
# .gitlab/agents/mks-agent
ci_access:
  groups:
  - id: infra
  projects:
  - id: my-group/my-app

```

После этого идем в Infrastructure > Kubernetes Clusters > Connect a cluster, выбираем из выпадающего списка нужного агента. Gitlab покажет для установки gitlab-agent через helm. Выглядеть будет так:

```
helm repo add gitlab <https://charts.gitlab.io>
helm repo update
helm upgrade --install mks-agent gitlab/gitlab-agent \\
    --namespace gitlab-agent \\
    --create-namespace \\
    --set image.tag=v15.2.0 \\
    --set config.token=<your_token> \\
    --set config.kasAddress=wss://<gitlab_domain>/-/kubernetes-agent/

```

Готово. Можете проверить gitlab-agent в Infrastructure > Kubernetes Clusters списке. Должно быть состоянии `connected` .

Теперь установим в кластер kubernetes необходимые компоненты для работы Werf. В [репозитории на Github](https://github.com/abrekhov/mks-gitlab-werf) я выложил манифесты для деплоя этих компонент (плюс там все для раскатки окружения). Эти манифесты я взял с официального сайта [из этого раздела](https://werf.io/documentation/v1.2/advanced/ci_cd/run_in_container/use_gitlab_ci_cd_with_kubernetes_executor.html).

Если вы скачали репозиторий, то просто выполните

```
cd werf
kubectl -n kube-system apply -f werf-fuse-device-plugin-ds.yaml
kubectl create namespace gitlab-ci
kubectl apply -f enable-fuse-pod-limit-range.yaml
kubectl apply -f sa-runner.yaml
cd ..

```

Дело за малым, осталось установить gitlab-runner.

В этом же репозитории `values.yaml` файл для официального gitlab-runner. Чтобы добавить shared runner зайдите в Gitlab > Admin > Shared Runners > Register an instance runner. Скопируем registration token. Теперь в скопированном репозитории делаем

```
cd gitlab-runner
helm repo add gitlab <https://charts.gitlab.io>
vim values.yaml # set your domain and registry token
helm install --namespace gitlab-ci gitlab-runner -f values.yaml gitlab/gitlab-runner

```

Проверяем, что раннер появился. Я специально поставил тег werf, чтобы вы потом не потерялись.

Окружение готово. На данном этапе у нас получается такая схема:

![](/files/SRbP6dgESZbKuKX1BlqB)

### Деплой приложения

Установим werf локально к себе на компьютер. Это поможет в будущем быстрее разрабатывать приложения, смотеть рендер манифестов и работать с секретами helm.

Используем [эту ссылку на официальный сайт](https://werf.io/installation.html).

Проверим версию

```
werf version
v1.2.122+fix2

```

Создаем проект в Gitlab, называем my-app-werf (к примеру). Добавляем в проект файл `werf.yaml`

```
# werf.yaml
configVersion: 1
project: my-app-werf
deploy:
  namespace: my-app-werf
---
image: my_app_werf
dockerfile: Dockerfile
```

Если у вас монорепо для микросервисов, то werf без проблем с этим справится. `werf.yaml` будет тогда выглядеть как-то так:

```
configVersion: 1
project: my-microservice-app-werf
deploy:
  namespace: my-microservice-app-werf
---
image: my_app_werf_backend
dockerfile: Dockerfile
context: backend # папка сервиса
---
image: my_app_werf_frontend
dockerfile: Dockerfile
context: frontend # папка сервиса

```

Для наглядности сделаем простой веб сервер на Go:

```
// main.go
package main
import (
"fmt"
"html"
"log"
"net/http"
"os"
)
func main() {
	http.HandleFunc("/", MainHandler)
	log.Print("Starting server...")
	log.Fatal(http.ListenAndServe(":"+os.Getenv("PORT"), nil))
}
func MainHandler(w http.ResponseWriter, r *http.Request) {
	fmt.Fprintf(w, "Hello, %q", html.EscapeString(r.URL.Path))
}

```

Напишем `Dockerfile`

```
# Dockerfile
FROM golang:alpine as builder
WORKDIR /app
COPY . .
RUN go build -o app
FROM alpine as prod
WORKDIR /app
COPY --from=builder /app/app /app/app
ENV PORT=8080
ENTRYPOINT [ "/app/app" ]

```

Создаем шаблоны helm для деплоя по пути `.helm/templates` :

```
# .helm/templates/regcred.yaml
{{- if .Values.dockerconfigjson -}}
apiVersion: v1
kind: Secret
metadata:
  name: regcred
type: kubernetes.io/dockerconfigjson
data:
  .dockerconfigjson: {{ .Values.dockerconfigjson }}
{{- end -}}

```

```
# .helm/templates/app.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  labels:
    app: my-app-werf
  name: my-app-werf
spec:
  selector:
    matchLabels:
      app: my-app-werf
  replicas: 1
  template:
    metadata:
      labels:
        app: my-app-werf
    spec:
      imagePullSecrets:
        - name: regcred
      containers:
        - name: my-app-werf
          image: "{{.Values.werf.image.my_app_werf}}"
          imagePullPolicy: IfNotPresent
          ports:
            - name: http
              containerPort: 8080
              protocol: TCP
---
apiVersion: v1
kind: Service
metadata:
  name: my-app-werf
  labels:
    app: my-app-werf
spec:
  ports:
  - port: 8080
    name: my-app-werf-service
  selector:
    app: my-app-werf

```

```
# .helm/templates/ingress.yaml
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: my-app-werf
  annotations:
    cert-manager.io/cluster-issuer: letsencrypt-cluster
spec:
  ingressClassName: nginx
  tls:
    - hosts:
        - "app-werf.apps.<your_domain>"
      secretName: app-werf-mks-tls
  rules:
    - host: "app-werf.apps.<your_domain>"
      http:
        paths:
          - path: /
            pathType: ImplementationSpecific
            backend:
              service:
                name: my-app-werf
                port:
                  number: 8080

```

Лично я использую nginx ingress с cert-manager поэтому в аннотациях указан cluster issuer.

Добавьте `.dockerignore` чтобы не копировать лишнее и кешировать только нужное.

```
.helm
.gitlab-ci.yml
werf.yaml
```

Остается вишенка на торте: gitlab CI/CD. Werf собирает все переменные окружения Gitlab и использует их при сборке и рендере манифестов. Плюс используется особый вид сборки, [подробнее тут](https://werf.io/documentation/v1.2/advanced/storage_layouts.html). Это опять же out of scope.

Пишем `.gitlab-ci.yml` и удивляемся как компактно он выглядит

```
# .gitlab-ci.yml
stages:
  - build-and-deploy
build_and_deploy:
	stage: build-and-deploy
	image: registry.werf.io/werf/werf
	variables:
		WERF_SYNCHRONIZATION: kubernetes://gitlab-ci
	script:
		- source $(werf ci-env gitlab --as-file)
		- werf converge
	tags: ["werf"]
```

Одна команда `werf converge` делает все разом: собирает docker образ, пушит в registry и деплоит приложение из собранного образа.

Предлагаю сверится по структуре проекта

```
.
├── .dockerignore
├── .gitlab-ci.yml
├── .helm
│   └── templates
│       ├── app.yaml
│       ├── ingress.yaml
│       └── regcred.yaml
├── Dockerfile
├── go.mod
├── main.go
└── werf.yaml
```

Перед пушем можно проверить как будут выглядеть манифесты локально следующей командой.

```
werf render --dev
```

Коммитим изменения, пушим. Вуаля! Так выглядит пайплайн в Gitlab CI/CD:

![](/files/ASSFkdLuEMG6iqbmbSmG)

Скриншот уже закешированной сборки

У нас получилась такая схема:

![](/files/3nbYvjxFFOz4qFXLTUtQ)

### Плюсы и минусы

### Плюсы

* Быстрое развертывание и простая интеграция.

### Минусы

* Нет поддержания конфигурации в актуальном состоянии как у ArgoCD

Откровенно говоря, ArgoCD и Werf могут друг друга дополнять. [Подробности тут](https://werf.io/documentation/v1.2/advanced/ci_cd/werf_with_argocd/ci_cd_flow_overview.html).


---

# Agent Instructions: Querying This Documentation

If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://book.konstantinsecurity.com/readme/architect/ci-cd/gitlab-runner-in-kubernetes-with-werf.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
