Hola mundo en Node.js con Docker
Para realizar el despliegue de la app Hola mundo en Node.js como un contenedor, primero tenemos que dockerizar la aplicación. Una vez construido el contenedor, habrá que publicar la imagen de contenedor en un registro como 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 Node.js. Los pasos se realizan en local, y al final configuraremos el pipeline de Jenkins para que se realicen automáticamente.
Creación del Dockerfile
Para crear el contendedor de Docker que empaquete la aplicación Node.js, vamos a definir el siguiente archivo Dockerfile que debe estar en la carpeta raíz del proyecto:
FROM node:10-alpine
# Create app directory
WORKDIR /usr/src/app
# Install app dependencies
# A wildcard is used to ensure both package.json AND package-lock.json are copied
# where available (npm@5+)
COPY package*.json ./
RUN npm install
# If you are building your code for production
# RUN npm ci --only=production
# Bundle app source
COPY . .
EXPOSE 3000
CMD [ "npm", "start" ]
El Dockerfile es muy sencillo, contiene los pasos básicos para ejecutar una aplicación en un contenedor.
Crea además un archivo .dockerignore en la misma carpeta que tu Dockerfile con el siguiente contenido:
node_modules/ .git/ .gitignore npm-debug.log
-
Puedes construir la imagen del contenedor:
docker build -t <your username>/nodeapp .
Tras ello, ejecuta: docker images. La imagen debe aparecer en la lista de imágenes de Docker en tu equipo:
Prueba la ejecución del contenedor en local:
docker run -p 3000:3000 -d --name hello-node <your username>/nodeapp
Comprueba que se ha iniciado la aplicación en http://localhost:3000.
Publicación de la imagen en el registro
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. De nuevo vamos a usar Google Cloud Container Registry.
Para poder hacer push debemos tener permisos de escritura, y por tanto debemos autenticarnos en el servicio Container Registry. Este proceso ya se hizo en la para el ejemplo de Java en la sección Autenticación en Container Registry. Ahora, simplemente comprueba que mantienes el login del Container Registry.
-
Comprueba el login al registro:
docker login https://gcr.io
|
Si En tal caso, no olvides añadir el archivo de credenciales al |
-
Etiqueta 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 etiquetamos la imagen ya construida con un nuevo nombre completo del registro de contenedores:
GOOGLE_CLOUD_PROJECT=cnsa-2022-user123
docker tag <your username>/nodeapp gcr.io/$GOOGLE_CLOUD_PROJECT/nodeapp:1.0
-
Comprueba que se ha etiquetado correctamente
-
A continuación, publica la imagen en el registro con
docker push
docker push gcr.io/$GOOGLE_CLOUD_PROJECT/nodeapp:1.0
-
Comprueba que se ha publicado correctamente.
La imagen del contenedor nodeapp 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.
docker run -p 3000:3000 -t --name nodeapp gcr.io/$GOOGLE_CLOUD_PROJECT/nodeapp:1.0
Despliegue en VM
Conecta a la instancia de despliegue para ejecutar el contenedor. Antes vamos a comprobar el login al registro. En la máquina de despliegue ya habíamos copiado el archivo de credenciales .json con premisos sobre Container Registry. A continuación se recuerdan los comandos necesarios para ello.
# 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
# ejecutamos el contenedor desde gcr.io
docker run -p 8080:3000 -t --name nodeapp gcr.io/$GOOGLE_CLOUD_PROJECT/nodeapp:1.0
Hemos publicado el contenedor en el puerto 8080 ya que es el que está abierto en las reglas del firewall de nuestro proyecto GCP.
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 o contenedor java que está corriendo con la aplicación PetClinic del ejemplo anterior. O bien utiliza el puerto 80 que también está abierto.
Integración y despliegue continuos con Jenkins
A continuación, vamos a automatizar en Jenkins todo el proceso:
-
la construcción de la imagen del contenedor,
-
la publicación de la imagen en el registro, y
-
el despliegue del contenedor.
Los plugins de Jenkins necesarios ya los tenemos configurados el ejemplo en Java.
-
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.nodeapp-Docker-abc123
Comenzamos por la construcción de la imagen:
pipeline {
agent any
environment {
CONTAINER_REGISTRY = 'gcr.io'
GOOGLE_CLOUD_PROJECT = 'cnsa-2022'
CREDENTIALS_ID = 'cnsa-2022-gcr'
}
tools {
// In Global tools configuration, install Node configured as "nodejs"
nodejs "nodejs"
}
stages {
stage("Git Checkout") {
steps {
// checkout scm
git 'https://github.com/ualcnsa/nodeapp.git'
}
}
stage('Install dependencies') {
steps {
sh 'npm install'
}
}
stage('Test') {
steps {
sh 'npm run test-jenkins'
}
post {
success {
junit '**/test*.xml'
}
}
}
stage("Build image") {
steps {
script {
dockerImage = docker.build(
"${env.CONTAINER_REGISTRY}/${env.GOOGLE_CLOUD_PROJECT}/nodeapp:${env.BUILD_ID}",
"-f Dockerfile ."
)
}
}
}
}
}
Para probar que la imagen del contenedor se ha creado bien, añade la siguiente fase que hace un despliegue "local" en la propia máquina de Jenkins, es decir, ejecuta un contenedor basado en la imagen que acabamos de crear:
stage("Run image locally") {
steps {
sh "docker stop nodeapp || true && docker rm nodeapp || true" (1)
sh "docker run -d -p 8080:3000 -t --name nodeapp ${env.CONTAINER_REGISTRY}/${env.GOOGLE_CLOUD_PROJECT}/nodeapp:${env.BUILD_ID}" (2)
}
}
| 1 | Por si ya se ha ejecutado el pipeline anteriormente, es necesario comprobar si el contenedor nodeapp ya se está ejecutando, y en tal caso pararlo con docker stop y eliminarlo con docker rm |
| 2 | Con docker run ejecuta el contenedor nodeapp 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. |
Tras ello, 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"'
}
}
El siguiente paso es publicar la imagen en el registro.
-
Primero, las credenciales en Jenkins para poder hacer
pushen Container Registry ya están creadas del ejemplo anterior (Si tienes algún problema, consulta la sección correspondiente del ejemplo de Java)
-
Define 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}")
}
}
}
}
Comprueba que se ha publicado correctamente en el registro.
Por último, quedaría el paso de desplegar al entorno de producción: la máquina virtual de despliegue.
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 runque primero 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{
sh '''
ssh -i ~/.ssh/id_rsa_deploy ubuntu@DNS_DEPLOY_INSTANCE "if docker ps -q --filter name=nodeapp | grep . ; then docker stop nodeapp ; fi" (1)
ssh -i ~/.ssh/id_rsa_deploy ubuntu@DNS_DEPLOY_INSTANCE "if docker ps -a -q --filter name=nodeapp | grep . ; then docker rm -fv nodeapp ; fi" (1)
ssh -i ~/.ssh/id_rsa_deploy ubuntu@DNS_DEPLOY_INSTANCE "docker run -d -p 8080:3000 -t --name nodeapp ${CONTAINER_REGISTRY}/${GOOGLE_CLOUD_PROJECT}/nodeapp:latest" (2)
'''
}
}
| 1 | Ejecuta en la instancia de despliegue el comando que detiene y elimina el contenedor nodeapp en caso de que ya se estuviera ejecutando |
| 2 | Ejecuta en la instancia de despliegue el comando para ejecutar el contenedor basado en la última versión de la imagen, lanzándolo en background y con -d para que el pipeline finalice y el contenedor permanezca en ejecución. |
La aplicación nodeapp 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 local, 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=nodeapp | grep . ; then docker stop nodeapp && docker rm -fv nodeapp; fi' (2)
sh 'docker rmi ${CONTAINER_REGISTRY}/${GOOGLE_CLOUD_PROJECT}/nodeapp:$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í: