Pipelines com a codi per a la integració i entrega contínua

Enviat per Pau Oliver el Dm, 29/05/2018 - 14:47

En molts projectes de l'inLab apliquem integració contínua, en què cada desenvolupador integra el seu codi de manera freqüent, com a mínim diàriament, i amb eines de construcció i testing automàtics. En l'Aplicació de sistemes d'integració contínua explicàvem en detall en què consisteix.

Una eina popular per a la integració i l'entrega contínua és Jenkins. Permet crear fluxos d'execució de tasques per a automatitzar tota mena d'accions. La interfície clàssica de Jenkins és així:

Jenkins Tradicional

Hi ha diferents seccions per a introduir els diferents aspectes de la configuració del projecte: la freqüència o manera de revisar si hi ha canvis de codi, els disparadors (triggers) per a construir, configuració de l'entorn, passos previs a la construcció, instruccions per a aquesta, i passos posteriors.

La interfície, tot i no destacar pel seu disseny, permet tanta configuració com sigui necessària. A més, a través de plugins o extensions, les funcionalitats disponibles són tantes que es fa difícil trobar alguna necessitat que no quedi coberta.

Tanmateix, és pràctic fer tota la configuració a través de la interfície gràfica? En un principi pot semblar prou còmode, però què passa si després de passar una bona estona configurant el projecte volem crear-ne un de nou amb només unes poques diferències? I si volem mantenir un historial de canvis per veure com ha evolucionat la configuració del projecte? I si volem visualitzar-la o compartir-la fàcilment sense haver de tenir o donar accés a la instància de Jenkins?

Jenkins Pipeline dóna resposta a aquestes limitacions. Una pipeline és el conjunt d'accions que defineixen un procés d'integració. Jenkins Pipeline és un conjunt d'extensions que permet definir pipelines com a codi. Aquestes s'escriuen en un llenguatge de domini específic (un DSL) en un fitxer anomenat Jenkinsfile.

Per a definir pipelines només cal una versió de Jenkins igual superior a 2.0 i tenir-hi el plugin "Jenkins Pipeline" instal·lat. Amb això ja podem crear el nostre Jenkinsfile. Aquest pot tenir dos tipus de sintaxi: declarativa o d'script. La de script empra una sintaxi basada en el llenguatge Groovy. La declarativa s'ha afegit més recentment i és l'opció recomanada pels mateixos creadors de Jenkins, ja que, en paraules seves, és una sintaxi més rica que la d'script alhora que resulta més senzilla d'escriure i llegir.

Un exemple de pipeline declarativa tindria un esquelet com el següent:

pipeline {
    agent any 
    stages {
        stage('Build') { 
            steps {
                // 
            }
        }
        stage('Test') { 
            steps {
                // 
            }
        }
        stage('Deploy') { 
            steps {
                // 
            }
        }
    }
}

Com podem veure, una pipeline es divideix en tantes fases (stages) com vulguem, i cadascuna agruparà un seguit de passos. L'agent indica on s'executarà la pipeline. En el cas anterior es defineix que tota la pipeline pot executar-se en qualsevol agent que hi hagi disponible, però també es pot definir un agent a nivell de cada stage. Jenkins soporta usar contenidors Docker com a agents, així que podem especificar que, per exemple, la fase de construcció requereix executar-se en un contenidor concret, mentre que la fase de deploy no requereix cap entorn Docker en concret. Jenkins s'encarregarà de construir o llançar el contenidor a partir de la imatge o el Dockerfile que s'especifiqui. Vegem ara la pipeline d'un projecte web nostre, fase a fase.

Fase de test

stage('Test') {
    agent {
        docker {
            image 'alekzonder/puppeteer'
        }
    }
    environment {
        NPM_CONFIG_CACHE = 'npm-cache'
        HOME = '/home/jenkins/data'
    }
    steps {
        echo 'Installing dependencies'
        sh 'npm install'
        echo 'Running tests'
        sh 'npm run test -- -c karma.jenkins.conf.js --code-coverage'
        junit 'reports/test-results/**/*.xml'
        archiveArtifacts 'reports/coverage/cobertura-coverage.xml'
        cobertura(autoUpdateHealth: false, autoUpdateStability: false, coberturaReportFile: 'reports/coverage/cobertura-coverage.xml', conditionalCoverageTargets: '70, 0, 0', failUnhealthy: false, failUnstable: false, lineCoverageTargets: '80, 0, 0', maxNumberOfBuilds: 0, methodCoverageTargets: '80, 0, 0', onlyStable: false, sourceEncoding: 'ASCII', zoomCoverageChart: false)
    }
}

El contenidor de la fase de test es crea d'una imatge de docker disponible a Dockerhub. La secció d'environment defineix certes variables d'entorn, i als steps instal·lem dependències, executem els tests i arxivem un fitxer de cobertura de codi. Què és això de la cobertura de codi? Els tests s'executen tot registrant quines parts del codi s'executen durant els tests i quines no, és a dir, monitoritzant la cobertura de codi. El resultat genera un fitxer en .xml que volem poder veure des de Jenkins després de l'execució de la pipeline. En això consisteix l'arxivament d'artefactes: guardar fitxers del node que executa una tasca per a fer-los accessibles fins i tot després de la destrucció del contenidor del contenidor de Docker.

