Gitlab.com - CI/CD di un sito statico su Kubernetes

September 25, 2018 0 Comments Kubernetes, gitlab, ci/cd

Pipeline, pipeline ovunque!

Buongiorno a tutti, in questa guida vi mostrerò un esempio di CI/CD di un deployment di un container contenente un sito statico in html. Questo esempio può essere tranquillamente adattato ad ogni tipo di deployment stateless che richiede un continuous integration/deployment.

Qui il link del progetto di esempio che ho creato ad hoc su gitlab.com: https://gitlab.com/nutellinoit/k8s-ci-cd-gitlab-example

Non utilizzerò l'integrazione nativa di Gitlab con Kubernetes bensì utilizzerò semplicemente il comando kubectl durante la pipeline.

Requisiti

  1. Dimestichezza con Docker
  2. Un cluster Kubernetes funzionante

Progetto Demo

L'alberatura del progetto è la seguente:

├── build_container
│  ├── Dockerfile
│  └── test.sh
├── kubernetes_manual
│  ├── create_login_docker_registry.sh
│  └── namespace.yml
├── rootfs
│  ├── canary
│  └── index.html
├── kube-deployment.yml
├── .gitlab-ci.yml
└── README.md

La cartella build_container contiene il file Dockerfile per le direttive di build, e un file di test che andremo ad aggiungere per testare successivamente se il container è stato creato correttamente.

La cartella rootfs contiene tutto il nostro stupendo e fantasmagorico sito html, formato da un file index.html e un file che ho chiamato canary su cui andremo ad eseguire il check di correttezza di build del container.

Sarà un sito fantasmagorico e automagico!

Infine nella root del progetto troviamo il file che useremo per aggiornare il deployment su kubernetes, kube-deployment.yml e il file importantissimo .gitlab-ci.yml che configura la pipeline del progetto gitlab.

In aggiunta a queste cartelle, ho messo anche una cartella di esempio dei pre-requisiti necessari per poter fare il deploy in produzione (la creazione del namespace e la creazione delle eventuali credenziali del registro da utilizzare per il pull, se il registro non è pubblico)

Il file .gitlab-ci.yml

Partiamo dal cuore del progetto, il file che definisce la pipeline.

# This file is a template, and might need editing before it works on your project.
# Official docker image.
image: docker:latest

build_website:  
  services:
  - docker:dind
  stage: build
  before_script:
  - docker login -u "$CI_REGISTRY_USER" -p "$CI_REGISTRY_PASSWORD" $CI_REGISTRY
  script:
  - rm -rf build_container/rootfs || true
  - cp -r rootfs build_container/rootfs
  - docker build --pull -t "$CI_REGISTRY_IMAGE":"$CI_PIPELINE_ID" build_container/
  - docker tag  "$CI_REGISTRY_IMAGE":"$CI_PIPELINE_ID" "$CI_REGISTRY_IMAGE":latest
  - docker push "$CI_REGISTRY_IMAGE":"$CI_PIPELINE_ID"
  - docker push "$CI_REGISTRY_IMAGE":latest
  - rm -rf build_container/rootfs || true


test_website:  
  services:
  - docker:dind
  stage: test
  before_script:
  - docker login -u "$CI_REGISTRY_USER" -p "$CI_REGISTRY_PASSWORD" $CI_REGISTRY
  script:
  - >
    docker run -t --rm
    -p 80:80
    $CI_REGISTRY_IMAGE:$CI_PIPELINE_ID
    /test.sh


deploy_website:  
  stage: deploy
  image: alpine:3.7
  environment:
    name: Production
  script:
  - apk update  && apk add --no-cache curl
  - curl -LO https://storage.googleapis.com/kubernetes-release/release/$(curl -s https://storage.googleapis.com/kubernetes-release/release/stable.txt)/bin/linux/amd64/kubectl
  - chmod +x ./kubectl && mv ./kubectl /usr/local/bin/kubectl
  - mkdir -p $HOME/.kube
  - echo $KUBE_CONFIG > ~/.kube/config
  - kubectl -n hellocicd get pods
  - sed -i "s/latest/$CI_PIPELINE_ID/g" kube-deployment.yml
  - kubectl -n hellocicd create -f kube-deployment.yml || true
  - kubectl -n hellocicd apply -f kube-deployment.yml

Utilizzeremo tre stage, build, test e deploy.


Build

Il primo stage, quello di build, si preoccupa di eseguire:

build_website:  
  services:
  - docker:dind
  stage: build
  before_script:
  - docker login -u "$CI_REGISTRY_USER" -p "$CI_REGISTRY_PASSWORD" $CI_REGISTRY
  script:
  - rm -rf build_container/rootfs || true
  - cp -r rootfs build_container/rootfs
  - docker build --pull -t "$CI_REGISTRY_IMAGE":"$CI_PIPELINE_ID" build_container/
  - docker tag  "$CI_REGISTRY_IMAGE":"$CI_PIPELINE_ID" "$CI_REGISTRY_IMAGE":latest
  - docker push "$CI_REGISTRY_IMAGE":"$CI_PIPELINE_ID"
  - docker push "$CI_REGISTRY_IMAGE":latest
  - rm -rf build_container/rootfs || true
