24 Apr 2022
Si tomaste contacto con Drogon y te estás preguntando cómo desplegar tu aplicación con contenedores, este post es para vos. Y para mi.
En un post anterior realizamos nuestra primera aplicación en Drogon. Ahora vamos a ver cómo desplegarla con contenedores, ya sea para usarla en nuestra propia PC o Kubernetes, Heroku, ECS o algún entorno en el que podamos sacar partido de Docker.
Este es el repositorio de la aplicación invoicer que usé como ejemplo. De momento no tiene funcionalidades. Apenas dispone de un endpoint /v1/invoices que retorna un json, y si lo invocamos sin path, o con /index.html, retorna un archivo html.
También les dejo el link al pull request con el código de este post.
El camino más obvio quizás es combinar la imagen oficial de Drogon con nuestro código, de modo que podamos compilar y ejecutar la aplicación usando la misma imagen.
En la raíz del repositorio de la aplicación voy a crear una carpeta llamada “docker” y dentro de esa carpeta, crearé otra carpeta con el nombre with-official-image. En esta última carpeta vamos a crear un archivo, llamado Dockerfile, con el siguiente contenido:
FROM drogonframework/drogon:latest
COPY . /code
WORKDIR /code
RUN rm -rf build \
&& mkdir build \
&& cd build \
&& cmake .. \
&& make
EXPOSE 80
ENTRYPOINT ["/code/docker/with-official-image/entrypoint.sh"]
La primer línea de este Dockerfile indica que usaremos como base la imagen oficial de Drogon.
La línea COPY . /code
copia nuestro código a la carpeta /code
del nuevo contenedor.
Podrías estar preguntándote por qué efectuamos el COPY desde el directorio actual, indicado por el punto, siendo que estamos parados en la carpeta docker/with-official-image. La razón de usar el punto, es que este Dockerfile, así como los otros que voy a añadir en este mismo post, estén pensados para ser ejecutados desde el directorio raíz del repositorio.
Con la línea WORKDIR /code
indicamos el directorio donde estaremos situados en el contenedor.
A continuación viene el comando RUN
, que contiene los comandos para eliminar y volver a crear la carpeta build, para asegurarnos que esté vacía al ejecutar los comandos cmake .. && make
.
La línea EXPOSE 80
indica que ese será el puerto que va a exponer el contenedor, que se corresponde con el puerto por default de la aplicación que configuramos en otro post.
Finalmente, definimos el ENTRYPOINT
, que es el comando que se ejecutará al iniciar el contenedor. El entrypoint de esta imagen es un pequeño archivo en bash que ejecuta la aplicación desde la carpeta build:
#! /bin/bash
cd build
./invoicer
Es importante que este archivo tenga permisos de ejecución.
Para validar nuestro Dockerfile en local, vamos a crear el siguiente docker-compose.yml en la misma capreta donde tenemos el Dockerfile:
version: "3.9"
services:
app:
build:
context: ../..
dockerfile: "${PWD}/Dockerfile"
ports:
- "8081:80"
Vale la pena notar el valor de context: Cuando se ejecute build con el Dockerfile, estaremos posicionados dos directorios arriba. Esto es, en el raíz.
Ejecutamos nuestro docker-compose con el comando docker-compose up -d --build
. El flag --build
es para reconstruir las imágenes antes de iniciar los contenedores.
Si todo ha ido bien, el comando docker-compose ps
debería mostrarnos nuestro contenedor, escuchando el puerto que hemos definido en docker-compose.yml. En mi caso es el 8081:
$ docker-compose ps
Name Command State Ports
---------------------------------------------------------------------------------------------------------
with-official-image_app_1 /code/docker/with-official ... Up 0.0.0.0:8081->80/tcp,:::8081->80/tcp
Si ponemos en nuestro navegador la url localhost:8081
deberíamos ver el html de nuestra aplicación y la interacción con la api rest.
En la sección anterior vimos cómo usar la imagen oficial de Drogon para construir y ejecutar un contenedor con nuestra aplicación. Si ejecutas docker image ls
, verás información sobre la imagen:
docker image ls --filter "reference=with*"
REPOSITORY TAG IMAGE ID CREATED SIZE
with-official-image_app latest c202be423ea4 31 minutes ago 1.19GB
Allí podemos observar que la imagen resultante pesa 1.19Gb. Es un tamaño considerable, teniendo en cuenta que nuestro ejecutable pesa unos 8 mb. Para una práctica de Docker y Drogon, 1.19Gb puede no representar problema alguno. Sin embargo, si nuestra imagen va a ser almacenada en algún sistema pago, será conveniente optimizar el peso de la imagen resultante.
En esta sección vamos a crear un Dockerfile que resulte en una imagen de menor tamaño, haciendo uso de los builds multietapa (en inglés, multi-stage) de Docker.
Un build multietapa consiste en un Dockerfile con más de una línea FROM
que dan inicio a las etapas en el multietapa. La etapa final es la imagen definitiva que se usará para crear los contenedores.
Esta será la estructura de nuestro Dockerfile:
# Etapa "common" - Instalamos las librerías comunes a ambas etapas en la etapa que denominaremos "common":
FROM ubuntu:20.04 as common
RUN apt-get update -yqq && \
apt-get install -y libssl-dev libjsoncpp-dev uuid-dev zlib1g-dev libc-ares-dev
# Etapa "builder" - Configuramos la imagen encargada de compilar la aplicación:
FROM common as builder
ENV TZ=UTC
RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone
RUN apt-get install -yqq --no-install-recommends software-properties-common \
curl wget cmake make pkg-config locales git gcc-10 g++-10 \
openssl \
&& rm -rf /var/lib/apt/lists/* \
&& locale-gen en_US.UTF-8
ENV LANG=en_US.UTF-8 \
LANGUAGE=en_US:en \
LC_ALL=en_US.UTF-8 \
CC=gcc-10 \
CXX=g++-10 \
AR=gcc-ar-10 \
RANLIB=gcc-ranlib-10 \
IROOT=/install
ENV DROGON_ROOT="$IROOT/drogon"
ADD https://api.github.com/repos/an-tao/drogon/git/refs/heads/master $IROOT/version.json
RUN git clone https://github.com/an-tao/drogon $DROGON_ROOT
WORKDIR $DROGON_ROOT
RUN ./build.sh
COPY . /code
WORKDIR /code
RUN rm -rf build \
&& mkdir build \
&& cd build \
&& cmake .. \
&& make
# Etapa "runtime" - Creamos la imagen que va a contener la carpeta build generada en la etapa "builder":
FROM common as runtime
WORKDIR /app
COPY --from=builder /code/build /app
EXPOSE 80
ENTRYPOINT ["./invoicer"]
La etapa “builder” está basada en la propia imagen oficial, quitando librerías que nuestra aplicación no necesita y añadiendo las líneas que copian nuestro código a la carpeta /code
y efectúan la compilación.
Si usamos este Dockerfile con el mismo docker-compose.yml de la sección anterior, arrancaremos la aplicación con nuestra nueva imagen.
Si ahora vemos la imagen en el listado docker image ls
, veremos que esta nueva versión pesará bastante menos que la anterior:
$ docker image ls --filter "reference=with*"
REPOSITORY TAG IMAGE ID CREATED SIZE
with-custom-runtime-image_app latest 05dabea8e824 25 minutes ago 153MB
with-official-image_app latest c202be423ea4 31 minutes ago 1.19GB
El gran punto flojo de este enfoque es que el proceso de crear la imagen final con nuestra aplicación compilada tarda bastante más que cuando usamos la imagen oficial. Esta situación podría mitigarse con más de un Dockerfile, generando una imagen por cada etapa y persistiéndolas en algún repositorio para reutilizarlas en nuestro Dockerfile correspondiente al runtime, como veremos a continuación.
En la sección anterior vimos cómo reducir el tamaño de la imagen final, partiendo nuestro Dockerfile en 3 etapas, aprovechando los builds multietapa de Docker. También observamos que el tiempo de compilación siguiendo este enfoque es bastante mayor al enfoque en donde utilizamos la imagen oficial, debido a que construimos más imágenes.
En este enfoque, exploramos una opción similar al multistage, pero persistiendo cada etapa en algún repositorio de imágenes con el que podamos contar. Para este blog, utilizaré el repositorio de imágenes del propio docker, aprovechando que las imágenes serán open source.
Vamos a iniciar sesión en DockerHub y creamos dos repositorios:
El próximo paso será construir las imágenes para cada repositorio.
Vamos a crear un archivo llamado Dockerfile-common con el siguiente contenido:
FROM ubuntu:20.04 as common
RUN apt-get update -yqq && \
apt-get install -y libssl-dev libjsoncpp-dev uuid-dev zlib1g-dev libc-ares-dev
Para poder subir una imagen al repositorio de imágenes, deberemos iniciar sesión, luego construir la imagen y efectuar un push
:
docker login --username=imefisto -p <contraseña>
docker build -t imefisto/invoicer_common -f Dockerfile-common .
docker push imefisto/invoicer_common
Reemplaza imefisto
con tu usuario.
A continuación, creamos el archivo Dockerfile para nuestro builder, al que llamaremos Dockerfile-builder:
FROM invoicer_common
ENV TZ=UTC
RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone
RUN apt-get install -yqq --no-install-recommends software-properties-common \
curl wget cmake make pkg-config locales git gcc-10 g++-10 \
openssl \
&& rm -rf /var/lib/apt/lists/* \
&& locale-gen en_US.UTF-8
ENV LANG=en_US.UTF-8 \
LANGUAGE=en_US:en \
LC_ALL=en_US.UTF-8 \
CC=gcc-10 \
CXX=g++-10 \
AR=gcc-ar-10 \
RANLIB=gcc-ranlib-10 \
IROOT=/install
ENV DROGON_ROOT="$IROOT/drogon"
ADD https://api.github.com/repos/an-tao/drogon/git/refs/heads/master $IROOT/version.json
RUN git clone https://github.com/an-tao/drogon $DROGON_ROOT
WORKDIR $DROGON_ROOT
RUN ./build.sh
Construimos la imagen y la subimos a nuestro repositorio:
docker build -t imefisto/invoicer_builder -f Dockerfile-builder .
docker push imefisto/invoicer_builder
Ahora estamos listos para usar las imágenes, previamente almacenadas en nuestra cuenta de docker, en nuestro último Dockerfile:
FROM imefisto/invoicer_builder as builder
COPY . /code
WORKDIR /code
RUN rm -rf build \
&& mkdir build \
&& cd build \
&& cmake .. \
&& make
FROM imefisto/invoicer_common as runtime
WORKDIR /app
COPY --from=builder /code/build /app
EXPOSE 80
ENTRYPOINT ["./invoicer"]
Reemplaza imefisto
con tu username para usar tus propias imágenes. De este modo, cada vez que hagamos un cambio en nuestro código, en tanto dichos cambios no implique modificar las librerías citadas en la imagen invoicer_common
, podremos ahorrar tiempo y a la vez tener una imagen relativamente liviana, en comparación al peso de la imagen conseguido en el primer enfoque. Si en algún caso necesitamos actualiar alguna librería, bastará con que volvamos a generar la invoicer_common
y, a continuación, invoicer_builder
y volvamos a subirlas al repositorio de imágenes.
Validamos nuestro último Dockerfile creando el docker-compose.yml como hicimos en los otros enfoques:
version: "3.9"
services:
app:
build:
context: ../..
dockerfile: "${PWD}/Dockerfile"
ports:
- "8081:80"
Tras ejecutar docker-compose up -d
, deberemos ser capaces de cargar la url http://localhost:8081
y todo debería ir bien.
En este post hemos explorado 3 enfoques para construir una imagen para nuestra aplicación basada en Drogon. En el primer enfoque, utilizamos la imagen oficial, en cuyo caso, la aplicación se construía muy rápido, pero la imagen resultante era algo pesada. En el enfoque siguiente, aprovechamos el build multistage de Docker y conseguimos que la imagen final fuese bastante más ligera que la anterior.
Finalmente, en el último enfoque probamos alojar las etapas common y builder como imágenes separadas en un repositorio online, para utilizarlas en nuestro Dockerfile final.
Cada enfoque tiene sus pros y sus contras y dependiendo la situación y el entorno de ejecución, nos resultará más conveniente uno u otro.
https://ml-cfd.com/openfoam/docker/2020/12/29/minimizing-docker-images.html
https://www.jmoisio.eu/en/blog/2020/06/01/building-cpp-containers-using-docker-and-cmake/
¡Tu mensaje fue recibido! Una vez que sea aprobado, estará visible para los demás visitantes.
Cerrar