Utilizar Harbor como cache de imágenes 23 Diciembre 2021

Utilizar Harbor como cache de imágenes

Es común encontrarse con equipos que utilizan, por ejemplo, runners de GitLab y ejecutan pipelines con mucha frecuencia. De esta manera descargan diferentes imágenes de contenedores, pero puede suceder que alcancen el limite de pull de imágenes.

Ahí es donde entra en juego Harbor, una herramienta de código abierto que, entre muchas otras cosas, permite almacenar imágenes de contenedores en una registry. Así la primera vez que se descargue una imagen de alguna otra registry, por ejemplo Docker Hub, se almacenará en la registry de Harbor y luego, cuando se requiera de la misma, se utilizará desde allí en lugar de descargarla nuevamente.

Instalación

Una decisión que surge en esta situación es dónde instalar Harbor. En este caso, contamos con un cluster de k8s donde se podría instalar fácilmente, existen charts que simplifican enormemente el proceso.

Pero, particularmente, nuestro cluster utiliza como almacenamiento un bucket (Amazon S3) y, por lo tanto, resultaría muy costoso almacenar allí imágenes de contenedores.

Por otro lado, ¿qué sucede ante una caída de los servicios del cluster? Si no están disponibles los servicios del cluster, no sería accesible la registry de Harbor. Y, si no funciona Harbor, podría no ser posible descargar imágenes y por lo tanto no se podrían ejecutar los contenedores que a fin de cuentas son el cluster.

Entonces, si no funciona el cluster, no hay Harbor, y si no hay harbor no hay cluster. Estamos ante una posible situación de deadlock, o en criollo, un problema del huevo y la gallina.

Es por eso que resolvimos instalarlo fuera del cluster existente, de manera convencional, para lo que fué necesario crear uan máquina virtual que cumpla con los requisitos de Harbor

Una vez que se dispone de la plataforma, se puede instalar el servidor de harbor, siguiendo los pasos de la documentación Oficial.

En resumen se debe renombrar el archivo harbor.yaml.tmpl a harbor.yaml y configurar los valores que allí se encuentran. Principalmente se debe comentar la configuración https, cambiar el puerto http (mas adelante veremos por qué), elegir contraseñas para harbor y para la base de datos, establecer la url y donde se guardarán los datos (imágenes) indicando el path en data_volume. Por ejemplo:

hostname: harbor-cache.example.com
http:
  port: 8080
# https:
  # https port for harbor, default is 443
  # port: 6443
  ...
external_url: harbor-cache.example.com
harbor_admin_password: Harbor12345
database:
  password: root123
  ...
data_volume: /data

Notas:

  • A tener en cuenta, Harbor asume que se ejecutará como root el servicio, lo cual es muy desaconsejable. Admite un usuario no privilegiado llamado harbor, pero es muy importante que el mismo tenga un UID = 10000, dado que este valor se encuentra hardcodeado en la configuración de los servicios. También es necesario cambiar el ownership de algunos de estos archivos:

    • common/config/core/env
    • common/config/registryctl/env
    • common/config/db/env
    • common/config/jobservice/env
  • Es posible versionar esta instalación utilizando gitops, dado que al finalizar los pasos antes mencionados, se dispone de un archivo docker-compose.yml que es todo lo que necesitamos para el despliegue. Versionando este archivo en git, podremos mantener una copia de seguridad de la instalación.

  • También es posible utilizar Ansible para automatizar toda la configuración de la VM donde se hosteará el servidor de harbor, incluyendo la descarga del repositorio con la configuración de harbor.

Configuración de Harbor

Algunas configuraciones no pueden realizarse automáticamente. Es por eso que una vez que esté corriendo el servicio de harbor se debe acceder a la UI para realizar algunas configuraciones de forma manual.

Registries

Nombraremos algunas de las registries más utilizadas para luego utilizarlas en los proyectos como proxy-caché:

provider name endpointUrl
Docker Hub docker_hub https://hub.docker.com
Docker Registry gcr.io https://gcr.io
Docker Registry k8s.gcr.io https://k8s.gcr.io
Docker Registry quay.io https://quay.io

Proyectos

En los proyectos de Harbor, se pueden crear registries, y asociarlas a un proxy-caché definido previamente. Por ejemplo:

