Nexus: запуск в Kubernetes та налаштування PyPi caching repository

Автор |  11/12/2024
 

У нас в Kubernetes запускаються GitHub Runner для білда і деплоя нашого Backend API, див. GitHub Actions: запуск Actions Runner Controller в Kubernetes.

Але з часом ми звернули увагу, що на NAT Gateway бігає якось забагато трафіку – див. VictoriaLogs: дашборда в Grafana з AWS VPC Flow Logs – мігруємо з Grafana Loki.

Проблема: трафік на AWS NAT Gateway

Коли почали перевіряти, то виявили цікаву деталь:

Тут через NAT GW пройшло 40.8 гігабайт даних за годину, з них 40.7 – Ingress.

З цих 40 GB в топі три Remote IP, кожен з яких передав нам майже по 10 GB трафіку (табличка зліва внизу на скріні вище).

В топі Remote IP у нас:

Remote IP       Value     Percent
------------------------------
20.60.6.4       10.6 GB	  28%
20.150.90.164   9.79 GB	  26%
20.60.6.100     8.30 GB	  22%
185.199.111.133 2.06 GB	  5%
185.199.108.133 1.89 GB	  5%
185.199.110.133 1.78 GB	  5%
185.199.109.133 1.40 GB	  4%
140.82.114.4    805  MB	  2%
146.75.28.223   705  MB	  2%
54.84.248.61    267  MB	  1%

А в топі по трафіку в Kubernetes – у нас чотири Kubernetes Pods IP:

Source IP        Pod IP      Value	Percent
-----------------------------------------------
20.60.6.4     => 10.0.43.98  1.54 GB	14%
20.60.6.100   => 10.0.43.98  1.49 GB	14%
20.60.6.100   => 10.0.42.194 1.09 GB	10%
20.150.90.164 => 10.0.44.162 1.08 GB	10%
20.60.6.4     => 10.0.44.208 1.03 GB	9%

І всі ці IP належать до подів з GitHub Runners, а “kraken” в імені – це як раз ті раннери для білдів і деплоїв нашого проекту “kraken“, бекенду:

Далі – цікавіше: якщо перевірити IP https://20.60.6.4 – то побачимо цікавий hostname:

*.blob.core.windows.net???

Шта? Дуже здивувався, бо у нас білдиться Python, і ніяких бібліотек від Mifcrosoft нема. Але потім з’явилась ідея: через те, що ми використовуємо кешування PiP і Docker в GitHub Actions для білдів Backend API, то скоріш за все це саме GitHub storage і є, і саме з нього ми ці кеши тягнемо в Kubernetes.

Аналогічна перевірка 185.199.111.133 та 140.82.114.4 нам показує *.github.io, а 54.84.248.61 – це вже athena.us-east-1.amazonaws.com.

Отже, що вирішили зробити – це запустити в Kubernetes локальне кешування з Sonatype Nexus, і його використовувати як проксі для PyPi.org і для Docker Hub images.

Про Docker caching поговоримо наступного разу, а сьогодні:

  • протестуємо Nexus локально з Docker на робочій машині
  • запустимо Nexus в Kubernetes з Helm-чарту
  • налаштуємо і перевіримо роботу PyPi cache для білдів
  • і подивимось на результати

Nexus: тестування локально з Docker

Запускаємо Nexus:

$ docker run -ti --rm --name nexus -p 8081:8081 sonatype/nexus3

Чекаємо кілька хвилин, бо Nexus на Java, тому стартує довго.

Отримуємо пароль адміна:

$ docker exec -ti nexus cat /nexus-data/admin.password
6221ad20-0196-4771-b1c7-43df355c2245

В браузері переходимо на http://localhost:8081, логінимось:

Якщо не зробили в Setup wizard, то заходимо в Security > Anonymous access, дозволяємо підключатись без аутентифікації:

Додавання репозиторію pypi (proxy)

Переходимо в Settings > Repositories, клікаємо Create repository:

Вибираємо тип pypi (proxy):

Створюємо репозиторій:

  • Name: pypi-proxy
  • Remote storage: https://pypi.org
  • Blob store: default

Знизу клікаємо Create repository.

Перевіримо які дані у нас зараз в default Blob storage – заходимо в контейнер Nexus:

$ docker exec -ti nexus bash                          
bash-4.4$

І дивимось каталог /nexus-data/blobs/default/content/ – зараз тут пусто:

bash-4.4$ ls -l /nexus-data/blobs/default/content/
total 8
drwxr-xr-x 3 nexus nexus 4096 Nov 27 11:02 directpath
drwxr-xr-x 2 nexus nexus 4096 Nov 27 11:02 tmp

Перевірка Nexus PyPi cache

