# 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, чтобы вы потом не потерялись.

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

![](https://296194292-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FLoAqAoOfr7XVUQw7Gff8%2Fuploads%2Fgit-blob-a5f5a1cf6fb52042e0eeff1ab3998c9c603196ab%2Fd9d9030cdb6604f919ff5969e5882acd.png?alt=media)

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

Установим 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:

![](https://296194292-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FLoAqAoOfr7XVUQw7Gff8%2Fuploads%2Fgit-blob-78901072e65d5e0141b9fc8cda537c27adc25dbb%2F2f85c32172ef38a70739c0f9a095b805.png?alt=media)

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

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

![](https://296194292-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FLoAqAoOfr7XVUQw7Gff8%2Fuploads%2Fgit-blob-13f11412482af782cf623d55b411399868e925ff%2Fe6d2cf10c53be3aee61e448d3be9abd4.png?alt=media)

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

### Плюсы

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

### Минусы

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

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