Forgejo Actions

Forgejo Actions

В данной статье мы расскажем, как построить CI/CD на базе Forgejo Actions. Установим Forgejo Runner в кластер Kubernetes. Зарегистрируем Forgejo Runner в Forgejo. Реализуем с помощью Forgejo Actions: вытягивание кода из Git-репозитория, сборку Docker-образов и развертывание в кластер Kubernetes

1. Введение

Forgejo Actions - это автоматически подгружаемые модули, из которых как из кирпичиков можно создавать рабочие потоки Forgejo. Список Forgejo Actions можно найти по ссылке и ознакомиться с их назначением и функциями. Forgejo Actions для работы с Docker можно найти тут

2. Рабочие потоки Forgejo

Рабочие потоки Forgejo - это аналог пайплайнов GitLab или Jenkins. Манифесты рабочих потоков размещают в директории .forgejo/workflows Git-репозитория разрабатываемого приложения. Рабочий поток запускается по событию (on.push, on.issues, on.pull_request и т.д) или вручную и может принимать параметры из Web-интерфейса Forgejo. Рабочий поток состоит из Job-ов, Job-ы состоят из шагов Steps, а в шагах можно выполнять Forgejo Actions или команды консоли в секции "run:"

3. Установка Forgejo Runners в Kubernetes

Рабочие потоки не выполняются самим экземпляром Forgejo. Вместо этого они передаются исполнителям Forgejo Runners, которые выполняют рабочие потоки и возвращают результат

Мы реализуем CI/CD процессы в кластере Kubernetes. Инструкция по установке Forgejo Runners в кластер Kubernetes отсутствует в официальной документации Forgejo. Однако, есть пример установки Forgejo Runners в Docker Compose, от которого мы и оттолкнемся

Прежде всего, нужно получить токен для регистрации Forgejo Runner в Forgejo в меню "Действия" -> "Исполнители" панели администратора Forgejo

Затем, зарегистрировать ранер в Forgejo. Мы используем для этого Job в Kubernetes

apiVersion: batch/v1
kind: Job
metadata:
  name: forgejo-runner-1-registration
  namespace: forgejo
spec:
  template:
    metadata:
    spec:
      restartPolicy: "Never"
      containers:
      - name: forgejo-runner-1-register
        image: "code.forgejo.org/forgejo/runner:12.6.4"
        imagePullPolicy: IfNotPresent
        env:
        - name: CONFIG_INSTANCE
          value: "https://forgejo.gitorion.ru"
        - name: CONFIG_NAME
          value: "forgejo-runner-1"
        - name: CONFIG_TOKEN
          value: "Y7HJQBZKLybvoUOPgz2PzEvpFWqKIbOccsKY6AhE"
        command:
          - sh
          - -c
          - |
            /bin/forgejo-runner register --no-interactive --token "${CONFIG_TOKEN}" --name "${CONFIG_NAME}" --instance "${CONFIG_INSTANCE}"
            cat /data/.runner

CONFIG_INSTANCE - URL инстанса Forgejo

CONFIG_NAME - имя ранера в Web-интерфейсе Forgejo

CONFIG_TOKEN - токен для регистрации ранера в Forgejo

В случае успеха, команда регистрации "forgejo-runner" соханит в файле /data/.runner параметры аутентификации, которые Forgejo Runner будет использовать при подключении к Forgejo

user@dc2-plane:~$ kubectl logs job/forgejo-runner-1-registration -n forgejo 
level=info msg="Registering runner, arch=amd64, os=linux, version=v12.6.4."
level=warning msg="Runner in user-mode."
level=info msg="No configuration file specified; using default settings."
level=debug msg="Successfully pinged the Forgejo instance server"
level=info msg="Runner registered successfully."
{
  "WARNING": "This file is automatically generated by forgejo-runner. Do not edit it manually unless you know what you are doing. Removing this file will cause act runner to re-register as a new runner.",
  "id": 17,
  "uuid": "42f428a8-42a0-4861-8d74-eea6864f797b",
  "name": "forgejo-runner-1",
  "token": "f4dbec24a9783dac428f72119dc76f0a2b1ec976",
  "address": "https://forgejo.gitorion.ru",
  "labels": [
    "docker:docker://data.forgejo.org/oci/node:lts"
  ]
}

