Dockerízate, parte 3

En esta tercera parte veremos como generar una imagen de nuestro software, ponerla en un contenedor y distribuirla. Si has seguido los anteriores Posts estamos a punto: disponemos de un entorno virtualizado con Vagrant, Docker y Docker-Compose instalado y un contenedor MongoDB funcionando como Servicio.

1. ¿Qué hay de mis contenedores?

Hemos terminado la primera versión de nuestra aplicación web. Llega el momento de que pase al entorno de certificación y no queremos tener que estar copiando ficheros de configuración entre máquinas, enviando el war por FTP ni generando tediosos manuales de instalación para el cliente. Estamos listos para crear nuestro primer contenedor. Recordamos que los contenedores se basan en imágenes de software. Por lo tanto, el primer paso es generar una imagen de nuestra aplicación web. En el ejemplo tenemos una aplicación Java desarrollada con SpringBoot y un servidor Tomcat embebido, lo que permite que nuestra aplicación sea ejecutable con un “java -jar miwebapp.war”. SpringBoot levantará el tomcat automáticamente. Supongamos que nuestra aplicación Web se compone de dos ficheros de configuración externos (logback.xml y environment.properties) y el War con el código y el Tomcat emebebido (WebApp.war). A continuación, vamos a la carpeta /workspace/shared de nuestra vagrant, creamos una nueva carpeta /workspace/shared/webapp (por organización) y lo copiamos en dicha carpeta (utilizamos una carpeta nueva porque todos los ficheros deben estar al mismo nivel jerárquico). Para generar una imagen, tenemos que crear un nuevo fichero Dockerfile en la raíz de la carpeta. Si listamos los ficheros tendríamos, al mismo nivel:
  • logback.xml
  • environment.properties
  • WebApp.war
  • Dockerfile
El fichero Dockerfile indica que va a contener la imagen. Para que nuestra aplicación web se ejecute necesitamos que tenga Java Instalado, el War y los ficheros de configuración. Una vez tengamos todo, tendríamos la siguiente configuración:
# FROM - Specifies the base image that the Dockerfile will use to build a new image.
FROM openjdk:8-jdk-alpine

# MAINTAINER - Specifies the Dockerfile Author Name and his/her email.
MAINTAINER  Pedro Delgado <pdelgado@futurespace.es>

# RUN - Runs any UNIX command to build the image.
COPY WebApp.war /lib/
COPY environment.properties /var/conf/
COPY logback.xml /var/conf/

# EXPOSE - This instruction exposes specified port to the host machine.
EXPOSE 8085

ENTRYPOINT ["java","-jar","/lib/WebApp.war"]
La directiva FROM indica la imagen base de la que partir. En nuestro caso, necesitamos una imagen que proporcione java8. De nuevo, DockerHub al rescate: https://hub.docker.com/_/openjdk/. Las sentencias COPY agregan a la imagen todos los ficheros necesarios para ejecutar la aplicación Web y con EXPOSE abrimos al mundo el puerto 8085 que habremos configurado previamente en el Tomcat embebido como puerto de escucha HTTP. Por último, ENTRYPOINT define qué acciones se ejecutarán como punto de entrada. La documentación (https://docs.docker.com/engine/reference/builder/#usage) contiene un detalle más exhaustivo de las directivas del Dockerfile. Una imagen ideal es aquella que no varía entre entornos. Se genera una vez, y se distribuye sin cambios. En nuestro caso estamos copiando ficheros de configuración dentro de la imagen que rara vez se mantienen entre entornos. La solución es externalizar los ficheros de la imagen y que el contenedor de turno tenga acceso a ellos en el host a través de un volumen. Pero, eso se escapa al ámbito de este post. Ya estamos listos para generar la imagen. Utilizamos el comando “build” de Docker para crear la imagen en nuestro repositorio local (de la máquina virtual Ubuntu en la que estamos trabajando). Volvemos a nuestra Vagrant y ejecutamos:
$/shared/webapp$ docker build -t miwebapp .
Sending build context to Docker daemon  50.14MB
Step 1/7 : FROM openjdk:8-jdk-alpine
[...]
Successfully built ab4c219b27bd
Successfully tagged miwebapp:latest
Perfecto, ya tenemos nuestra aplicación como imagen lista para ser “contenerizada”.

2. Container Up!