....

Descrizione dei passaggi:

  1. Prima di eseguire i comandi dello script, il processo di build si loggherà all'interno del registro del progetto utilizzando delle variabili d'ambiente di sistema di gitlab già valorizzate (su gitlab sono valorizzate di default)
  2. Si pulisce la cartella build_container/rootfs se per qualche motivo è presente
  3. Si copia ricorsivamente il contenuto della cartella rootfs all'interno della cartella build_container in modo che durante il build Docker possa copiarla all'interno dell'immagine, essendo build_container il contesto.
  4. Si va ad eseguire il build dalla cartella build_container, nominando l'immagine come il nome dato da gitlab, nel nostro caso registry.gitlab.com/nutellinoit/k8s-ci-cd-gitlab-example taggandola con l'id della pipeline
  5. Si aggiunge il tag latest
  6. Si pushano le due immagini
  7. Si pulisce la cartella rootfs all'interno di build_container

Test

Una volta effettuato il build, andremo a vedere se effettivamente i file del nostro sito statico siano stati aggiunti nella directory corretta. Utilizzeremo lo script di test.sh che non è altro che un controllo se il file canary esiste nella cartella /usr/share/nginx/html

#!/usr/bin/env sh

if [ ! -f /usr/share/nginx/html/canary ]; then  
    echo "something wrong"
    exit 127
fi  

Il nostro stage sarà quindi il seguente:

...
test_website:  
  services:
  - docker:dind
  stage: test
  before_script:
  - docker login -u "$CI_REGISTRY_USER" -p "$CI_REGISTRY_PASSWORD" $CI_REGISTRY
  script:
  - >
    docker run -t --rm
    -p 80:80
    $CI_REGISTRY_IMAGE:$CI_PIPELINE_ID
    /test.sh
...

Descrizione dei passaggi

  1. Come prima il processo deve loggarsi nel registro prima di eseguire il pull del container
  2. Lanciamo il container appena creato, utilizzando l'id della pipeline corrente lanciando come entrypoint principale il nostro file test.sh

In questo modo se il nostro build per qualche motivo non sia andato come doveva, riusciamo a bloccare il deploy.


Deploy

Requisiti del deploy

Qui la faccenda diventa interessante, ci servirà:

  • Un file kubeconfig contenente le credenziali per poter accedere al cluster
  • Python, per lanciare una riga da command line per trasformarlo da yaml a json
  • Un namespace già configurato su kubernetes, nel nostro caso dovrà chiamarsi hellocicd e gli eventuali permessi per poterci interagire

Partiamo quindi a convertire il nostro file kubeconfig in json, lanciamo il comando:

python -c 'import sys, yaml, json; json.dump(yaml.load(sys.stdin), sys.stdout, indent=4)' < kubeconfigdemo.yml > kubeconfigdemo.json  

Prendiamo quindi il nostro json e andiamo su gitlab. Dovremmo aggiungere una variabile pipeline nelle impostazioni del progetto, KUBE_CONFIG. Di seguito un piccolo video guida:

Gitlab Video

Fatto questo, siamo pronti per vedere lo stage deploy

deploy_website:  
  stage: deploy
  image: alpine:3.7
  environment:
    name: Production
  script:
  - apk update  && apk add --no-cache curl
  - curl -LO https://storage.googleapis.com/kubernetes-release/release/$(curl -s https://storage.googleapis.com/kubernetes-release/release/stable.txt)/bin/linux/amd64/kubectl
  - chmod +x ./kubectl && mv ./kubectl /usr/local/bin/kubectl
  - mkdir -p $HOME/.kube
  - echo $KUBE_CONFIG > ~/.kube/config
  - kubectl -n hellocicd get pods
  - sed -i "s/latest/$CI_PIPELINE_ID/g" kube-deployment.yml
  - kubectl -n hellocicd create -f kube-deployment.yml || true
  - kubectl -n hellocicd apply -f kube-deployment.yml

Descrizione dei passaggi

  1. Utilizziamo l'immagine alpine e installiamo l'utility kubectl per interagire con il cluster kubernetes
  2. Creiamo la cartella .kube all'interno della home corrente (è la cartella di default che l'utility kubectl utilizza per caricare il config)
  3. Copiamo il nostro JSON salvato nella variabile pipeline KUBE_CONFIG nel file ~/.kube/config
  4. Sostituiamo "latest" dal file di deployment di kubernetes con l'id della pipeline, per avere sempre il container con l'immagine esatta corrispondente alla pipeline
  5. Creiamo il deployment se non esiste ( il || true serve nel caso esista, non vogliamo che il processo di deploy si fermi)
  6. Aggiorniamo il deployment

Se tutto va bene ci troviamo nella situazione di avere nel nostro cluster kubernetes il nostro nuovo fiammante deployment!

$ kubectl -n hellocicd get pods
NAME                                 READY     STATUS    RESTARTS   AGE  
hellocicd-webiste-78d566856d-9xhq6   1/1       Running   0          3m  

alla prossima!

Samuele Chiocca
Padova, italy Website