Code, Learn

i3 Hub: un caso di studio con GitLab CI

21 Settembre 2020 - 5 minuti di lettura

La pubblicazione è uno step fondamentale per rendere la propria web app utilizzabile dal pubblico.

Questo step non dovrebbe essere di ostacolo e possibilmente dovrebbe essere trasparente e automatico per ogni sviluppatore.

In questo articolo viene descritta l’architettura Hub che usiamo in Intré per pubblicare velocemente minisiti e proof of concept.

Visione di alto livello

La soluzione proposta vuole rispondere ad un’esigenza specifica dello sviluppatore: “ho l’applicazione che funziona sulla mia macchina, come la rendo disponibile su Internet?”.

Affinché l’architettura sia sensata e funzionale sono necessarie:

  • una infrastruttura che permetta di servire app;
  • una modalità di pubblicazione che permetta di aggiungere app.

Infrastruttura

Scopo di questo articolo non è descrivere in dettaglio la parte infrastrutturale ma è d’obbligo descriverla brevemente per capire tutti gli elementi coinvolti.

Le varie applicazioni vengono eseguite come container Docker [1] su una macchina host. I container, che sono istanze di immagini Docker, espongono delle porte che vengono mappate sulle porte dell’host stesso.

Un proxy esposto su Internet si occupa di mappare i nomi DNS sulle porte specifiche esposte dalle applicazioni. A livello di questo proxy viene fatto il setup dei certificati per HTTPS.

Pubblicazione

La pubblicazione di un’applicazione si appoggia su tre ulteriori elementi infrastrutturali:

  • un registro Docker, nel nostro caso Harbor [2], per contenere le immagini Docker;
  • GitLab CI [3] per eseguire gli script di configurazione;
  • un runner Gitlab [4] che eseguirà i comandi specificati.

Con questo setup il progetto dovrà avere un dockerfile [5] che permetta di creare un’immagine contenente l’applicazione.

Configurazione

La configurazione avviene tramite i seguenti step:

  • creazione dell’immagine Docker della app;
  • pubblicazione dell’immagine sul registry;
  • avvio di un container basato su quell’immagine;
  • configurazione dei proxy.

Di seguito approfondiremo la struttura dello script GitLab che si occupa della fase build & deploy.

GitLab CI – il file .gitlab-ci.yaml

Lo script di GitLab CI deve essere salvato in un file chiamato .gitlab-ci.yaml, committato nella root del progetto.

La prima sezione dello script  è relativa alla definizione di alcune variabili da usare nei successivi step:

variables:
  IMAGE_NAME: "gilde"
  IMAGE_TAG: "latest"
  CONTAINER_NAME: "gilde"

Sono quindi definiti due step per la creazione dell’immagine Docker e la pubblicazione sul registry Harbor:

build_image:
  script:
    - docker image build -t "$REGISTRY_PATH"/"$IMAGE_NAME":"$IMAGE_TAG" .
  only:
    - master

push_image:
  script:
    - docker push "$REGISTRY_PATH"/"$IMAGE_NAME":"$IMAGE_TAG"
  only:
    - master

Ogni step è composto da:

  • un nome arbitrario, ad esempio build_image;
  • una lista di comandi da eseguire;
  • una condizione (opzionale ) che limita l’esecuzione dello script ad uno specifico branch.

Nota: gli step sopra elencati fanno uso delle variabili definire all’inizio del file. Viene usata una ulteriore variabile $REGISTRY_PATH: questa è definita nella sezione Settings CI / CD del gruppo di progetti della pagina web del vostro GitLab. In questo modo ogni progetto può beneficiare da questa configurazione comune.

In un ulteriore step start_container sono definiti i comandi per avviare il container: noterete che la maggior parte di essi sono gli stessi che potete eseguire per creare e istanziare l’immagine Docker sul vostro computer.

start_container:
  script:
    - docker -H "$DOCKER_HOST" stop "$CONTAINER_NAME" || true
    - docker -H "$DOCKER_HOST" rm "$CONTAINER_NAME" || true
    - 'echo starting container $CONTAINER_NAME with ports "$CONTAINER_PORT_MAPPING"'
    - docker -H "$DOCKER_HOST" run --name "$CONTAINER_NAME" -d --restart always -p "$CONTAINER_PORT_MAPPING" "$REGISTRY_PATH"/"$IMAGE_NAME":"$IMAGE_TAG"
  only:
    - master

