Development

La optimización de implementaciones de centros de datos múltiples utilizando registro Docker

Proporcionar una interfaz de implementación consistente y estandarizada, introdujimos el despliegue de servicios como contenedores Docker.

April 20 2016

Usando Docker para desplegar servicios en la producción

En Infobip corremos más de 400 servicios a través de 6 centros de datos. Los servicios son desplegados por los equipos de desarrollo cada pocos minutos - actualmente un total de alrededor de 90 despliegues por día.

Para proporcionar una interfaz de implementación consistente y estandarizada, introdujimos el despliegue de servicios como contenedores Docker - los equipos son capaces de empaquetar sus servicios como imágenes Docker y desplegarlos de la misma manera, independientemente de la tecnología o el idioma utilizado para generar el servicio.

Dado que las imágenes Docker pueden ser bastante grandes (cientos de megabytes) sabíamos desde el principio que la distribución de imágenes Docker de manera eficiente a todos los centros de datos sería un reto.

La distribución de imágenes de servicio Docker de manera eficiente a todos los centros de datos

Al implementar un servicio de empaquetado por imagen Docker como un contenedor Docker en una máquina de centro de datos que queremos utilizar la extracción de Docker para descargar imágenes Docker a la máquina y ejecutar el contenedor.

Almacenamos imágenes Docker de nuestras aplicaciones en un registro Docker central privado - para esto usamos Artifactory que actúa como registro Docker. Fue una elección obvia, puesto que ya lo utilizamos para almacenar todos los otros artefactos.

Podríamos halar Dockers directamente del centro de Artifactory Docker repo en cada despliegue, pero esto añadiría datos transversales significativos de sobrecarga a la red central cuando se transfiera la misma imagen Docker a diferentes máquinas.

Para ilustrar: Una imagen Docker de una aplicación Java de Infobip se compone de dos capas: una capa de imagen base alrededor de 160 MB (Alpine Linux + Oracle JDK instalado) y una capa de aplicación de imagen de alrededor de 60 MB (Spring Java app).

Java app Docker img

Con el fin de implementar la misma aplicación en 10 máquinas diferentes en el mismo centro de datos, tendríamos que transferir un total de 10 * 60 MB en centros de datos (desde el repositorio central a las máquinas en los centros de datos remotos) - y esto es en el mejor de los casos donde asumimos la imagen de Java Docker base ya está en el equipo destino.

Esta sobrecarga de red adicional puede minimizarse mediante el uso de los centros de datos de registro Docker actuaando como un proxy caché para el repositorio central privado de Docker.

Cuando se utiliza el registro caché de proxy del centro de datos local Docker  para el mismo escenario de 10 máquinas, sólo la aplicación de 60MB Docker se transporta a través de los centros de datos; las otras 9 máquinas conseguirían la capa de aplicación en caché de la memoria caché de registro local. (Esto supone que estas 10 máquinas están desplegadas en una manera secuencial que actualmente es el caso).

Docker registry

Alternativamente, podríamos modificar un flujo de imagen Docker exportación-transportación-cache-importación, pero decidimos probar con registro de código abierto Docker que tiene incorporada la capacidad de actuar como proxy y como caché.

Tomando el registro Docker a dar una vuelta

El funcionamiento de un registro privado Docker es simple y directo como se describe aquí:

La creación de un registro Docker para actuar como proxy caché tampoco es complicado y existe una guía de cómo hacerlo, por lo que fuimos capaces de iniciar rápidamente la imagen espejo del registro Docker halando a través del modo caché sólo para descubrir que no funciona como nos esperabamos. :(

Como se indica en este documento, en este momento no es posible reflejar otro registro privado:

Docker gotcha

Como nos gustó mucho la idea de ahorrar ancho de banda y reducir el tiempo de implementación, estábamos decididos a trabajar de alguna manera en torno a esta característica que falta del registro Docker.

Haciendo que funcione

Artifactory expone la Docker API para hablar a cada repositorio Docker que alberga - por ejemplo, para obtener etiquetas disponibles de imagenes busybox almacenados en Artifactory repositorio Docker local podemos enviar esta solicitud:

$ curl -u mzagar http://artifactory:8081/artifactory/api/docker/docker-local/v2/busybox/tags/list
Enter host password for user 'mzagar':
{
  "name" : "busybox",
  "tags" : [ "latest" ]
}

Dado que el registro del Docker es capaz sólo de reflejar el Hub Docker público central, tuvimos una idea para interceptar todas las peticiones HTTP que el espejo del registro Docker haría al URL del registro Docker a distancia y volver a escribirlo para acomodarla a nuestro Artifactory Docker API URL.

Básicamente nos haría imagen del registro Docker cree que está hablando con la central del Docker, pero en cambio, habla con nuestro repositorio Artifactory Docker.

Un poco de la magia HAProxy

Para volver a escribir solicitud HTTP del registro del Docker envía a un registro central Docker utilizamos HAProxy - corriendo como un contenedor Docker por supuesto.

Aquí está la configuración HAProxy que especifica cómo hacer la reescritura:

haproxy.cfg

global
  log 127.0.0.1 local0
  log 127.0.0.1 local1 notice
 
defaults
  log global
  mode http
  option httplog
  option dontlognull
  option forwardfor
  timeout connect 5000ms
  timeout client 60000ms
  timeout server 60000ms
  stats uri /admin?stats
 
frontend docker
  bind *:80
  mode http
  default_backend artifactory

backend artifactory
  reqirep ^([^\ ]*)\ /v2/(.*) \1\ /artifactory/api/docker/docker-local/v2/\2
  http-request add-header Authorization Basic\ %[env(BASIC_AUTH_PASSWORD)]
  server artifactory ${ARTIFACTORY_IP}:${ARTIFACTORY_PORT} check

Cada / v2 / * solicitud enviada por el espejo Docker se vuelven a grabar /artifactory/api/docker/docker-local/v2/*  y es enviado a nuestro servidor Artifactory.

Estamos utilizando las variables de entorno para especificar la dirección IP y el puerto Artifactory, y también para añadir Autorización de cabecera fija para autenticarse como usuario Artifactory válido.

Esta configuración también expone las estadísticas HAProxy para el usuario y servidores de servicios de fondo que proporciona una buena manera de comprobar si Artifactory está vivo y accesible y la cantidad de tráfico que genera la imagen.

Conectarlo con el registro Docker

Lo siguiente que necesitamos es conectar el registro Docker a hablar con HAProxy que luego encamina la petición HTTP dónde y cómo los queremos - hicimos esto utilizando docker-compose. Así es como el archivo completo docker-compose.yml  se ve:

docker-compose.yml

haproxy:
  image: haproxy:latest
  container_name: hap
  restart: always
  ports:
    - 80:80
  volumes:
    - ${WORK}/haproxy/haproxy.cfg:/usr/local/etc/haproxy/haproxy.cfg
  environment:
    - ARTIFACTORY_IP=${ARTIFACTORY_IP}
    - ARTIFACTORY_PORT=${ARTIFACTORY_PORT}
    - BASIC_AUTH_PASSWORD=${BASIC_AUTH_PASSWORD}
  log_driver: "json-file"
  log_opt:
    max-size: "10m"
    max-file: "10"

mirror:
  image: registry:2.4.0
  container_name: registry
  restart: always
  ports:
    - 5000:5000
  volumes:
    - ${WORK}/docker-registry:/var/lib/registry
    - ${REGISTRY_CERTIFICATE_FOLDER}:/certs
  environment:
    - REGISTRY_HTTP_TLS_CERTIFICATE=${REGISTRY_HTTP_TLS_CERTIFICATE}
    - REGISTRY_HTTP_TLS_KEY=${REGISTRY_HTTP_TLS_KEY}
  command: serve /var/lib/registry/config.yml
  links:
    - haproxy:haproxy
  log_driver: "json-file"
  log_opt:
    max-size: "10m"
    max-file: "10"

Utilizamos una gran cantidad de variables de entorno para ser flexible cuando se inicia este dúo de aplicación. Espera componer haproxy.cfg  en vivo en una carpeta HAProxy, y el registro Docker config.yml en vivo en la carpeta docker-registry relativa a la carpeta TRABAJO especificada.

Vamos a utilizar un simple script bash para activar docker-compose con todas las variables de entorno necesarias:

run-mirror.sh

#!/bin/sh

export WORK=`pwd`
export ARTIFACTORY_IP='10.10.10.10'
export ARTIFACTORY_PORT='8081'
export REGISTRY_CERTIFICATE_FOLDER='/etc/ssl/example'
export REGISTRY_HTTP_TLS_CERTIFICATE='/certs/cert.pem'
export REGISTRY_HTTP_TLS_KEY='/certs/cert.pem'
export BASIC_AUTH_PASSWORD='basicauthbase64encodedstring=='

docker-compose up --force-recreate

Después de iniciar docker-compose  nuestra imagen está disponible en mirror.ib-ci.com:5000 - primero se comprueba el contenido de nuestra imagen:

$ ./run-mirror.sh

$ curl https://mirror.example.com:5000/v2/_catalog
{"repositories":[]}

Como era de esperar, el caché de imagen está vacía. Ahora halamos la imagen busybox desde nuestro repositorio central de Docker y medimos el tiempo que se tarda en descargar la imagen:

$ time docker pull mirror.example.com:5000/busybox
Using default tag: latest
latest: Pulling from busybox
9d7588d3c063: Pull complete
a3ed95caeb02: Pull complete
Digest: sha256:000409ca75cd0b754155d790402405fdc35f051af1917ae35a9f4d96ec06ae50
Status: Downloaded newer image for mirror.example.com:5000/busybox:latest
real	0m 3.66s
user	0m 0.02s
sys	0m 0.00s

Tomó alrededor de 3,5 segundos para descargar la imagen por primera vez.

Podemos ver que la imagen busybox ahora se almacena en la memoria caché de la imagen Docker:

$ curl https://mirror.example.com:5000/v2/_catalog
{"repositories":["busybox"]}

Vamos a quitar la imagen busybox local y halar de él de nuevo - se espera que el segundo se lleve a cabo más rápido que el primero, ya que la imagen se almacena en caché y la imagen no tiene que buscarlo desde el repositorio central:

$ docker rmi mirror.example.com:5000/busybox
Untagged: mirror.example.com:5000/busybox:latest
Deleted: sha256:a84c36ecc374f680d00a625d1f0ba52426a536775ee7277f21728369dc42499b
Deleted: sha256:1a879e2f481d67c4537144f80f5f6d776542c7d3a0bd7721fdf6aa1ec024af24
Deleted: sha256:a193ed10c686545c776af2bb8cfe20d3e5badf5c936fbf0e8f389d769018a3f9

$ time docker pull mirror.example.com:5000/busybox
Using default tag: latest
latest: Pulling from busybox
9d7588d3c063: Pull complete
a3ed95caeb02: Pull complete
Digest: sha256:000409ca75cd0b754155d790402405fdc35f051af1917ae35a9f4d96ec06ae50
Status: Downloaded newer image for mirror.example.com:5000/busybox:latest
real	0m 0.28s
user	0m 0.01s
sys	0m 0.00s

Hooray! Dado que el registro tenía la imagen en caché local, el segundo tomó sólo 0,28 segundos!

Conclusión

Hemos establecido con éxito instancias de memoria caché de registro de imagen Docker locales en cada centro de datos remoto para reflejar nuestro registro central de Artifactory Docker y optimizamos la cantidad de datos que necesitamos transferir entre los centros de datos al desplegar nuestras aplicaciones.

Todavía hay mucho espacio para mejorar este proceso, pero se llevó a cabo nuestro objetivo principal de la optimización de la distribución de imágenes Docker en centros de datos remotos de una manera relativamente simple y transparente.

Continuamos mejorando nuestro proceso de implementación Docker al día para dar una experiencia de implementación sin problemas a nuestros equipos de desarrollo.