Тепер перевіримо чи наш проксі-кеш працює.

Знаходимо IP контейнера з Nexus:

$ docker inspect nexus | jq '.[].NetworkSettings.IPAddress'
"172.17.0.2"

Запускаємо ще один контейнер з Python:

$ docker run -ti --rm python bash
root@addeba5d307c:/# 

І виконуємо pip install --index-url http://172.17.0.2:8081/repository/pypi-proxy/simple setuptools --trusted-host 172.17.0.2

root@addeba5d307c:/# time pip install --index-url http://172.17.0.2:8081/repository/pypi-proxy/simple  setuptools  --trusted-host 172.17.0.2
Looking in indexes: http://172.17.0.2:8081/repository/pypi-proxy/simple
Collecting setuptools
  Downloading http://172.17.0.2:8081/repository/pypi-proxy/packages/setuptools/75.6.0/setuptools-75.6.0-py3-none-any.whl (1.2 MB)
     ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 1.2/1.2 MB 81.7 MB/s eta 0:00:00
Installing collected packages: setuptools
Successfully installed setuptools-75.6.0
...
real    0m2.595s
...

Бачимо, що виконався Downloading, і це зайняло 2.59 секунди.

Глянемо, що у нас тепер в default Blob storage в Nexus:

bash-4.4$ ls -l /nexus-data/blobs/default/content/
total 20
drwxr-xr-x 3 nexus nexus 4096 Nov 27 11:02 directpath
drwxr-xr-x 2 nexus nexus 4096 Nov 27 11:21 tmp
drwxr-xr-x 3 nexus nexus 4096 Nov 27 11:21 vol-05
drwxr-xr-x 3 nexus nexus 4096 Nov 27 11:21 vol-19
drwxr-xr-x 3 nexus nexus 4096 Nov 27 11:21 vol-33

Вже якісь дані з’явились, ок.

Тестуємо pip ще раз – спочатку видалимо встановлений пакет:

root@addeba5d307c:/# pip uninstall setuptools

І встановлюємо його ще раз, але тепер додаємо --no-cache-dir, аби не використовувати локальний кеш в контейнері:

root@5dc925fe254f:/# time pip install --no-cache-dir --index-url http://172.17.0.2:8081/repository/pypi-proxy/simple setuptools --trusted-host 172.17.0.2
Looking in indexes: http://172.17.0.2:8081/repository/pypi-proxy/simple
Collecting setuptools
  Downloading http://172.17.0.2:8081/repository/pypi-proxy/packages/setuptools/75.6.0/setuptools-75.6.0-py3-none-any.whl (1.2 MB)
     ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 1.2/1.2 MB 942.9 MB/s eta 0:00:00
Installing collected packages: setuptools
Successfully installed setuptools-75.6.0
...
real    0m1.589s

Тепер часу зайняло 1.52 секунди замість 2.59.

Окей – наче все працює?

Давайте запустимо Nexus в Kubernetes.

Запуск Nexus в Kubernetes

Є такий чарт – stevehipwell/nexus3.

Можна написати маніфести самому, можна спробувати цей чарт.

Що нам може бути цікаво з вальюсів чарту:

  • config.anonymous.enabled: працювати Nexus буде локально в Kubernetes з доступом тільки по ClusterIP, тому поки це в PoC і чисто для кешу PiP – можна без аутентифікації
  • config.blobStores: поки можна залишити як є, але пізніше, можливо, підключити окремий EBS або AWS Elastic File System, див. також persistence.enabled
  • config.job.tolerations та nodeSelector: якщо треба ранити на окремій ноді, див. Kubernetes: Pods та WorkerNodes – контроль розміщення подів на нодах
  • config.repos: відразу через values створити репозиторії
  • ingress.enabled: не наш кейс, але можливість є
  • metrics.enabled: потім можна буде подивитись на моніторинг

Спочатку давайте встановимо з дефолтними параметрами, потім накидаємо власні values.

Додаємо репозиторій:

$ helm repo add stevehipwell https://stevehipwell.github.io/helm-charts/
"stevehipwell" has been added to your repositories

Створюємо окремий неймспейс ops-nexus-ns:

$ kk create ns ops-nexus-ns
namespace/ops-nexus-ns created

Встановлюємо чарт:

$ helm -n ops-nexus-ns upgrade --install nexus3 stevehipwell/nexus3

Запускався він хвилин 5 – я вже думав дропати чарт, і писати самому, але врешті-решт таки стартанув – Java, шо поробиш.

Перевіряємо що у нас тут є:

$ kk -n ops-nexus-ns get all
NAME           READY   STATUS    RESTARTS   AGE
pod/nexus3-0   4/4     Running   0          6m5s

