Saltar al contenido

Dockerizando un API

Si quieres que tu modelo de predicción sea accesible para toda tu organización, crear un API puede ser la solución. En este blogpost te explicaré cómo dockerizar un API y distinguir los comandos para construir una imagen, en contraste con los usados al correr e instanciar un contenedor. De esta manera, entenderás el comportamiento del contenedor en un caso puntual donde se requiere el uso de un proxy. Aprenderás los conceptos clave para crear una imágen de Docker, cómo usar Dockerfile y cómo ejecutar los comandos de Docker. Al finalizar, tendrás una comprensión completa de cómo dockerizar un API y cómo configurar tu contenedor para interactuar con un proxy. ¡Acompáñame en esta aventura!

Acompañamos en iniciativas y proyectos de ciencia de datos, ingeniería e infraestructura. Visita nuestra página ixpantia y contáctanos.

Introducción

Aunque el enfoque del artículo no está en profundizar sobre los microservicios, es importante mencionarlo ya que el uso de contenedores se ha vuelto muy popular en este tipo de arquitecturas.

Además, veremos cómo crear un Dockerfile y cómo utilizarlo para construir y ejecutar un contenedor que aloje nuestro API. También abordaremos temas como la diferencia entre imágenes y contenedores, y cómo estas pueden impactar en la forma en que implementamos nuestro API, incluyendo la necesidad de configurar un proxy para conectarnos a servicios externos.

Microservicios

La idea de los microservicios no es nueva, ha existido por casi dos décadas. Es una arquitectura de software que se enfoca en dividir una aplicación en pequeños servicios independientes que se comunican entre sí a través de API’s. Cada microservicio puede ser desarrollado, implementado y escalado de forma independiente, lo que facilita la evolución y mantenimiento de la aplicación.

La adopción de contenedores ha permitido que esta arquitectura se vuelva más popular en los últimos años. Los contenedores son una tecnología que permite empaquetar el código junto a todas sus dependencias mínimas para que el servicio o aplicación se ejecute de forma rápida y fiable en cualquier entorno.

Docker es una de las tecnologías de contenedores más populares actualmente, y es comúnmente asociada con los microservicios debido a su capacidad para empaquetar y desplegar aplicaciones en contenedores de forma rápida y eficiente.

Es importante destacar que Docker no es la única tecnología de contenedores y que los microservicios no dependen exclusivamente de los contenedores para ser implementados. Sin embargo, la combinación de microservicios y contenedores ha demostrado ser una forma efectiva de construir y escalar aplicaciones modernas.

Antes de profundizar en cómo Docker puede ser utilizado para implementar API’s, es importante mencionar que este blogpost no se centra en los microservicios en sí mismos. Si bien los microservicios están estrechamente relacionados con los contenedores y, por lo tanto, con Docker, son un tema complejo y amplio que merece su propio espacio para un enfoque dedicado.

Si desea aprender más sobre microservicios, se recomienda buscar recursos adicionales. Martin Fowler y su blog son una buena referencia para profundizar en este tema.

Docker 🐳

Ahora bien, en docker hay 3 conceptos fundamentales que uno debe tener claro:

  • Imágen
  • Contenedor
  • Dockerfile

Qué es una imágen?

Primero, explicaré el concepto de imágen con una definición de libro simple y luego con una analogía para que sea más sencillo entenderlo (al menos eso intento).

Al iniciar en este mundo de Docker, me era muy difícil distinguir entre una imágen y un contenedor.

Según la documentación oficial, una imágen es una plantilla sólo de lectura con instrucciones para crear un contenedor de Docker, siendo así la base de los contenedores.

Es una colección ordenada de cambios en un filesystem raíz y sus correspondientes parámetros de ejecución para su uso dentro del entorno de ejecución de un contenedor.

Una imágen se construye con un simple comando docker:

docker build

Y la pregunta surge, ¿dónde exactamente escribo estas instrucciones para poder construir la imágen que deseo?. Pues bien, ahí entra otro concepto importante, el Dockerfile.

(Si aún es difícil entender los conceptos 🫠, no desesperes que te prometo que después de esto todo tendrá más sentido).

Dockerfile

Un Dockerfile no es más que un archivo de texto que contiene los comandos que un usuario puede llamar en la línea de comando para ensamblar una imágen. Docker puede crear imágenes automáticamente al leer las instrucciones del Dockerfile.

Dentro de este Dockerfile hay comandos bastante comunes que veremos ahora a modo de ejemplo muy sencillo para entender el concepto fundamental (no se ahondará en cada uno de los comandos a detalle porque no es el propósito del blogpost pero sí es un buen inicio para entender lo que se viene, así que atento).

El siguiente es un ejemplo de Dockerfile:

# Indicamos la imagen base sobre la que vamos a construir nuestro contenedor
FROM alpine:latest

