Development

Como otimizar a implantação de vários datacenters com o registro do Docker

Para fornecer uma interface consistente e padronizada de implantação, apresentamos a implantação de serviços como containers do Docker.

June 14 2016

Como utilizar o Docker para implantar serviços na produção

Na Infobip executamos mais de 400 serviços em seis datacenters. Os serviços são implantados por equipes de desenvolvimento em intervalos de alguns minutos – atualmente totalizando cerca de 90 implantações por dia.

Para oferecer uma interface de implantação consistente e padronizada, apresentamos a implantação de serviços como containers do Docker – as equipes podem agrupar seus serviços como imagens do Docker e implantá-los da mesma forma, independentemente da tecnologia ou a linguagem utilizada para desenvolver o serviço.

Como as imagens do Docker podem ter um tamanho considerável (de centenas de megabytes), desde o começo, sabíamos que seria um desafio distribuí-las de forma eficiente para todos os datacenters.

Como distribuir imagens de serviços do Docker com eficiência para todos os datacenters

Ao implantar um pacote de serviços de imagem como container do Docker em uma máquina de datacenter, devemos usar o docker pull para baixar as imagens do Docker para essa máquina e executar o container.

Armazenamos as imagens de nossos aplicativos em um registro central privado do Docker – para isso, utilizamos o Artifactory, que funciona como registro do Docker. Foi uma escolha óbvia, pois já o utilizamos para armazenar outros de nossos artefatos.

Poderíamos ter realizado o docker pull diretamente no repositório central do Docker no Artifactory para cada implantação, porém, isso resultaria em custos significativos para a rede de datacenters cruzados no transporte da mesma imagem Docker entre máquinas.

Para ilustrar: uma imagem no Docker para um aplicativo Java da Infobip consiste em duas camadas: uma camada base com cerca de 160 MB (alpine linux + oracle jdk instalados) e uma camada de imagem do aplicativo com cerca de 60 MB (aplicativo Spring Java).

Java app Docker img

Para implantar o mesmo aplicativo em dez máquinas diferentes no mesmo datacenter, teríamos de transferir um total de 10 x 60 MB entre datacenters (do repositório central para as máquinas em datacenters remotos) – e isso na melhor das hipóteses, na qual supomos que a imagem base do Java no Docker já está na máquina que é o alvo.

Esse custo adicional de rede pode ser minimizado utilizando-se um registro de Docker local para o datacenter, que funcionará como proxy cache para o repositório central privado no Docker.

Utilizando um registro de proxy cache no Docker para um datacenter local no mesmo cenário de 10 máquinas, somente a camada do aplicativo de 60 MB do Docker será transportada entre os datacenters; As outras nove máquinas receberiam a camada do aplicativo armazenada no cache do registro local. (Supondo que essas dez máquinas sejam implantadas de forma sequencial, que atualmente é o caso).

Docker registry

Como alternativa, podemos criar um fluxo personalizado para importar para o cache, transportar e exportar a imagem do Docker, mas decidimos experimentar o registro open source do Docker que possui capacidade integrada de funcionar como proxy e cache.

Teste do registro do Docker

É fácil e prático executar um registro privado no Docker, como descrito a seguir:https://docs.Docker.com/registry.

Também não é complicado configurar um registro do Docker para funcionar como proxy cache e existe um manual para isso, então foi rápido conseguir iniciar o espelhamento do registro no Docker em modo pull through cache e perceber que não funcionou como esperávamos :(

Como afirmado neste documento, atualmente não é possível fazer o espelhamento de outro registro privado:

Docker gotcha

Mas como gostamos muito da ideia de economizar banda larga e reduzir o tempo de implantação, estávamos determinados a contornar a ausência desta função do registro do Docker de alguma forma.

A solução

O Artifactory expõe a API do Docker por conversar com todos os repositórios do Docker que hospeda – por exemplo, para obter as tags disponíveis de imagens busybox armazenadas em um repositório local do Docker no Artifactory, podemos enviar o seguinte pedido:

$ 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" ]
}

Como o registro do Docker atualmente só consegue espelhar o Hub central público do Docker, tivemos a ideia de interceptar todo pedido HTTP que o espelhamento do registro do Docker faria para a URL do registro remoto do Docker e reescrevê-lo para servir à nossa URL da API do Docker no Artifactory.

Basicamente, faríamos o espelhamento do registro do Docker pensar que estaria conversando com o hub central do Docker, mas, em vez disso, conversaria com o nosso repositório do Docker no Artifactory.

Um toque de mágica HAProxy

Para reescrever o pedido HTTP que o registro do Docker envia ao registro central do Docker, utilizamos o HAProxy – funcionando como container do Docker, claro.

Esta é a configuração do HAProxy que especifica como reescrever:

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 pedido /v2/* enviado pelo espelhamento do Docker é reescrito como /artifactory/api/docker/docker-local/v2/* e enviado ao nosso servidor no Artifactory.

Estamos utilizando variáveis de ambiente para especificar o IP e a porta do Artifactory, e também para acrescentar um cabeçalho fixo de Autorização para autenticar como usuário válido do Artifactory.

Essa configuração também expõe as estatísticas do HAProxy a servidores front-end e back-end, o que é um bom jeito de verificar se o Artifactory está no ar e accessível, e quanto tráfego o espelhamento gera.

Como associar ao registro do Docker

Em seguida, precisamos associar ao registro do Docker para que ele converse com o HAProxy, que em seguida detectará o local do pedido HTTP e a forma que o queremos – fizemos isso usando docker-compose. O arquivo completo docker-compose.yml ficou assim:

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 muitas variáveis de ambiente para garantir a flexibilidade ao iniciar essa dupla de aplicativos. A composição espera que o haproxy.cfg fique em uma pasta de HAProxy e o config.yml do registro do Docker, na pasta docker-registry relacionada à pasta WORK especificada.

Utilizaremos um script bash simples para executar o docker-compose com todas as variáveis de ambiente necessárias:

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

Após iniciar o docker-compose, nosso espelhamento fica disponível em mirror.ib-ci.com:5000 – primeiro, verificamos o conteúdo do nosso espelhamento:

$ ./run-mirror.sh

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

Como esperado, o cache do espelhamento está vazio. Agora puxamos a imagem busybox do nosso repositório central no Docker e calculamos o tempo que leva para realizar o download da imagem:

$ 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

Foram necessários cerca de 3,5 segundos para a realização do primeiro download da imagem.

Podemos ver que a imagem busybox agora está armazenada no cache de espelhamento do Docker:

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

Vamos remover a imagem busybox local e puxar de novo – esperamos que a segunda tentativa seja mais rápida que a primeira, pois a imagem já está no cache e não é mais necessário que o espelhamento a busque no repositório 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

Viva! Como o registro já tinha a imagem no cache local, na segunda vez que puxamos levou apenas 0,28 segundos!

Conclusão

Conseguimos configurar instâncias de cache para o espelhamento do registro do Docker local com sucesso em todos os datacenters remotos para espelhar nosso registro central privado do Docker no Artifactory e otimizamos a quantidade de dados que precisamos transferir entre datacenters ao implantar nossas aplicações.

Ainda há muito espaço para melhorar esse processo, mas conseguimos atingir nossa meta inicial de otimizar a distribuição de uma imagem do Docker entre datacenters remotos de forma relativamente simples e transparente.

Continuamos melhorando nosso processo no Docker todos os dias para oferecer uma experiência de implantação fluida às nossas equipes de desenvolvimento.

(By Mario Zagar, Senior Software Architect / Division Lead)