I comandi Docker devono essere eseguiti non sul runner bensì sull’host Docker finale e questo è possibile tramite il parametro -H che si appoggia ad una ulteriore variabile definita a livello di gruppo.

Alcuni comandi sono racchiusi tra apici per evitare che caratteri speciali contenuti nel comando stesso rendano invalida la sintassi del file yaml.

Nota: avrete notato che manca all’appello la variabile $CONTAINER_PORT_MAPPING. Abbiate pazienza, ci torneremo a breve.

GitLab CI- script comuni

Per rendere la soluzione Hub più fruibile si è rivelata la necessità di fornire script che semplificassero alcune operazioni. Dopo vari tentativi con extend e include di yaml, non abbastanza flessibili per i nostri scopi, la soluzione migliore si è rivelata quella di creare un repository contenente script bash da invocare puntualmente.

Possiamo quindi svelare un altro elemento definito nello script subito dopo la dichiarazione delle variabili

before_script:
  - export SCRIPTS_DIR=$(mktemp -d)
  - git clone -q --depth 1 "$SCRIPTS_REPO" "$SCRIPTS_DIR"
  - 'freeport=$(bash $SCRIPTS_DIR/portFinder.sh "$DOCKER_HOST")'
  - 'echo found a free port $freeport'
  - export CONTAINER_PORT_MAPPING="$freeport:80"
  - docker login -u 'robot$gitlab' -p "$cicd_token" "$REGISTRY_HOST"

La parte essenziale sono le prime due righe in cui viene creata una cartella temporanea e in quella cartella viene clonato il repository con gli script comuni.

Ancora una volta l’url del repository di script è fornito globalmente. È utile sapere che il clone può sfruttare delle credenziali disponibili solo per l’ambiente di CI / CD: l’url dovrebbe essere così formato https://gitlab-ci-token:${CI_JOB_TOKEN}@gitlab.intre.it/intre/hub-configurator.git; la variabile ${CI_JOB_TOKEN} viene iniettata da GitLab

Nota: gli export delle variabili vanno necessariamente inseriti nella sezione before_script ma non nei successivi script. Per capirci, l’export viene eseguito ma la variabile non sarà disponibile negli step successivi.

Veniamo dunque alla nostra variabile CONTAINER_PORT_MAPPING. Come ho anticipato, il container espone una porta che deve essere poi mappata su una porta dell’host. La porta del container è fissa e nel nostro caso è la 80. La porta dell’host invece deve essere scelta tra una delle porte libere. Per semplificare questa operazione è stato approntato uno script portFinder che identifica una porta disponibile. Viene quindi composta la stringa di port mapping che potrà quindi essere usata all’avvio del container.

Infine viene eseguita l’istruzione docker login usando un robot account per abilitare la connessione al registry.

Nota: mentre il token viene passato tramite variabile configurata a livello di gruppo, il nome è stato riportato nel codice in quanto Harbor genera nomi che iniziano obbligatoriamente con la stringa robot$ e purtroppo il carattere $ non è ammesso come valore di una variabile GitLab.

GitLab CI – setup del proxy

Come ultimo script del file ci occupiamo del setup del proxy e del nome di dominio:

expose_container:
  script:
    - 'containerport=$(bash $SCRIPTS_DIR/getPort.sh "$CONTAINER_NAME" "$DOCKER_HOST")'
    - 'echo exposing container "$FE_CONTAINER_NAME" with hostname $HOSTNAME with port $containerport'
    - 'bash $SCRIPTS_DIR/exposeInternet.sh "$HOSTNAME" "$containerport"'
  only: 
    - master

Viene inizialmente recuperata la porta del container scelta negli step precedenti dopodiché viene chiamato uno script di utility che prende come parametri l’hostname da configurare (definito nelle variabili a inizio file) e la porta da mappare. Questo script si occuperà di eseguire il setup sul proxy NGINX [6] e generare il certificato HTTPS tramite Let’s Encrypt [7].

Conclusioni

Tramite questo setup ogni progetto ha la possibilità di essere esposto su Internet in HTTPS copiando e adattando un file yaml relativamente semplice.

Articolo scritto da