projectName accessLevel storageQuota proxyCache
proxy.docker.io Public -1 docker_hub-https://hub.docker.com
proxy.gcr.io Public -1 gcr.io-https://gcr.io
proxy.k8s.gcr.io Public -1 k8s.gcr.io-https://k8s.gcr.io
proxy.quay.io Public -1 quay.io-https://quay.io

Autenticación con AD (via dex)

Se pueden configurar varios métodos de autenticación desde el menú Configuración en la UI de Harbor. En este caso, se utilizará AD mediante dex, un servicio que se encuentra actualmente en el cluster. Primero se agrega un cliente en la configuración de dex:

config:
  ...
  staticClients:
  ...
  - id: harbor-cache-oidc-dex
    name: Harbor Cache
    redirectURIs:
    - https://harbor-cache.example.com/c/oidc/callback
    secret: <generar-secret>
...

Esta configuración corresponde a un archivo de valores utilizado con helmfile, una forma de desplegar charts de Helm, particularmente el chart de dex. El secret se puede generar aleatoreamente con openssl rand -base64 20

Luego en la UI, agregar la configuración acorde:

Auth Mode OIDC
OIDC Provider Name dex
OIDC Endpoint https://dex.example.com
OIDC Client ID harbor-cache-oidc-dex
OIDC Client Secret <secret-generado-anteriormente>
Group Claim Name groups
OIDC Admin Group cluster_admins
OIDC Scope openid,profile,email,groups
Verify Certificate [x]
Automatic onboarding [ ]
Username Claim ______

De esta forma los usuarios pertenecientes al grupo cluster_admins obtendrán permisos de administrador.

Certificados

En el caso de haber utilizado un certificado autofirmado habrá que configurar todos los clientes que quieran interactuar con nuestro registry de la siguiente manera (asumiendo que utilizan docker):

# Bajar certificado
sudo openssl s_client -showcerts -connect harbor-cache.example.com:443 < /dev/null | sed -ne '/-BEGIN CERTIFICATE-/,/-END CERTIFICATE-/p' > ca.crt

# Ubicarlo en la configuración de docker
sudo mkdir -p /etc/docker/certs.d/harbor-cache.example.com/
sudo mv ca.crt /etc/docker/certs.d/harbor-cache.example.com/ca.crt

# Reiniciar docker
sudo systemctl daemon-reload
sudo systemctl restart docker

# Loguearse a la registry
docker login harbor-cache.example.com -u admin -p Harbor12345 

También habría que declarar el certificado y la key en la sección https del archivo harbor.yaml

Una mejor opción es utilizar Traefik para para lo cual se puede agregar un servicio en el docker-compose.yml:

  traefik:
    image: traefik:${VERSION}
    container_name: traefik
    restart: ${RESTART}
    command: 
      - --api.insecure=true 
      - --providers.docker=true 
      - --providers.docker.exposedbydefault=false 
      - --log.level=${LOG}
      - --entrypoints.http.address=:80 
      - --entrypoints.https.address=:443
      - --certificatesresolvers.${PROVIDER}.acme.dnschallenge=true
      - --certificatesresolvers.${PROVIDER}.acme.dnschallenge.provider=${PROVIDER}
      - --certificatesresolvers.${PROVIDER}.acme.dnschallenge.delayBeforeCheck=0
      - --certificatesresolvers.${PROVIDER}.acme.dnschallenge.resolvers=${RESOLVER}
      - --certificatesresolvers.${PROVIDER}.acme.email=${EMAIL}
      - --certificatesresolvers.${PROVIDER}.acme.storage=/certs/acme.json
    environment: 
      - TZ
      - AWS_ACCESS_KEY_ID
      - AWS_REGION
      - AWS_SECRET_ACCESS_KEY
      - AWS_HOSTED_ZONE_ID
    ports:
      - "80:80"
      - "443:443"
      # Do not expose Traefik Dashboard
      # - "8089:8080"
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock:ro
      - traefik_certs:/certs
    networks:
      - harbor

Además, se debe agregar las siguientes labels en el servicio proxy:

  proxy:
    ...
    labels:
      - "traefik.enable=true"
      # default route over https
      - "traefik.http.routers.harbor.rule=Host(`harbor-cache.example.com`)"
      - "traefik.http.routers.harbor.entrypoints=https"
      - "traefik.http.routers.harbor.tls.certresolver=${PROVIDER}"
      # HTTP to HTTPS
      - "traefik.http.middlewares.redirect-to-https.redirectscheme.scheme=https"
      - "traefik.http.routers.harbor-redirs.rule=hostregexp(`{host:.+}`)"
      - "traefik.http.routers.harbor-redirs.entrypoints=http"
      - "traefik.http.routers.harbor-redirs.middlewares=redirect-to-https"