Fase de desplegament

El següent pas és construir el projecte, una fase semblant a l'anterior però executant comandes de construcció en comptes de test. Per últim, fem el desplegament a una màquina de producció o preproducció.

stage('Deploy') {
    parallel {
        stage('DEMO') {
            when {
                branch 'develop'
            }
            steps {
                echo 'Deploying to DEMO'
                script {
                    docker.withRegistry('http://our.docker.registry.url:5000', 'registry-credentials-id') {
                    echo 'Building docker image'
                    def currentImage = docker.build('project/web:latest')
                    echo 'Pushing docker image to docker registry'
                    currentImage.push()
                    }
                }
                echo 'Publish docker-compose'
                sshPublisher(publishers: [sshPublisherDesc(configName: 'remote-machine-name', transfers: [sshTransfer(execCommand: 'cd ~/project/web && source ./scripts/prod/deploy_app.sh our.docker.registry.url:5000', execTimeout: 240000, sourceFiles: 'docker-compose.*, scripts/prod/deploy_app.sh', remoteDirectory: 'project/web')], usePromotionTimestamp: false, useWorkspaceInPromotion: false, verbose: false)])
            }
        }
        stage('PROD') {
            when {
                branch 'master'
            }
            steps {
                echo 'Deploying to PROD'
            }
        }
    }
}

El primer que crida l'atenció és que tenim dues fases dins d'una mateixa fase, sota la declaració de parallel. Resulta que una fase (stage) permet subagrupar les seves tasques en altres fases, les quals s'executaran independentment sense esperar-se mútuament - d'aquí que rebin el nom de fases paral·leles. En el nostre cas, però, les fases DEMO i PROD no s'executaran mai alhora, i és que van precedides de la clàusula when. Com molts haureu deduit, aquesta fa que s'executi la fase només quan es compleix certa condició. La complexitat de les condicions pot ser tan elevada com calgui, la sintaxi suportada dóna força llibertat. En l'exemple mostrat, tanmateix, la decisió es basa simplement en la branca de git en què estem.

Si Jenkins ha fet pull de la branca develop, desplegarem el resultat a la màquina de preproducció; si estem a la branca de master, a la màquina de producció. Fixeu-vos que el desplegament té dues parts: generar una imatge de docker amb la versió actual del codi i pujar-la a un registre propi, i connectar-se per SSH a la màquina corresponent per a executar l'script de desplegament. Aquest script fa un docker pull per a baixar-se la imatge que acabem de generar, i a continuació la desplega. Així es posa en marxa, de manera totalment automatitzada, la versió més recent del codi, i només si abans ha passat els tests.

Per acabar, podem mencionar que la pipeline té aspectes configurables com ara quantes execucions han de guardar-se per a poder-les consultar a posteriori, i quan ha d'executar-se la pròpia pipeline. Vegeu el següent exemple definint aquests aspectes.

options {
    buildDiscarder(logRotator(numToKeepStr: '10', artifactNumToKeepStr: '10'))
}
triggers {
    pollSCM('H/5 * * * *')
}

pollSCM defineix cada quan s'ha de consultar el repositori remot de git per a trobar canvis. Segueix el format de tasques cron. Hi ha una web fantàstica per a entendre les definicions d'horaris de cron: https://crontab.guru/

Amb això ja tindríem una pipeline funcional. Paga la pena dedicar una estona a definir un bon procés automatitzat de desplegament, a partir d'aquí els desenvolupadors podem dedicar-nos a treballar en el codi del projecte en si. Sobretot si fem que Jenkins ens avisi d'errors sense haver d'entrar a la web a consultar l'estat de cada execució. Nosaltres hem configurat Jenkins perquè ens enviï missatges d'Slack. Es poden definir accions a executar després de la Pipeline o després de cada fase amb la clàusula post. Allí podem enviar, a través del plugin d'Slack, diferents missatges segons l'estat en què hagi acabat l'execució.

post {
    failure {
        slackSend(channel: '#impd-ci', color: 'danger', message: "FAILED: Job '${env.JOB_NAME} [${env.BUILD_NUMBER}]' (${env.RUN_DISPLAY_URL}).")
    }
}

Blue Ocean

La pipeline que hem vist aquí, definint-la per codi, la podem visualitzar de manera més gràfica gràcies a la nova interfície de Jekins, anomenada Blue Ocean. Amb la representació gràfica ens podem fer una idea ràpidament del procés que la compon.

Si necessiteu més informació per a crear una pipeline de codi, podeu començar amb aquest tutorial de la web de Jenkins.

Segueix-nos a

Els nostres articles del bloc d'inLab FIB

         
         

inLab FIB incorpora esCert

Icona ESCERT

First LogoCSIRT Logo

inLab és membre de

CIT UPC