# Ejecutamos algunos comandos para instalar una aplicación en el contenedor
RUN apk add --update nginx && rm -rf /var/cache/apk/*

# Establecemos el puerto interno del contenedor en el puerto 80 y lo exponemos 
para que sea accesible desde fuera del contenedor
EXPOSE 80

# Indicamos el comando que se ejecutará cuando el contenedor arranca
CMD ["nginx", "-g", "daemon off;"]

Ajá! ¿Te diste cuenta del comentario de la última línea? Hay algunos comandos como el FROM, RUN y EXPOSE en este caso, que se utilizan para construir una imagen, es decir, se ejecutan con el comando docker build y NO se vuelven a ejecutar.

Y esto es importante entenderlo porque los desenlaces inesperados ocurren tratando de implementar el proxy para el manejo de las solicitudes.

Si uno no tiene un background técnico, este concepto es un poco difícil de digerir.

En términos no-técnicos y como yo logré entenderlo (shoutout a mi jefe y experto en Docker Frans van Dunné [ixpantia] quien hizo esta genial analogía que creo sirve mucho), podemos ver a la imágen como la comida congelada lista para cocinar y comer.

Pongamos un ejemplo, los nuggets. Tenemos pasos para elaborarlo. Primero, cortamos el pollo en trozos medianos, le agregamos condimentos, lo pasamos por harina, huevo y pan molido. Finalmente, le damos una precocción y está listo para ir al congelador y poder utilizarlo más adelante.

Cuando sacamos los nuggets del congelador, no podemos pasarlos nuevamente por el huevo, ese paso ya se realizó y es algo que no se repite. Los únicos pasos que quedan son freirlos (u hornearlo si estamos a dieta) y emplatarlos.

Entonces, importante entender que la imágen es una serie de pasos que una vez realizados, lo que se obtuvo como resultado de ese proceso (los nuggets), pasa a la congeladora🥶, no se puede cambiar nada, ni al correr el contenedor. Sería como meter un nugget de pollo 🐤 congelado a la freidora, y esperar que salga un dedo de pescado 🐟 (ilógico, ¿verdad?).

Contenedor

Una vez que se ha construido la imagen usando el Dockerfile, se puede ejecutar esa imagen como un contenedor usando el comando docker run.

El contenedor es una instancia en ejecución de esa imagen.

En el caso de nuestro ejemplo, cuando ejecutamos el comando docker run, se ejecutará la última línea del Dockerfile, es decir, CMD. Los pasos anteriores (como la instalación de las dependencias) ya se han completado durante la construcción de la imagen.

Si empleamos la analogía de los nuggets, al ejecutar el comando docker run, se ejecutan sólo los pasos restantes, freir y emplatar. La instancia en ejecución (el contenedor) sería los nuggets de pollo listos para comerse.

Proxy

Todo muy bien con Docker por ahora, espero. volviendo a lo que nos compete, APIs. Es normal que al construir un API requiramos de un proxy.

Muchos nos habremos topado en algún momento de nuestras vidas con este ícono o error: Error clásico con proxy. Pero, ¿qué es en sí un proxy? Un proxy es un servidor que sirve de intermediario entre un cliente y otro servidor. Entonces, es una capa intermedia que permite que el cliente se comunique con el servidor a través del mismo sin tener que conectarse directamente. De acuerdo al uso que le queramos dar, el proxy puede configurarse para realizar diversas funciones.

En nuestro caso lo podríamos usar para enrutar las solicitudes que llegan a un puerto local(5434 por poner un ejemplo) hacia otro puerto de una base de datos remota. Entonces, el proxy se encuentra escuchando activamente el puerto local y si llega alguna solicitud se redirige a la base de datos remota.

El emplear un proxy da muchos beneficios, entre algunos se encuentra el hecho de agregar una capa adicional de seguridad que reduce vulnerabilidades de acceso a la base de datos, mejora escalabilidad, rendimiento y control de acceso (por mencionar unos cuantos).

Si deseas profundizar más en este tema, existen diversas fuentes y recursos disponibles en línea que pueden ser de gran ayuda como el que ofrece digitalocean.

El objetivo de este blogpost no es enseñar cómo implementar un proxy, sino más bien explicar cómo funciona la creación de un contenedor con Docker que pueda manejar casos puntuales como la necesidad de utilizar un proxy.

Una vez que todo corre local y hemos hecho las pruebas respectivas con respecto a permisos y demás consideraciones a tener en cuenta (independientemente de cómo lo hayamos implementado), estamos listos para el despliegue y para ello decidimos usar docker (para eso llegaste a este post, ¿cierto?).

Es ahora el momento de recordar y poner a prueba lo que vimos recientemente sobre Dockerfile.

Recordarás la ahora famosa congeladora, ¿verdad? 🧊. Ya sabes entonces los comandos básicos y cuáles son para el build de la imágen y cuáles son para el run del contenedor y esto es importante ya que es el punto de fallo y de dolor de cabeza muchas veces por no entender este concepto (me pasó a mí).

Te hago una pregunta sencilla, los comandos para instalar el proxy y para ejecutarlo se especifican en el dockerfile con RUN o con CMD o ¿alguna otra opción?

Pongamos un ejemplo puntual que manejaremos a partir de ahora para visualizarlo más claro.

En este caso, se tiene una instancia de PostgreSQL en GCP y se decide por ende emplear el cloud-sql-proxy por conveniencia para lograr la conexión a la base de datos.

Siguiendo los pasos de la documentación oficial, tenemos los siguientes comandos:

  • Para instalarlo:
curl -o cloud-sql-proxy https://storage.googleapis.com/cloud-sql-connectors/cloud-sql-proxy/v2.0.0/cloud-sql-proxy.linux.amd64

chmod +x cloud-sql-proxy
  • Para ejecutarlo en segundo plano:
./cloud-sql-proxy --port 5434 --credentials-file /secrets/llave.json INSTANCE_CONNECTION_NAME &

Siendo este el caso, ¿cómo se traducen estos comandos en el dockerfile? 🤔 Pensemos en la congeladora, si los 3 comandos los especificamos con un RUN, se ejecutarían sólo al construir la imágen. Estarían congelados y por ende al levantar el contenedor y poner a prueba nuestro API no podríamos enrutar la solicitud a través del proxy porque no lo encontraríamos en el contexto del contenedor.

Entonces, descartamos el poner los 3 comandos en la congeladora, sin embargo, sí debemos instalar el cloud-sql-proxy al construir la imágen ya que se debe instalar en el sistema operativo base del contenedor, es decir, depende de él.

Y ahora, ¿qué hacemos para ejecutarlo?

Si pensaste en los comandos que se emplean al correr un contenedor, estamos por el camino correcto y estás entendiendo el concepto. Si no pensaste en ello, no te preocupes, ahora explicaré a detalle, pero primero debemos distinguir entre 2 comandos importantes (prometo que serán los últimos para recordar🙏🏼).

CMD

ENTRYPOINT

Ambos se ejecutan al levantar el contenedor pero hay una diferencia importante que debemos resaltar.

Por un lado CMD especifica el comando que se ejecutará al iniciar el contenedor, mientras que ENTRYPOINT especifica el comando principal que se ejecutará en el contenedor.

CMD se puede emplear de 3 formas: - Para ejecutar un comando (forma preferida) - Para proporcionar parámetros adicionales como default a ENTRYPOINT - En forma shell.

Por otro lado, ENTRYPOINT se especifica en 2 formas: - La forma ejecutable (forma preferida) - La forma shell

Normalmente, la forma ejecutable se emplea para correr un script que queremos que corra al iniciar el contenedor.

Aclaración sólo puede haber 1 CMD por dockerfile y esto es importante aclarar ya que en nuestro API no sólo debemos correr el proxy al levantar el contenedor, no nos olvidemos que debemos también iniciar el servidor del API. Entonces, tenemos 2 comandos que ejecutar al levantar el contenedor.

¿Cómo solucionar esto? Pues hay 3 posibles maneras: - Emplear tanto CMD como ENTRYPOINT. - Emplear sólo CMD. - Emplear sólo ENTRYPOINT.

En este caso, mostraremos un ejemplo combinando ambos comandos. Primero, armamos un shell script (start.sh) que se ve de la siguiente manera:

#! /bin/bash
./cloud-sql-proxy --port 5434 --credentials-file secrets/llave.json INSTANCE_CONNECTION_NAME &

# Inicia el API
exec "$@"

y así quedarían las últimas líneas de nuestro Dockerfile:

# Instala cloud SQL proxy
RUN curl -o cloud-sql-proxy https://storage.googleapis.com/cloud-sql-connectors/cloud-sql-proxy/v2.0.0/cloud-sql-proxy.linux.amd64
RUN chmod +x cloud-sql-proxy
RUN chmod +x start.sh

# Inicia SQLproxy para este contenedor:
ENTRYPOINT ["./start.sh"]

# Inicia el api
CMD ["Rscript", "plumber.R"]

Instalamos el proxy y lo hacemos ejecutable (tanto el proxy como el shell script) al construir la imágen. Finalmente, corremos al arrancar el contenedor el shell script por medio del comando ENTRYPOINT y empleamos el CMD en su forma para ejecutar un comando e iniciar el servidor del API.

Si llegaste a este punto, ¡felicidades!. Espero que hayas aprendido cómo crear un contenedor de Docker para una API, entender la diferencia entre imagen y contenedor, y cómo manejar la configuración de un proxy.

Ya no es tan aterrador entenderlo verdad? Espero ahora no le temas y anímate a dockerizar tu API 🙂.


Este blog lo mantiene el equipo de ixpantia y la comunidad de gente interesada en datos de la cual estamos contentos de formar parte ¿Tienes una idea para publicar algo aquí? ¡Escríbenos! Estamos siempre interesados en material e ideas nuevas. © 2019-2022 ixpantia