Fecha: 2019-10-14 Tiempo de lectura: 9 minutos Categoría: Sistemas Tags: linux / entorno / docker / swarm / deployment / python / falcon / httpie
En los anteriores artículos de la serie vimos como montar un entorno entero basado en docker swarm; añadimos un par de servicios de infraestructura básica, como son el balanceador y un cluster de bases de datos. Eran pasos que se hacen una sola vez y raramente se modifican. Ahora toca provisionar aplicaciones, en un proceso que vamos a repetir frecuentemente.
Y es que ha llegado el momento de la verdad: nuestros desarrolladores han completado una release, y nos toca ponerla en funcionamiento. Para el caso, vamos a suponer que se trata de una API de gatitos, que nos permite (en su primera versión) proveer las operaciones más básicas de alta, baja, modificación y consulta.
NOTA: La aplicación usada es esta, que está hecha con python y falcon; con ella ilustro los ejemplos, aunque podéis escribir la vuestra propia en el lenguaje que más os apetezca.
Normalmente tendríamos un toolkit para descargar el código de algún repositorio, compilar lo que tocara, crear la imagen de docker y publicar en algún registro; para no complicar el asunto en exceso, voy a hacer estos pasos manualmente.
El tarball con el código, o el clon del repositorio no nos sirven demasiado;
queremos imagenes docker en un registro para que el cluster pueda hacer los
respectivos docker pull. Así pues tenemos que hacer un docker build y el
correspondiente docker push.
No tenemos un registro privado, pero es fácil de hacer, tanto su uso simple, como un uso profesional con autenticación y SSL. Por simplicidad, voy a utilizar Docker Hub, aunque será solo de forma temporal; luego eliminaré la imagen.
Empezamos con el build:
gerard@builder:~/kittenapi$ docker build -t sirrtea/kittenapi .
...
Successfully built 008e3cc7144a
Successfully tagged sirrtea/kittenapi:latest
gerard@builder:~/kittenapi$
Asumiendo que ya hemos creado el repositorio en Docker Hub, solo necesitamos hacer un push, para que la imagen esté disponible en un registro accesible por todas la partes que lo puedan necesitar.
gerard@builder:~/kittenapi$ docker login
Login with your Docker ID to push and pull images from Docker Hub. If you don't have a Docker ID, head over to https://hub.docker.com to create one.
Username: sirrtea
Password:
...
Login Succeeded
gerard@builder:~/kittenapi$
gerard@builder:~/kittenapi$ docker push sirrtea/kittenapi
The push refers to repository [docker.io/sirrtea/kittenapi]
...
latest: digest: sha256:4116e4398a0e2852b0dd2dad0b6d080af9711f107400eaefaf644edeb5f0bf7a size: 1365
gerard@builder:~/kittenapi$
Nuestra nueva aplicación necesita una nueva base de datos mongodb. Ya creamos el cluster en un artículo anterior, pero es necesario crearle el usuario porque activamos la autenticación y, sin autenticarse, no va a poder hacer nada.
Para ello necesitamos buscar el nodo primario del cluster, ya que la creación
de un usuario es una escritura, que solo se acepta en un primario. Nos conectamos
a un nodo cualquiera, nos autenticamos como admin y sacamos un rs.status();
con la salida es trivial saber cuál es el primario, al que nos vamos a conectar.
gerard@docker05:~$ docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
077a6f40b30e sirrtea/mongo:debian "/usr/bin/mongod --c…" About a minute ago Up About a minute mongo_mongo02.1.ralisf5jh5etw5yc0fx3q95wi
gerard@docker05:~$ docker exec -ti 077a6f40b30e mongo
MongoDB shell version v4.0.11
...
rs:SECONDARY> use admin
switched to db admin
rs:SECONDARY> db.auth("admin", "s3cr3t")
1
rs:SECONDARY> rs.status()
...
"members" : [
{
...
"name" : "mongo01:27017",
...
"stateStr" : "PRIMARY",
...
rs:SECONDARY>
Nos conectamos al primario, nos autenticamos y creamos un usuario para nuestra nueva aplicación, con el nombre de la base de datos, el usuario y la password que veamos conveniente. En mi caso, el nombre de la base de datos y el usuario coinciden; la contraseña ha sido autogenerada.
gerard@docker04:~$ docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
092ce50d0bf6 sirrtea/mongo:debian "/usr/bin/mongod --c…" 5 minutes ago Up 5 minutes mongo_mongo01.1.uy2lcliipqo7glhp5y5miymq8
gerard@docker04:~$ docker exec -ti 092ce50d0bf6 mongo admin
...
rs:PRIMARY> db.auth("admin", "s3cr3t")
1
rs:PRIMARY> db.createUser({user: "kittenapi", pwd: "LCg1SMxoWDg7gkuQ", roles: [{role: "readWrite", db: "kittenapi"}]})
Successfully added user: {
"user" : "kittenapi",
"roles" : [
{
"role" : "readWrite",
"db" : "kittenapi"
}
]
}
rs:PRIMARY>
Y con esto ya tenemos un usuario para trabajar con la base de datos de la aplicación.
Para mantener una estructura similar al resto de artículos, vamos a suponer que tenemos un clon del repositorio de ficheros de stack, con una carpeta para la base de datos, una carpeta para los balanceadores, y una carpeta para cada aplicación, como sigue:
gerard@docker01:~$ mkdir kittenapi
gerard@docker01:~$ cd kittenapi/
gerard@docker01:~/kittenapi$
La aplicación en sí misma no es muy compleja; no necesita secretos, ni configuraciones; solamente tenemos que aplicar las variables de entorno para configurar la aplicación, y las labels necesarias para que traefik lo reconozca. Haciendo memoria, este contenedor necesita estar conectada a las 2 redes creadas:
TRUCO: Se recomienda que no se ejecuten aplicaciones en los managers y que se utilicen solo para gestionar el cluster; en mi caso también deben alojar los contenedores de traefik. La forma para limitar el deploy es usando labels.
gerard@docker01:~/kittenapi$ docker node update --label-add usage=apps docker04
docker04
gerard@docker01:~/kittenapi$ docker node update --label-add usage=apps docker05
docker05
gerard@docker01:~/kittenapi$ docker node update --label-add usage=apps docker06
docker06
gerard@docker01:~/kittenapi$
De esta forma podemos escribir un fichero tipo compose de este estilo:
gerard@docker01:~/kittenapi$ cat kittenapi.yml
version: '3'
services:
kittenapi:
image: sirrtea/kittenapi
environment:
MONGODB_URI: mongodb://kittenapi:LCg1SMxoWDg7gkuQ@mongo01:27017,mongo02:27017,mongo03:27017/kittenapi?replicaSet=rs&authSource=admin
networks:
- frontend
- backend
deploy:
replicas: 2
labels:
traefik.http.routers.kittenapi.rule: Host(`kittenapi.example.com`)
traefik.http.services.kittenapi.loadbalancer.server.port: 8080
traefik.enable: "true"
placement:
constraints:
- node.labels.usage == apps
networks:
frontend:
external: true
backend:
external: true
gerard@docker01:~/kittenapi$
NOTA: Las labels sirven para la versión 2.0 de traefik, que es la usada en el entorno.
Y con esto ya podemos desplegar:
gerard@docker01:~/kittenapi$ docker stack deploy -c kittenapi.yml kittenapi
Creating service kittenapi_kittenapi
gerard@docker01:~/kittenapi$
Comprobamos que todo ha levantado como debe:
gerard@docker01:~/kittenapi$ docker stack ps kittenapi
ID NAME IMAGE NODE DESIRED STATE CURRENT STATE ERROR PORTS
tng8x8ly8ufq kittenapi_kittenapi.1 sirrtea/kittenapi:latest docker05 Running Running 8 seconds ago
owjvcv7dillq kittenapi_kittenapi.2 sirrtea/kittenapi:latest docker06 Running Running 8 seconds ago
gerard@docker01:~/kittenapi$
La comprobación es simple y se puede hacer de dos maneras:
NOTA: La máquina desktop accede al puerto 80 del gateway mediante un port forwarding en el puerto 8000 (es el host de VirtualBox). Esto es arbitrario y va a depender de vuestro setup de red.
TRUCO: Como no he puesto los registros DNS, vamos a probarlo con la cabecera HTTP
Host. También nos vamos a ayudar de una herramienta magnífica llamada HTTPie,
que nos simplifica el uso de la API y nos formatea la salida.
Partimos de una base de datos vacía, así que la API no nos devuelve ningún recurso
para la colección kittens (estamos probando el método GET); sin sorpresas:
gerard@desktop:~$ http get :8000/kittens Host:kittenapi.example.com
HTTP/1.1 200 OK
...
[]
gerard@desktop:~$
Probamos ahora el método POST, creando algunos gatitos para la colección:
gerard@desktop:~$ http post :8000/kittens Host:kittenapi.example.com name=Ginger
HTTP/1.1 201 Created
...
gerard@desktop:~$ http post :8000/kittens Host:kittenapi.example.com name=Snowball
HTTP/1.1 201 Created
...
gerard@desktop:~$ http post :8000/kittens Host:kittenapi.example.com name=Molly
HTTP/1.1 201 Created
...
gerard@desktop:~$ http get :8000/kittens Host:kittenapi.example.com
HTTP/1.1 200 OK
...
[
{
"id": 1,
"name": "Ginger"
},
{
"id": 2,
"name": "Snowball"
},
{
"id": 3,
"name": "Molly"
}
]
gerard@desktop:~$
Para probar los métodos más inusuales (PUT y DELETE), hacemos las peticiones de modificación y borrado, cambiando el nombre de un gatito y eliminando otro:
gerard@desktop:~$ http put :8000/kittens/2 Host:kittenapi.example.com name=Snowball2
HTTP/1.1 200 OK
...
gerard@desktop:~$ http delete :8000/kittens/1 Host:kittenapi.example.com
HTTP/1.1 200 OK
...
gerard@desktop:~$ http get :8000/kittens Host:kittenapi.example.com
HTTP/1.1 200 OK
...
[
{
"id": 2,
"name": "Snowball2"
},
{
"id": 3,
"name": "Molly"
}
]
gerard@desktop:~$
Y con esto nos damos por satisfechos.
Para añadir más servicios a nuestro cluster, es tan simple como repetir todo el artículo, a excepción de las labels, que ya estarían presentes. Estos servicios puede estar en la misma stack, o estar repartidos en varias stacks, de acuerdo con la organización lógica que queráis imponer en vuestro workflow.
Probar con la cabecera Host es útil, pero no es cómodo. Para llegar a esta API,
se necesita un registro DNS en condiciones, para que todos los usuarios de la API
puedan llegar cómodamente por nombre y les resuelva a una dirección IP a la que
puedan acceder (posiblemente pública).
Un cambio de nombre de dominio no solo depende de la entrada DNS; si lo cambiáis, recordad que traefik hace virtualhosts, tal como se define el las labels del servicio. No os olvidéis de cambiarlas.
Ninguna API debería servirse por HTTP plano. De hecho, ninguna web debería. Traefik soporta SSL a través de LetsEncrypt de forma nativa; si esto no es posible, delegad la capa SSL al balanceador o proxy externo.
Si nos quedamos cortos de recursos, el cluster es ampliable; solo necesitamos clonar de nuevo la máquina docker base, configurar el gateway para que le asigne una dirección IP fija y ejecutar el join token del swarm.
Recordad que swarm no va a recolocar ningún servicio que no sea estrictamente necesario; podemos escalar los servicios para forzar nuevas instancias, que irían a parar a los nodos más desocupados, escalando nuevamente a la baja para eliminar contenedores de donde sobren.
Hay que tener presente que todos los servicios tienen restricciones de placement;
para que el nuevo servidor sea candidato para traefik deberá ser un manager
(acordáos de keepalived), para mover algún nodo de la base de datos hará falta
otra label (los datos no se moverán, así que confiad en la replicación del
replica set), y para alojar aplicaciones hace falta la label usage=apps.
No se están haciendo backups de ninguna parte del sistema. Hay que identificar las partes stateful de cada servicio para saber que es lo que hay que tener respaldado, a saber:
Por suerte, los servicios de backup pueden ser contenedores que ejecutan scripts y sacan el resultado a un servicio externo; esto hace que todo quede en el swarm y no necesitemos modificar nuestros servidores. Esto hace el entorno 100% reconstruíble.