NAME                TYPE        CLUSTER-IP       EXTERNAL-IP   PORT(S)    AGE
service/nexus3      ClusterIP   172.20.160.147   <none>        8081/TCP   6m5s
service/nexus3-hl   ClusterIP   None             <none>        8081/TCP   6m5s

NAME                      READY   AGE
statefulset.apps/nexus3   1/1     6m6s

Додавання Admin user password

Створимо Kubernetes Secret з паролем:

$ kk -n ops-nexus-ns create secret generic nexus-root-pass --from-literal=password=p@ssw0rd
secret/nexus-root-pass created

Пишемо файл nexus-values.yaml, в якому задаємо ім’я Kubernetes Secret і ключ з паролем, заодно включаємо Anonymous Access:

rootPassword:
  secret: nexus-root-password
  key: password

  config:  
    enabled: true
    anonymous:
      enabled: true

Додавання репозиторію в Nexus через Helm chart values

Тут трохи довелось робити “методом тика”, але завелось.

Отже, в values.yaml чарту сказано: “Repository configuration; based on the REST API (API reference docs require an existing Nexus installation and can be found at **Administration** under _System_ → _API_) but with `format` & `type` defined in the object.

Подивимось специфікацію Nexus API – які поля передаються в API request:

А що по формату?

Поля Format і Type можемо глянути в якомусь існуючому репозиторії:

Описуємо репозиторій і інші потрібні параметри – в мене все раз наразі виглядає так:

rootPassword:
  secret: nexus-root-password
  key: password
  
persistence:
  enabled: true
  storageClass: gp2-retain

resources:
  requests:
    cpu: 1000m
    memory: 1500Mi  

config:  
  enabled: true
  anonymous:
    enabled: true
  repos:
    - name: pip-cache
      format: pypi
      type: proxy
      online: true
      negativeCache:
        enabled: true
        timeToLive: 1440
      proxy:
        remoteUrl: https://pypi.org
        metadataMaxAge: 1440
        contentMaxAge: 1440
      httpClient:
        blocked: false
        autoBlock: true
        connection: 
        retries: 0
        useTrustStore: false
      storage:
        blobStoreName: default
        strictContentTypeValidation: false

Тут досить простий сетап, якийсь тюнінг якщо треба то буду робити потім. Але з ним вже їздить, стріляє працює, кешує.

Деплоїмо:

$ helm -n ops-nexus-ns upgrade --install nexus3 stevehipwell/nexus3 -f nexus-values.yml

У випадку помилок типу “Could not create repository“:

$ kk -n ops-nexus-ns logs -f nexus3-config-9-2cssf
Configuring Nexus3...
Configuring anonymous access...
Anonymous access configured.
Configuring blob stores...
Configuring scripts...
Script 'cleanup' updated.
Script 'task' updated.
Configuring cleanup policies...
Configuring repositories...
ERROR: Could not create repository 'pip-cache'.

Перевіряємо логи – Nexus хоче передачу майже всіх полів, в данному випадку не вистачало config.repos.httpClient.contentMaxAge:

nexus3-0:nexus3 2024-11-27 12:34:16,818+0000 WARN  [qtp554755438-84] admin org.sonatype.nexus.siesta.internal.resteasy.ResteasyViolationExceptionMapper - (ID af473d22-3eca-49ea-adb9-c7985add27e7) Response: [400] '[ValidationErrorXO{id='PARAMETER strictContentTypeValidation', message='must not be null'}, ValidationErrorXO{id='PARAMETER negativeCache', message='must not be null'}, ValidationErrorXO{id='PARAMETER metadataMaxAge', message='must not be null'}, ValidationErrorXO{id='PARAMETER contentMaxAge'[]ust not be null]arg0.httpClient]ntMaxAge]]TypeValidation]TER httpClient', message='must not be null'}]'; mapped from: [PARAMETER]

Під часу деплою, коли ми задаємо параметр config.enabled=true, чарт запускає ще один Kubernetes Pod, який власне виконує конфігурацію Nexus.

Перевіримо доступ і репозиторій – відкриваємо собі доступ:

$ kk -n ops-nexus-ns port-forward pod/nexus3-0 8082:8081
Forwarding from 127.0.0.1:8082 -> 8081
Forwarding from [::1]:8082 -> 8081

Заходимо на http://localhost:8082/#admin/repository/repositories:

Ресурсів, особливо Memory, Nexus хоче багато, бо знов-таки – Java:

Тому є сенс в values відразу виставити requests.

Перевірка Nexus в Kubernetes

Запускаємо Pod з Python:

$ kk run pod --rm -i --tty --image python bash
If you don't see a command prompt, try pressing enter.
root@pod:/# 

