Montando un docker registry «Como Dios Manda ™»

registry¡Buenas fans de los contenedores! En este nuevo episodio vamos a ver cómo montar una de las piezas más olvidadas dentro de una infraestructura docker: el Registry. Cuando montamos una infraestructura de contenedores de cierto nivel las partes más sexys (self-healing, orquestación, ingress load balancing, self-healing… Ouh yeah, sigue hablándome sucio) se llevan toda la fama y las partes más básicas, pero «feas», las dejamos de lado. Sin embargo, sin un Registry propio no podríamos montar nuestro sexy sistema de clustering.

¿Que es un Registry?

Para los más despistados (como yo no hace tanto) vamos a empezar por lo más básico. Un Registry es un lugar donde almacenar imágenes de contenedores que luego utilizará(n) el/los engines para crear nuestros amados contenedores. Es lo que en el mundo pre-container hacía un repositorio de paquetes (rpm/apt/…) o para nuestros ene/amigos developers puede hacer npm o un repositorio de artefactos. Un Registry es una de las piezas clave a la hora de crear nuestros entornos Docker en cuanto empezamos a crear nuestras propias imágenes (minuto 1). Un Registry propio nos permitirá que los distintos motores docker que tengamos (¿he oído cluster docker?) puedan descargar las imágenes que desarrollemos y  que no necesariamente queramos que sean públicas.

¿Y porqué montar el nuestro?

Desde Docker Inc. se ofrece un servicio de Registry que solemos usar a diario: Docker Hub. Este servicio se ofrece como SaaS y tiene una capa de uso gratuita. En este plan gratuito tenemos opción de un Registry privado y uno público. Si necesitamos más, podemos mirar su lista de precios que tampoco son muy elevados.

Entonces, ¿Por qué montar nuestro propio registro? Los motivos más evidentes son los siguientes:

  • Tener el Registry en nuestra infraestructura nos ahorra ancho de banda y nos da mejor tiempo de acceso/descarga. Esto que puede parecer poco importante a día de hoy, en clusters donde lanzamos actualizaciones o entra en juego el self-healing tiene su importancia.
  • Tener una copia «controlada» de imágenes públicas nos protege ante cambios inesperados, o desaparición, de las mismas (que os vamos a contar a los usuarios de npm).
  • Tener tantos namespaces privados como queramos, con las ACL que queramos y la auth conectada contra nuestro sistema de auth preferido (ldap,…).

Y en el fondo porque a los BOFHers nos gusta controlar los servicios 😀

Para ilustrarlo vamos a ver un flujo de trabajo habitual:

  • Desarrollamos un APP en el último lenguaje de moda, pongamos por ejemplo que es Trump Script 😀
  • Hemos decidido que se va a desplegar sobre Docker.  Más que nada porque como le pidamos a operaciones que nos instale el último commit de TS  en sus sagrados servidores nos van a lanzar una mirada asesina de BOFH-er que nos va a dejar petrificados.
  • Así que a la par que programamos, empezamos a crear nuestro Dockerfile.  Empezamos tal que … FROM fulanodetal/lenguajemolon:latest …
  • ¡WARNING! ¡Camino al abismo detectado! Tenemos un registry, así que vamos a intentar hacerlo bien. No tenemos más que hacer un pull de esa imagen, tagearla con nuestro Registry+namespace y hacer un push.
  • Ahora sí, hacemos nuestro Dockerfile con un FROM registry.midominio.com/miproyecto/lenguajequemola:version … (recordad niños, SIEMPRE usamos un tag concreto, nunca latest)
  • Hacemos el build y lo tageamos a nuestro registry ( docker build -t registry.midominio.com/miproyecto/miapp:VERSION ) y pusheamos
  • Empezamos el ciclo normal de deploy en local/dev/stage/pre/prod
  • La APP acaba en un Cluster Docker Rock solid.
  • Todos somos felices, los cambios en la APP fluyen a PROD de manera estable y continua como el agua en un jardín japonés (clack, clack, clak …..).

Fuente: https://commons.wikimedia.org/wiki/File:Shishi-odishi-2.jpg

¿Y qué es Como Dios Manda ™?

Ahora que estamos convencidos de montar nuestro Registry, ¿qué es lo que necesitamos para considerarlo «Como Dios Manda ™ » ? Vamos a tener que cumplir 3 mandamientos:

SSL: A día de hoy no debería hacer falta argumentar esto. En este caso en concreto si no usamos SSL hay que tocar muchas cosas,  tunear los engines, bla bla bla. SSL es obligatorio y punto.
Auth/Authz: Queremos controlar quién (autenticación) puede (autorización) subir o bajar nuestras imágenes. Todo esto siempre ligado a espacios de nombres que nos den un control fino. Ya si lo conectamos con nuestro sistema previo… Miel sobre hojuelas.
UI: Somos hackers, pero una UI que nos haga más amigable su gestión es algo que se agradece:D Además el cliente (si le damos acceso) siempre agradece estas cosas.

¿ Dónde lo ponemos ?

HuaweiRH2288HV2Entre los motivos que hemos mencionado para montar nuestro propio registry uno de ellos era el hecho de tenerlo «cerca» de nuestra infraestructura. El punto exacto ya dependerá de nuestras necesidades.  Podemos tenerlo en nuestra cloud privada o en la cloud pública. Podemos tener uno para toda la organización o implementar uno por proyecto de suficiente entidad y que se ejecute dentro de la infraestructura del mismo.  La idea es que sea tan sencillo de montar (o eso vamos a intentar) que la decisión no dependa de este factor. Eso sí, una cosa muy importante a tener en cuenta es la necesidad de almacenamiento: ¡Mucho!

 

¿ Cómo lo  vamos a montar ?

Vamos a usar 3 piezas de software juntas que nos permitan alcanzar nuestro deseado registry como dios manda. Y todas ellas en forma de contenedor claro:

  • Un Registry. Vamos a usar el propio registry de docker inc, en su versión 2. Lo podemos descargar desde docker hub: https://hub.docker.com/_/registry/.
  • Un frontal nginx que se encarge de los certs y la gestión de los dominios.
  • Una UI + auth/authz. Por suerte la gente de SUSE tiene liberado una buena pieza de software que hace todo esto: https://github.com/SUSE/Portus . Por desgracia la instalación está pensada para Suse, o a lo sumo instalación manual. Como nos encantan los contenedores vamos a instalarlo haciendo uso de Docker.
  • Un backend de BBDD, MariadDB en este caso.
  • Un agente que ejecuta tareas periódicas (cron).

Adicionalmente necesitamos un certificado SSL que nos sirva para dos dominios registry.midominio.com y portus.midominio.com. Para ello podemos usar un wildcard o usar certificados de let’s encrypt para pruebas. Pero esto último va más allá de este artículo, que si no no teminaremos nunca.

Vamos a ver un pequeño diagrama de lo que vamos a construir:

post_registry_esquema

Para montarlo vamos a usar contenedores docker evidentemente. Vamos a usar docker-compose (por lo tanto necesitamos tenerlo instalado) para definir nuestro servicio. Sirva este post para ver un ejemplo de diseño de arquitectura software en un entorno de contenedores. En este caso vamos a usar la versión 2 de docker-compose file en lugar de la nueva (versión 3) que está pensada para entornos cluster. Nos parece más interesante no complicar en exceso el post y centrarnos en el Registry.

Nos tiramos al barro!

Vamos a empezar por montar un escenario que nos permita testarlo todo. Vamos a usar docker-machine y virtualbox para ello (dos dependencias solo para dev, en prod podemos montarlo como más tilín nos haga). Empezamos por crear un nuevo host docker:

docker-machine create -d virtualbox registry

#esperamos a que termine de crear/arrancar 

eval $(docker-machine env registry)

A partir de ahora todos los comandos que lancemos en esa shell serán enviados al docker host. Hacemos un docker info para comprobar que todo está OK.

docker info 

Containers: 0
 Running: 0
 Paused: 0
 Stopped: 0
Images: 0
Server Version: 1.13.1
Storage Driver: aufs
 Root Dir: /mnt/sda1/var/lib/docker/aufs
 Backing Filesystem: extfs
 Dirs: 0
 Dirperm1 Supported: true
Logging Driver: json-file
Cgroup Driver: cgroupfs
Plugins: 
 Volume: local
 Network: bridge host macvlan null overlay
Swarm: inactive
Runtimes: runc
Default Runtime: runc
Init Binary: docker-init
containerd version: aa8187dbd3b7ad67d8e5e3a15115d3eef43a7ed1
runc version: 9df8b306d01f59d3a8029be411de015b7304dd8f
init version: 949e6fa
Security Options:
 seccomp
  Profile: default