Выделенный фрагмент сохраняем в файл .runner и создаем из него Secret в Kubernetes

kubectl create secret generic forgejo-runner-1-credentials --from-file=.runner -n forgejo

Создаем ConfigMap с конфигурационным файлом Forgejo Runner

apiVersion: v1
kind: ConfigMap
metadata:
  name: forgejo-runner-1-config
  namespace: forgejo
data:
  config.yaml: |
    cache:
      dir: ""
      enabled: true
      external_server: ""
      host: ""
      port: 0
    container:
      docker_host: ''
      enable_ipv6: false
      force_pull: false
      network: ""
      options: ""
      privileged: false
      valid_volumes: []
      workdir_parent: null
    host:
      workdir_parent: null
    log:
      job_level: info
      level: info
    runner:
      capacity: 1
      env_file: .env
      envs:
        A_TEST_ENV_NAME_1: a_test_env_value_1
        A_TEST_ENV_NAME_2: a_test_env_value_2
      fetch_interval: 2s
      fetch_timeout: 5s
      file: .runner
      insecure: false
      labels: []
      timeout: 3h

Следуя примеру для Docker Compose, создаем Deploymet в Kubernetes, который запустит модуль Forgejo Runner. В Deployment подключаем Secret и ConfigMap, созданные выше, к модулю Forgejo Runner

apiVersion: apps/v1
kind: Deployment
metadata:
  name: forgejo-runner-1
  namespace: forgejo
spec:
  replicas: 1
  selector:
    matchLabels:
      app.kubernetes.io/name: forgejo-runner-1
      app.kubernetes.io/instance: forgejo-runner-1
  template:
    metadata:
      labels:
        app.kubernetes.io/name: forgejo-runner-1
        app.kubernetes.io/instance: forgejo-runner-1
    spec:
      containers:
        - name: runner
          securityContext:
            privileged: true
          image: "code.forgejo.org/forgejo/runner:12.6.4"
          imagePullPolicy: IfNotPresent
          command:
            - "sh"
            - "-c"
            - |
              while ! nc -z 127.0.0.1 2376 </dev/null; do
                echo 'waiting for docker daemon...';
                sleep 5;
              done
              /bin/forgejo-runner --config /etc/runner/config.yaml daemon
          resources:
            {}
          env:
            - name: DOCKER_HOST
              value: tcp://127.0.0.1:2376
            - name: DOCKER_CERT_PATH
              value: /certs/client
            - name: DOCKER_TLS_VERIFY
              value: "1"
          volumeMounts:
            - name: runner-config
              mountPath: /etc/runner
            - name: docker-certs
              mountPath: /certs
            - name: runner-credentials
              mountPath: /data/.runner
              subPath: .runner
        - name: dind
          securityContext:
            privileged: true
          image: "docker.io/library/docker:29.3.0-dind"
          imagePullPolicy: IfNotPresent
          resources:
            {}
          env:
            - name: DOCKER_TLS_CERTDIR
              value: /certs
          volumeMounts:
            - name: docker-certs
              mountPath: /certs
      volumes:
        - name: runner-config
          configMap:
            name: forgejo-runner-1-config
        - name: runner-credentials
          secret:
            secretName: forgejo-runner-1-credentials
        - name: docker-certs
          emptyDir: {}

Зарегистрированный модуль Forgejo Runner появится в Web-интерфейсе Forgejo

В модуле Forgejo Runner запущены два контейнера:

"runner" - в этом контейнере запущен процесс forgejo-runner, получающий от Forgejo задание на выполнение рабочих потоков и возвращающий результат

"dind" - контейнер Docker-in-Docker, о назначении которого мы расскажем в следующем пункте

Forgejo Runner и Forgejo запущены в одном namespace Kubernetes