Знаходимо Kubernetes Service для Nexus:

$ kk -n ops-nexus-ns get svc
NAME        TYPE        CLUSTER-IP       EXTERNAL-IP   PORT(S)    AGE
nexus3      ClusterIP   172.20.160.147   <none>        8081/TCP   78m
nexus3-hl   ClusterIP   None             <none>        8081/TCP   78m

Знов запускаємо pip install:

root@pod:/# time pip install --index-url http://nexus3.ops-nexus-ns.svc:8081/repository/pip-cache/simple setuptools --trusted-host nexus3.ops-nexus-ns.svc
Looking in indexes: http://nexus3.ops-nexus-ns.svc:8081/repository/pip-cache/simple
Collecting setuptools
  Downloading http://nexus3.ops-nexus-ns.svc:8081/repository/pip-cache/packages/setuptools/75.6.0/setuptools-75.6.0-py3-none-any.whl (1.2 MB)
     ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 1.2/1.2 MB 86.3 MB/s eta 0:00:00
Installing collected packages: setuptools
Successfully installed setuptools-75.6.0
...

real    0m3.958s

Встановило setuptools-75.6.0 за 3.95 секунди.

Перевіримо в http://localhost:8082/#browse/browse:pip-cache:

Видаляємо setuptools з нашого поду:

root@pod:/# pip uninstall setuptools

І встановлюємо ще раз, знов з --no-cache-dir:

root@pod:/# time pip install --no-cache-dir --index-url http://nexus3.ops-nexus-ns.svc:8081/repository/pip-cache/simple setuptools --trusted-host nexus3.ops-nexus-ns.svc
Looking in indexes: http://nexus3.ops-nexus-ns.svc:8081/repository/pip-cache/simple
Collecting setuptools
  Downloading http://nexus3.ops-nexus-ns.svc:8081/repository/pip-cache/packages/setuptools/75.6.0/setuptools-75.6.0-py3-none-any.whl (1.2 MB)
     ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 1.2/1.2 MB 875.9 MB/s eta 0:00:00
Installing collected packages: setuptools
Successfully installed setuptools-75.6.0
..

real    0m2.364s

Тепер це зайняло 2.364s.

Залишилось оновити GitHub Workflows – відключити там всякі кеші, і додати використання Nexus.

GitHub та результати по AWS NAT Gateway трафіку

На Workflow детально зупинятись не буду, бо це у кожного своє, але якщо кратко, то відключаємо кешування PiP:

...
    - name: "Setup: Python 3.10"
      uses: actions/setup-python@v5
      with:
        python-version: "3.10"
        # cache: 'pip'
        check-latest: "false"
        # cache-dependency-path: "**/*requirements.txt"
...

Це збереже близько 540 мегабайт на завантаженні архіву з кешем.

Далі у нас є step, який виконує pip install через виклик make:

...
    - name: "Setup: Dev Dependencies"
      id: setup_dev_dependencies
      #run: make dev-python-requirements
      run: make dev-python-requirements-nexus
      shell: bash
...

А в Makefile я зробив нову таску, аби можна було швидко повернути на старий конфіг:

...
dev-python-requirements:
  python3 -m pip install --no-compile -r dev-requirements.txt

dev-python-requirements-nexus:
  python3 -m pip install --index-url http://nexus3.ops-nexus-ns.svc:8081/repository/pip-cache/simple --no-compile -r dev-requirements.txt --trusted-host nexus3.ops-nexus-ns.svc
...

У Workflow відключаємо всякі кеши типу actions/cache:

..
    # - name: "Setup: Get cached api-generator images"
    #   id: api-generator-cache
    #   uses: actions/cache@v4
    #   with:
    #     path: ~/_work/api-generator-cache
    #     key: api-generator-cache
...

Ну і порівняємо результати.

Білд зі старим конфігом, без Nexus і з кешами GitHub – трафік Kubernetes Pod раннера, який цей білд виконував:

3.55 гігабайт  трафіку, білд-деплой зайняли 4 хвилини 11 секунд часу.

І ця сама GitHub Actions джоба, але вже зі змерженими змінами і використанням Nexus і без GitHub caching.

В логах бачимо, що пакети дійсно беруться з Nexus:

Трафік:

329 мегабайт, білд-деплой зайняли 4 хвилини 20 секунд часу.

Ну і на цьому поки все.

Що буде зробити далі – це подивитись як Nexus можна моніторити, які в нього є метрики і які з них можна зробити алерти, і далі додати ще Docker кеш, бо доволі часто стикаємось з лімітами Docker Hub – “429 Too Many Requests – Server message: toomanyrequests: You have reached your pull rate limit. You may increase the limit by authenticating and upgrading“.