PetClinic con Docker
Para realizar el despliegue de PetClinic como contenedor, primero tenemos que dockerizar la aplicación, luego publicar la imagen de contenedor en un registro como DockerHub o Google Container Registry, y por último ejecutar el contenedor en la instancia de despliegue.
A continuación se describe cómo crear un contenedor Docker de la aplicación PetClinic. Los pasos se realizan en local, y al final configuraremos el pipeline de Jenkins para que se realicen automáticamente.
|
Para trabajar con contenedores Docker en tu equipo local, debes tener Docker instalado. Recuerda iniciar Docker Desktop en Windows, o iniciar el servicio Docker en Linux o Mac. Comprueba que está funcionado ejecutando el comando |
|
Hasta ahora nos estamos basando en el proyecto PetClinic original (genericamente llamado upstream) disponible en GitHub. En esta sección necesitarás poder hacer cambios sobre el mismo, básicamente vamos a añadir al proyecto un archivo |
Creación del Dockerfile multi-stage
Para construir aplicaciones Maven con Docker y luego crear el contendedor de Docker que empaquete la aplicación, la opción recomendada es usar Multi-Stage Builds en tu Dockerfile, de manera que:
-
Una primera fase o stage construye el
.jara partir de una imagen de Maven, como por ejemplomaven:3.8.4-openjdk-11-slim -
Y luego en una seguna fase, ese
.jarlo copia en el contenedor basado en la imagen OpenJDK-11, que es el entorno de ejecución Java necesario.
La documentación de PetClinic indica cómo construir el contenedor usando Spring Boot build plugin. A nosotros nos interesa ver cómo hacerlo de forma genérica para cualquier aplicación Java basada en Maven. Vamos a definir el siguiente archivo Dockerfile que debe estar en la carpeta raíz del proyecto PetClinic:
FROM maven:3.8.4-openjdk-11-slim AS build (1)
WORKDIR /app
# First copy only the pom file. This is the file with less change
COPY ./pom.xml .
# Download the package and make dependencies cached in docker image
RUN mvn -B -f ./pom.xml -s /usr/share/maven/ref/settings-docker.xml clean dependency:go-offline
# Copy the actual code
COPY ./ .
# Then build the code
RUN mvn -B -f ./pom.xml -s /usr/share/maven/ref/settings-docker.xml clean package
# Start with a base image containing Java runtime
FROM openjdk:11-slim (2)
# Make port 8080 available to the world outside this container
EXPOSE 8080
# The application's jar file
ARG JAR_FILE=target/*.jar
# Copy the application's jar to the container
COPY ${JAR_FILE} app.jar
# Run the jar file
ENTRYPOINT ["java","-jar","/app.jar"]
| 1 | Primera fase o stage de build, construye la aplición llamando a los goals clean package de Maven. Contiene los pasos básicos para construir una aplicación Java basada en Maven. Dicha construcción la divide en dos partes, primero copia el pom.xml y descarga las dependencias con dependency:go-offline, y luego copia todos los fuentes y construye el proyecto con package. De esta forma se optimiza la reconstrucción del contenedor ya que la descarga de dependencias es una etapa que dura varios minutos. |
| 2 | Segunda fase, crea una imagen con el entorno de ejecución de Java 11 y el empaquetado .jar de la aplicación PetClinic. Contiene los pasos básicos para ejecutar una aplicación String Boot en un contenedor: partiendo de una imagen de openjdk, copia el archivo target/*.jar en el contenedor con el nombre app.jar y lo ejecuta mediante el comando ENTRYPOINT para que no haya ninguna shell sobre el proceso java. |
Construye el contenedor con docker build, y ten paciencia, tardará varios minutos!!!
docker build -t petclinic-docker .
|
Si estás trabajando en Windows, la construcción da un error por problemas de codificación de los saltos de línea diferentes entre Windows y Linux. Para resolverlo, sustituye en la primera fase del Dockerfile la linea de construcción que llama a
|
Tras la construcción de la imagen, prueba la ejecución del contenedor en local:
docker run -it -p 8080:8080 -t petclinic-docker
Comprueba que se ha iniciado la aplicación en http://localhost:8080.
Para el contenedor con CTRL+C.
Una vez creada la imagen con docker build y probada su ejecución con docker run, el siguiente paso será publicar la imagen en un registro de contenedores, mediante docker push. Podemos usar DockerHub pero en este caso vamos a usar Google Cloud Container Registry.
Autenticación en Container Registry
Para poder hacer push debemos tener permisos de escritura, y por tanto debemos autenticarnos en el servicio Container Registry.
Authentication permite conectar al Registro de Contenedores con tus credenciales, y hacer push y pull de imágenes. Para ello debes configurar los los permisos necesarios para accdeder al registro, utilizando un JSON key file como método de autenticación:
-
En la Consola Google Cloud, seleccionar el proyecto Google Cloud.
-
En el menú de navegación seleccionar
IAM y administración | Cuentas de servicio. -
Seleccionar
Crear cuenta de servicio. -
Darle un nombre (p.e.
container-registry) -
Seleccionar "Crear y continuar".
-
En el paso
Conceder a esta cuenta de servicio acceso al proyectodel asistente, seleccionar el rolCloud Storage → Admisnitrador de almacenamiento. Continuar y Listo. -
Editar la Cuenta de servicio. En la sección
ClavesseleccionarAgregar clave | Crear nueva clave. -
Dejar
JSONen el tipo de clave. -
Seleccionar
Crear. A continuación se descargará la clave privada.
-
Guarda el archivo
.jsonen la carpetasecretde tu proyecto PetClinic.
|
No olvides añadir la carpeta |
-
Use the service account key as your password to authenticate with Docker. Sustituye
keyfile.jsonpor el nombre de tu archivo de credenciales:-
En Linux:
-
cat keyfile.json | docker login -u _json_key --password-stdin https://gcr.io
-
En Windows:
docker login -u _json_key --password-stdin https://gcr.io < keyfile.json
Publicación y despliegue manual
-
Construir el contenedor con el nombre completo incluyendo la referencia a Container registry (gcr.io). Primero definimos una variable de entorno con el nombre de nuestro proyecto GCP, y luego construimos de nuevo la imagen con el nombre completo del registro de contenedores:
GOOGLE_CLOUD_PROJECT=cnsa-2022-user123
docker build -t gcr.io/$GOOGLE_CLOUD_PROJECT/petclinic:1.0 .
-
A continuación vamos a publicar con
docker push: habilita la API de Container Registry en tu proyecto GCP, accediendo en el menú a Contaner Registry > Images:
-
Publica la imagen con
:docker push [HOSTNAME]/[PROJECT-ID]/[IMAGE]:[TAG]
docker push gcr.io/$GOOGLE_CLOUD_PROJECT/petclinic:1.0
-
Comprueba que se ha publicado correctamente.
La imagen del contenedor PetClinic ya está disponible en el registro privado de nuestro proyecto GCP. Utilizando nuestras credenciales podremos hacer docker pull de dicha imagen para descargarla en cualquier máquina con docker, y ejecutarlo con docker run.
GOOGLE_CLOUD_PROJECT=cnsa-2022-user123
docker run -p 8080:8080 -t --name petclinic gcr.io/$GOOGLE_CLOUD_PROJECT/petclinic:1.0
Si conectas a la instancia de despliegue que creamos al principio de esta actividad, y ejecutas el comando docker run anterior, dará un error de autenticación:
Para arreglarlo, habrá que copiar en la máquina de despliegue el archivo de credenciales .json con premisos sobre Container Registry. A continuación se muestran los comandos necesarios para ello. Una vez disponible este archivo en la instancia de despliegue ejecutar el comando docker login y tras ello ya si podremos hacer docker pull y docker run.
# Compiamos el archivo de credenciales
scp ./secret/file.json ubuntu@DNS_MAQUINA_DEPLOY:~/keyfile.json
# Conectamos a la máquina de despliegue
ssh ubuntu@DNS_MAQUINA_DEPLOY
# Autenticamos docker contra Container Registry
cat keyfile.json | docker login -u _json_key --password-stdin https://gcr.io
# Variable de entorno con el nombre del proyecto
GOOGLE_CLOUD_PROJECT=cnsa-2022-user123
# ejecutamos el contenedor desde gcr.io
docker run -d -p 8080:8080 -t --name petclinic gcr.io/$GOOGLE_CLOUD_PROJECT/petclinic:1.0
|
Si la ejecución de |
Es posible que la ejecución del contenedor de un error, porque el puerto 8080 ya esté en uso:
Error starting userland proxy: listen tcp 0.0.0.0:8080: bind: address already in use.
Para solucionarlo, bien detén el proceso java que está corriendo con la aplicación PetClinic tal y como la desplegamos en la sección anterior (), o bien utiliza otro puerto, por ejemplo, el 80, que debe estar disponible:if pgrep java; then pkill java; fi
docker run -p 80:8080 -t --name petclinic gcr.io/$GOOGLE_CLOUD_PROJECT/petclinic:1.0
Pero ten en cuenta que si el contenedor ya se ha creado y no ha podido iniciarse porque el puerto 8080 estaba ocupado, si intentas volver a crearlo con docker run te dirá que el contenedor ya existe. Revisa si está ya creado y en ese caso inicialó.
ubuntu@web-deploy-vm-tf:~$ docker ps -a
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
6e174d959f3b gcr.io/cnsa-2022/petclinic:1.0 "java -jar /app.jar" About a minute ago Created petclinic
ubuntu@web-deploy-vm-tf:~$ docker start petclinic
petclinic
Ya puedes comprobar en tu navegador que la aplicación PetClinic se está ejecutando en el puerto 8080 de la máquina de despliegue.
Integración y despliegue continuo
Hasta ahora hemos realizado todos los pasos de construcción, prueba y despliegue manualmente. A continuación, vamos a automatizar en Jenkins todo el proceso, cuyas principales tareas son:
En Jenkins, son necesarios los siguientes plugins para trabajar con Docker y pipelines, y con Container Registry: Docker Pipeline, que ya está instalado, y tendrás que instalar Google Container Registry Auth.
Definimos un nuevo proyecto en Jenkins de tipo pipeline, con el nombre sustituyendo abc123 por nuestro nombre de usuario. Son necesarios 3 fases (stages) en el pipeline: build image, push image, y deploy container.PetClinic-Docker-abc123
Construcción y despliegue del contenedor
Comenzamos por la construcción de la imagen:
pipeline {
agent any
environment {
CONTAINER_REGISTRY = 'gcr.io'
GOOGLE_CLOUD_PROJECT = 'cnsa-2022-abc123'
CREDENTIALS_ID = 'cnsa-2022-gcr'
}
tools {
maven "Default Maven"
}
stages {
stage("Checkout code") {
steps {
// checkout scm
git branch:'main', url:'https://github.com/ualcnsa/spring-petclinic.git'
}
}
stage('Compile, Test, Package') {
steps {
sh "mvn clean package -Dcheckstyle.skip"
}
post {
success {
junit '**/target/surefire-reports/TEST-*.xml'
archiveArtifacts 'target/*.jar'
}
}
}
stage("Build image") {
steps {
script {
dockerImage = docker.build(
"${env.CONTAINER_REGISTRY}/${env.GOOGLE_CLOUD_PROJECT}/petclinic:${env.BUILD_ID}",
"--rm -f Dockerfile ."
)
}
}
}
}
}
|
Si consultas la salida por consola de la ejecución del pipeline, verás que se algunas tareas se repiten dos veces, como por ejemplo la ejecución de los tests. ¿Por qué crees que es debido? ¿Podría eliminarse alguna fase del pipeline? |
Para probar que la imagen del contenedor se ha creado bien, añade esta fase que hace un despliegue en un entorno de "Staging" o "Testing", que en este tutorial va a ser "local" en la propia máquina de Jenkins, es decir, ejecuta un contenedor basado en la imagen que acabamos de crear:
stage("Deploy to Testing (locally)") {
steps {
sh "docker stop petclinic || true && docker rm petclinic || true" (1)
sh "docker run -d -p 8080:8080 -t --name petclinic ${env.CONTAINER_REGISTRY}/${env.GOOGLE_CLOUD_PROJECT}/petclinic:${env.BUILD_ID}" (2)
}
}
| 1 | Por si ya se ha ejecutado el pipeline anteriormente, y no se ha eliminado el contenedor de la ejecución anterior, es necesario comprobar si el contenedor petclinic ya se está ejecutando y, en tal caso, pararlo con docker stop y eliminarlo con docker rm |
| 2 | Con docker run ejecuta el contenedor petclinic a partir de la imagen recién construida. Para que el pipeline pueda finalizar y el contenedor siga ejecutándose, se añade -d que indica modo detached que ejecuta el contenedor en background. |
La aplicación debe estar accesible en el puerto 8080 en tu máquina de Jenkins. Para asegurarnos que la aplicación se está ejecutando bien, debemos problarlo "manualmente". Para automatizar esta prueba, lo adecuado sería realizar unos tests end-to-end, con Selenium. Esto se explicará en otra actividad, dedicada al testing.
stage('End-to-end Test image') {
// Ideally, we would run some end-to-end tests against our running container.
steps{
sh 'echo "End-to-end Tests passed"'
}
}
Publicación en el registro
El siguiente paso es publicar la imagen en el registro.
-
Primero, es necesario crear unas credenciales en Jenkins para poder hacer
pushen Container Registry:-
Go to jenkins home, Manage Jenkins, click on “Manage credentials” and “(global)”
-
Click on “Add Credentials” in left menu.
-
Select Google Service Account from private key for the “Kind” field, and enter your project. Then upload the JSON private key.
-
-
Una vez guardadas las credenciales, vamos a definir la fase para publicar la imagen del contenedor:
stage("Push image") {
steps {
script {
docker.withRegistry('https://'+ CONTAINER_REGISTRY, 'gcr:'+ GOOGLE_CLOUD_PROJECT) {
dockerImage.push("latest")
dockerImage.push("${env.BUILD_ID}")
}
}
}
}
Comprobar que se ha publicado correctamente en el registro.
Despliegue en producción
Por último, quedaría el paso de desplegar al entorno de producción. Una vez empaquetada como un contenedor, Google Cloud permite desplegar de varias formas:
Para nosotros, la máquina virtual de despliegue es nuestro entorno de producción en el que vamos a desplegar el contenedor.
Los pasos para el despliegue de la nueva imagen del contenedor consistirán en ejecutar los siguientes comandos sobre la máquina de despliegue:
-
docker stopdel contenedor por si estuviera ejecutándose -
docker rmpara eliminar el contenedor existente, que puede estar basado en una imagen de una versión anterior -
docker runpara ejecutar el contenedor, que automáticamente hará undocker pullde la imagen actualizada del registro. Lo lanzaremos en el puerto 80 ya que el 8080 está ocupado por el despliegue que hicimos sin contenedor.
Estas acciones debemos añadirlas a un stage del pipeline de Jenkins que se encargará de desplegar el nuevo contenedor automáticamente. En el siguiente código, sustituye DNS_DEPLOY_INSTANCE por el nombre DNS de tu instancia de despliegue. También puedes definirla como una variable de entorno al inicio del pipeline.
stage('Deploy to Production') {
steps{
// Check to manual approving deploy to production.
// It implemenents Continuous Delivery instead of Continuous Deployment
input message: "Proceed Deploy to Production?" (1)
sh '''
ssh -i ~/.ssh/id_rsa_deploy ubuntu@DNS_DEPLOY_INSTANCE "if docker ps -q --filter name=petclinic | grep . ; then docker stop petclinic ; fi" (2)
ssh -i ~/.ssh/id_rsa_deploy ubuntu@DNS_DEPLOY_INSTANCE "if docker ps -a -q --filter name=petclinic | grep . ; then docker rm -fv petclinic ; fi" (3)
ssh -i ~/.ssh/id_rsa_deploy ubuntu@DNS_DEPLOY_INSTANCE "docker run -d -p 80:8080 -t --name petclinic ${CONTAINER_REGISTRY}/${GOOGLE_CLOUD_PROJECT}/petclinic:latest" (4)
'''
}
}
| 1 | Pide confirmación al usuario, que tendrán que pulsar un botón de Proceed para continuar la ejecución del pipeline. Permite asegurar que el despliegue a producción requiere intervención de una persona, implementando entrega continua (continuous delivery) en lugar de despliegue continuo (continuous deployment). |
| 2 | Ejecuta en la instancia de despliegue el comando docker stop que detiene el contenedor petclinic en caso de que ya se estuviera ejecutando de un despliegue anterior. Esto se comprueba con docker ps …. |
| 3 | Ejecuta en la instancia de despliegue el comando docker rm que elimina el contenedor petclinic en caso de que exista de un despliegue anterior. Esto se comprueba con docker ps -a …. Estos dos pasos, primero parar el contenedor y luego eliminar el contenedor, son necesarios antes de volver a lanzar un nuevo contenedor con el mismo nombre. Se ejecuta en dos pasos para evitar errores en caso de que el contenedor exista pero no esté en ejecución, lo que podría dar lugar a un error en el despliegue. |
| 4 | Ejecuta en la instancia de despliegue el comando para ejecutar el contenedor basado en la última versión de la imagen, lanzándolo con -d que indica modo detached que ejecuta el contenedor en background, para que el pipeline finalice y el contenedor permanezca en ejecución. |
|
Algunos comandos útiles de Docker:
Usalos si te aparece algun mensaje de error del tipo |
La aplicación PetClinic debe estar accesible en producción, en el puerto 8080 en la instancia de despliegue. Para asegurarnos, debemos problarlo "manualmente". Para automatizar esta prueba en producción, lo adecuado de nuevo sería realizar unos tests end-to-end, con Selenium. Esto se explicará en otra actividad, dedicada al testing.
stage('End-to-end Test on Production') {
// Ideally, we would run some end-to-end tests against our running container.
steps{
sh 'echo "End-to-end Tests passed on Production"'
}
}
Por último, es una buena práctica eliminar las imágenes que se van generando en cada build, para liberar espacio en la máquina de Jenkins. Primero paramos y eliminamos el contenedor que desplegamos anteriormente en la fase del pipeline Deploy to Testing (locally); luego eliminamos la imagen.
stage('Remove Unused docker image') {
steps{
// input message:"Proceed with removing image locally?" (1)
sh 'if docker ps -q --filter name=petclinic | grep . ; then docker stop petclinic && docker rm -fv petclinic; fi' (2)
sh 'docker rmi ${CONTAINER_REGISTRY}/${GOOGLE_CLOUD_PROJECT}/petclinic:$BUILD_NUMBER' (3)
}
}
| 1 | Pide confirmación al usuario, que tendrán que pulsar un botón de Proceed para continuar la ejecución del pipeline |
| 2 | Para y elimina el contenedor local |
| 3 | Elimina la imagen de contenedor en local con docker rmi para liberar espacio. |
El pipeline completo, con todas sus fases, debe quedar así:
|
ENHORABUENA!!! Has conseguido definir un pipeline completo de integración y despliegue continuos, y con contenedores. Este proceso se puede aplicar, con pequeñas adaptaciones, a cualquier otro proyecto Java basados en Maven. Si usas otras tecnologías, como NodeJs, hay que adaptar cada una de las fases a su equivalente en en la tecnología concreta. Vamos a ver como hacerlo con NodeJs en la siguiente sección. |