user@dc2-plane:~$ kubectl get pod -n forgejo
NAME                                READY   STATUS    RESTARTS        AGE
forgejo-7d874cffc7-7j9bn            1/1     Running   2 (141m ago)    34h
forgejo-runner-1-77f9d78b5c-spdfb   2/2     Running   7 (7m38s ago)   30h

4. Выполнение рабочих потоков Forgejo

Рабочие потоки Forgejo выполняются в контейнере, запускаемом в "dind". Данный контейнер создается из Docker-образа от разработчиков Forgejo:

data.forgejo.org/oci/node:lts

На картинке ниже приведен лог рабочего потока Forgejo. Красным выделен момент закачки образа "node:tls" и запуск контейнера. Чуть ниже скачиваются и инициализируются Forgejo Actions: "actions/checkout@v4", "docker/login-action@v4", "docker/setup-buildx-action@v3", "docker/build-push-action@v6"

Подключимся к контейнеру "dind" и продемонстрируем Docker-образ "node:tls" и контейнер, который был создан из этого Docker-образа и запущен в "dind"

user@dc2-plane:~$ kubectl get pod -n forgejo
NAME                                READY   STATUS    RESTARTS        AGE
forgejo-7d874cffc7-7j9bn            1/1     Running   4 (7h49m ago)   2d9h
forgejo-runner-1-77f9d78b5c-t7b62   2/2     Running   0               6h33m

user@dc2-plane:~$ kubectl exec -it forgejo-runner-1-77f9d78b5c-t7b62  -n forgejo -c dind -- /bin/sh
/ # docker image ls
IMAGE                                   ID             DISK USAGE   CONTENT SIZE   EXTRA
data.forgejo.org/oci/node:lts           5a593d74b632       1.64GB          424MB
moby/buildkit:buildx-stable-1           37539dd4d60f        352MB          110MB    U 

/ # docker container ls
CONTAINER ID   IMAGE                                   COMMAND                  CREATED         STATUS         PORTS     NAMES
96676137e946   moby/buildkit:buildx-stable-1           "/usr/bin/buildkitd-…"   2 minutes ago   Up 2 minutes             buildx_buildkit_builder-4704e496-bc68-4c0a-b084-ed8934d237c00
c3eb664565dd   data.forgejo.org/oci/node:lts           "tail -f /dev/null"      2 minutes ago   Up 2 minutes             FORGEJO-ACTIONS-TASK-734_WORKFLOW-ad1047f74ca190334cd4c2a3fcea535849f43339dbb9bbd4b28082043a8032f2_JOB-staging

5. Вытягиваем Git-репозиторий проекта

Git-репозиторий разрабатываемого проекта содержит: код проекта, Dockerfile для сборки Docker-образа проекта и helm-чарт для развертывания проекта в кластер Kubernetes, которые потребуются рабочим потокам Forgejo. Вытягиваем Git-репозиторий проекта в контейнер рабочего потока с помощью Forgejo Action "actions/checkout@v4"

- name: checkout
  uses: actions/checkout@v4

6. Передача секретов в рабочий поток Forgejo

В рабочих потоках Forgejo потребуется собирать Docker-образы приложений и отправлять их в приватный репозиторий Docker-образов Forgejo. Приватный репозитори Docker-образов Forgejo потребует аутентификацию с помощью логина и пароля. Чтобы не скомпрометировать логин и пароль, передадим их в рабочий поток через секреты.

Создаем секреты в меню "Действия" -> "Секреты" настроек Git-репозитория проекта

Используем созданные выше секреты в Forgejo Actions "docker/login-action@v4", выполняющем аутентификацию в приватном реестре Docker-образов

- name: Docker Login
  uses: docker/login-action@v4
  with:
    registry: forgejo.gitorion.ru
    username: ${{ secrets.DOCKER_REGISTRY_LOGIN }}
    password: ${{ secrets.DOCKER_REGISTRY_PASS }}

Попробуем вывести логин и пароль в окно лога рабочего потока командой

- name: Docker Login Show
  run: |
    echo username: ${{ secrets.DOCKER_REGISTRY_LOGIN }}
    echo password: ${{ secrets.DOCKER_REGISTRY_PASS }}