Kernel Version: 4.4.47-boot2docker
Operating System: Boot2Docker 1.13.1 (TCL 7.2); HEAD : b7f6033 - Wed Feb  8 20:31:48 UTC 2017
OSType: linux
Architecture: x86_64
CPUs: 1
Total Memory: 995.8 MiB
Name: registry2
ID: U7CE:DTAH:NYMO:K5XL:YBWJ:YK7G:DX65:QAU4:NUOF:5GPM:M5C5:4JPV
Docker Root Dir: /mnt/sda1/var/lib/docker
Debug Mode (client): false
Debug Mode (server): true
 File Descriptors: 14
 Goroutines: 22
 System Time: 2017-02-16T06:56:11.495893444Z
 EventsListeners: 0
Username: jlatorre
Registry: https://index.docker.io/v1/
Labels:
 provider=virtualbox
Experimental: false
Insecure Registries:
 127.0.0.0/8
Live Restore Enabled: false

Para que nuestra arquitectura funcione necesitamos los siguientes archivos de configuración:

  • Configuración del Registry: /opt/registry/config.yml
  • Cert para que el registry hable con Portus: /opt/registry/portus.crt
  • Configuración de portus general: /opt/portus/config.yml
  • Configuración de portus de BBDD: /opt/portus/database.yml
  • Configuración de NGINX para el Registry: /opt/nginx/registry.irontec.com.conf
  • Configuración de NGINX para el Portus: /opt/nginx/portus.irontec.com.conf
  • Docker compose con la definición de la arquitectura: /opt/rcdm/docker-compose.yml

Vamos a empezar por preparar la estructura de directorios necesaria:

for DIR in certs mysql nginx portus rcdm registry registry_data
do 
docker-machine ssh registry sudo mkdir -p /opt/$DIR
docker-machine ssh registry sudo chgrp staff /opt/$DIR
docker-machine ssh registry sudo chmod g+w /opt/$DIR
done

Aparte de los dirs de los archivos de configuración hemos creado un directorio para los datos del Registry (/opt/registry_data) y otro para los de Mysql (/opt/mysql). En producción estos directorios (sobre el todo el registry_data) seguramente querramos que estén en una partición/disco dedicado.

Debemos copiar nuestro certificado válido para los dos subdominios que vamos a usar. En nuestro caso será wildcard, ya que disponemos de uno para nuestro dominio. Es importante que este certificado sea válido, ya que de lo contrario tendremos problemas a la hora de añadir el Registry al Portus.

openssl genrsa 2048 > wildcard.key
Generating RSA private key, 2048 bit long modulus
............................................................................................+++
..........................................................................+++
e is 65537 (0x10001)

openssl req -new -x509 -nodes -sha1 -days 3650 -key wildcard.key > wildcard.crt

You are about to be asked to enter information that will be incorporated
into your certificate request.
What you are about to enter is what is called a Distinguished Name or a DN.
There are quite a few fields but you can leave some blank
For some fields there will be a default value,
If you enter '.', the field will be left blank.
-----
Country Name (2 letter code) [AU]:ES
State or Province Name (full name) [Some-State]:Bizkaia
Locality Name (eg, city) []:Bilbao
Organization Name (eg, company) [Internet Widgits Pty Ltd]:Irontec: Internet y Sistemas sobre GNU / Linux
Organizational Unit Name (eg, section) []:Sistemas
Common Name (e.g. server FQDN or YOUR name) []:*.irontec.com
Email Address []:[email protected]

Ahora ya podemos copiarlo:

docker-machine scp ./wildcard.crt registry:/opt/certs/
docker-machine scp ./wildcard.key registry:/opt/certs/

Con los siguientes comandos vamos a crear todos los archivos de configuración necesarios. Por comodidad vamos a lanzarlos desde la máquina virtual dónde está el engine Docker. Así que primero hacemos un ssh a la misma y luego un sudo. Acto seguido instalamos docker-compose que necesitaremos más adelante:

docker-machine ssh registry
                        ##         .
                  ## ## ##        ==
               ## ## ## ## ##    ===
           /"""""""""""""""""\___/ ===
      ~~~ {~~ ~~~~ ~~~ ~~~~ ~~~ ~ /  ===- ~~~
           \______ o           __/
             \    \         __/
              \____\_______/
 _                 _   ____     _            _
| |__   ___   ___ | |_|___ \ __| | ___   ___| | _____ _ __
| '_ \ / _ \ / _ \| __| __) / _` |/ _ \ / __| |/ / _ \ '__|
| |_) | (_) | (_) | |_ / __/ (_| | (_) | (__|   <  __/ |
|_.__/ \___/ \___/ \__|_____\__,_|\___/ \___|_|\_\___|_|
Boot2Docker version 1.13.1, build HEAD : b7f6033 - Wed Feb  8 20:31:48 UTC 2017
Docker version 1.13.1, build 092cba3
docker@registry:~$ sudo -s
root@registry:/home/docker# 
curl -L "https://github.com/docker/compose/releases/download/1.10.0/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose
chmod +x /usr/local/bin/docker-compose

Vamos con los archivos de configuración. Lo primero es crear la configuración del registry, la parte importante es la de notifications, donde le indicamos que notifique a Portus los nuevos push:

cat << EOL > /opt/registry/config.yml
version: 0.1
storage:
  filesystem:
    rootdirectory: /registry_data
  delete:
    enabled: true
http:
  addr: 0.0.0.0:5000
  debug:
    addr: 0.0.0.0:5001
auth:
  token:
    rootcertbundle: /etc/docker/registry/portus.crt
notifications:
  endpoints:
    - name: portus
      url: http://web:3000/v2/webhooks/events
      timeout: 500ms
      threshold: 5
      backoff: 1s

EOL

Necesitamos crear el cert que va usar el registry para conectarse al portus. Vamos a copiar un certe autofirmado que se incluye en la distribución de Portus. Evidentemente esto habría que cambiarlo en producción:

cat << EOL > /opt/registry/portus.crt
-----BEGIN CERTIFICATE-----
MIIFejCCA2ICAQEwDQYJKoZIhvcNAQEFBQAwgYIxCzAJBgNVBAYTAkRFMRAwDgYD
VQQIDAdCYXZhcmlhMRMwEQYDVQQHDApOdWVyZW1iZXJnMQ0wCwYDVQQKDARTVVNF
MRgwFgYDVQQDDA9wb3J0dXMudGVzdC5sYW4xIzAhBgkqhkiG9w0BCQEWFHJvb3RA
cG9ydHVzLnRlc3QubGFuMB4XDTE1MDQxMjE1NTUwMloXDTI1MDQwOTE1NTUwMlow
gYIxCzAJBgNVBAYTAkRFMRAwDgYDVQQIDAdCYXZhcmlhMRMwEQYDVQQHDApOdWVy
ZW1iZXJnMQ0wCwYDVQQKDARTVVNFMRgwFgYDVQQDDA9wb3J0dXMudGVzdC5sYW4x
IzAhBgkqhkiG9w0BCQEWFHJvb3RAcG9ydHVzLnRlc3QubGFuMIICIjANBgkqhkiG
9w0BAQEFAAOCAg8AMIICCgKCAgEAyWA25/CoT+VsvCbwwx71KQ9YRy5gzadOufi3
2t4NpP8O27tbemc4coIsEDLRBJSXxhBv97mTvfjAU4/nO0tJDgEHlrpl+p5IA6Up
3aYY2YqqY3riv+YI+e+RDcTau9Zd/ZxuB5OjpQocY16PGTP9dcUmn49oZ7xb3NUi
eoDHp2cS9UaTUzjNrxR+z6GrhjkLE9k5j1hi48v75/Ee/jL6W7rEiajJbuQDBkxc
mDmflalrrUAJnmCe1RpYRgbKEryBrFzUwBGsjqGwRnYwVNKc1CTnah986gj0Qx1O
FiPexIQrumCKY9Z7FwBrTm+8Ip0zdwfRMz7qZ6zfJqjcj5/1lNpXC/mBJ5k2HLgj
6eGSuQTBLHJNMu5S0dtG1vGnhQF6RjM1f/K+vwOAinrUJx6bSV/guwBdo8zg6m/o
krUvRAuP+l4ucyJP5T/JS53QXtJYSLNUdPVpec76EJOY1WrEBoyfdty2D3EHtnIF
GpTesW0hD9Jz0ofLXBA3UCd+Gi/Wr2A0wzpn3VfONDqFa6xiljpT2YgBKpa1eucC
+3JmVFRn6BY9jo76paC6Ygu/QzOfuF1nsv0aYdL9Lwdjf3HUBDFHUBJreJkh0QQ5
yZMXMhdFI4yEKvYJLAiA7tUwAQ6xvDegy+JOsRMsEvDRNNfueczEk345FlrqRXI4
KBPcdBsCAwEAATANBgkqhkiG9w0BAQUFAAOCAgEAGlCH3DFJJvOFrVO33Zp7lygq
XMd/XDMOLG1gwJ1cVvZPVaNGKgcB/v1Rjhf9R39fxum5uvw005ZX+APj1rtOgkO/
fC9K0MA/kCIUjmU+NiH+UTDgcaChXXtVQ+PVAoWfKfEvwt6czcyQ4n+/hS0qJIjj
vOuFpnI9VBOxgN85tnjBAZ/7PPxg8FoUss51wtRXmML45rCW77Q2NiGH717Mo110
xiue/+giTf7wP17Xl+Gvs4Fsm9rSDv0xhMYDjVbwU62ycQqXvDQVbbzkGjdNbKn2
Fzo/C8bCQOYuPzUo18b3PoplEkO/b780Lv7t7m9lTHAB4X81MO0yg8vNPrISK2Af
VMJFDK4PsCdpGVFzY9Z+Jo5mGXV/n/nxRdaNujmANFeUl0Od1PuaDf+8w98GAuae
mKTlyV6C5cPMVjwgDeGMdGj0yz7Ht/PXwy4KltHSzSrfUww9sr5F3Kcpekh2mcb2
NKXxXZ03b9AaWBPYEU2vD0N/MV7NwJqffW+/tLhMh/IVO991LTLFFKwZ31L+cHCj
ozJubbxDwix8wjTYw+Vj6dJyZrqb3IfLDgl2+ReaF1i80CKm4e+iikK+dmC88Av8
FwdbTJL+QYEIxwHLz45cuHslqdD2josZYidrk1xBuQLMFN98jR+kwalmAT9dlSoA
vUZzjl/Is5XRXOjaJNE=
-----END CERTIFICATE-----
EOL

Vamos ahora con la otra pata de nuestra arquitectura, Portus. Incluimos un archivo de configuración con muchas opciones deshabilitadas que en producción deberíamos ajustar (auth ldap, smtp, borrado de imagenes,…).

cat << EOL > /opt/portus/config.yml
email:
  from: "[email protected]"
  name: "Portus"
  reply_to: "[email protected]"

  # If enabled, then SMTP will be used. Otherwise 'sendmail' will be used
  # (defaults to: /usr/sbin/sendmail -i -t).
  smtp:
    enabled: false
    address: "smtp.example.com"
    port: 587,
    user_name: "[email protected]"
    password: "password"
    domain: "example.com"

gravatar:
  enabled: true

delete:
  enabled: false

ldap:
  enabled: false

  hostname: "ldap_hostname"
  port: 389

  # Available options: "plain", "simple_tls" and "starttls". The default is
  # "plain", the recommended is "starttls".
  method: "plain"

  # The base where users are located (e.g. "ou=users,dc=example,dc=com").
  base: ""

  # User filter (e.g. "mail=george*").
  filter: ""

  # The LDAP attribute where to search for username. The default is 'uid'.
  uid: "uid"

  # LDAP credentials used to search for a user.
  authentication:
    enabled: false
    bind_dn: ""
    password: ""

  # Portus needs an email for each user, but there's no standard way to get
  # that from LDAP servers. You can tell Portus how to get the email from users
  # registered in the LDAP server with this configurable value. There are three
  # possibilities:
  #
  #   - disabled: this is the default value. It means that Portus won't do a
  #     thing when registering LDAP users (users will be redirected to their
  #     profile page until they setup an email account).
  #   - enabled where "attr" is empty: for this you need "ldap.base" to have
  #     some value. In this case, the hostname will be guessed from the domain
  #     component of the provided base string. For example, for the dn:
  #     "ou=users,dc=example,dc=com", and a user name "user", the resulting
  #     email is "[email protected]".
  #   - enabled where "attr" is not empty: with this you specify the attribute
  #     inside a LDIF record where the email is set.
  #
  # If something goes wrong when trying to guess the email, then it just falls
  # back to the default behavior (empty email).
  guess_email:
    enabled: false
    attr: ""

first_user_admin:
  enabled: true

signup:
  enabled: true

check_ssl_usage:
  enabled: true

registry:
  jwt_expiration_time:
    value: 5
  catalog_page:
    value: 100

machine_fqdn:
  value: "<%= ENV['PORTUS_FQDN_VALUE'] %>"

display_name:
  enabled: false

user_permission:
  # Allow users to change the visibility or their personal namespace. If this is
  # disabled, only an admin will be able to change this. It defaults to true.
  change_visibility:
    enabled: true

  # Allow users to create/modify teams if they are an owner of it. If this is
  # disabled only an admin will be able to do this. This defaults to true.
  manage_team:
    enabled: true

  # Allow users to create/modify namespaces if they are an owner of it. If this
  # is disabled, only an admin will be able to do this. This defaults to true.
  manage_namespace:
    enabled: true

EOL

Necesitamos también configurar la parte BBDD de Portus:

cat << EOL > /opt/portus/database.yml
default: &default
  adapter: mysql2
  encoding: utf8
<% if ENV['COMPOSE'] %>
  host: <%= ENV['PORTUS_DB_HOST'] %>
  username: root
  password: <%= ENV['PORTUS_DB_PASSWORD'] %>
<% end %>

development:
  <<: *default
  database: portus_development

staging:
  <<: *default
  database: portus_staging

# Warning: The database defined as "test" will be erased and
# re-generated from your development database when you run "rake".
# Do not set this db to the same as development or production.
test:
  <<: *default
  database: portus_test

production:
  <<: *default
  <% if ENV["PORTUS_PRODUCTION_HOST"] %>
  host:     <%= ENV["PORTUS_PRODUCTION_HOST"] %>
  <% end %>
  <% if ENV["PORTUS_PRODUCTION_USERNAME"] %>
  username: <%= ENV["PORTUS_PRODUCTION_USERNAME"] %>
  <% end %>
  <% if ENV["PORTUS_PRODUCTION_PASSWORD"] %>
  password: <%= ENV["PORTUS_PRODUCTION_PASSWORD"] %>
  <% end %>
  <% if ENV["PORTUS_PRODUCTION_DATABASE"] %>
  database: <%= ENV["PORTUS_PRODUCTION_DATABASE"] %>
  <% end %>

EOL

Podemos ya definir el frontal Nginx. Creamos la configuración  nginx para el vhost con Portus, que es bastante sencilla:

cat << EOL > /opt/nginx/portus.irontec.com.conf
server {
    listen       443 ssl;
    server_name  portus.irontec.com;
    ssl_prefer_server_ciphers on;
    ssl_session_cache shared:SSL:20m;
    ssl_session_timeout 180m;
    ssl_protocols TLSv1 TLSv1.1 TLSv1.2;
    ssl_ciphers EECDH+CHACHA20:EECDH+AES128:RSA+AES128:EECDH+AES256:RSA+AES256:EECDH+3DES:RSA+3DES:!MD5;

    ssl_certificate     /certs/wildcard.crt;
    ssl_certificate_key /certs/wildcard.key;

    location / {
        proxy_pass http://web:3000/;
    }

    error_page   500 502 503 504  /50x.html;
    location = /50x.html {
        root   /usr/share/nginx/html;
    }

}
EOL

Creamos la conf nginx para el vhost con el Registry. Esta tiene algún ajuste más fino por la naturaleza de la comunicación que vamos a tener con el registry:

cat << EOL > /opt/nginx/registry.irontec.com.conf
server {
    listen       443 ssl;
    server_name  registry.irontec.com;
    ssl_prefer_server_ciphers on;
    ssl_session_cache shared:SSL:20m;
    ssl_session_timeout 180m;
    ssl_protocols TLSv1 TLSv1.1 TLSv1.2;
    ssl_ciphers EECDH+CHACHA20:EECDH+AES128:RSA+AES128:EECDH+AES256:RSA+AES256:EECDH+3DES:RSA+3DES:!MD5;

    ssl_certificate     /certs/wildcard.crt;
    ssl_certificate_key /certs/wildcard.key;
    ##
    # Docker-specific stuff.

    proxy_set_header Host \$http_host;   # required for Docker client sake
    proxy_set_header X-Forwarded-Host \$http_host;
    proxy_set_header X-Real-IP \$remote_addr;
    proxy_set_header X-Forwarded-For \$proxy_add_x_forwarded_for;
    proxy_set_header X-Scheme \$scheme;

    # disable any limits to avoid HTTP 413 for large image uploads
    client_max_body_size 0;

    # required to avoid HTTP 411: see Issue #1486
    # (https://github.com/docker/docker/issues/1486)
    chunked_transfer_encoding on;


    location / {
        proxy_pass http://registry:5000/;
        proxy_read_timeout 900;
        proxy_set_header Host \$host;
        proxy_set_header X-Real-IP \$remote_addr;
        proxy_set_header X-Forwarded-For \$proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto \$scheme;
        proxy_buffering on;
        auth_basic off;
    }

    error_page   500 502 503 504  /50x.html;
    location = /50x.html {
        root   /usr/share/nginx/html;
    }

}
EOL

Ahora que tenemos la configuración individual de los servicios ya podemos crear un fichero compose donde definimos nuestra arquitectura:

cat << EOL > /opt/rcdm/docker-compose.yml
version: '2'
## IMPORTANTE: antes de lanzar docker-compose hay que definir las siguientes variables:
##	REGISTRY_FQDN
##	PORTUS_FQDN
##	DB_PASSWORD
## Por ejemplo: REGISTRY_FQDN="registry.midominio.com" PORTUS_FQDN="portus.midominio.com" DB_PASSWORD="misecreto" docker-compose up -d

services:
  nginx:
    image: nginx:alpine
    restart: always
    ports:
      - 443:443
    volumes:
      - /opt/nginx:/etc/nginx/conf.d:ro
      - /opt/certs:/certs:ro
    links:
       - registry
       - web
  web:
    image: irontec/portusweb:devel
    restart: always
    command: puma -b tcp://0.0.0.0:3000 -w 3
    environment:
      - PORTUS_MACHINE_FQDN_VALUE=\$REGISTRY_FQDN
      - PORTUS_FQDN_VALUE=\$PORTUS_FQDN
      - PORTUS_DB_HOST=db
      - PORTUS_DB_PASSWORD=\$DB_PASSWORD
    volumes:
      - /opt/portus/database.yml:/portus/config/database.yml:ro
      - /opt/portus/config.yml:/portus/config/config.yml:ro
    ports:
      - 3000:3000
    links:
      - db
    extra_hosts:
      - "registry.irontec.com:\$DOCKER_IP"

  crono:
    image: irontec/portusweb:devel
    restart: always
    entrypoint: bash ./bin/crono
    environment:
      - PORTUS_MACHINE_FQDN_VALUE=\$REGISTRY_FQDN
      - PORTUS_DB_HOST=db
      - PORTUS_DB_PASSWORD=\$DB_PASSWORD
    links:
      - db
  registry:
    image: library/registry:2.3.1
    restart: always
    environment:
      - REGISTRY_AUTH_TOKEN_REALM=https://\$PORTUS_FQDN/v2/token
      - REGISTRY_AUTH_TOKEN_SERVICE=\$REGISTRY_FQDN
      - REGISTRY_AUTH_TOKEN_ISSUER=\$REGISTRY_FQDN
    volumes:
      - /opt/registry_data:/registry_data
      - /opt/registry:/etc/docker/registry:ro

    ports:
      - 5000:5000
      - 5001:5001 # required to access debug service
    links:
      - web
    extra_hosts:
      - "portus.irontec.com:\$DOCKER_IP"
  db:
    image: library/mariadb:10.0.23
    restart: always
    environment:
      MYSQL_ROOT_PASSWORD: \$DB_PASSWORD
    volumes:
      - /opt/mysql:/var/lib/mysql
    ports:
      - 3306:3306
EOL

Ya estamos listos para poder levantar nuestra arquitectura. ¡Vamos a ello! Como explica la cabecera del docker-compose.yml es necesario primero definir una serie de variables que haremos mediante un export:

cd /opt/rcdm
#Definimos las variables necesarias:
export REGISTRY_FQDN="registry.irontec.com" PORTUS_FQDN="portus.irontec.com" DB_PASSWORD="secreto" export DOCKER_IP="$(ip -one -4 a l dev eth1 | awk '{ print $4}' | awk -F '/' '{ print $1 }')" 
#Hacemos un pull primero para que se baje todas las imágenes, tardará un rato
docker-compose pull
#Ya podemos levantar
docker-compose up -d

Vamos a comprobar si todo ha arrancado bien:

#Miramos si están UP, deberíamos ver 5 contenedores
docker ps

CONTAINER ID        IMAGE                     COMMAND                  CREATED             STATUS              PORTS                              NAMES
9d3f21f049ab        nginx:alpine              "nginx -g 'daemon ..."   11 seconds ago      Up 10 seconds       80/tcp, 0.0.0.0:443->443/tcp       rcdm_nginx_1
8687f1e215aa        library/registry:2.3.1    "/bin/registry /et..."   11 seconds ago      Up 10 seconds       0.0.0.0:5000-5001->5000-5001/tcp   rcdm_registry_1
b2678f0c34a6        irontec/portusweb:devel   "bash ./bin/crono"       12 seconds ago      Up 11 seconds       3000/tcp                           rcdm_crono_1
98cf920b50c5        irontec/portusweb:devel   "puma -b tcp://0.0..."   12 seconds ago      Up 11 seconds       0.0.0.0:3000->3000/tcp             rcdm_web_1
69aee69346ca        library/mariadb:10.0.23   "/docker-entrypoin..."   13 seconds ago      Up 12 seconds       0.0.0.0:3306->3306/tcp             rcdm_db_1

#Miramos logs si fuese necesario
docker-compose logs

Deberíamos tener 5 contenedores en status UP. Si vemos que alguno no arranca o se reinicia tendremos que hacer uso del comando docker logs rcdm_XXXX_1 para ver cuál es el motivo y solucionarlo.

Ya solo nos falta inicializar la base de datos. Para que Portus pueda empezar a funcionar es necesario crear la estructura de base de datos y alimentarla con unos pocos datos iniciales. Para esto, al ser una aplicación ruby, podemos lanzar un par de comandos rake que harán todo el trabajo. Para lanzarnos vamos a usar la definición de nuestro servicio portus (web), decirle a docker-compose que cree un docker (run) partiendo de esa configuración, ejecute el comando (rake ….) y al terminar elimine el contenedor creado  (–rm).

#inicializamos la BBDD
docker-compose run --rm web rake db:migrate:reset
docker-compose run --rm web rake db:seed

Esta es una manera habitual de lanzar comandos o tareas en entornos de contenedores, donde fuera del contenedor no disponemos de las herramientas necesarias. Otro ejemplo suele ser el uso del comando cliente de mysql o mysqldump y situaciones similares.

Ahora sí deberíamos tener Portus UP & RUNNING. Nos salimos de la VM de docker engine (o usamos otra terminal) y añadimos a nuestro /etc/hosts la IP del docker host para que resuelva los 2 FQDNs y abrimos la web de portus.

echo "$(docker-machine ip registry) registry.irontec.com portus.irontec.com" | sudo tee -a /etc/hosts
x-www-browser https://portus.irontec.com/

A jugar

Ya podemos empezar a jugar con nuestro registry/portus.  Lo primero es entrar a la web de Portus crear un usuario (este primer usuario será admin, esto se ajusta en la conf de Portus creada hace unos momentos).

Portus - 1

Una vez creado el usuario lo siguiente que nos pide Portus es que demos de alta el Registry. Importante: si hemos usado un cert autofirmado deberemos usar como hostname registry:5000 y deshabilitar el uso de SSL.

Portus -2

Y ahora ya podemos ver los namespace. Por defecto al crear un usuario se le asocia un namespace que vemos que está vacío.

Portus - 3

Una vez tenemos Portus configurado podemos ahora conectar nuestro engine al registry

docker login registry.irontec.com

… Y empezar por subir nuestra primera imagen:

docker pull alpine
docker tag alpine registry.irontec.com/admin/alpine
docker push registry.irontec.com/admin/alpine
docker image rm registry.irontec.com/admin/alpine alpine
docker run --rm -it registry.irontec.com/admin/alpine echo "Hola mundo"

Una vez pusheada podemos ir a Portus y ver de manera gráfica cómo se ha subido la imagen:

Portus - 4

A partir de aquí podemos ir jugando. Probar los namespaces, crear más usuarios, crear grupos, asignar permisos en los namespaces a los grupos, tener namespaces públicos, webhooks, etc.

Disclaimer

Todo este post tiene solo un fin didáctico, por lo que la instalación que hemos hecho no está pensada para producción. Los que no hayáis hecho un CUT&PASTE furioso os habréis dado cuenta de que en el docker-compose se señala una imagen docker de propia cosecha irontec/portusweb:devel . Esta imagen, como claramente indica el tag, está pensada para desarrollo, generada directamente del git de portus y lanzada sin opciones de entorno de producción. También habréis notado que hay un certificado publicado directamente en el post y que es un cert autofirmado generado por los developers de Portus.

Otras opciones

Queda también como examen para el lector el explorar otras opciones de montar un «Registry Como Dios Manda ™». La más evidente para los usuarios de gitlab es hacer uso de su módulo registry. Para los usuarios de solución cloud cada proveedor ofrece un registry, que aunque no cumpla todas nuestras leyes para ser un «Registry Como Dios Manda ™», puede ser una buena solución. Amazon tiene el ECR y Google su Container Registry. También hay más UI-s para Registry, con diferentes grados de madurez. Finalmente Docker Inc, dentro de su solución «on premise» Docker Data Center, ofrece Docker Trusted Registry pero entraríamos ya en la necesidad de licenciamiento.

Fin

Y eso es todo por aquí amigos. Para ser mi primer post en el blog me ha salido un buen ladrillo sobre algo no muy sexy 🙂 Para próximas entradas espero poder traeros temas más interesante como clusterring, gestión de secretos y otras cosas bellas de Docker.



¿Te gusta este post? Es solo un ejemplo de cómo podemos ayudar a tu empresa...

1 Comentario

¿Por qué no comentas tú también?

Queremos tu opinión :)