GitLab, un completo strumento DevOps – Parte 3: gestire la CI mediante runner di tipo container

Introduzione

È inutile negarlo, quando le cose iniziano a complicarsi è sempre difficile mantenere alta la qualità dei progetti. In fondo tutta questa storia dei DevOps nasce da lì. Per quanto sia una buzzword ormai sulla bocca di tutti, è innegabile, la metodologia DevOps cerca di risolvere un problema che è evidente: i ritmi di sviluppo richiesti dalle applicazioni sono cambiati.

Un’applicazione moderna deve evolvere, mantenere le performance e rimanere sicura, il tutto in tempi brevissimi, non complicando il codice e consentendo a tutte le persone che collaborano di avere lo stesso grado di integrazione.

Pare facile, ma poi uno guarda questa immagine:

Testing Workflow in the Real World, credits https://devops.com

E si rende conto che no, non lo è affatto.

Analizzare il proprio codice

Considerare quindi i test del proprio codice come elemento essenziale del proprio ciclo di sviluppo è indubbiamente critico. I dettami del metodo DevOps non aggiungono nulla a tutto questo – che sì, è solo buonsenso -, semplicemente spingono per azzerare quello che nell’immagine sopra viene identificato come “Time needed to get test to work in test environment“.

In parole povere, creare una replicabilità certa per i propri test permette di salvare enormi quantità di tempo durante il ciclo di vita dell’applicazione. E qual’è lo strumento che consente di avere lo stesso tipo di ambiente, in termini di librerie e dipendenze, su sistemi diversi?

Indovinato, sono i container.

Controllare codice mediante container

Una delle forme più semplici di controllo preliminare del codice è quella mediante linter. Disponibili per praticamente tutti i linguaggi, i linter sono dei programmi che analizzano i sorgenti e forniscono dei report sullo stato del codice.

Il codice utilizzato nel progetto diesempio è di tipo markdown, nel Docker registry di MMUL esiste un linter per il controllo di questo tipo di codice chiamato markdown-linter utilizzabile in questa forma a linea di comando:

rasca@anomalia [~/myfirstproject]> sudo docker run -it -v /home/rasca/myfirstproject:/builds/rasca/myfirstproject www.mmul.it:5000/markdown-linter:latest markdownlint /builds/rasca/myfirstproject/*.md

Quindi mappando la cartella del progetto myfirstproject all’interno di /builds/rasca/myfirstproject/ (mediante opzione -v /home/rasca/myfirstproject:/builds/rasca/myfirstproject/) i file con estensione .md verranno analizzati (il perché è stato scelto di mappare lo specifico path /builds/rasca/myfirstproject/ sarà chiaro appena verrà descritta la sezione GitLab).

Escludendo quindi dall’output la sezione di download del container, quello che il comando restituisce sono essenzialmente due linee:

/builds/rasca/myfirstproject/MyFirstFile.md: 1: MD041/first-line-heading/first-line-h1 First line in file should be a top level heading [Context: "This is my first documentation..."]
/builds/rasca/myfirstproject/README.md: 3: MD012/no-multiple-blanks Multiple consecutive blank lines [Expected: 1; Actual: 2]

Quindi no, il codice in questione non è di qualità. Ottimo pretesto per attivare una pipeline CI che bocci il commit, evidenziandone il fallimento.

In maniera automatica.

Per ciascuno dei commit che verranno effettuati all’interno del repository myfirstproject.

Ecco, questo è un approccio DevOps.

La CI secondo GitLab

GitLab potrebbe essere utilizzato unicamente come motore di repository, solo cioè per registrare i commit ed eventualmente gestire le issue, ma uno dei maggiori punti di forza di questo progetto è certamente il suo motore CI.

Il meccanismo è semplice, è sufficiente che all’interno del progetto interessato esista un file denominato .gitlab-ci.yml ed il processo di CI verrà automaticamente attivato.

Il formato di .gitlab-ci.yml

Il file contenente le informazione relative alla CI è di tipo YAML e, come ampiamente descritto nella documentazione ufficiale di GitLab, descrive tutte le fasi (o stage) necessari alla verifica del progetto.

Sulla base di quanto illustrato sopra, per effettuare la verifica del codice markdown il file YAML .gitlab-ci.yml potrebbe avere questo contenuto:

image: www.mmul.it:5000/markdown-linter
check:
  script:
    - markdownlint *.md

Analizzando ogni linea:

  1. image: www.mmul.it:5000/markdown-linter indica che la CI andrà eseguita all’interno dello specifico container lanciato dall’immagine di cui è riportato l’indirizzo;
  2. check indica il nome della fase, o più propriamente lo stage (e può essere arbitrario);
  3. script indica una sezione dello stage che prevede l’esecuzione di uno o più comandi
  4. - markdownlint *.md riporta l’effettivo comando che verrà eseguito;

Il problema è che, quant’anche si decidesse di effettuare un commit e conseguente push del file .gitlab-ci.yml la pipeline che ne scaturirebbe fallirebbe miseramente, ed il motivo è molto semplice: GitLab non sa dove eseguire le operazioni descritte.

GitLab ha bisogno di un runner.

Che cos’è un runner per GitLab?

Un runner è, come dice il nome stesso, un esecutore. Una componente che, opportunamente configurata, può essere utilizzata da GitLab per far sì che ciascuna modifica passi attraverso un processo per venire opportunamente validata.

Esistono svariati tipi di runner configurabili in GitLab, e l’argomento è certamente estendibile a dismisura, ma per quanto concerne questa specifica serie di articoli ci limiteremo a descrivere come implementare un runner che sfrutti il container markdown-linter utilizzato, poco sopra, durante il controllo manuale del codice.

La prima cosa da fare però è configurare il servizio gitlab-runner che si occuperà di gestire le richieste che arrivano dal server GitLab.

Attivare il container gitlab-runner

La domanda è lecita: perché il servizio gitlab-runner non è integrato all’interno del server GitLab? La risposta, se ci si pensa, è molto semplice: per essere scalabile. Di fronte alla necessità di testare centinaia di commit allo stesso tempo e su ambienti potenzialmente diversi avere la possibilità di costruirsi batterie di runner diventa essenziale.

Pertanto gitlab-runner è il servizio che va configurato laddove si vuole abilitare una macchina ad essere disponibile come veicolo delle esecuzioni dei runner.

Esistono versioni pacchettizzate di gitlab-runner che installano un servizio controllabile mediante systemd, ma, rispettando l’approccio utilizzato nei precedenti articoli, la configurazione descritta sarà mediante container, un container nominato, manco a dirlo, gitlab-runner.

Dopo aver creato la cartella che ne conterrà le configurazioni:

rasca@anomalia [~]> sudo mkdir -p /home/rasca/gitlab/gitlab-runner/config

si attiva in questo modo:

rasca@anomalia [~]> sudo docker run -d --name gitlab-runner --restart unless-stopped -v /home/rasca/gitlab/gitlab-runner/config:/etc/gitlab-runner -v /var/run/docker.sock:/var/run/docker.sock -v /etc/hosts:/etc/hosts -v /home/rasca/gitlab/config/ssl/gitlab.mmul.local.crt:/etc/gitlab-runner/certs/gitlab.mmul.local.crt gitlab/gitlab-runner

Analizzando la linea di comando:

  • sudo docker run -d --name gitlab-runner --restart unless-stopped avvia un container denominato gitlab-runner in modalità detouched che verrà sempre (in caso di reboot o crash) riavviato, a meno di non essere stoppato manualmente;
  • -v /home/rasca/gitlab/gitlab-runner/config:/etc/gitlab-runner mappa la cartella delle configurazioni (creata vuota sopra) con il path locale nel container;
  • -v /var/run/docker.sock:/var/run/docker.sock mappa il socket del demone docker di sistema a quello del container. Questa opzione è essenziale per permettere al runner di avviare a sua volta dei container. Di essere quindi un docker nel docker;
  • -v /etc/hosts:/etc/hosts mappa il file /etc/hosts locale a quello del container in modo da permettergli la risoluzione del nome scelto, in questo caso gitlab.mmul.local, rispondente a 172.17.0.1;
  • -v /home/rasca/gitlab/config/ssl/gitlab.mmul.local.crt:/etc/gitlab-runner/certs/gitlab.mmul.local.crt mappa il singolo file relativo al certificati (lo stesso utilizzato anche dal container del servizio GitLab) in modo che il servizio possa rilevare come valido il certificato SSL dell’host gitlab.mmul.local;
  • gitlab/gitlab-runner indica che il container sarà avviato da un’immagine presente sul Docker HUB nel progetto gitlab con il nome di gitlab-runner;

Una volta avviato il container si avrà quindi il servizio gitlab-runner in attesa di richieste. Il server GitLab però non sa ancora nulla di questo servizio e, come è facile notare, non vi è in esso alcuna menzione del container specifico markdown-linter menzionato sopra.

Questo perché è necessario registrare l’esecutore specifico per quel container che, guardacaso, è un container.

Registrare il runner che verrà utilizzato nella pipeline

Nella sezione del progetto “Settings” -> “CI / CD“, cliccando sul bottone “Runners” si dovrebbe ottenere una schermata simile alla seguente:

Osservando “Set up a specific Runner manually” vengono indicate tre informazioni essenziali:

  1. L’installazione di GitLab Runner (fatta nel paragrafo precedente);
  2. L’indicazione della URL relativa a GitLab, cioè https://gitlab.mmul.local;
  3. L’indicazione del Token da utilizzare per la registrazione;

Alla luce di tutto questo, la registrazione del runner è effettuabile attraverso il container gitlab-runner, mediante il comando docker exec:

rasca@anomalia [~]> sudo docker exec -it gitlab-runner gitlab-runner register --non-interactive --name="markdown-linter" --url="https://gitlab.mmul.local" --registration-token="ZCC2wHXa6psixmBebdJg" --executor="docker" --docker-image="www.mmul.it:5000/markdown-linter:latest" --docker-volumes="/etc/hosts:/etc/hosts"

Analizzando la linea di comando:

  • sudo docker exec -it gitlab-runner gitlab-runner register esegue all’interno del container gitlab-runner in modalità interattiva il comando gitlab-runner register; a cui vengono passate diverse opzioni;
  • --non-interactive specifica di non richiedere iterazione con l’utente;
  • --name="markdown-linter" assegna il nome al runner;
  • --url="https://gitlab.mmul.local" --registration-token="ZCC2wHXa6psixmBebdJg" definiscono url e token relativi al server GitLab;
  • --executor="docker" stabilisce che il runner sarà un container docker (che il servizio gitlab-runner può gestire in quanto, come descritto sopra, ne controlla il socket);
  • --docker-image="www.mmul.it:5000/markdown-linter:latest" specifica l’immagine interessata;
  • --docker-volumes="/etc/hosts:/etc/hosts" mappa nel container come di consueto il file /etc/hosts per consentire, anche al runner, di risolvere correttamente l’indirizzo gitlab.mmul.local;

L’output prodotto sarà qualcosa di simile a questo:

Runtime platform                                    arch=amd64 os=linux pid=789 revision=a987417a version=12.2.0
Running in system-mode.                            
                                                   
Registering runner... succeeded                     runner=ZCC2wHXa
Runner registered successfully. Feel free to start it, but if it's running already the config should be automatically reloaded!

E dall’interfaccia il runner dovrà risultare effettivamente registrato:

Quanto creato dal comando è osservabile all’interno del file di configurazione di gitlab-runner, che vede aggiunto il seguente contenuto:

rasca@anomalia [~]> sudo cat /home/rasca/gitlab/gitlab-runner/config/config.toml 
concurrent = 1
check_interval = 0

[session_server]
  session_timeout = 1800

[[runners]]
  name = "markdown-linter"
  url = "https://gitlab.mmul.local"
  token = "yt5Ss3V11gjLjzaHpSGu"
  executor = "docker"
  [runners.custom_build_dir]
  [runners.docker]
    tls_verify = false
    image = "www.mmul.it:5000/markdown-linter:latest"
    privileged = false
    disable_entrypoint_overwrite = false
    oom_kill_disable = false
    disable_cache = false
    volumes = ["/etc/hosts:/etc/hosts", "/cache"]
    shm_size = 0
  [runners.cache]
    [runners.cache.s3]
    [runners.cache.gcs]

Pertanto, sì, i runner possono essere definiti anche a mano, modificando il file di configurazione e riavviando il servizio (mediante riavvio del container).

Bene, è giunta l’ora di scatenare il primo processo di CI.

Eseguire il primo processo di CI

Il contenuto del file .gitlab-ci.yml all’interno del repository myfirstproject è quindi il seguente:

image: www.mmul.it:5000/markdown-linter
check:
  script:
    - markdownlint *.md

La sequenza di comandi da eseguire per effettuare il push di questo file nel repository sarà quindi:

rasca@anomalia [~/myfirstproject]> git add .gitlab-ci.yml
rasca@anomalia [~/myfirstproject]> git commit -m "Enabling CI"
rasca@anomalia [~/myfirstproject]> git push

Lo stato dei commit sarà quindi qualcosa di simile a questa immagine:

La X rossa indica che qualcosa non va, ed essendo un link sarà possibile cliccarci sopra per accedere al dettaglio:

La pipeline in questione (in questo caso la numero 12) è fallita. Ma come? Dal menù di sinistra, cliccando nella sezione “CI / CD” si potrà accedere alla sezione “Jobs” che mostrerà la lista dei job eseguiti. Il primo in alto fallito è a sua volta cliccabile per ottenere il dettaglio:

Come manualmente verificato, anche l’automazione della CI certifica come i file .md abbiano delle anomalie.

Da notare, osservando l’output del job, il messaggio

Reinitialized existing Git repository in /builds/rasca/myfirstproject/.git/

che certifica come la cartella da cui vengono lanciati i comandi nel container sia /builds/rasca/myfirstproject/. Proprio per questo nell’esempio manuale ad inizio articolo la mappatura della cartella di lavoro era stata fatta in maniera speculare. Ciò che è stato ottenuto è la riproduzione automatizzata dell’esatto processo manuale.

Risolvere i problemi mediante un nuovo commit

I problemi rilevati sono due e, senza scendere nel dettaglio della sintassi markdown, per risolverli bisognerà modificarli come segue:

MyFirstFile.md necessita di un carattere “#” all’inizio della prima riga:

# This is my first documentation file

mentre in README.md va tolta la riga vuota in fondo:

# MyFirstProject

Due semplici modifiche quindi, che opportunamente caricate nel repository:

rasca@anomalia [~/myfirstproject]> git add MyFirstFile.md README.md
rasca@anomalia [~/myfirstproject]> git commit -m "Adding fixes suggested by linter"
rasca@anomalia [~/myfirstproject]> git push

Scateneranno un nuovo processo di CI che questa volta avrà esito positivo:

Lo stato è quindi “Passed“, la pipeline ha avuto successo.

Conclusioni

Per quanto possa sembrare complesso a qualsiasi neofita dell’argomento, questo articolo sorvola solamente la superficie del macrocosmo relativo a GitLab. Non sono stati ancora trattati ad esempio gli aspetti di Continuous Delivery (fare cioè in modo che un commit non possa andare in master se non passa la CI) o come correlare le Issue viste nello scorso articolo a dei commit per innescare il processo collaborativo proprio della metodologia DevOps.

Per ora intanto la speranza è quella di aver suscitato sufficiente curiosità e descritto in maniera coerente gli strumenti necessari ad un approfondimento personale.

In attesa di una nuova puntata, buon divertimento!

Da sempre appassionato del mondo open-source e di Linux nel 2009 ho fondato il portale Mia Mamma Usa Linux! per condividere articoli, notizie ed in generale tutto quello che riguarda il mondo del pinguino, con particolare attenzione alle tematiche di interoperabilità, HA e cloud.
E, sì, mia mamma usa Linux dal 2009.

Tags: , , , ,