Все данные, переданные через секреты в рабочий поток Forgejo, будут скрыты в окне лога рабочего потока

7. Сборка Docker-образа

Прежде всего, инициализируем плагин buildx, используя Forgejo Action "docker/setup-buildx-action@v3". Тут можно передать дополнительные параметры. Например, мы передаем в параметрах CA-сертификат, которым подписан доменный сертификат приватного реестра Docker-образов Forgejo

- name: Buildx Setup
  uses: docker/setup-buildx-action@v3
  with:
    config-inline: |
      [registry."forgejo.gitorion.ru"]
        ca=["/etc/ssl/certs/cicd.pem"]

Теперь можно собирать Docker-образ проекта с помощью Forgejo Action "docker/build-push-action@v6"

- name: Docker Build and Push
  uses: docker/build-push-action@v6
  with:
    context: .
    push: true
    tags: forgejo.gitorion.ru/owneruser/frontend/main:latest
    cache-from: type=registry,ref=forgejo.gitorion.ru/owneruser/frontend/main:buildcache
    cache-to: type=registry,ref=forgejo.gitorion.ru/owneruser/frontend/main:buildcache,mode=max

Docker-образ проекта и кэш сборки Docker-образа будут сохранены в приватном репозитории Docker-образов Forgejo

При последующих сборках, Forgejo Action "docker/build-push-action@v6" возьмет неизменившиеся слои Docker-образа из кэша сборки Docker-образа и не будет тратить время и ресурсы на их пересборку

8. Развертывание проекта в Kubernetes

К сожалению, мы не нашли Forgejo Actions от разработчиков Forgejo для работы с Kubernetes. Однако, нам ничего не мешает в рабочем потоке Forgejo запускать команду helm с помощью "run:"

Команда Helm подключается к API Kubernetes, используя адрес и аутентификационные данные из файла контекста. Файл контекста $HOME/.kube/config должен присутствовать в домашней директории пользователя, запускающего команду Helm. В случае Forgejo Actions это пользователь, от имени которого выполняется рабочий поток Forgejo. Безопасно передать контекст Kubernetes в рабочий поток Forgejo можно с помощью секретов. Создаем секрет KUBECONFIG в меню "Действия" -> "Секреты" настроек Git-репозитория проекта. Значение секрета заполняем содержимым файла контекста Kubernetes

В рабочем потоке Forgejo переносим содержимое секрета KUBECONFIG в файл $HOME/.kube/config и выполняем команду Helm

- name: Deploy
  run: |
       mkdir -p $HOME/.kube
       echo "${{ secrets.KUBECONFIG }}" > $HOME/.kube/config
       helm upgrade --namespace staging --create-namespace frontend-main --history-max 50 --install ./helm --set image=forgejo.gitorion.ru/owneruser/frontend/main:latest
       echo secrets.KUBECONFIG: "${{ secrets.KUBECONFIG }}"
       cat $HOME/.kube/config

Если попытаться вывести секрет ${{ secrets.KUBECONFIG }} или файл $HOME/.kube/config в окно лога рабочего потока, то Forgejo скроет их содержимое

9. Защита рабочих потоков Forgejo

Обязательно нужно запретить доступ к директории с рабочими потоками .forgejo/workflows всем пользователям и разрешить только пользователям из белого списка

В рабочих потоках Forgejo предоставляется доступ к API Kubernetes, поэтому доступ к рабочим потокам Forgejo должны иметь только DevOps и системные администраторы, которым разрешено администировать кластер Kubernetes

Решить эту задачу можно добавив серверный pre-receive хук в Git-репозиторий проекта. Pre-receive - это серверный скрипт, который запускается в тот момент, когда кто-то пытается отправить свои изменения в Git-сервер (команда git push). В данном скрипте нужно выяснить, что изменения вносятся в директорию .forgejo/workflows. И если пользователь не принадлежит к списку разрешенных, вывести сообщение об ошибке и завершить скрипт с кодом ошибки "1"

Пользователь "ivanov" не принадлежит к списку разрешенных пользователей, и его попытка внести изменения в рабочий поток .forgejo/workflows/production.yaml потерпела неудачу