Про запуск 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