Es una buena idea manejar todas estas variables de entorno utilizando Direnv.

# Global
export \
TZ=America/Argentina \
RESTART=unless-stopped \

# Traefik
export \
VERSION=v2.5 \
LOG=INFO \

# ACME DNS-01 challenge
export \
PROVIDER=route53 \
RESOLVER=8.8.8.8:53 \
EMAIL=juan.sanchez@mikroways.com \

# AWS Route53
export \
AWS_ACCESS_KEY_ID=AK.... \
AWS_REGION=us-east-1 \
AWS_SECRET_ACCESS_KEY=5HC.... \
AWS_HOSTED_ZONE_ID=Z3... \

Los valores de estas variables se obtienen al tramitar el certificado de Let’s Encrypt.

Iniciar o detener el servicio

Si accedemos por ssh a la instancia donde hemos instalado Harbor, podemos realizar las siguientes acciones básicas:

# Acceder a la carpeta raíz de harbor
harbor@ar-harbor-01:~$ cd /opt/harbor/
direnv: error /opt/harbor/.envrc is blocked. Run `direnv allow` to approve its content

# Habilitar direnv para cargar las variables de entorno
harbor@ar-harbor-01:/opt/harbor$ direnv allow
direnv: loading /opt/harbor/.envrc
direnv: export +AWS_ACCESS_KEY_ID +AWS_HOSTED_ZONE_ID +AWS_REGION +AWS_SECRET_ACCESS_KEY +EMAIL +LOG +PROVIDER +RESOLVER +RESTART +TZ +VERSION ~PATH

# Iniciar los servicios con docker-compose
harbor@ar-harbor-01:/opt/harbor$ docker-compose up -d
Creating volume "harbor_traefik_certs" with default driver
Recreating traefik     ... done
Starting harbor-log ... done
Starting harbor-db     ... done
Starting redis         ... done
Starting registry      ... done
Starting harbor-portal ... done
Starting registryctl   ... done
Starting harbor-core   ... done
Starting nginx             ... done
Starting harbor-jobservice ... done

# Verificar el estado de los contenedores
harbor@ar-harbor-01:/opt/harbor$ docker-compose ps
      Name                     Command                  State                                 Ports
------------------------------------------------------------------------------------------------------------------------------
harbor-core         /harbor/entrypoint.sh            Up (healthy)
harbor-db           /docker-entrypoint.sh 96 13      Up (healthy)
harbor-jobservice   /harbor/entrypoint.sh            Up (healthy)
harbor-log          /bin/sh -c /usr/local/bin/ ...   Up (healthy)   127.0.0.1:1514->10514/tcp
harbor-portal       nginx -g daemon off;             Up (healthy)
nginx               nginx -g daemon off;             Up (healthy)   0.0.0.0:8080->8080/tcp,:::8080->8080/tcp
redis               redis-server /etc/redis.conf     Up (healthy)
registry            /home/harbor/entrypoint.sh       Up (healthy)
registryctl         /home/harbor/start.sh            Up (healthy)
traefik             /entrypoint.sh --api.insec ...   Up             0.0.0.0:443->443/tcp,:::443->443/tcp,
                                                                    0.0.0.0:80->80/tcp,:::80->80/tcp
# Detener el servicio
docker-compose down

Nota: si se apaga el servicio utilizando la opción -v o --volumes, se eliminaran los volúmenes, por lo que se borrarán también los certificados generados, habrá que regenerarlos y esto puede ocasionar problemas si se hace varias veces seguidas.

Sin embargo, se puede limpiar el espacio utilizado luego de haber reiniciado los servicios. Para ello, MIENTRAS esté corriendo el servicio correr el comando docker system prune --volumes. Este comando elimina los contenedores detenidos junto con las imágenes, redes y volúmenes asociados a estos. Si nuestros servicios se encuentran corriendo no se eliminará nada importante.

Resta configurar el cluster existente para que utilice el cache de imágenes. Esto será diferente según el container runtime que se utilice, lo cual veremos en el siguiente artículo.