Про запуск Nexus писав в пості Nexus: запуск в Kubernetes та налаштування PyPi caching repository, тепер до PyPi хочеться додати кешування Docker images, тим більш Docker Hub з 1-го квітня 2025 вводить нові ліміти – див. Docker Hub usage and limits (дяка @Anatolii).
Робити будемо як завжди – спочатку вручну локально на робочій машині, подивимось, як воно працює, а потім додамо конфіг для Helm-чарту, задеплоїмо в Kubernetes, і подивимось, як налаштувати ContainerD для використання цього mirror.
Зміст
Запуск Sonatype Nexus локально з Docker
Створюємо локальний каталог для nexus data, аби дані зберігались при рестарті Docker, міняємо юзера, бо будемо ловити помилки типу:
... mkdir: cannot create directory '../sonatype-work/nexus3/log': Permission denied mkdir: cannot create directory '../sonatype-work/nexus3/tmp': Permission denied ...
Виконуємо:
$ mkdir /home/setevoy/Temp/nexus3-data/ $ sudo chown -R 200:200 /home/setevoy/Temp/nexus3-data/
Запускаємо Nexus з цією директорією, і додаємо порти для доступу до самого Nexus та для Docker registry, який створимо далі:
$ docker run -p 8080:8081 -p 8092:8092 --name nexus3 --restart=always -v /home/setevoy/Temp/nexus3-data:/nexus-data sonatype/nexus3
Чекаємо, поки все запуститься, і отримуємо пароль для admin:
$ docker exec -ti nexus3 cat /nexus-data/admin.password d549658c-f57a-4339-a589-1f244d4dd21b
Заходимо в браузері на http://localhost:8080, логінимось в систему:
Setup wizard можна пропустити, або швиденько проклікати “Next” і задати новий пароль адміну.
Створення Docker cache repository
Переходимо в Administration > Repository > Repositories:
Клікаємо Create repository:
Вибираємо тип docker (proxy):
Задаємо ім’я, HTTP-порт, на якому будуть прийматись конекти від docker-daemon, і дозволяємо anonymous docker pull:
Далі задаємо адресу, з якої будемо пулити образи – https://registry-1.docker.io, решту параметрів поки можна залишити без змін:
Окремо варто згадати можливість створення
docker (group)
, де налаштовується єдиний конектор для кількох репозиторіїв в Nexus. Але мені це поки не потрібно, хоча в майбутньому – можливо.
Див. Grouping Docker Repositories та Using Nexus OSS as a proxy/cache for Docker images.
Включення Docker Security Realm
Хоча ми і не використовуємо аутентифікацію, але реалм треба включити.
Переходимо в Security > Realms, додаємо Docker Bearer Token Realm:
Перевірка Docker mirror
Знаходимо IP контейнера, в якому запущено Nexus:
$ docker inspect nexus3 ... "NetworkSettings": { .. "Networks": { ... "IPAddress": "172.17.0.2", ...
Порт для Docker cache в Nexus ми задавали 8092.
На робочій машині редагуємо файл /etc/docker/daemon.json
, задаємо registry-mirrors
та insecure-registries
, бо у нас нема SSL:
{ "insecure-registries": ["http://172.17.0.2:8092"], "registry-mirrors": ["http://172.17.0.2:8092"] }
Ребутаємо локальний Docker service:
$ systemctl restart docker
Виконуємо docker info
, перевіряємо, що зміни застосовані:
$ docker info ... Insecure Registries: 172.17.0.2:8092 ::1/128 127.0.0.0/8 Registry Mirrors: http://172.17.0.2:8092/
Виконуємо docker pull nginx
– запит має піти через Nexus, і там зберегти копії даних:
Якщо дані не з’являються – то скоріш за все проблема з аутентифікацією.
Для перевірки до /etc/docker/daemon.json
додаємо debug=true
:
{ "insecure-registries": ["http://172.17.0.2:8092"], "registry-mirrors": ["http://172.17.0.2:8092"], "debug": true }
Рестартимо локальний Docker, виконуємо docker pull
і дивимось логи с journalctl -u docker
:
$ sudo journalctl -u docker --no-pager -f ... level=debug msg="Trying to pull rabbitmq from http://172.17.0.2:8092/" level=info msg="Attempting next endpoint for pull after error: Head \"http://172.17.0.2:8092/v2/library/rabbitmq/manifests/latest\": unauthorized: " level=debug msg="Trying to pull rabbitmq from https://registry-1.docker.io" ...
Першого разу я забув включити Docker Bearer Token Realm.
А другого разу в мене в ~/.docker/config.json
був збережений токен для https://index.docker.io, і Docker намагався використати його. В такому випадку можна просто видалити/перемістити config.json
, і виконати pull ще раз.
Окей.
А що там с ContainerD? Бо в AWS Elastic Kubenretes Service у нас не Docker.
ContainerD та registry mirrors
Чесно кажучи, з containerd
болі було більше, ніж з Nexus. Тут і його TOML для конфігів, і різні версії самого ContainerD та конфігурації, і deprecated параметри.
В мене просто очі болять від такого формату:
[plugins] [plugins.'io.containerd.cri.v1.runtime'] [plugins.'io.containerd.cri.v1.runtime'.containerd] [plugins.'io.containerd.cri.v1.runtime'.containerd.runtimes] [plugins.'io.containerd.cri.v1.runtime'.containerd.runtimes.runc] [plugins.'io.containerd.cri.v1.runtime'.containerd.runtimes.runc.options]
Anyway, воно все ж запрацювало, тому поїхали.
Давайте спочатку теж зробимо локально, потім вже будемо налаштовувати в Kubernetes.
На Arch Linux встановлюємо з pacman:
$ sudo pacman -S containerd crictl
Генеруємо дефолтний конфіг для containerd:
$ sudo mkdir /etc/containerd/ $ containerd config default | sudo tee /etc/containerd/config.toml
В файл /etc/containerd/config.toml
додаємо параметри для mirrors
:
... [plugins] [plugins.'io.containerd.cri.v1.images'] ... [plugins.'io.containerd.cri.v1.images'.registry] [plugins.'io.containerd.cri.v1.images'.registry.mirrors."docker.io"] endpoint = ["http://172.17.0.2:8092"] ...
Тут:
plugins.'io.containerd.cri.v1.images
: параметри для image service, управління образамиregistry
: налаштування registry для images servicemirrors."docker.io"
: дзеркала для docker.ioendpoint
: куди звертатись, коли треба спулити образ з docker.io
Перезапускаємо containerd
:
$ sudo systemctl restart containerd
Перевіряємо, що нові параметри застосовані:
$ containerd config dump | grep -A 1 mirrors [plugins.'io.containerd.cri.v1.images'.registry.mirrors] [plugins.'io.containerd.cri.v1.images'.registry.mirrors.'docker.io'] endpoint = ['http://172.17.0.2:8092']
Виконуємо crictl pull
:
$ sudo crictl pull ubuntu
І перевіряємо Nexus:
З’явився образ Ubuntu.
Тут наче все працює – давайте спробуємо це все діло налаштувати в Kubernetes.
Nexus Helm chart values та Kubernetes
В частині Додавання репозиторію в Nexus через Helm chart values трохи писав про те, як і які values додавались для запуску з Nexus Helm chart в Kubernetes для кешу PyPi.
Трохи їх оновимо:
- додамо окремий blob store: підключимо окремий persistentVolume, бо в дефолтному лише 8 гіг, і якщо для PyPi цього більш-менш достатньо, то для Docker images буде замало
- додамо
additionalPorts
: тут задаємо порт, на якому буде Docker cache - включимо Ingress
Всі values – values.yaml.
В мене деплоїться з Terraform під час налаштування Kubernetes-кластеру.
Все разом, з PyPi, в мене зараз виглядає так:
resource "helm_release" "nexus" { namespace = "ops-nexus-ns" create_namespace = true name = "nexus3" repository = "https://stevehipwell.github.io/helm-charts/" #repository_username = data.aws_ecrpublic_authorization_token.token.user_name #repository_password = data.aws_ecrpublic_authorization_token.token.password chart = "nexus3" version = "5.7.2" # also: # Environment: # INSTALL4J_ADD_VM_PARAMS: -Djava.util.prefs.userRoot=${NEXUS_DATA}/javaprefs -Xms1024m -Xmx1024m -XX:MaxDirectMemorySize=2048m values = [ <<-EOT # use existing Kubernetes Secret with admin's password rootPassword: secret: nexus-root-password key: password # enable storage persistence: enabled: true storageClass: gp2-retain # create additional PersistentVolume to store Docker cached data extraVolumes: - name: nexus-docker-volume persistentVolumeClaim: claimName: nexus-docker-pvc # mount the PersistentVolume into the Nexus' Pod extraVolumeMounts: - name: nexus-docker-volume mountPath: /data/nexus/docker-cache resources: requests: cpu: 100m memory: 2000Mi limits: cpu: 500m memory: 3000Mi # enable to collect Nexus metrics to VictoriaMetrics/Prometheus metrics: enabled: true serviceMonitor: enabled: true # use dedicated ServiceAccount # still, EKS Pod Identity isn't working (yet?) serviceAccount: create: true name: nexus3 automountToken: true # add additional TCP port for the Docker caching listener service: additionalPorts: - port: 8082 name: docker-proxy containerPort: 8082 hosts: - nexus-docker.ops.example.co # to be able to connect from Kubernetes WorkerNodes, we have to have a dedicated AWS LoadBalancer, not only Kubernetes Service with ClusterIP ingress: enabled: true annotations: alb.ingress.kubernetes.io/group.name: ops-1-30-internal-alb alb.ingress.kubernetes.io/target-type: ip ingressClassName: alb hosts: - nexus.ops.example.co # define the Nexus configuration config: enabled: true anonymous: enabled: true blobStores: # local EBS storage; 8 GB total default size ('persistence' config above) # is attached to a repository in the 'repos.pip-cache' below - name: default type: file path: /nexus-data/blobs/default softQuota: type: spaceRemainingQuota limit: 500 # dedicated sorage for PyPi caching - name: PyPILocalStore type: file path: /nexus-data/blobs/pypi softQuota: type: spaceRemainingQuota limit: 500 # dedicated sorage for Docker caching - name: DockerCacheLocalStore type: file path: /data/nexus/docker-cache softQuota: type: spaceRemainingQuota limit: 500 # enable Docker Bearer Token Realm realms: enabled: true values: - NexusAuthenticatingRealm - DockerToken # cleanup policies for Blob Storages # is attached to epositories below cleanup: - name: CleanupAll notes: "Cleanup content that hasn't been updated in 14 days downloaded in 28 days." format: ALL_FORMATS mode: delete criteria: isPrerelease: lastBlobUpdated: "1209600" lastDownloaded: "2419200" 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 cleanup: policyNames: - CleanupAll - name: docker-cache format: docker type: proxy online: true negativeCache: enabled: true timeToLive: 1440 proxy: remoteUrl: https://registry-1.docker.io metadataMaxAge: 1440 contentMaxAge: 1440 httpClient: blocked: false autoBlock: true connection: retries: 0 useTrustStore: false storage: blobStoreName: DockerCacheLocalStore strictContentTypeValidation: false cleanup: policyNames: - CleanupAll docker: v1Enabled: false forceBasicAuth: false httpPort: 8082 dockerProxy: indexType: "REGISTRY" cacheForeignLayers: "true" EOT ] }
Деплоїмо, відкриваємо порт:
$ kk -n ops-nexus-ns port-forward svc/nexus3 8081
Перевіряємо Realm:
Перевіряємо сам Docker repository:
Гуд.
Ingress/ALB для Nexus Docker cache
Так як ContainerD на EC2 не відноситься до Kubernetes, то і доступу до Kubernetes Service з ClusterIP в нього нема. Відповідно, він не зможе виконати pull образів з порта 8082 на nexus3.ops-nexus-ns.svc.cluster.local
.
В Helm chart є можливість створити окремий Ingress, в якому задаємо всі параметри:
... # to be able to connect from Kubernetes WorkerNodes, we have to have a dedicated AWS LoadBalancer, not only Kubernetes Service with ClusterIP ingress: enabled: true annotations: alb.ingress.kubernetes.io/group.name: ops-1-30-internal-alb alb.ingress.kubernetes.io/target-type: ip ingressClassName: alb hosts: - nexus.ops.example.co ...
В мене використовується анотація alb.ingress.kubernetes.io/group.name
для обєднання кількох Kubernetes Ingress через один AWS LoadBalancer, див. Kubernetes: єдиний AWS Load Balancer для різних Kubernetes Ingress.
Важливий нюанс тут: в параметрах Ingress не треба додавати порти і хости, які задані в Service.
Тобто, для:
... # add additional TCP port for the Docker caching listener service: additionalPorts: - port: 8082 name: docker-proxy containerPort: 8082 hosts: - nexus-docker.ops.example.co ...
В Helm-чарті автоматично створиться роут на Ingress:
$ kk -n ops-nexus-ns get ingress nexus3 -o yaml ... spec: ingressClassName: alb rules: - host: nexus.ops.example.co http: paths: - backend: service: name: nexus3 port: name: http path: / pathType: Prefix - host: nexus-docker.ops.example.co http: paths: - backend: service: name: nexus3 port: name: docker-proxy path: / pathType: Prefix
Див. ingress.yaml.
Го далі.
Налаштування ContainerD mirror на Kubernetes WorkerNode
Вже бачили локально, в AWS EKS в принципі все теж саме.
Єдине, що локально у нас версія v2.0.3, а в AWS EKS – 1.7.25, тому формат конфігу буде трохи різний.
На AWS EKS WorkerNode/EC2 перевіряємо файл /etc/containerd/config.toml
:
... [plugins."io.containerd.grpc.v1.cri".registry] config_path = "/etc/containerd/certs.d:/etc/docker/certs.d" ...
Поки руками додаємо сюди нове дзеркало – тут і є трохи відмінності від того, що ми робили локально. Див. приклади в Configure Registry Credentials Example – GCR with Service Account Key Authentication.
Тобто, для containerd версії 1 – версія конфігу == 2, а для containerd версії 2 – версія конфігу == 3…? Okay, man…
На EC2 конфіг буде виглядати так:
[plugins."io.containerd.grpc.v1.cri".registry] [plugins."io.containerd.grpc.v1.cri".registry.mirrors] [plugins."io.containerd.grpc.v1.cri".registry.mirrors."docker.io"] endpoint = ["http://nexus-docker.ops.example.co"]
Порт не задаємо, бо на AWS ALB це розрулиться через hostname, який зароутить запит на потрібний Listener.
Рестартимо сервіс:
[root@ip-10-0-32-218 ec2-user]# systemctl restart containerd
Виконуємо pull якогось образу:
[root@ip-10-0-46-186 ec2-user]# crictl pull nginx
Перевіряємо Nexus:
Все є.
Залишилось додати налаштування ContainerD при створенні сервері з Karpenter.
Karpenter та конфіг для ContainerD
В мене EC2NodeClass для Karpenter створюються в Terraform, див. Terraform: створення EKS, частина 3 – установка Karpenter.
Звістно, всі ці операції краще виконувати на тестовому оточенні, або створити окремий NodeClass та NodePool.
Зараз там через AWS EC2 UserData конфігуриться ~ec2-user/.ssh/authorized_keys
для SSH (див. AWS: Karpenter та SSH для Kubernetes WorkerNodes), і сюди ж можемо додати створення файлу для ContainerD mirror.
В дефолтному варіанті ми бачили, що containerd
буде перевіряти такі каталоги:
... [plugins."io.containerd.grpc.v1.cri".registry] config_path = "/etc/containerd/certs.d:/etc/docker/certs.d" ...
Отже, в них можемо додати новий файл.
Але пам’ятаєте, як в тому анекдоті – “А тепер забудьте все, чому вас вчили в університеті”?
Ну, от – забудьте все, що ми робили з конфігами containerd
вище, бо це вже deprecated way. Тепер стільно-модно-молодьожно робити з Registry Host Namespace.
Ідея в тому, що в /etc/containerd/certs.d
створюється каталог для registry, в ньому файл hosts.toml
, а вже в ньому – описується налаштування registry.
В нашому випадку виглядати це буде так:
[root@ip-10-0-45-117 ec2-user]# tree /etc/containerd/certs.d/ /etc/containerd/certs.d/ └── docker.io └── hosts.toml
І в hosts.toml
:
server = "https://docker.io" [host."http://nexus-docker.ops.example.co"] capabilities = ["pull", "resolve"]
Окей. Описуємо це все діло в UserData нашого тестового EC2NodeClass.
Так тут наш “улюблений” YAML – то приведу весь конфіг, аби не мати проблем з відступами, бо трохи погемороївся:
resource "kubectl_manifest" "karpenter_node_class_test_latest" { yaml_body = <<-YAML apiVersion: karpenter.k8s.aws/v1 kind: EC2NodeClass metadata: name: class-test-latest spec: kubelet: maxPods: 110 blockDeviceMappings: - deviceName: /dev/xvda ebs: volumeSize: 40Gi volumeType: gp3 amiSelectorTerms: - alias: al2@latest role: ${module.eks.eks_managed_node_groups["${local.env_name_short}-default"].iam_role_name} subnetSelectorTerms: - tags: karpenter.sh/discovery: "atlas-vpc-${var.aws_environment}-private" securityGroupSelectorTerms: - tags: karpenter.sh/discovery: ${var.env_name} tags: Name: ${local.env_name_short}-karpenter nodeclass: test environment: ${var.eks_environment} created-by: "karpenter" karpenter.sh/discovery: ${module.eks.cluster_name} userData: | #!/bin/bash set -e mkdir -p ~ec2-user/.ssh/ touch ~ec2-user/.ssh/authorized_keys echo "${var.karpenter_nodeclass_ssh}" >> ~ec2-user/.ssh/authorized_keys chmod 600 ~ec2-user/.ssh/authorized_keys chown -R ec2-user:ec2-user ~ec2-user/.ssh/ mkdir -p /etc/containerd/certs.d/docker.io cat <<EOF | tee /etc/containerd/certs.d/docker.io/hosts.toml server = "https://docker.io" [host."http://nexus-docker.ops.example.co"] capabilities = ["pull", "resolve"] EOF systemctl restart containerd YAML depends_on = [ helm_release.karpenter ] }
Для створення EC2 маю окремий тестовий Pod, який має tolerations
і nodeAffinity
(див. Kubernetes: Pods та WorkerNodes – контроль розміщення подів на нодах), через які Karpenter має створити EC2 саме з “class-test-latest” EC2NodeClass:
apiVersion: v1 kind: Pod metadata: name: test-pod spec: containers: - name: nginx image: nginx command: ['sleep', '36000'] restartPolicy: Never tolerations: - key: "TestOnly" effect: "NoSchedule" operator: "Exists" affinity: nodeAffinity: requiredDuringSchedulingIgnoredDuringExecution: nodeSelectorTerms: - matchExpressions: - key: component operator: In values: - test
Деплоїмо зміни, деплоїмо Pod, логінимось на EC2 та перевіряємо конфіг:
[root@ip-10-0-45-117 ec2-user]# cat /etc/containerd/certs.d/docker.io/hosts.toml server = "https://docker.io" [host."http://nexus-docker.ops.example.co"] capabilities = ["pull", "resolve"]
Ще раз виконуємо pull якого-небудь PHP:
[root@ip-10-0-43-82 ec2-user]# crictl pull php
І перевіряємо Nexus:
Готово.
Корисні посилання
- Proxy Repository for Docker
- Using Nexus OSS as a proxy/cache for Docker images
- Configuring Docker private registry and Docker proxy on Nexus Artifactory
- Nexus Proxy Docker Repository with Nginx and Traefik
- How to overwrite the Docker Hub default registry in the ContainerD Runtime
- containerd Registry Configuration
- Registry Configuration – Introduction
- Container Runtime Interface (CRI) CLI
- Configure Image Registry