1 puntos por GN⁺ 2024-07-13 | 1 comentarios | Compartir por WhatsApp
  • Las imágenes de contenedores normalmente se suben a un registro dedicado, pero si expones un bucket de S3 por HTTP y colocas los archivos en las rutas esperadas, puede usarse como destino de docker pull
  • En un image builder personalizado, crear imágenes que pudieran hacerse pull en cuestión de segundos se volvió un cuello de botella, y el tiempo de push de las capas representaba una gran parte
  • En un benchmark de carga de una capa de 198 MiB, ECR midió 24–28 MiB/s y S3 115–190 MiB/s, por lo que S3 fue hasta 8 veces más rápido
  • La diferencia se debe a que S3 puede subir en paralelo los chunks de una sola capa, mientras que ECR, basado en la OCI Distribution Spec, debe procesar los chunks de forma secuencial
  • Este enfoque no reemplaza funciones de registro como docker push, validación de imágenes, escaneo de seguridad o control de acceso a repositorios privados, por lo que debe verse como una optimización experimental

Condiciones para que docker pull funcione desde un bucket de S3

  • Para usar S3 como un registro de contenedores, hay que exponer el bucket por HTTP y subir los archivos de la imagen a las rutas que Docker espera
  • Si se cumplen las condiciones, se puede traer la imagen con docker pull como si fuera un registro común
  • La imagen de demostración ejecuta cowsay y funciona usando la URL del bucket como si fuera el nombre de la imagen con docker run --rm .../cowsay
  • En la demo se usa Cloudflare R2
    • La razón para elegirlo fue el egress gratuito
    • R2 y S3 son compatibles a nivel de API, por lo que se subió la imagen a R2 con el AWS SDK

Por qué se consideró S3 en lugar de un registro dedicado

  • Las imágenes de contenedores normalmente se alojan en registros dedicados como DockerHub, GitHub Container Registry o ECR
  • El objetivo del image builder personalizado es llegar desde los requisitos hasta una imagen que pueda hacerse pull en cuestión de segundos
  • En un entorno de AWS, ECR es la opción más sencilla, pero en la velocidad real de carga aparece una gran diferencia entre S3 y ECR
  • Al agregar trazas de ejecución (trace) al código para optimizar el rendimiento, se confirmó que el tiempo dedicado a hacer push de las capas al registro de contenedores era un cuello de botella importante

Resultado de subir una capa de 198 MiB

  • Un pequeño benchmark comparó el tiempo requerido y el throughput al subir una capa de 198 MiB a ECR y a S3 respectivamente
  • Las velocidades observadas fueron las siguientes
    • ECR: mínimo 24 MiB/s, 8.2 segundos
    • ECR: máximo 28 MiB/s, 7.0 segundos
    • S3: mínimo 115 MiB/s, 1.7 segundos
    • S3: máximo 190 MiB/s, 1.0 segundos
  • Según los resultados, S3 es hasta 8 veces más rápido que ECR
  • El código del experimento se ejecutó en AWS, y S3 y ECR estaban conectados internamente mediante una VPC, sin pasar por Internet pública
    • Esta condición ofrece una latencia y un ancho de banda tan buenos como sea posible

La diferencia de velocidad creada por la carga paralela de chunks

  • Las cargas a S3 pueden subir en paralelo los chunks de una sola capa
  • Si hay suficiente ancho de banda, la carga paralela de chunks aumenta mucho el throughput
  • La documentación de AWS también recomienda subir chunks en paralelo para maximizar el uso del ancho de banda
  • ECR implementa la OCI Distribution Spec
    • Esta especificación es el estándar que permite que docker pull y docker push funcionen en distintos registros
    • El push de capas debe avanzar de forma secuencial y, aun cuando se suban chunks, el chunk anterior debe terminar antes de pasar al siguiente
  • Si en S3 también se prueba una carga secuencial, el throughput baja a un nivel similar al de ECR

Estructura real de solicitudes de docker pull

  • Las solicitudes internas de docker pull se componen de varias solicitudes HEAD y GET
  • Un flujo de ejemplo es el siguiente
    • Verificar si existe el manifiesto de la imagen: HEAD /v2/my-image/manifests/latest
    • Descargar el manifiesto de la imagen: GET /v2/my-image/manifests/latest
    • Descargarlo de nuevo por el hash del manifiesto: GET /v2/my-image/manifests/sha256:...
    • Descargar el blob de metadatos de la imagen: GET /v2/my-image/blobs/sha256:...
    • Descargar el blob de una capa de la imagen: GET /v2/my-image/blobs/sha256:...
  • Al final, docker pull se parece mucho a un proceso de descarga por HTTP de los archivos necesarios
  • Si un servidor de archivos estáticos coloca los archivos necesarios en las rutas esperadas y configura el header Content-Type correspondiente a cada solicitud, puede permitir hacer pull de imágenes de contenedores
  • Un bucket de S3 puede cumplir estas dos condiciones, así que, si se configura con cuidado, funciona como un registro de contenedores