La idea es que nuestro contenedor viaje entre entornos, pero primero probaremos en nuestro entorno virtual un “snapshot” de lo que distribuiremos al cliente. La imagen que hemos creado de nuestra aplicación Web esta accesible en el repositorio local, por lo que editamos el fichero de configuración de Docker-Compose para agregar el nuevo servicio:
version: '3'
services:
    mongodb:
        image: mongo:latest
        container_name: "mongodb"
        volumes:
          - ./data/:/data/
        ports:
          - 27017:27017
        expose:
            - 27017
        networks:
            - webapp-network
    miwebapp:
        image: miwebapp:latest
        container_name: "miwebapp"
        volumes:
          - webapp-logs:/var/logs/
        ports:
          - 8085:8085
        expose:
            - 8085
        links:
            - mongodb
        depends_on:
            - mongodb
        networks:
            - webapp-network
networks:
    webapp-network:
        driver: bridge
volumes:
    webapp-logs:
        external: true
Hemos introducido varios cambios. Los más significativos son:
  • Links: Nos permiten comunicación entre contenedores. Desde el servicio miwebapp tendremos acceso a MongoDB utilizando el nombre del servicio como host: http://mongodb:27017.
  • Networks: Crea una red de servicios que se comunican entre sí.
  • Volumes: La información generada dentro de un contenedor no es persistente y desaparece al detener el servicio. Para persistir dicha información, debemos crear un volumen donde guardarla. Los volúmenes se crean en el host y se asocian a la ruta indicada del contenedor. Hay varios tipos de volúmenes en función del caso de uso: https://docs.docker.com/storage/volumes/. En nuestro caso, Mongo utiliza la carpeta /data par almacenar la base de datos y MiWebApp expone la ruta /var/logs donde genera las trazas de uso.
Para acceder al volumen como usuario root en la ruta:
$>cd /var/lib/docker/volumes/webapp-logs/_data/
Terminada la configuración de contenedores, iniciamos los servicios desde la carpeta /docker, donde se encuentra el docker-compose.yml:
$> docker-compose up -d
Starting mongodb ... done
Starting miwebapp... done
$> docker-compose ps
 Name                Command               State            Ports
---------------------------------------------------------------------------
mongodb   docker-entrypoint.sh mongod  Up      0.0.0.0:27017->27017/tcp
miwebapp java -jar /lib/WebApp.war      Up      0.0.0.0:8085->8085/tcp
Ya tendríamos todo funcionando. Por último, solo nos quedaría mapear el puerto 8087 en el fichero de configuración Vagrantfile, para poder acceder a la aplicación web desde fuera del entorno virtual:
# MiWebApp
  config.vm.network "forwarded_port", guest: 8085, host: 8087
¡Ahora sí! ya podríamos acceder, desde nuestro sistema local Windows, a la aplicación web, desplegada como un contenedor en el entorno virtual, a través del puerto 8087: http://localhost:8087/miwebapp/login.xhtml.

3. Contenedor derecho a producción

Solo nos queda portar nuestro entorno virtual a otros entornos. En el caso de un uso general, tendríamos que desplegar nuestras aplicaciones en Certificación, Preproducción y Producción. Para ello, tenemos que hacer visible nuestra imagen fuera de nuestro local y agregarla a un repositorio que tenga acceso al cliente. Disponemos de varias opciones, siendo las más usadas:
  • DockerHub (https://hub.docker.com/)
  • Amazon Web Services Elastic Container Registry (https://aws.amazon.com/es/ecr/)
  • Google Container Registry (https://cloud.google.com/container-registry/)
  • Azure Container Registry (https://azure.microsoft.com/es-es/services/container-registry/)
Por supuesto, si los desarrollos son para un cliente final nos interesa un repositorio privado al que solo tenga acceso nuestra organización. Con acceso a los repositorios y Docker instalado en todos los entornos donde desplegar nuestro sistema, tendríamos montado el sistema de distribución de nuestro entorno de modo contenerizado. Como hemos configurado las imágenes con la versión “latest”, con cada nuevo despliegue, simplemente tendríamos que volver a levantar los servicios con “docker-compose up” y los contenedores se actualizarían a la última versión que hubiésemos subido al repositorio. ¡Se acabaron los días de los desplegables! Además, Docker es completamente integrable con un sistema de integración continua CI/CD por lo que, agregando un Jenkins y SonarQube a la ecuación, podemos generar despliegues automatizados y de calidad, reduciendo el proceso a unos pocos clicks.
Avatar

Ingeniero en Informática de Sistemas por la Universidad Politécnica de Madrid, vive en el mundo del desarrollo de aplicaciones desde que cayó en sus manos un Spectrum. Especializado en Backend Java, sus días transcurren entre frameworks, arquitecturas y microservicios. Desconecta practicando fotografía y deportes de montaña (rutas, Snowboarding... lo que proceda según la época). Actualmente, trabaja como Designer en Future Space.