Imagínate tener más de 5 proyectos frontend que desplegar en los ambientes de qa y producción, para automatizar el despliegue con Jenkins y para evitar tener la lógica de los stages en cada proyecto y se vuelvan grandes y difícil de mantener, tendremos que utilizar los Shared Libraries.
Los shared libraries sirven para reutilizar código de pipelines entre varios proyectos, además hacen que estos pipelines estén limpios y sean fáciles de reutilizar.
Lo que primero debemos hacer es crear un proyecto en un repositorio de código como GitHub o GitLab y crear la siguiente estructura:
https://gitlab.com/repo/shared-library
(root del repo de la librería)
├── vars/
│ ├── gitVersioning.groovy # Lógica de versionamiento semántico
│ ├── dockerBuildPush.groovy # Lógica de construcción y subida de Docker
│ ├── deployDocker.groovy # Lógica de desplegar imagen con docker run
│ └── otros...
└── src/ # (Opcional, para otros archivos de infra)
Configuración en Jenkins
Agregamos el repositorio como una librería en Jenkins en la configuracion global, para hacer esto vamos a:
Administrar Jenkins → System → Global Trusted Pipeline Libraries
Una vez dentro completamos con los siguientes datos:
- Name: my-shared-library (Nombre clave que se usara para llamar al shared library)
- Default version: main (Nombre de la rama que se usará por defecto)
Dejamos checked las opciones:
- Allow default version to be overridden
- Include @Library changes in job recents changes
Continuamos completando:
- Retrieval method ponemos: Modern SCM
- Source code Management: Git
- Project repository: https://gitlab.com/repo/shared-library (nuestro repositorio GitLab)
- Credentials: Seleccionamos la que da acceso al código del repositorio
En behaivours no completamos nada y en library path dejamos el valor por defecto ./
Luego le damos en guardar.
Usar esta shared library:
Tener definido el código o función que deseamos reutilizar, ejemplo de deployDocker.groovy
def call(Map config = [:]) {
def sshUser = config.sshUser ?: config.sshUser
def sshHost = config.sshHost ?: config.sshHost
def credentialsId = config.credentialsId
def registryUrl = config.registryUrl
def imageName = config.imageName
def fullImage = "${registryUrl}/${imageName}"
def port = config.port
if (!credentialsId) {
error "Error: 'credentialsId' is needed for SSH authentication."
}
script {
sshagent(credentials: [credentialsId]) {
echo "--- Deploying using Docker Run to ${sshHost} ---"
sh """
ssh -o StrictHostKeyChecking=no ${sshUser}@${sshHost} '
set -e
echo "Pulling image ${fullImage}:latest..."
docker pull ${fullImage}:latest
echo "Recreating container ${imageName}..."
docker stop ${imageName} || true
docker rm ${imageName} || true
docker run -d --restart unless-stopped \
--name ${imageName} \
-p ${port}:80 \
${fullImage}:latest
'
"""
}
}
}
Para reutilizar este código en un repositorio (en este caso, un proyecto frontend en Angular), creamos un archivo Jenkinsfile.
Estructura del proyecto angular:
angular.json
CHANGELOG.md
deploy.ps1
Dockerfile
Jenkinsfile
karma.conf.js
nginx.conf
node_modules/
package.json
README.md
src/
tsconfig.app.json
tsconfig.json
tsconfig.spec.json
yarn.lock
En el Jenkinsfile completamos:
@Library('my-shared-library') _
pipeline {
agent {
node {
label "${params.NODE_LABEL ?: 'main'}"
}
}
// Avoid checking out code before 'Prepare' stage
options {
skipDefaultCheckout()
timeout(time: 1, unit: 'HOURS')
}
parameters {
choice(name: 'NODE_LABEL', choices: ['main', 'children'], description: 'Selecciona el nodo/label para el build')
choice(name: 'ENV_TYPE', choices: ['stage', 'production'], description: 'Entorno de despliegue')
choice(name: 'RELEASE_TYPE', choices: ['auto', 'patch', 'minor', 'major'], description: 'Tipo de incremento')
}
environment {
// variables
ARTIFACTORY_PATH = 'myapp'
IMAGE_NAME = 'repo-myapp'
GIT_REPO_URL = 'gitlab.com/repo/myapp.git'
ARTIFACTORY_REPO = 'repo-dist'
CONTAINER_NAME = 'repo_myapp'
DEPLOY_PATH_PROD = 'D:\\app\\myapp'
PORT = '8010'
//constants
REGISTRY_URL = 'docker.myregistry.com'
DOCKER_CREDS = 'docker-registry-credentials'
GIT_CREDS = 'gitlab-credentials'
ARTIFACTORY_URL = 'https://artifactory.myregistry.com/artifactory/'
ARTIFACTORY_ACCESS_TOKEN = credentials('artifactory-access-token')
JOB_SAFE_NAME = env.JOB_NAME.replaceAll('[^A-Za-z0-9]', '_')
SERVER_HOST = credentials('SSH_HOST')
SERVER_USER = credentials('SSH_USER')
SERVER_SSH_CREDS = 'server-ssh-credentials-id'
DISCORD_WEBHOOK = credentials('DISCORD_WEBHOOK_URL')
WIN_HOST = 'ssh-server-repo.myregistry.com'
WIN_CRED_ID = 'WINDOWS_PROD_CRED_ESPG'
}
stages {
stage('Prepare') {
steps {
sh 'chown -R $(id -u):$(id -g) . || true'
cleanWs()
checkout scm
notifyPreBuild(
imageName: env.IMAGE_NAME,
envType: env.ENV_TYPE,
discordWebhook: env.DISCORD_WEBHOOK
)
}
}
stage('Deploy QA (Linux)') {
when { expression { params.ENV_TYPE == 'stage' } }
stages {
stage('Versioning') {
when { expression { return isMainBranch() } }
steps {
script {
docker.image('dalthonmh/node-alpine-git').inside('-u 0:0') {
gitVersioningJs(
gitCreds: env.GIT_CREDS,
repoUrl: env.GIT_REPO_URL,
releaseType: params.RELEASE_TYPE
)
}
}
}
}
stage('Generate Dist Angular') {
steps {
script {
def dockerArgs =
'-u 0:0 ' +
"-v /var/jenkins_home/caches/${env.JOB_SAFE_NAME}/yarn:/usr/local/share/.cache/yarn " +
"-v /var/jenkins_home/caches/${env.JOB_SAFE_NAME}/angular:/root/.angular"
docker.image('node:24-alpine').inside(dockerArgs) {
sh 'cp ./src/environments/environment.prod.ts ./src/environments/environment.ts'
angularBuild(envType: params.ENV_TYPE)
sh 'chown -R $(id -u):$(id -g) dist/'
}
}
}
}
stage('Package & Upload') {
when { expression { return isMainBranch() } }
steps {
script {
sh 'zip -qr dist.zip dist/'
docker.image('releases-docker.jfrog.io/jfrog/jfrog-cli-v2-jf').inside('-u 0:0') {
echo 'Uploading to Artifactory...'
publishArtifact(
artifactoryUrl: env.ARTIFACTORY_URL,
artifactoryRepo: env.ARTIFACTORY_REPO,
artifactoryPath: env.ARTIFACTORY_PATH,
artifactoryAccessToken: env.ARTIFACTORY_ACCESS_TOKEN,
)
}
}
}
}
stage('Build & Push Docker') {
steps {
sh 'ls -la dist/'
dockerBuildPush(
registryUrl: env.REGISTRY_URL,
imageName: env.IMAGE_NAME,
dockerCreds: env.DOCKER_CREDS
)
}
}
stage('Deploy') {
when { expression { return isMainBranch() } }
steps {
deployDocker(
sshUser: env.SERVER_USER,
sshHost: env.SERVER_HOST,
credentialsId: env.SERVER_SSH_CREDS,
registryUrl: env.REGISTRY_URL,
imageName: env.IMAGE_NAME,
port: env.PORT
)
}
}
}
}
stage('Deploy Prod (Windows)') {
when { expression { params.ENV_TYPE == 'production' } }
steps {
deployProdWindows(
deployPath: env.DEPLOY_PATH_PROD,
winHost: env.WIN_HOST,
winCredID: env.WIN_CRED_ID
)
}
}
}
post {
success {
notifySuccess(
imageName: env.IMAGE_NAME,
envType: env.ENV_TYPE,
discordWebhook: env.DISCORD_WEBHOOK
)
}
failure {
notifyFailed(
imageName: env.IMAGE_NAME,
envType: env.ENV_TYPE,
discordWebhook: env.DISCORD_WEBHOOK
)
}
}
}
De esa forma podremos reutilizar el código entre varios proyectos.