Fecha: 2021-02-25 Tiempo de lectura: 6 minutos Categoría: Sistemas Tags: docker / swarm / registro
Muchas veces nos encontramos que es más fácil y barato contratar un servicio de registro Docker en el cloud. Así nos olvidamos del hosting, certificados SSL, backups y demás tareas de administración. Otras veces preferimos recortar en costes y hacer un registro local en nuestra propia infraestructura, como ya hicimos aquí y aquí.
Esta última opción nos plantea algunas opciones:
Aunque si nos fijamos, la configuración por defecto de Docker ya nos dan parte del trabajo hecho…
gerard@atlantis:~$ docker info
...
Insecure Registries:
127.0.0.0/8
...
gerard@atlantis:~$
Eso significa que cualquier registro local, con IP 127.x.x.x va a ser de confianza para docker; va hacer las peticiones por HTTPS (confiando en el certificado) y si no se trata de HTTPS, las hará por HTTP. Eso vale para registros locales, escuchando en localhost, que viene a ser algo así como “la máquina en la que estamos”.
En un entorno de swarm no nos sirve demasiado, porque cada nodo consultaría su registro local. Eso supone un reto adicional, o eso creía hasta que me acordé de este pequeño trozo de la documentación (aunque el texto describe un ejemplo usando el puerto 8080):
When you access port 8080 on any node, Docker routes your request to an active container.
Es decir, que si publicamos nuestro registro en el puerto 5000, todos los nodos van a poder acceder al registro en el puerto 5000 de cualquier nodo, incluso de sí mismo.
Como confiamos en las direcciones locales, podemos usar certificados autofirmados o incluso HTTP plano; solo debemos tener la precaución de utilizar la dirección local al utilizar el registro (por ejemplo, 127.0.0.1). Usar HTTP o HTTPS, autenticación o no, es una decisión que tendremos que tomar de acuerdo a la facilidad de acceso a nuestro entorno por terceros usuarios (de nuestra empresa o de fuera).
Para no complicar el artículo utilizamos un swarm pequeño, a modo de ejemplo. Partimos de un docker swarm simple, de dos nodos; puesto que este es un ejemplo rápido y no productivo, de momento nos vale.
gerard@server01:~$ docker node ls
ID HOSTNAME STATUS AVAILABILITY MANAGER STATUS ENGINE VERSION
swyr79nl5vbe70o0bigx4s054 * server01 Ready Active Leader 20.10.3
siaz5eahkoznldau8208k87op server02 Ready Active 20.10.3
gerard@server01:~$
Supongamos que tenemos un entorno swarm totalmente cerrado, en el que solo tienen acceso los administradores más confiables de nuestro equipo. Por ello decidimos que no necesitamos HTTP ni autenticación, lo que hace más breve y conciso el artículo.
El stack no guarda ninguna complicación; solo hay que tener en cuenta que hay que publicar el puerto y que, como usamos volúmenes locales, el contenedor se debe desplegar siempre en el mismo nodo.
gerard@server01:~/stacks/tools$ cat stack.yml
version: '3'
services:
registry:
image: registry:2.7
volumes:
- registry_data:/var/lib/registry
ports:
- "5000:5000"
deploy:
placement:
constraints:
- "node.hostname==server02"
volumes:
registry_data:
gerard@server01:~/stacks/tools$
Vamos a desplegar nuestro servicio de registro con el típico script de deploy:
gerard@server01:~/stacks/tools$ cat deploy.sh
#!/bin/bash
docker stack deploy -c stack.yml tools
gerard@server01:~/stacks/tools$
gerard@server01:~/stacks/tools$ ./deploy.sh
Creating network tools_default
Creating service tools_registry
gerard@server01:~/stacks/tools$
Ahora esperamos a que levante el servicio de registro…
gerard@server01:~/stacks/tools$ docker stack ps tools
ID NAME IMAGE NODE DESIRED STATE CURRENT STATE ERROR PORTS
oh7abrq1f1xa tools_registry.1 registry:2.7 server02 Running Running 11 seconds ago
gerard@server01:~/stacks/tools$
Y verificamos que ambos nodos (server01 y server02) acceden al registro en su dirección local, aunque de momento el registro está vacío y no alberga ninguna imagen:
gerard@server01:~$ curl http://127.0.0.1:5000/v2/_catalog
{"repositories":[]}
gerard@server01:~$
gerard@server02:~$ curl http://127.0.0.1:5000/v2/_catalog
{"repositories":[]}
gerard@server02:~$
Supongamos que tenemos nuestra aplicación, lista para crear la imagen y subirla. La aplicación en sí misma es ahora irrelevante; cualquiera nos valdría. Para este caso concreto hemos preparado un contexto para la aplicación de ejemplo escrita con el framework Flask.
gerard@server01:~/build/helloworld$ cat app.py
from flask import Flask
app = Flask(__name__)
@app.route('/')
def hello_world():
return 'Hello, World!\n'
gerard@server01:~/build/helloworld$
gerard@server01:~/build/helloworld$ cat requirements.txt
gunicorn==20.0.4
Flask==1.1.2
gerard@server01:~/build/helloworld$
gerard@server01:~/build/helloworld$ cat Dockerfile
FROM python:3.9-alpine
COPY app.py requirements.txt /app/
RUN pip install --no-cache-dir -r /app/requirements.txt
CMD ["gunicorn", "--bind=0.0.0.0:8080", "--chdir=/app", "app:app"]
gerard@server01:~/build/helloworld$
Construimos la imagen con los comandos habituales, dándole un nombre y un tag,
precedidos por la dirección de nuestro registro, para que un docker push sepa
en qué registro subirlo en un futuro cercano.
gerard@server01:~/build/helloworld$ docker build -t 127.0.0.1:5000/helloworld:v1 .
...
Successfully tagged 127.0.0.1:5000/helloworld:v1
gerard@server01:~/build/helloworld$
Podemos verificar que disponemos de nuestra imagen como imagen local:
gerard@server01:~/build/helloworld$ docker images
REPOSITORY TAG IMAGE ID CREATED SIZE
127.0.0.1:5000/helloworld v1 43988269a061 About a minute ago 55.3MB
python 3.9-alpine 770dd9c7c0e8 3 days ago 44.7MB
gerard@server01:~/build/helloworld$
Y la subimos al registro con el típico docker push. En este caso no hay que
hacer login porque hemos decidido que no hace falta autenticación, al tratarse
de un entorno cerrado y aislado de curiosos.
gerard@server01:~/build/helloworld$ docker push 127.0.0.1:5000/helloworld:v1
The push refers to repository [127.0.0.1:5000/helloworld]
...
gerard@server01:~/build/helloworld$
Finalmente verificamos que ambos nodos ven la misma imagen en el registro, a pesar de que la subida se hizo desde el primer nodo; se trata pues del mismo registro.
gerard@server01:~$ curl http://127.0.0.1:5000/v2/_catalog
{"repositories":["helloworld"]}
gerard@server01:~$
gerard@server02:~$ curl http://127.0.0.1:5000/v2/_catalog
{"repositories":["helloworld"]}
gerard@server02:~$
En un futuro podemos querer desplegar un servicio basado en la imagen que hemos
creado en el paso anterior. Ello no entraña ninguna dificultad y basta con indicar
la procedencia de la imagen: 127.0.0.1:5000/helloworld:v1. Cada nodo que lo
necesite descargará la imagen de la dirección local que, como ya hemos visto, se
trata del servicio de registro alojado en server02.
Podemos hacer algo como lo siguiente:
gerard@server01:~/stacks/apps$ cat stack.yml
version: '3'
services:
helloworld:
image: 127.0.0.1:5000/helloworld:v1
ports:
- "8080:8080"
deploy:
replicas: 4
gerard@server01:~/stacks/apps$
gerard@server01:~/stacks/apps$ cat deploy.sh
#!/bin/bash
docker stack deploy -c stack.yml apps
gerard@server01:~/stacks/apps$
gerard@server01:~/stacks/apps$ ./deploy.sh
Creating network apps_default
Creating service apps_helloworld
gerard@server01:~/stacks/apps$
Si esperamos un poco veremos que el nodo leader (uno de los managers), va a repartir a los diferentes nodos las tareas de desplegar los contenedores necesarios según la especificación que le hemos indicado (son 4 en este ejemplo, 2 en cada nodo por casualidad). Cada nodo que lo necesite se descargará la imagen para poder levantar el contenedor; en este caso, server02 la descargará, pero server01 no lo hará, puesto que ya la tenía tras hacer el build.
gerard@server01:~$ docker stack ps apps
ID NAME IMAGE NODE DESIRED STATE CURRENT STATE ERROR PORTS
i0fm4sm559o6 apps_helloworld.1 127.0.0.1:5000/helloworld:v1 server02 Running Running 51 seconds ago
ai68kl2xwxwg apps_helloworld.2 127.0.0.1:5000/helloworld:v1 server01 Running Running 59 seconds ago
wx86w8bluhe5 apps_helloworld.3 127.0.0.1:5000/helloworld:v1 server02 Running Running 51 seconds ago
0huco1cl92t1 apps_helloworld.4 127.0.0.1:5000/helloworld:v1 server01 Running Running 59 seconds ago
gerard@server01:~$
Solo falta comprobar que el servicio funciona y nuestra aplicación se comporta como esperamos…
gerard@server01:~$ curl http://127.0.0.1:8080/
Hello, World!
gerard@server01:~$