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). Подробнее тут.

Если у вас еще на подключен 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 я выложил манифесты для деплоя этих компонент (плюс там все для раскатки окружения). Эти манифесты я взял с официального сайта из этого раздела.

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

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

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

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

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

Используем эту ссылку на официальный сайт.

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

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 и использует их при сборке и рендере манифестов. Плюс используется особый вид сборки, подробнее тут. Это опять же 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:

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

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

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

Плюсы

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

Минусы

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

Откровенно говоря, ArgoCD и Werf могут друг друга дополнять. Подробности тут.

Last updated