Límites del enfoque experimental

  • Este enfoque todavía es experimental, y es difícil sacar conclusiones firmes antes de investigarlo más
  • S3 no es un registro de contenedores en sentido estricto
    • No se puede hacer docker push
    • Que docker pull funcione es el resultado de que la estructura de solicitudes HTTP encaja con la forma de servir archivos estáticos
  • Los registros de contenedores existentes ofrecen más funciones que simplemente subir archivos a un bucket
    • Se puede confiar en que una imagen subida mediante el método estándar de push es realmente válida
    • Pueden ofrecer escaneo automático de seguridad y alertas para las capas
    • Pueden definir permisos de acceso a repositorios privados de forma nativa
  • Si funciona como se espera, también podría ser posible alojar imágenes públicas de contenedores en Cloudflare R2

1 comentarios

 
GN⁺ 2024-07-13
Opiniones en Hacker News
  • Es una lástima que la OCI Distribution Spec no se lea como una especificación bien diseñada.
    Según la especificación, el push de capas debe hacerse de forma secuencial, así que aunque se suban por chunks, hay que esperar a que termine cada chunk antes de pasar al siguiente. Por lo que probé en DockerHub y GHCR, la subida por chunks en sí igual está rota, y los clientes suelen subir cada blob/capa completo de una vez. La especificación incluso recomienda un formato de valor para Content-Range que no coincide con el formato de RFC7233.
    Claro que existe paralelismo a nivel de blob, pero no dentro de un blob. También me molesta que se haya perdido la oportunidad de estandarizar la paginación de la lista de tags. Al eliminarse por error el texto relacionado del estándar [1], cada registry terminó implementándolo a su manera.
    [1] https://github.com/opencontainers/distribution-spec/issues/4...

    • Docker y las tecnologías alrededor de los contenedores en general son así. Docker como experiencia de usuario es excelente, pero como tecnología está cerca de ser un desastre.
      No es que quiera criticarlo por completo. En la práctica fue revolucionario, hizo mucho más fácil usar los namespaces de Linux que antes y cambió el mundo para mejor. Solo que siempre priorizó la experiencia de usuario por encima de la solidez técnica, y eso en sí no necesariamente es malo. Así como hay muchas empresas aburridas que resuelven problemas caros con Perl o con CSV intercambiados por FTP, una tecnología aburrida o incluso mala puede aportar mucho valor si se ofrece en un buen paquete.
      Aun así, a veces queda un sabor amargo pensando que podría haber sido mucho mejor de lo que es hoy.
    • Sumado a eso, no sé si el problema es la especificación OCI o si AWS es el raro, pero a diferencia de GitLab o Nexus, AWS ECR no soporta la creación automática de carpetas.
      Por ejemplo, una forma como .dkr.ecr..amazonaws.com/foo/bar/baz:tag no funciona; solo permite almacenamiento plano, por lo que los nombres de imagen o tags se vuelven excesivamente largos. En teoría, se podría imitar algo similar creando objetos de repositorio ECR en Terraform, pero no es ideal en pipelines donde la ruta de la imagen resultante es dinámica. Habría que darle al rol de IAM del pipeline de CI una cantidad incómoda de permisos, y tampoco me gusta que recursos de AWS se administren fuera del repositorio central de Terraform.
      [1] https://stackoverflow.com/questions/64232268/storing-images-...
  • Cloudflare alguna vez publicó como open source un servidor de registry de contenedores que usa R2.
    Me pregunto si alguien lo ha usado.
    [1]: https://github.com/cloudflare/serverless-registry

    • Se ve bien. Aunque dice que tiene un límite de 500 MB por capa.
      Para algunos usos quizá no sea un gran problema, pero en otros casos puede ser un criterio de descarte inmediato.
  • Soy el autor. Si alguien sabe por qué en la especificación OCI el push de capas tiene que ser secuencial, me gustaría que me lo contara.
    Me intriga si es un simple accidente histórico o si hay alguna razón oculta. Para aclarar: por supuesto que varias capas se pueden subir en paralelo; aquí me refiero a la parte en la que el contenido de una sola capa debe subirse de forma secuencial.

    • Puede ser porque simplifica la limpieza. Si no se llegó al “último” chunk, queda claro que después de N+Timeout es una carga incompleta y se puede eliminar.
      Eso simplifica el detalle de implementación de cómo manejar cargas parciales. De lo contrario, al final de cada chunk habría que comprobar si ya están todos los demás chunks y completar el proceso. Dicho eso, esto es un detalle de implementación, y dudo que haya sido un diseño significativo o intencional. El enfoque con S3 parece que funcionaría bien, y en una empresa donde antes distribuíamos imágenes grandes hicimos algo parecido. Los 0,10 dólares por GB al mes se acumulaban bastante.
      Se pierden las funciones extra de ECR, pero personalmente creo que esas funciones son bastante limitadas.
    • No he trabajado con pushes, pero este enfoque me alegra. En los primeros días de Docker no había contenedores de registro privado que fueran usables, así que poníamos las imágenes detrás de nginx para hacer pull, por eso leí el artículo con interés.
    • Implementé un registro compatible con OCI [1], y como la especificación era compleja, en la mayoría de los casos seguí el comportamiento de la implementación de referencia [2] en lugar de la especificación.
      Cuando el cliente finaliza la carga de un blob, debe proporcionar el digest del blob completo. Este requisito parece existir para que el servidor pueda verificar la integridad de los bytes recibidos. Si el servidor recién empezara a comprobar el digest en la solicitud HTTP final, tendría que volver a leer todo el contenido del blob que ya escribió en el almacenamiento durante las solicitudes HTTP anteriores. Para capas grandes, esa latencia puede ser difícil de tolerar. Por requisitos de un cliente específico, validamos que funcionara hasta con blobs de 150 GiB.
      En cambio, en la implementación mantenemos el cálculo del digest a lo largo de toda la secuencia de solicitudes. Mientras recibimos los datos del blob por chunks, calculamos el digest en paralelo y los transmitimos al almacenamiento de blobs. Entre cada solicitud, serializamos el estado del cálculo del digest dentro de la URL de carga que se devuelve al cliente en el encabezado Location. Se maneja más o menos en este código: https://github.com/sapcc/keppel/blob/7e43d1f6e77ca72f0020645...
      Tengo entendido que la implementación de referencia usa el mismo enfoque. Como el cálculo del digest solo puede hacerse de forma secuencial, la carga también tiene que avanzar secuencialmente.
      [1] https://github.com/sapcc/keppel
      [2] https://github.com/distribution/distribution
    • Gracias por el artículo del blog. Mencionaste que “durante los últimos 4 meses colaboraste con Outerbounds para desarrollar un builder de imágenes de contenedor personalizado”, y aunque dijiste que eso daba para otro artículo, me pregunto si podrías compartir aunque sea algunos detalles.
      Incluso un enlace al repositorio de GitHub estaría bien. El contexto es que estoy buscando una forma de crear imágenes OCI de manera programática desde $PROGRAMMING_LANGUAGE, o considerando implementarlo yo mismo. Algo como Buildah, pero en lugar de una interfaz de línea de comandos, con una API para un lenguaje de programación real. Claro que se podría invocar Buildah como subproceso, pero es algo engorroso, y también hay que preocuparse por interactuar con el estado interno de Buildah o limpiarlo; además, actualmente Buildah no es compatible con Mac.
    • No se me ocurre de inmediato una razón clara; podría estar relacionado con la carga.
      Creo que en algún momento añadí pushes paralelos a Docker, aunque tal vez estoy confundiendo pull con push. Revisándolo después, mi trabajo fue más bien de paralelización de comprobaciones, no del push final. Si se especifica sobre qué capa “va encima” una capa, puede ser porque el ID al que apunta ya tiene que existir.
  • Es un caso de uso bastante interesante.
    Personalmente, simplemente uso Nexus. Funciona suficientemente bien y soporta desde imágenes OCI hasta paquetes apt, repositorios Maven personalizados, NuGet y npm. Eso sí, la configuración y el uso de recursos son algo molestos, especialmente en cuanto a las políticas de limpieza: https://www.sonatype.com/products/sonatype-nexus-repository
    Aun así, me encanta que docker pull sea “simplemente” un conjunto de solicitudes HEAD y GET. Me gustaría ver en más tecnologías este tipo de decisión sensata: usar lo que lleva mucho tiempo funcionando bien y no complicarlo innecesariamente. Me sorprende que no haya más almacenes de contenedores simples con autenticación y limpieza. Nexus y Harbor son bastante complejos para usarlos en la práctica.

    • Estoy usando Gitea solo para paquetes. Maneja Docker, npm, Python, etc.
      Me sorprende que nadie en este hilo lo haya mencionado.
  • Distribution de CNCF, antes Docker Registry, incluye una función para respaldar el registro con URL firmadas de CloudFront que traen los datos desde S3 [1].
    https://distribution.github.io/distribution/storage-drivers/...

  • Me pregunto qué problema hay con https://github.com/distribution/distribution.

    • No lo había visto antes, y efectivamente soporta S3. Pero me pregunto si sirve las descargas directamente al cliente desde S3, o si solo usa S3 como backend de almacenamiento propio y, al hacer pull, en la práctica actúa como proxy.
  • Este enfoque parece muy caro, y habría sido bueno que el artículo también tratara el costo. Me da curiosidad tanto S3 como R2

    • El nivel estándar de S3 cuesta una quinta parte de ECR en términos de costo por GB almacenado
      El costo del tráfico saliente hacia Internet gratuito es el mismo, pero los repositorios públicos de ECR tienen una excepción que hace gratis el tráfico saliente para uso interno de AWS
    • Al final, el costo es el de S3. Depende de la región y del nivel de almacenamiento, pero el costo de almacenamiento por GB, los costos de GET/PUT y el costo de ancho de banda se pueden consultar en el sitio web de AWS: https://aws.amazon.com/s3/pricing/
  • Fuera de las herramientas de desarrollo no uso mucho Docker, pero nunca entendí por qué tendría que existir un registro privado de contenedores
    Simplemente parece rentismo. Me da curiosidad qué ventajas reales tiene frente a crear algo como un archivo de imagen administrado por uno mismo y usarlo como quiera

    • No es obligatorio usarlo. Se puede usar docker save y docker import
      docker save alpine:3.19 > alpine.tar
      docker load < alpine.tar
      Pero ahora hay que administrar ese archivo tar, y todos los sistemas deben saber dónde está y cómo acceder a él. O bien se puede usar el método que Docker ya ofrece, sin reinventar la rueda
    • Es muy probable que haya imágenes que no quieras hacer públicas. Normalmente esas imágenes deben ser accesibles desde infraestructura como clústeres de k8s o runners de CI/CD
      Por eso tienes que crear tu propio registro o pagarle a alguien para que lo gestione. Claro que, si solo usas imágenes para desarrollo, todo esto no importa y basta con guardar las imágenes en la máquina de desarrollo
    • Es la misma razón por la que no intercambiamos archivos por email en lugar de usar un repositorio de código
      Porque se necesita un repositorio central que tenga todas las versiones anteriores y al que varios consumidores puedan acceder fácilmente. No quieres compilar la app y luego empujarla manualmente a todos los lugares donde podría ejecutarse. La compilas una vez, la subes a un repositorio central y haces que todos los lugares apunten a ese repositorio
      Tampoco tienes por qué pagar por hosting de repositorios privados. Hay muchas herramientas que puedes alojar tú mismo
    • Los registros privados en la nube son muy útiles en proyectos que tienen requisitos obligatorios de autenticación/autorización relacionados con imágenes Docker
      Todo se puede configurar por entorno con Terraform, Bicep o Pulumi
    • También está el problema de cómo administrar eso. Si quieres usar las mismas herramientas que usas para imágenes públicas, basta con operar un registro de contenedores
  • ECR en realidad parece estar diseñado para permitir subir las capas de imagen en varias partes
    Entre las API de ECR relacionadas están InitiateLayerUpload API, que se llama al iniciar la carga de cada capa de imagen; UploadLayerPart API, que se llama por cada fragmento de capa (máximo 20 MB); y PutImage API, que empuja el manifiesto de imagen con las referencias a las capas de imagen después de subir la capa. Lo raro es que los fragmentos de capa deben subirse con codificación base64, lo que aumenta los datos aproximadamente un 33%

    • Probé usar esa API directamente y, lamentablemente, ahí también exigía cargas en orden
  • La idea de usar la disposición de rutas de archivos como medio para controlar endpoints es interesante
    Sin embargo, me pregunto cómo manejaría el header Docker-Content-Digest. No es obligatorio, pero se recomienda incluirlo en la respuesta, muchos clientes lo esperan y podrían rechazar capas que no tengan ese header. Además, se podrían perder funciones como la API de referrers de la especificación OCI 1.1. Parece bastante complicado de implementar