Ми користуємось GitHub Actions для деплоїв, і врешті-решт прийшли до того, що хочеться запускати Runners на своїх серверах в Kubernetes, бо:
- self-hosted GitHub Runners дешевші – фактично, платимо тільки за сервери, на яких запускаються джоби
- нам потрібно запускати SQL migrations на AWS RDS в приватних сабнетах
- перформанс – ми можемо використовувати будь-який тип AWS EC2 та типи дисків, і не обмежувати себе в CPU/Memory та IOPS
Загальна документація – About self-hosted runners.
GitHub для цього має окремий контролер – Actions Runner Controller (ARC), який ми власне і будемо використовувати.
Запускати будемо на AWS Elastic Kubernetes Service v1.30, Karpenter 1.0 для скейлінгу EC2, та AWS Elastic Container Registry для Docker images.
У self-hosted раннерів є Usage limits, але навряд чи ми з ними зіткнемося.
Отже, що будемо робити:
- з Helm встановимо Actions Runner Controller та Scale Set з Runners для тестового репозиторію, подивимось, як воно все взагалі працює
- створимо окремий Karpenter NodePool з
taints
для запуску Runners на окремих інстансах - спробуємо реальні білди і деплої для нашого Backend API, подивимось, які будуть помилки
- створимо власний Docker image для раннерів
- спробуємо в роботі Docker in Docker mode
- створимо окремий Kubernetes StorageClass з high IOPS, і подивимось, як це вплине на швидкість білдів-деплоїв
Спочатку зробимо все швиденько руками, аби побачити як воно працює – а потім будемо тюнити і запускати реальний білд-деплой.
Зміст
Аутентифікація в GitHub
Документація – Authenticating to the GitHub API.
Тут є два варіанти – більш кошерний для production через GitHub App, або через персональний токен.
З GitHub App виглядає як більш правильне рішення, але ми невеликий стартап, і через персональний токен буде простіше – тому поки зробимо так, а “потім” (с) при потребі зробимо вже “як треба”.
Переходимо до свого профайлу, клікаємо Settings > Developer settings > Personal access tokens, клікаємо Generate new token (classic):
Self-hosted runners поки будемо використовувати тільки для одного репозиторію, тому задаємо права тільки на repo
:
Expiration було б добре задати, але це PoC (який потім, як завжди, піде в production), тому поки ОК – нехай буде вічний.
Створюємо Kubernetes Namespace для раннерів:
$ kk create ns ops-github-runners-ns namespace/ops-github-runners-ns created
Створюємо в ньому Kubernetes Secret з токеном:
$ kk -n ops-github-runners-ns create secret generic gh-runners-token --from-literal=github_token='ghp_FMT***5av' secret/gh-runners-token created
Перевіряємо його:
$ kk -n ops-github-runners-ns get secret -o yaml apiVersion: v1 items: - apiVersion: v1 data: github_token: Z2h***hdg== kind: Secret ...
Запуск Actions Runner Controller з Helm
Actions Runner Controller складається з двох частин:
gha-runner-scale-set-controller
: власне сам контролер – його Helm-чарт створить необхідні Kubernetes CRD та запустить поди контролераgha-runner-scale-set
: відповідає за запуск Kubernetes Pods з GitHub Action Runners
Крім того, ще є легасі-версія – actions-runner-controller
, але ми її використовувати не будемо.
Хоча в документації Scale Sets Controller теж називається Actions Runner Controller, і при цьому є ще й легасі Actions Runner Controller… Трохи плутає, майте на увазі, що частина нагуглених прикладів/документації може бути саме про легасі-версію.
Документація – Quickstart for Actions Runner Controller, або повний варіант – Deploying runner scale sets with Actions Runner Controller.
Встановлення Scale Set Controller
Зробимо окремий Namespace для контролера:
$ kk create ns ops-github-controller-ns namespace/ops-github-controller-ns created
Встановлюємо чарт – там values доволі простий, ніяких апдейтів робити не треба:
$ helm -n ops-github-controller-ns upgrade --install github-runners-controller \ > oci://ghcr.io/actions/actions-runner-controller-charts/gha-runner-scale-set-controller
Перевіряємо поди:
$ kk -n ops-github-controller-ns get pod NAME READY STATUS RESTARTS AGE github-runners-controller-gha-rs-controller-5d6c6b587d-fv8bz 1/1 Running 0 2m26s
Перевіряємо нові CRD:
$ kk get crd | grep github autoscalinglisteners.actions.github.com 2024-09-17T10:41:28Z autoscalingrunnersets.actions.github.com 2024-09-17T10:41:29Z ephemeralrunners.actions.github.com 2024-09-17T10:41:29Z ephemeralrunnersets.actions.github.com 2024-09-17T10:41:30Z
Встановлення Scale Set для Runners
Кожен Scale Set (ресурс AutoscalingRunnerSet
) відповідає за конкретні Runners, які ми будемо використовувати через runs-on
в workflow-файлах.
Задаємо дві змінні оточення – потім це передамо через власний файл values:
INSTALLATION_NAME
: ім’я runners (в values задається черезrunnerScaleSetName
)GITHUB_CONFIG_URL
: URL GitHub Organization або репозиторію в форматіhttps://github.com/<ORG_NAME>/<REPO_NAME>
$ INSTALLATION_NAME="test-runners" $ GITHUB_CONFIG_URL="https://github.com/***/atlas-test"
Встановлюємо чарт, передаємо githubConfigUrl
та githubConfigSecret
– тут у нас вже є створений секрет, використовуємо його:
$ helm -n ops-github-runners-ns upgrade --install test-runners \ > --set githubConfigUrl="${GITHUB_CONFIG_URL}" \ > --set githubConfigSecret=gh-runners-token \ > oci://ghcr.io/actions/actions-runner-controller-charts/gha-runner-scale-set
Ще раз перевіряємо поди в неймспейсі контролера – має додатись новий, з ім’ям test-runners-*-listener – він буде відповідати за запуск подів з ранерами для групи “test-runners“:
$ kk -n ops-github-controller-ns get pod NAME READY STATUS RESTARTS AGE github-runners-controller-gha-rs-controller-5d6c6b587d-fv8bz 1/1 Running 0 8m38s test-runners-694c8c97-listener 1/1 Running 0 40s
А створюється він з AutoscalingListeners
:
$ kk -n ops-github-controller-ns get autoscalinglisteners NAME GITHUB CONFIGURE URL AUTOSCALINGRUNNERSET NAMESPACE AUTOSCALINGRUNNERSET NAME test-runners-694c8c97-listener https://github.com/***/atlas-test ops-github-runners-ns test-runners
Перевіряємо поди в неймспейсі з самими ранерами – тут поки що пусто:
$ kk -n ops-github-runners-ns get pod No resources found in ops-github-runners-ns namespace.
Власне, для початку на цьому і все – можна починати запускати джоби. А там по ходу діла будемо дивитись “де впало”, і додавати конфігурації.
Тест з GitHub Actions Workflow
Спробуємо запустити якийсь мінімальний білд, просто аби впевнитись, що в цілому схема працює.
В тестовому репозиторії створюємо файл .github/workflows/test-gh-runners.yml
.
В runs-on
задаємо ім’я нашого пулу раннерів – test-runners:
name: "Test GitHub Runners" concurrency: group: github-test cancel-in-progress: false on: workflow_dispatch: permissions: # allow read repository's content by steps contents: read jobs: aws-test: name: Test EKS Runners runs-on: test-runners steps: - name: Test Runner run: echo $HOSTNAME
Пушимо в репозиторій, запускаємо білд, чекаємо хвилину, і бачимо ім’я раннера:
Перевіряємо поди в Kubernetes:
$ kk -n ops-github-runners-ns get pod NAME READY STATUS RESTARTS AGE test-runners-p7j9h-runner-xhb94 1/1 Running 0 6s
Цей жеж раннер і відповідний Runner Scale Set буде в Settings > Actions > Runners:
І джоба завершилась:
Окей – воно працює. Що далі?
- треба створити Karpenter NodePool з серверів виключно під GitHub Runners
- треба задати
requests
на поди - треба подивитись як раннери зможуть білдити Docker-образи
Створення Karpenter NodePool
Створимо окремий Karpenter NodePool з taints
, аби на цих EC2 запускались тільки поди з GitHub Runners (див. Kubernetes: Pods та WorkerNodes – контроль розміщення подів на нодах):
apiVersion: karpenter.sh/v1 kind: NodePool metadata: name: github1abgt spec: weight: 20 template: metadata: labels: created-by: karpenter component: devops spec: taints: - key: GitHubOnly operator: Exists effect: NoSchedule nodeClassRef: group: karpenter.k8s.aws kind: EC2NodeClass name: defaultv1a requirements: - key: karpenter.k8s.aws/instance-family operator: In values: ["c5"] - key: karpenter.k8s.aws/instance-size operator: In values: ["large", "xlarge"] - key: topology.kubernetes.io/zone operator: In values: ["us-east-1a"] - key: karpenter.sh/capacity-type operator: In values: ["spot", "on-demand"] # total cluster limits limits: cpu: 1000 memory: 1000Gi disruption: consolidationPolicy: WhenEmptyOrUnderutilized consolidateAfter: 600s budgets: - nodes: "1"
Типи інстансів тут не тюнив, скопіював з NodePool нашого Backend API – потім подивимось, скільки ресурсів раннери будуть використовувати для роботи.
Ще є сенс потюнити disruption
– consolidationPolicy
, consolidateAfter
та budgets
. Наприклад, якщо всі девелопери працюють за одною таймзоною – то WhenEmptyOrUnderutilized
робити вночі, а вдень видаляти тільки по WhenEmpty
, і задати вищий consolidateAfter
, аби нові джоби не чекали зайвого часу на створення EC2. Див. Karpenter: використання Disruption budgets.
Автоматизація Helm deploy Scale Set
Тут варіантів кілька:
- можемо використати Terraform
resource helm_release
- можемо створити власний чарт, і в ньому встановлювати чарти GitHub Runners через Helm Dependency
Або зробити ще простіше – створити репозиторій з конфігами-вальюсами, додати Makefile
– і поки що деплоїти вручну.
Я скоріш за все заміксую схему:
- сам контролер буде встановлюватись з Terraform коду, який розгортає весь Kubernetes кластер – там встановлюються інші контролери типу ExternalDNS, ALB Ingress Controller, etc
- для створення Scale Sets з пулами раннерів під кожен репозиторій зроблю окремий Helm chart в окремому репозиторії, і в ньому у
templates/
додам конфіг-файли для кожного пула раннерів- але поки це ще в PoC – то Scale Sets буде встановлюватись з
Makefile
який виконуєhelm install -f values.yaml
- але поки це ще в PoC – то Scale Sets буде встановлюватись з
Створюємо власний values.yaml
, задаємо runnerScaleSetName
, requests
та tolerations
до tains
з нашого NodePool:
githubConfigUrl: "https://github.com/***/atlas-test" githubConfigSecret: gh-runners-token runnerScaleSetName: "test-runners" template: spec: containers: - name: runner image: ghcr.io/actions/actions-runner:latest command: ["/home/runner/run.sh"] resources: requests: cpu: 1 memory: 1Gi tolerations: - key: "GitHubOnly" effect: "NoSchedule" operator: "Exists"
Додамо простенький Makefile
:
deploy-helm-runners-test: helm -n ops-github-runners-ns upgrade --install test-eks-runners -f test-github-runners-values.yaml oci://ghcr.io/actions/actions-runner-controller-charts/gha-runner-scale-set
Деплоїмо:
$ make deploy-helm-runners-test
Перевіряємо чи змінився конфіг цього пулу раннерів:
$ kk -n ops-github-runners-ns describe autoscalingrunnerset test-runners Name: test-runners Namespace: ops-github-runners-ns ... API Version: actions.github.com/v1alpha1 Kind: AutoscalingRunnerSet ... Spec: Github Config Secret: gh-runners-token Github Config URL: https://github.com/***/atlas-test Runner Scale Set Name: test-runners Template: Spec: Containers: Command: /home/runner/run.sh Image: ghcr.io/actions/actions-runner:latest Name: runner Resources: Requests: Cpu: 1 Memory: 1Gi Restart Policy: Never Service Account Name: test-runners-gha-rs-no-permission Tolerations: Effect: NoSchedule Key: GitHubOnly Operator: Exists
Аби перевірити, що буде використовуватись новий Karpenter NodePool – запускаємо тестовий білд, і перевіряємо NodeClaims:
$ kk get nodeclaim | grep git github1abgt-dq8v5 c5.large spot us-east-1a Unknown 20s
ОК, інстанс створюється, і под з раннером теж:
$ kk -n ops-github-runners-ns get pod NAME READY STATUS RESTARTS AGE test-runners-6s8nd-runner-2s47n 0/1 ContainerCreating 0 45s
Тут все працює.
Білд Backend API
А тепер давайте спробуємо запустити білд і деплой нашого бекенду з реальним кодом і GitHub Actions Workflows.
Створюємо новий values для нового пула раннерів:
githubConfigUrl: "https://github.com/***/kraken" githubConfigSecret: gh-runners-token runnerScaleSetName: "kraken-eks-runners" ...
Деплоїмо новий Scale Set:
$ helm -n ops-github-runners-ns upgrade --install kraken-eks-runners \ > -f kraken-github-runners-values.yaml \ > oci://ghcr.io/actions/actions-runner-controller-charts/gha-runner-scale-set
Редагуємо workflow проекту – міняємо runs-on: ubuntu-latest
на runs-on: kraken-eks-runners
:
... jobs: eks_build_deploy: name: "Build and/or deploy backend" runs-on: kraken-eks-runners ...
Запускаємо білд, новий Pod створився:
$ kk -n ops-github-runners-ns get pod NAME READY STATUS RESTARTS AGE kraken-eks-runners-pg29x-runner-xwjxx 1/1 Running 0 11s
І білд пішов:
Але тут жеж впав з помилками, що не може знайти make
та git
:
GitHub Runners image та “git: command not found”
Перевіримо вручну – запускаємо ghcr.io/actions/actions-runner:latest
локально:
$ docker run -ti ghcr.io/actions/actions-runner:latest bash To run a command as administrator (user "root"), use "sudo <command>". See "man sudo_root" for details. runner@c8aa7e25c76c:~$ make bash: make: command not found runner@c8aa7e25c76c:~$ git bash: git: command not found
Ну, те, що нема make
– ще якось можна зрозуміти. Але на GitHub Runners не додати “в коробку” git
?
Ось в цій GitHub Issue люди теж дивуються такому рішенню.
Але ок… Маємо, що маємо. Що ми можемо зробити – це створити власний образ, де за базу будемо брати ghcr.io/actions/actions-runner
, і встановлювати все, що нам необхідно для щастя.
Див. Software installed in the ARC runner image. Також є інші образи, не від GitHub – Runners, але я їх не пробував.
Отже, наш базовий образ GitHub Runners використовує Ubuntu 22.04, тому можемо з apt
встановити всі потрібні пакети.
Описуємо Dockerfile
– я тут вже додав і AWS CLI, і кілька пакетів для Python:
FROM ghcr.io/actions/actions-runner:latest RUN sudo curl https://raw.githubusercontent.com/helm/helm/main/scripts/get-helm-3 | sudo bash RUN sudo apt update && \ sudo apt -y install git make python3-pip awscli python3-venv
Але ще можливі warnings типу такого:
WARNING: The script gunicorn is installed in ‘/home/runner/.local/bin’ which is not on PATH.
Consider adding this directory to PATH or, if you prefer to suppress this warning, use –no-warn-script-location.
Тому в Dockerfile додав PATH
:
FROM ghcr.io/actions/actions-runner:latest ENV PATH="$PATH:/home/runner/.local/bin" RUN sudo curl https://raw.githubusercontent.com/helm/helm/main/scripts/get-helm-3 | sudo bash RUN sudo apt update && \ sudo apt -y install git make python3-pip awscli python3-venv
Створюємо репозиторій в AWS ECR:
Білдимо образ:
$ docker build -t 492***148.dkr.ecr.us-east-1.amazonaws.com/github-runners/kraken -f Dockefile.kraken .
Логінимось в ECR:
$ aws --profile work ecr get-login-password --region us-east-1 | docker login --username AWS --password-stdin 492***148.dkr.ecr.us-east-1.amazonaws.com
Пушимо:
$ docker push 492***148.dkr.ecr.us-east-1.amazonaws.com/github-runners/kraken
Міняємо image
в нашому values:
... runnerScaleSetName: "kraken-eks-runners" template: spec: containers: - name: runner image: 492***148.dkr.ecr.us-east-1.amazonaws.com/github-runners/kraken:latest command: ["/home/runner/run.sh"] ...
Деплоїмо, запускаємо білд – і маємо нову проблему.
Помилка “Cannot connect to the Docker daemon” та Scale Set containerMode
“Docker in Docker”
Тепер виникає проблема з Docker:
docker: Cannot connect to the Docker daemon at unix:///var/run/docker.sock. Is the docker daemon running?.
Бо під час білду нашого бекенду запускається ще один Docker-контейнер для генерації OpenAPI docs.
Тому в нашому випадку нам потрібно використати Docker in Docker (хоча дуже не люблю цю схему).
Документація GitHub – Using Docker-in-Docker mode.
У Scale Sets для цього є окремий параметр containerMode.type=dind
.
Додаємо в наш values:
... runnerScaleSetName: "kraken-eks-runners" containerMode: type: "dind" template: spec: ...
Деплоїмо Helm, і тепер маємо два контейнери в поді з раннером – один сам runner
, інший – dind
:
==> New container [kraken-eks-runners-trb9h-runner-klfxk:dind] ==> New container [kraken-eks-runners-trb9h-runner-klfxk:runner]
Запускаємо білд, і… Маємо нову помилку 🙂
Привіт DinD та Docker volumes.
Помилка виглядає так:
Error: ENOENT: no such file or directory, open ‘/app/openapi.yml’
Docker in Docker та Docker volumes
Виникає вона через те, що в коді API створюється директорія в /tmp
, в якій генерується файл openapi.yml
, з якого потім генерується HTML з документацією:
... def generate_openapi_html_definitions(yml_path: Path, html_path: Path): print("Running docker to generate HTML") app_volume_path = Path(tempfile.mkdtemp()) (app_volume_path / "openapi.yml").write_text(yml_path.read_text()) if subprocess.call( [ "docker", "run", "-v", f"{app_volume_path}:/app", "--platform", "linux/amd64", "-e", "yaml_path=/app/openapi.yml", "-e", "html_path=/app/openapi.html", "492***148.dkr.ecr.us-east-1.amazonaws.com/openapi-generator:latest", ] ): ...
Тут Path(tempfile.mkdtemp())
створює нову директорію в /tmp
– але це виконується всередині контейнера kraken-eks-runners-trb9h-runner-klfxk:runner, а docker run -v f"{app_volume_path}:/app"
запускається всередині контейнера kraken-eks-runners-trb9h-runner-klfxk:dind.
Давайте просто глянемо на маніфест поду:
$ kk -n ops-github-runners-ns describe autoscalingrunnerset kraken-eks-runners ... Template: Spec: Containers: ... Image: 492***148.dkr.ecr.us-east-1.amazonaws.com/github-runners/kraken:0.8 Name: runner ... Volume Mounts: Mount Path: /home/runner/_work Name: work ... Image: docker:dind Name: dind ... Volume Mounts: Mount Path: /home/runner/_work Name: work ...
Тобто, у обох контейнерів є спільний каталог /home/runner/_work
, який створюється на хості/EC2, і маунтиться в Kubernetes Pod до обох Docker-контейнерів.
А каталог /tmp
в контейнері runner
– “локальний” для нього, і недоступний для контейнера з dind
.
Тому як варіант – просто створювати новий каталог для файлу openapi.yml
всередині /home/runner/_work
:
... # get $HONE, fallback to the '/home/runner' home = os.environ.get('HOME', '/home/runner') # set app_volume_path == '/home/runner/_work/tmp/' app_volume_path = Path(home) / "_work/tmp/" # mkdir recursive, exist_ok=True in case the dir already created by openapi/asyncapi app_volume_path.mkdir(parents=True, exist_ok=True) (app_volume_path / "openapi.yml").write_text(yml_path.read_text()) ...
Або зробити ще краще – на випадок, якщо білд буде запускатись на GitHub hosted Runners, то додати перевірку того, на якому саме раннері запущена джоба, і відповідно вибирати де створювати каталог.
В values нашого Scale Set додаємо змінну RUNNER_EKS
:
... template: spec: containers: - name: runner image: 492***148.dkr.ecr.us-east-1.amazonaws.com/github-runners/kraken:0.8 command: ["/home/runner/run.sh"] env: - name: RUNNER_EKS value: "true" ...
А в коді – перевірку цієї змінної, і в залежності від неї задаємо каталог app_volume_path
:
... # our runners will have the 'RUNNER_EKS=true' if os.environ.get('RUNNER_EKS', '').lower() == 'true': # Get $HOME, fallback to the '/home/runner' home = os.environ.get('HOME', '/home/runner') # Set app_volume_path to the '/home/runner/_work/tmp/' app_volume_path = Path(home) / "_work/tmp/" # mkdir recursive, exist_ok=True in case the dir already created by openapi/asyncapi app_volume_path.mkdir(parents=True, exist_ok=True) # otherwize if it's a GitHub hosted Runner without the 'RUNNER_EKS', use the old code else: app_volume_path = Path(tempfile.mkdtemp()) (app_volume_path / "openapi.yml").write_text(yml_path.read_text()) ...
Запускаємо білд ще раз – і тепер все працює:
Помилка “Access to the path ‘/home/runner/_work/_temp/_github_home/.kube/cache’ is denied”
Ще іноді виникає проблема, коли в кінці білда-деплоя джоба завершується з повідомленням “Error: The opeation was canceled“:
В логах раннера при цьому є і причина – він не може видалити директорію _github_home/.kube/cache
:
... kraken-eks-runners-wwn6k-runner-zlw7s:runner [WORKER 2024-09-20 10:55:23Z INFO TempDirectoryManager] Cleaning runner temp folder: /home/runner/_work/_temp kraken-eks-runners-wwn6k-runner-zlw7s:runner [WORKER 2024-09-20 10:55:23Z ERR TempDirectoryManager] System.AggregateException: One or more errors occurred. (Access to the path '/home/runner/_work/_temp/_github_home/.kube/cache' is denied.) kraken-eks-runners-wwn6k-runner-zlw7s:runner [WORKER 2024-09-20 10:55:23Z ERR TempDirectoryManager] ---> System.UnauthorizedAccessException: Access to the path '/home/runner/_work/_temp/_github_home/.kube/cache' is denied. kraken-eks-runners-wwn6k-runner-zlw7s:runner [WORKER 2024-09-20 10:55:23Z ERR TempDirectoryManager] ---> System.IO.IOException: Permission denied ...
І дійсно, якщо перевірити каталог /home/runner/_work/_temp/_github_home/
з контейнера runner
– то він туди доступу не має:
runner@kraken-eks-runners-7pd5d-runner-frbbb:~$ ls -l /home/runner/_work/_temp/_github_home/.kube/cache ls: cannot open directory '/home/runner/_work/_temp/_github_home/.kube/cache': Permission denied
Але доступ є з контейнера з dind
, який цей каталог і створює:
/ # ls -l /home/runner/_work/_temp/_github_home/.kube/cache total 0 drwxr-x--- 3 root root 78 Sep 24 08:36 discovery drwxr-x--- 3 root root 313 Sep 24 08:36 http
При цьому створює його від root
, хоча решта каталогів – від юзера 1001:
/ # ls -l /home/runner/_work/_temp/ total 40 -rw-r--r-- 1 1001 1001 71 Sep 24 08:36 79b35fe7-ba51-47fc-b5a2-4e4cdf227076.sh drwxr-xr-x 2 1001 1001 24 Sep 24 08:31 _github_workflow ...
А 1001 – це юзер runner
з контейнера runner:
runner@kraken-eks-runners-7pd5d-runner-frbbb:~$ id runner uid=1001(runner) gid=1001(runner) groups=1001(runner),27(sudo),123(docker)
Цікаво, що помилка виникає не постійно, а час від часу, хоча в самому workflow нічного не міняється.
Каталог .kube/config
створюється з action bitovi/github-actions-deploy-eks-helm, який виконує aws eks update-kubeconfig
з власного Docker-контейнера, і запускається від рута, бо запускається в Docker in Docker.
З варіантів приходить в голову два рішення:
- або просто додати костиль у вигляді додаткової команди
chown -r 1001:1001 /home/runner/_work/_temp/_github_home/.kube/cache
в кінці деплою (хоча можна таким жеж костилем просто видаляти директорію) - або змінити
GITHUB_HOME
в іншу директорію – тодіaws eks update-kubeconfig
буде створювати.kube/cache
в іншому місці, і контейнер зrunner
зможе виконати Cleaning runner temp folder
Хоча я все одно не розумію, чому Cleaning runner temp folder виконується не кожного разу, і, відповідно, це “плаваючий баг”. Подивимось далі, як воно буде в роботі.
Підключення High IOPS Volume
Одна з причин, чому ми хочемо перейти на власні раннери – це пришвидшити білди-деплої.
Але велику частку часу займаються команди типу docker load
&& docker save
.
Тому хочеться спробувати підключити AWS EBS з високим IOPS, бо дефолтний gp2
має 100 IOPS на кожен GB розміру – див. Amazon EBS volume types.
Створюємо новий Kubernetes StorageClass:
apiVersion: storage.k8s.io/v1 kind: StorageClass metadata: name: gp3-iops provisioner: kubernetes.io/aws-ebs parameters: type: gp3 iopsPerGB: "16000" throughput: "1000" allowVolumeExpansion: true volumeBindingMode: WaitForFirstConsumer
У values нашого пула раннерів додаємо блок volumes
, де перевизначаємо параметри для диска work
, який по дефолту створюється з emptyDir: {}
– задаємо новий storageClassName
:
githubConfigUrl: "https://github.com/***/kraken" githubConfigSecret: gh-runners-token runnerScaleSetName: "kraken-eks-runners" containerMode: type: "dind" template: spec: containers: - name: runner image: 492***148.dkr.ecr.us-east-1.amazonaws.com/github-runners/kraken:0.9 command: ["/home/runner/run.sh"] env: - name: RUNNER_EKS value: "true" resources: requests: cpu: 2 memory: 4Gi volumes: - name: work ephemeral: volumeClaimTemplate: spec: accessModes: [ "ReadWriteOnce" ] storageClassName: "gp3-iops" resources: requests: storage: 10Gi
Деплоїмо ці зміни AutoscalingRunnerSet, запускаємо наш деплой, і – поди з раннерами створюються, але тут жеж вбиваються, а сама джоба фейлиться.
Помилка “Access to the path ‘/home/runner/_work/_tool’ is denied”
Дивимось логи раннерів, і бачимо, що:
kraken-eks-runners-gz866-runner-nx89n:runner [RUNNER 2024-09-24 10:15:40Z ERR JobDispatcher] System.UnauthorizedAccessException: Access to the path ‘/home/runner/_work/_tool’ is denied.
На документацію Error: Access to the path /home/runner/_work/_tool is denied я вже натикався, коли вище шукав рішення з помилкою “Access to the path ‘/home/runner/_work/_temp/_github_home/.kube/cache’ is denied“, ось воно і знадобилось.
Додаємо ще один initContainer
, в якому виконуємо chown
:
... template: spec: initContainers: - name: kube-init image: ghcr.io/actions/actions-runner:latest command: ["sudo", "chown", "-R", "1001:123", "/home/runner/_work"] volumeMounts: - name: work mountPath: /home/runner/_work containers: - name: runner image: 492***148.dkr.ecr.us-east-1.amazonaws.com/github-runners/kraken:0.9 command: ["/home/runner/run.sh"] ...
І тепер все працює.
Порівняємо результати.
Джоба “Build and/or deploy backend” займала 9 хвилин:
А стало 6 хвилин:
В цілому на цьому поки все.
Не скажу, що все прям працює з коробки, трохи повозитись 100% треба буде – але працює. Будемо пробувати переводити всі білди на свої раннери.