Introduzione

Kubernetes è più che oggetti di base e semplici configurazione.

Vi sono molti altri oggetti e ambienti utili nello sviluppo di applicativi più complessi, e utilities per la pacchettizzazione, distribuzione e gestione dei nostri applicativi.

Vengono qui esaminati:

  • un numero di oggetti Kubernetes più complessi
  • aspetti più avanzati di oggetti già noti
  • l'utility di pacchettizzazione Helm

Si parte dal cluster Kind con configurazione standard già usato in precedenza.

Licenza

La presente documentazione è distribuita secondo i termini della GNU Free Documentation License Versione 1.3, 3 novembre 200t8.

Ne è garantita assoluta libertà di copia, ridistribuzione e modifica, sia a scopi commerciali che non.

L'autore detiene il credito per il lavoro originale ma nè credito nè responsabilità per le modifiche.

Qualsiasi lavoro derivato deve essere conforme a questa stessa licenza.

Il testo pieno della licenza è a:

https://www.gnu.org/licenses/fdl.txt

Tecnologia Kubernetes

Preparazione cluster KinD

Questi sono i passi per la preparazione di un cluster Kubernetes basato su Kind.

Partiamo da un sistema con il seguente software installato:

  • Docker - meglio se ultima versione
  • Docker Compose
    • come script standalone in Python
    • (meglio) come componente di Docker stesso

I comandi qui illustrati useranno l'utility standalone. Per usare invece il Docker Compose integrato in Docker, sostituire docker compose al posto di docker-compose.

NOTA

I files di configurazione del cluster Kind standard sono raccolti nel file tar cluster-kind-std.tar Scompattare tale file nella directory di login. Procedere quindi alla sezione Lancio del Cluster Kubernetes.

kubectl

Scaricare e installare kubectl:

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
sudo mv ./kubectl /usr/local/bin/kubectl
sudo chown root:root /usr/local/bin/kubectl

kind

Installiamo ora kind:

curl -Lo ./kind https://github.com/kubernetes-sigs/kind/releases/download/v0.14.0/kind-linux-amd64 && \
  chmod +x ./kind && \
  sudo mv ./kind /usr/local/bin/kind

Script di Attivazione del Cluster

Tutti gli esercizi utilizzeranno la versione del cluster costruito con Kind.

Editare il file di configurazione:

cd
vim std.sh
#!/bin/sh
set -o errexit

# crea il contenitore del registry se non esiste
reg_name='kind-registry'
reg_port='5000'
if [ "$(docker inspect -f '{{.State.Running}}' "${reg_name}" 2>/dev/null || true)" != 'true' ]; then
  docker run \
    -d --restart=always -p "127.0.0.1:${reg_port}:5000" --name "${reg_name}" \
    -v $HOME/.docker/registry:/var/lib/registry registry:2
fi

# crea un cluster con il registry locale abilitato in containerd
cat <<EOF | kind create cluster --image kindest/node:v1.24.0 --config=-
kind: Cluster
apiVersion: kind.x-k8s.io/v1alpha4
containerdConfigPatches:
- |-
  [plugins."io.containerd.grpc.v1.cri".registry.mirrors."localhost:${reg_port}"]
    endpoint = ["http://${reg_name}:5000"]
nodes:
- role: control-plane
  extraMounts:
    - hostPath: /data
      containerPath: /data
- role: worker
  extraMounts:
    - hostPath: /data
      containerPath: /data
- role: worker
  extraMounts:
    - hostPath: /data
      containerPath: /data
EOF

# connette il registry alla rete del cluster se non connesso
if [ "$(docker inspect -f='{{json .NetworkSettings.Networks.kind}}' "${reg_name}")" = 'null' ]; then
  docker network connect "kind" "${reg_name}"
fi

# Documenta il local registry
cat <<EOF | kubectl apply -f -
apiVersion: v1
kind: ConfigMap
metadata:
  name: local-registry-hosting
  namespace: kube-public
data:
  localRegistryHosting.v1: |
    host: "localhost:${reg_port}"
    help: "https://kind.sigs.k8s.io/docs/user/local-registry/"
EOF

Renderla eseguibile:

chmod +x ~/std.sh

Shell Scripts per la Gestione del Cluster

setup

sudo vim ~/setup.sh
#! /bin/sh
echo "Creating cluster ..."
echo
echo "~/std.sh"
~/std.sh
echo "Cluster info follows ..."
kubectl cluster-info --context kind-kind

echo
echo "---> Loading and configuring the Metallb load balancer ..."
echo
echo "-- Namespace"
echo "kubectl apply -f scripts/metallb-ns.yml"
kubectl apply -f scripts/metallb-ns.yml
echo
echo "-- Deployment and service"
echo "kubectl apply -f scripts/metallb-svc.yml"
kubectl apply -f scripts/metallb-svc.yml
echo
echo "-- Configmap"
echo "kubectl apply -f scripts/metallb-configmap.yml"
kubectl apply -f scripts/metallb-configmap.yml
echo
echo "Wait up to 120s for Metallb controller deployment to be ready ..."
kubectl wait deployment -n metallb-system controller --for condition=Available=True --timeout=120s
echo
echo "    CLUSTER NOW READY"
echo " ===> All resources in namespace 'default'"
kubectl get all
echo
echo " ===> All resources in namespace 'metallb-system'"
kubectl get all -n metallb-system

Renderla eseguibile:

sudo chmod +x ~/setup.sh

teardown

sudo vim ~/teardown.sh
echo "About to delete cluster 'kind'"
echo "Press Control-C within 10 seconds to interrupt"
sleep 10
kind delete cluster
echo "Cluster deleted"

Renderla eseguibile:

sudo chmod +x ~/teardown.sh

Manifests di Gestione del Cluster

Metallb Load Balancer

Scarico dei manifest necessari dalla rete e trasferimento al contenitore kub:

mkdir -p ~/scripts

wget https://raw.githubusercontent.com/metallb/metallb/v0.12.1/manifests/namespace.yaml
sudo mv namespace.yaml ~/scripts/metallb-ns.yml

wget https://raw.githubusercontent.com/metallb/metallb/v0.12.1/manifests/metallb.yaml
sudo mv metallb.yaml ~/scripts/metallb-svc.yml

Configmap per Metallb

sudo vim ~/scripts/metallb-configmap.yml
apiVersion: v1
kind: ConfigMap
metadata:
  namespace: metallb-system
  name: config
data:
  config: |
    address-pools:
    - name: default
      protocol: layer2
      addresses:
      - 172.18.255.200-172.18.255.250

Lancio del Cluster Kubernetes

Scaricare l'immagine del cluster:

docker pull kindest/node:v1.24.0

Partenza del cluster:

~/setup.sh

Impiegherà un po' di tempo aggiuntivo, poichè scaricherà la sua immagine di default del cluster.

Il File Kubeconfig

Kubeconfig è un file Yaml con tutti i dettagli del cluster Kubernetes, i certificati, i token segreti.

Può essere autogenerato dall'utility di costruzione cluster o ricevuto dal cloud provider.

La sua locazione tipica ès $HOME/.kube/config.

Si può usare un altro file, ma occorre indicarlo con la variabile d'ambiente KUBECONFIG.

Per esempio:

export KUBECONFIG=$HOME/.kube/dev_cluster_config

oppure:

kubectl get nodes --kubeconfig=$HOME/.kube/dev_cluster_config
KUBECONFIG=$HOME/.kube/dev_cluster_config kubectl get nodes

Per esaminare il nostro, generato da Kind:

less ~/.kube/config

Una struttura più generica può essere:

apiVersion: v1
clusters:
- cluster:
    certificate-authority-data: <ca-data-here>
    server: https://your-k8s-cluster.com
  name: <cluster-name>
contexts:
- context:
    cluster:  <cluster-name>
    user:  <cluster-name-user>
  name:  <cluster-name>
current-context:  <cluster-name>
kind: Config
preferences: {}
users:
- name:  <cluster-name-user>
  user:
    token: <secret-token-here>

Qualora vi fossero più files di configurazione di cluster, si può compiere il merge coi comandi, per esempio:

KUBECONFIG=config:dev_config:test_config kubectl config view --merge --flatten > config.new
mv $HOME/.kube/config $HOME/.kube/config.old
mv $HOME/.kube/config.new $HOME/.kube/config

Vi sono tre configurazioni principali:

  • clusters - con il nome, la URL del server e il certificato
  • contexts - con il nome, il cluster a cui si riferisce e l'utente con cui accede
  • users - con il nome e le credenziali d'accesso, spesso un Client Certificate

Sono degli arrays, e possono avere più elementi.

Contesto di kubectl

kubectl invia le sue richieste al Server API di un determinato cluster, il contesto.

Listare i contesti disponibili:

kubectl config get-contexts

Cambiare contesto:

kubectl config use-context <context-name>  

Si può anche cancellare un contesto, ma non è consigliabile, meglio usare l'utility specifica per la gestione del cluster, p.es. kind.

kubectl config delete-context <context-name>

Aspetti Avanzati I

Un particolare cluster Kubernetes ha un numero di oggetti, o risorse, disponibili.

Per listarli usare il comando:

kubectl api-resources

Daemonset

Un DaemonSet si assicura che ogni nodo del cluster abbia un pod dell'immagine specificata.

Si può restringere il numero di nodi coinvolti con le direttive nodeSelector, nodeAffinity, Taints, e Tolerations.

Usi di DaemonSet:

  • Raccolta di Log - p.es. fluentd , logstash, fluentbit
  • Monitoraggio di Cluster - p.es. Prometheus
  • Benchmark - p.es. kube-bench
  • Sicurezza - sistemi di detezione intrusioni o scansori di vulnerabilità
  • Storaggio - p.es. un plugin di storaggio su ogni nodo
  • Gestione di Rete - p.es. il plugin Calico per CNI

Esempio di DaemonSet

vim ~/scripts/fluentd.yml
apiVersion: apps/v1
kind: DaemonSet
metadata:
  name: fluentd
  namespace: logging
  labels:
    app: fluentd-logging
spec:
  selector:
    matchLabels:
      name: fluentd
  template:
    metadata:
      labels:
        name: fluentd
    spec:
      containers:
      - name: fluentd-elasticsearch
        image: quay.io/fluentd_elasticsearch/fluentd:v2.5.2
        resources:
          limits:
            memory: 200Mi
          requests:
            cpu: 100m                                      
            memory: 200Mi                                  
        volumeMounts:                                      
        - name: varlog                                     
          mountPath: /var/log                              
      terminationGracePeriodSeconds: 30                    
      volumes:                         
      - name: varlog                   
        hostPath:                      
          path: /data/varlog

E' opportuno che ogni DaemonSet sia specificato entro il suo namespace esclusivo.

Creare il namespace ed applicare il manifest:

kubectl create ns logging
kubectl apply -f scripts/fluentd.yml

Verifica con:

kubectl get daemonset -n logging
kubectl get pods -n logging -o wide
kubectl describe daemonset -n logging

Editazione Istantanea

Cambiamento istantaneo dei parametri di configurazione con:

kubectl edit daemonset fluentd -n logging

L'editor invocato è quello di sistema o quello indicato dalla variabile d'ambiente EDITOR.

Si può editare soltanto la sezione spec, non ststus.

In caso di errore l'editazione viene rilanciata.

Dopo il salvataggio di un'editazione corretta, avviene un rolling update.

Taints

Un Taint si applica ai nodi.

Il Taint ha la sintassi:

kubectl taint nodes <nodo> <chiave>=<valore>:<effetto>

L'effetto si applica sul nodo indicato a tutti i pod degli oggetti che hanno la chiave e il valore indicato.

L'effetto può essere:

  • NoSchedule
  • PreferNoSchedule
  • NoExecute

Esempio:

kubectl taint node kind-worker2 app=fluentd-logging:NoExecute

Lista di tutti i taints associati ai nodi:

kubectl get node -o custom-columns=NAME:.metadata.name,TAINT:.spec.taints[*]

Rimuovere un taint:

kubectl taint node kind-worker2 app-

Notare alla fine del comando il nome dell'etichetta seguito dal segno meno.

Tolerations

Modificare il file di specifiche del DaemonSet:

vim ~/scripts/fluentd.yml
  ...
  tolerations:
  - key: app
    value: fluentd-logging
    operator: Equal
    effect: NoExecute
  containers:
  ...

E riapplicare:

kubectl apply -f ~/scripts/fluentd.yml

Questo genera una eccezione al Taint.

Rimuovere il DaemonSet dell'esercizio:

kubectl delete -f ~/scripts/fluentd.yml

nodeSelector

Si può usare nodeSelector per avere i pod solo su alcuni nodi specifici. Il controller DaemonSet creai i pod sui nodi che hanno la chiave e il valore del selector.

Per esempio:

kubectl label node kind-worker2 type=platform-tools

E modificare le specifiche inserendo:

    ...
    spec:
      nodeSelector:
        type: platform-tools
      containers:
    ...

Ricreare il Daemonset e verificare:

kubectl apply -f ~/scripts/fluentd.yml
kubectl get pods -n logging -o wide

Per visualizzare i labels di un nodo:

kubectl get node kind-worker2 -o custom-columns=LABELS:.metadata.labels

Per rimuovere un label di nome type:

kubectl label node kind-worker2 type-

Rimuovere il DaemonSet dell'esercizio:

kubectl delete -f ~/scripts/fluentd.yml

nodeAffinity

Il controller DaemonSet crea i pod sui nodi che hanno il corrispondente nodeAffinity.

Vi sono due tipi di nodeAffinity:

  • requiredDuringSchedulingIgnoredDuringExecution - il pod non è schedulato se non si applica la regola
  • preferredDuringSchedulingIgnoredDuringExecution - lo scheduler prova a trovare un nodo conforme con la regola. Se non vi sono nodi che corrispondono, il pod viene schedulato ugualmente

Nel seguente DaemonSet vengono usate entrambe le regole di affinity:

  • required che i nodi abbiano una certa etichetta
  • preferred i nodi che abbiano l'etichetta di tipo t2.large

Modificare il file di specifiche con il seguente.

vim ~/scripts/fluentd.yml
apiVersion: apps/v1
kind: DaemonSet
metadata:
  name: fluentd
  namespace: logging
  labels:
    app: fluentd-logging
spec:
  selector:
    matchLabels:
      name: fluentd
  template:
    metadata:
      labels:
        name: fluentd
    spec:
      affinity:
        nodeAffinity:
          requiredDuringSchedulingIgnoredDuringExecution:
            nodeSelectorTerms:
            - matchExpressions:
              - key: type
                operator: In
                values:
                - platform-tools
          preferredDuringSchedulingIgnoredDuringExecution:
          - weight: 1
            preference:
              matchExpressions:
              - key: instance-type
                operator: In
                values:
                - t2.large
      containers:
      - name: fluentd-elasticsearch
        image: quay.io/fluentd_elasticsearch/fluentd:v2.5.2
        resources:
          limits:
            memory: 200Mi
          requests:
            cpu: 100m                                      
            memory: 200Mi                                  
        volumeMounts:                                      
        - name: varlog                                     
          mountPath: /var/log                              
      terminationGracePeriodSeconds: 30                    
      volumes:                         
      - name: varlog                   
        hostPath:                      
          path: /data/varlog

Ricreare il Daemonset e verificare:

kubectl apply -f ~/scripts/fluentd.yml
kubectl get pods -n logging -o wide

E' meglio usare preferredDuringSchedulingIgnoredDuringExecution invece di requiredDuringSchedulingIgnoredDuringExecution poichè è impossibile lanciare nuovi pod se il numero di nodi richiesto è maggiore del numero di nodi disponibile.

Priorità dei Pod

Determina l'importanza relativa di un pod.

E' utile settare una più alta PriorityClass ad un DaemonSet se questo ha componenti critici, per evitare che i suoi pod vengano sfavoriti da altri pod in caso di competizione di risorse.

PriorityClass definisce la priorità del pod. Valore limite 1 milione. Numeri più alti con priorità più alta.

Creazione di un oggetto priority class:

vim ~/scripts/prioclass.yml
apiVersion: scheduling.k8s.io/v1
kind: PriorityClass
metadata:
  name: high-priority
value: 100000
globalDefault: false
description: "daemonset priority class"
kubectl apply -f ~/scripts/prioclass.yml
kubectl get priorityClass

Aggiungere al nostro DaemonSet modificando il file di specifiche:

...
spec:
  priorityClassName: high-priority
  containers:
  ...
  terminationGracePeriodSeconds: 30
  volumes:
  ...

Ricreare il Daemonset e verificare:

kubectl apply -f ~/scripts/fluentd.yml
kubectl get pods -n logging -o wide

StatefulSet

Applicativi stateful gestiscono dati e hanno bisogno di tracciarli continuamente, per esempio MySQL, Oracle e PostgreSQL.

Uno StatefulSet è il controller appropriato per un applicativo stateful.

Ad ogni pod gestito viene assegnato un numero identificativo ordinale anzichè casuale e i pod vengono creati in ordine e cancellati in ordine inverso. Un nuovo pod è creato clonando il pod precedente solo quando è nello stato Running.

Le richieste di lettura da un volume associato vengono inviate a tutti i pod dello StatefulSet. Le richieste di scrittura sul volume associato vengono inviate solo al primo pod, e i dati modificati sono sincronizzati agli altri pod.

Cancellare un pod di uno StatefulSet non rimuove i volumi associati con l'applicativo.

Per l'esercizio che segue occorrono due Persistent Volumes, che i pod dello Stateful Set useranno. Normalmente un Provisioner creerebbe volumi dinamici all'occorrenza.

Il manifest che segue combina la definizione dei due persistent volumes con quella dello StatefulSet:

vim ~/scripts/stateful.yml
apiVersion: v1
kind: PersistentVolume
metadata:
  name: www-volume-1
spec:
  storageClassName: standard
  capacity:
    storage: 20Gi
  accessModes:
    - ReadWriteOnce
  hostPath:
    path: /data/www/
---
apiVersion: v1
kind: PersistentVolume
metadata:
  name: www-volume-2
spec:
  storageClassName: standard
  capacity:
    storage: 20Gi
  accessModes:
    - ReadWriteOnce
  hostPath:
    path: /data/www/
---
apiVersion: v1
kind: Service
metadata:
  name: nginx
  labels:
    app: nginx
spec:
  ports:
  - port: 80
    name: web
  clusterIP: None
  selector:
    app: nginx
---
apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: web
spec:
  serviceName: "nginx"
  replicas: 2
  selector:
    matchLabels:
      app: nginx
  template:
    metadata:
      labels:
        app: nginx
    spec:
      containers:
      - name: nginx
        image: registry.k8s.io/nginx-slim:0.8
        ports:
        - containerPort: 80
          name: web
        volumeMounts:
        - name: www
          mountPath: /usr/share/nginx/html
  volumeClaimTemplates:
  - metadata:
      name: www
    spec:
      accessModes: [ "ReadWriteOnce" ]
      resources:
        requests:
          storage: 1Gi

Aprire due finestre di terminale.

Nella prima digitare:

kubectl get pods --watch -l app=nginx

Nella seconda digitare:

kubectl apply -f ~/scripts/stateful.yml

Mentre nella seconda viene generato l'applicativo, il rapporto nella prima finestra è:

NAME    READY   STATUS    RESTARTS   AGE
web-0   0/1     Pending   0          0s
web-0   0/1     Pending   0          1s
web-0   0/1     ContainerCreating   0          1s
web-0   1/1     Running             0          5s
web-1   0/1     Pending             0          0s
web-1   0/1     Pending             0          2s
web-1   0/1     ContainerCreating   0          3s
web-1   1/1     Running             0          9s

I pod sono generati in sequenza.

Il pod web-1 viene generato solo quando il pod web-0 è nello stato di Running.

Il servizio è dato da:

kubectl get service nginx

Lo StatefulSet è dato da:

kubectl get statefulset web

I pod dell'applicativo si possono vedere con:

kubectl get pods -l app=nginx

Ogni pod ha uno hostname basato sul suo numero ordinale. Si possono vedere con:

for i in 0 1; do kubectl exec "web-$i" -- sh -c 'hostname'; done

Il loro nome è risolto al DNS di cluster. Si può compire un'interrogazione lanciando un pod aggiuntivo nel cluster:

kubectl run -i --tty --image busybox:1.28 dns-test --restart=Never --rm

E al pronto risultante dare:

nslookup web-0.nginx

Proviamo a cancellare esplicitamente i pod dello StatefulSet.

In una finestra digitare:

kubectl get pod --watch -l app=nginx

E nell'altra finestra digitare:

kubectl delete pod -l app=nginx

I pod vengono cancellati, ma subito ricreati, in ordine.

Scalare uno StatefulSet

In una finestra dare:

kubectl get pods --watch -l app=nginx

E nell'altra finestra:

kubectl scale sts web --replicas=5

Si nota che la creazione di nuovi pod procede per gradi, il nuovo pod sequenziale non viene creato finchè il precedente non è nello ststo di Running.

Si può anche scalare a decrescere, applicando una patch:

kubectl patch sts web -p '{"spec":{"replicas":3}}'

E' da notare che i Persistent Volume Claims sono ancora 5:

kubectl get pvc -l app=nginx

I PVC creati da uno StatefulSet non vengono cancellati anche quando sono cancellati i pod che li usano.

E' da notare che durante l'operazione di scale sono stati costruiti tre ulteriori Persistent Volumes, ma con una policy di Delete, anzichè Retain.

Cancellazione di uno StatefulSet

Tramite il manifest:

kubectl delete -f ~/scripts/stateful.yml

I PVC e i loro PV non vengono cancellati. I PV creati da manifest entrano in stato Terminating ma non sono rimossi finchè i corrispondenti PVC non vengono cancellati.

Cancellare a mano i PVC:

kubectl delete pvc www-web-0 www-web-1 www-web-2 www-web-3 www-web-4

I rimanenti PV sono temporaneamente visibili, ma vengono presto cancellati.

Gestione di Deployment e Rollout

Un Deployment, specificato da un manifest in Yaml, gestisce Pods e ReplicaSets in modo flessibile, ottenendo lo stato desiderato a partire dallo stato corrente.

NOTA: Non gestire manualmente i ReplicaSet che appartengono ad un Deployment.

Generazione di Deployment

Esempio. Un Deployment di nginx che gestisce un RepkicaSet con 3 pod.

vim ~/scripts/nginx-depl.yml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: nginx-deployment
  labels:
    app: nginx
spec:
  replicas: 3
  selector:
    matchLabels:
      app: nginx
  template:
    metadata:
      labels:
        app: nginx
    spec:
      containers:
      - name: nginx
        image: nginx:1.14.2
        ports:
        - containerPort: 80

Creare il deployment:

kubectl apply -f ~/scripts/nginx-depl.yml

Visualizzare il deploymant:

kubectl get deployments

Ogni deploymant è un rollout. Visualizzarne lo status:

kubectl rollout status deployment/nginx-deployment

Visualizzare il replicaset:

kubectl get rs

Visualizzare i pod e le loro etichette:

kubectl get pods --show-labels

Ogni pod acquisisce automaticamente l'etichetta pod-template-hash, che lo collega al ReplicaSet.

Update di Deployment

Cambiare la versione di nginx usata:

kubectl set image deployment/nginx-deployment nginx=nginx:1.16.1

Si può naturalmente modificare il file di specifiche ~/scripts/nginx-depl.yml e risottometterlo.

Si può inoltre editare direttamente con:

kubectl edit deployment/nginx-deployment

Verificare lo stato del rollout:

kubectl rollout status deployment/nginx-deployment

Solo un certo numero di pod sono disattivati durante il rollout. Default 25% - parametro maxUnavailable.

Solo un certo numero di pod nuovi sono creati durante il rollout. Default 25% - parametro maxSurge.

I dettagli sul deployment si ottengono con:

kubectl describe deployments

Ogni rollout che cambia i pod crea una revisione. La revisione non viene creata quando si cambia il numero di pod con un'operazione di scale.

Rollback di un Deployment

Utile quando la nuova versione è in un loop di crash.

Per esempio un update con un errore tipografico:

kubectl set image deployment/nginx-deployment nginx=nginx:1.161

Il sintomo di fallimento è l'output del comando:

kubectl rollout status deployment/nginx-deployment

che rimane piantato a lungo.

Altri sintomi si deducono dai comandi:

kubectl get rs
kubectl get pods
kubectl describe deployment

La storia delle revisioni si ottiene con:

kubectl rollout history deployment/nginx-deployment

Per avere dettagli di una specifica revisione:

kubectl rollout history deployment/nginx-deployment --revision=2

Per compiere il rollback alla revisione precedente:

kubectl rollout undo deployment/nginx-deployment

Per compiere il rollback ad una revisione specifica:

kubectl rollout undo deployment/nginx-deployment --to-revision=2

Controllare il risultato con:

kubectl get deployment nginx-deployment

e con:

kubectl describe deployment nginx-deployment

Scalare un Deployment

Oltre che modificare il manifest, ~/scripts/nginx-depl.yml, e risottometterlo, si può modificare il Deployment col comando diretto:

kubectl scale deployment/nginx-deployment --replicas=10

Se è presente uno autoscaler, si può anche dare il comando:

kubectl autoscale deployment/nginx-deployment --min=10 --max=15 --cpu-percent=80

Questo introduce in Kubernetes un oggetto HPA - Horizontal Pod Autoscaler. Questo avviene anche se lo autoscaler non è abilitato.

Si possono vedere gli oggetti HPA con:

kubectl get hpa
kubectl get hpa -w

L'oggetto si cancella con:

kubectl delete hpa nginx-deployment

Il cluster Kind non fornisce un autoscaler. Minikube si.

Terminare l'Esercizio

kubectl delete -f ~/scripts/nginx-depl.yml

Kubernetes Jobs

Un Job è un oggetto che crea uno o più pod, i quali eseguono una sola volta poi terminano.

Semplice Job

Esempio: un job che calcola il valore di Pi Greco a 2000 cifre decimali.

Scriviamo il Manifest:

vim ~/scripts/jobpi.yml
apiVersion: batch/v1
kind: Job
metadata:
  name: pi
spec:
  template:
    spec:
      containers:
      - name: pi
        image: perl:5.34.0
        command: ["perl",  "-Mbignum=bpi", "-wle", "print bpi(2000)"]
      restartPolicy: Never
  backoffLimit: 4

Li parametro backoffLimit è il numero massimo di riprove prima di dichiarare fallimento definitivo, con valore di default 6.

Sottoponiamo il manifest a Kubernetes:

kubectl apply -f ~/scripts/jobpi.yml

Controlliamo l'esistenza:

kubectl get job

e del pod sottostante:

kubectl get pod

I Job si possono descrivere:

kubectl describe job pi

Quando un Job termina lo si vede dalla colonna COMPLETIONS. Quando il suo pod termina va nello stato Completed.

Per listare tutti i pod non Completed (ancora attivi) che appartengono ad un Job si può usare l'espressione:

pods=$(kubectl get pods --selector=batch.kubernetes.io/job-name=pi --output=jsonpath='{.items[*].metadata.name}')
echo $pods

Per vedere l'output dei pod di un Job si può usare:

kubectl logs jobs/pi
3.141592653589793238462643383279502884197169.....

Rimuoviamo il Job tramite Manifest:

kubectl delete -f ~/scripts/jobpi.yml

La rimozione di un job rimuove i suoi pod.

CronJob

Un CronJob compie attività a intervalli regolari schedulati nel futuro.

E' l'equivalente dell'ambiente cron nel mondo Unix/Linux.

Esempio. Un cronJob che scrive l'ora corrente e un messaggio ogni minuto.

vim ~/scripts/cronjob.yml
apiVersion: batch/v1
kind: CronJob
metadata:
  name: hello
spec:
  schedule: "* * * * *"
  jobTemplate:
    spec:
      template:
        spec:
          containers:
          - name: hello
            image: busybox:1.28
            imagePullPolicy: IfNotPresent
            command:
            - /bin/sh
            - -c
            - date; echo Hello from the Kubernetes cluster
          restartPolicy: OnFailure

A intervalli di un minuto viene creato un nuovo pod che esegue il comando specificato.

La schedulazione è data dal campo .spec.schedule, che ha lo stesso formato del cron di Unix.

# ┌───────────── minute (0 - 59)
# │ ┌───────────── hour (0 - 23)
# │ │ ┌───────────── day of the month (1 - 31)
# │ │ │ ┌───────────── month (1 - 12)
# │ │ │ │ ┌───────────── day of the week (0 - 6) (Sunday to Saturday)
# │ │ │ │ │                                   OR sun, mon, tue, wed, thu, fri, sat
# │ │ │ │ │ 
# │ │ │ │ │
# * * * * *

Sono disponibili delle macro:

MacroEquivalente a
@yearly0 0 1 1 *
@monthly0 0 1 * *
@weekly0 0 * * 0
@daily0 0 * * *
@hourly0 * * * *

Il campo .spec.jobTemplate ha lo stesso schema di un Job. Si possono anche usare in esso metadati come labels e annotations.

Sottomettiamo il Manifest:

kubectl apply -f ~/scripts/cronjob.yml

Controlliamo l'esistenza del CronJob:

kubectl get cronjob
NAME    SCHEDULE    SUSPEND   ACTIVE   LAST SCHEDULE   AGE
hello   * * * * *   False     0        14s             30s

Dopo qualche minuto controlliamo i Job e i Pod:

kubectl get cronjobs
kubectl get jobs
kubectl get pods

Notare che il nome è costruito a partire dal nome del CronJob.

I nostri pod del Cronjob scrivono ciascuno al suo standard output, quindi nei logs. L'output di un pod è visibile con, p.es.:

kubectl logs hello-28474168-pdh77
Tue Feb 20 17:38:04 UTC 2024
Hello from the Kubernetes cluster

Calibrazione dei Job

Storia dei Job

Anche dopo molti minuti il rapporto mantiene una storia di poche entries. La storia mantenuta è controllata dai parametri:

  • .spec.successfulJobsHistoryLimit - lunghezza della storia dei jobs/pods che hanno avuto successo (default 3)
  • .spec.failedJobsHistoryLimit - lunghezza della storia dei jobs/pods che sono falliti (default 1)

Partenza Ritardata dei Job

Il campo opzionale .spec.startingDeadlineSeconds definisce una tolleranza in secondi per la partenza di un job (default: infinito), se per qualsiasi ragione il job non riesce a partire al momento schedulato.

Non settare mai il parametro inferiore a 10 secondi, o il job può non venire mai eseguito.

Se passa il tempo di tolleranza il job è considerato fallito.

Concorrenza

E' quando il job precedente è ancora in esecuzione e viene schedulato un nuovo job uguale.

Questo è controllato dal parametro .spec.concurrencyPolicy che può avere i seguenti valori:

  • Allow - (default) concesso
  • Forbid - proibito. Il nuovo job schedulato fallisce.
  • Replace - il nuovo job schedulato parte. Il vecchio job non ancora completato, fallisce.

Sospensione

Si può sospendere l'esecuzione di un Job o CronJob settando il parametro .spec.suspend al valore true (default: false).

I job già partiti non sono toccati.

Si possono cambiare i cronjob già attivi, oltre che modificare e sottomettere il Manifest. Per esempio:

kubectl patch cronjob hello -p '{"spec":{"suspend":true}}'

La stringa di patch può essere in Json o Yaml.

ATTENZIONE: Quando un CronJob è sospeso e passano i momenti di schedulazione dei suoi Job, questi vengono accodati. Quando si toglie la sospensione i Job accodati vengono eseguiti simultaneamente. Il comportamento dipende dai settaggi .spec.startingDeadlineSeconds e .spec.concurrencyPolicy.

Terminare l'Esercizio

kubectl delete -f ~/scripts/cronjob.yml

Labels ed Annotazioni

Le Label sono usate principalmente per identificare e raggruppare risorse da Kubernetes ed hanno un significato semantico.

Le Annotation sono per Kubernetes degli oggetti opachi e non hanno impatto sulle operazioni interne. Sono metadati non identificativi aggiunte arbitrariamente ad un oggetto.

Sono dei clients come strumenti e librerie che eventualmente raccolgono questi dati.

Sia le annotazioni che le etichette sono mappe chiave-valore.

Un'annotazione ha la struttura:

[prefisso/]mome: "valore"

Il nome:

  • è un requisito
  • é lungo al massimo 63 caratteri, inizia e termina con un carattere alfanumerico, può avere trattini, punti e underscores

Il prefisso:

  • è opzionale
  • è lungo al massimo 253 caratteri
  • è un dominio DNS

Il valore

  • è un requisito
  • è una stringa formattata JSON

Esempio:

apiVersion: v1
kind: Pod
metadata:
  name: annotations-demo
  annotations:
    imageregistry: "http://localhost:5000/"
spec:
  containers:
  - name: nginx
    image: nginx:1.14.2
    ports:
    - containerPort: 80

Specifica da quale registry provengono le immagini. E'da notare però che l'immagine è comunque scaricata dal Docker Hub e non dal registry locale.

Le annotations sono lette da tools esterni, non dal Kubernetes corrente.

Le annotazioni sono in genere poste manualmente nel Manifest dell'oggetto creato. Però in certi casi possono essere create ed usate da tools esterni di controllo o gestione.

Per esempio lo Horizontal Pod Autoscaler (HPA) genera annotazioni.

Oltre che porre annotazioni nel Manifest di un oggetto si può annotare un oggetto già esistente col comando kubectl annotate.

Esempi:

  • Update del pod 'foo' con la annotation 'description' e il valore 'my frontend'
    • kubectl annotate pods foo description='my frontend'
  • Update di un pod identificato nel file "pod.json"
    • kubectl annotate -f pod.json description='my frontend'
  • Riscrivere un'annotazione esistente
    • kubectl annotate --overwrite pods foo description='my frontend running nginx'
  • Update di un'annotazione in tutti i pod del namespace (-A: in tutti i namespace)
    • kubectl annotate pods --all description='my frontend running nginx'
  • Update del pod 'foo' solo se la risorsa non è cambiata dalla versione 1
    • kubectl annotate pods foo description='my frontend running nginx' --resource-version=1
  • Rimuovere un'annotazione
    • kubectl annotate pods foo description-

Esempio. Sia dato il Manifest:

vi ~/scripts/annotations.yml
apiVersion: v1
kind: Pod
metadata:
  name: nginx-pod
  labels:
    app: dashboard
    environment: test
  annotations:
    build-info: |
      {
        "commit": "abcd123",
        "timestamp": "2023-05-01T10:00:00Z"
      }
spec:
  containers:
    - name: frontend-nginx
      image: nginx

Una volta creato l'oggetto si possono compiere delle query basate sia sulle label che sulle annotation.

kubectl get pods nginx-pod -o yaml | grep -A2 'labels:'
  labels:
    app: dashboard
    environment: test
kubectl get pods nginx-pod -o yaml | grep -A5 'annotations:'
  annotations:
    build-info: |
      {
        "commit": "abcd123",
        "timestamp": "2023-05-01T10:00:00Z"
      }
kubectl get pods --selector='app=dashboard'
NAME        READY   STATUS    RESTARTS   AGE
nginx-pod   1/1     Running   0          5m33s
kubectl get pods --selector='app in (dashboard), environment in (test)'
NAME        READY   STATUS    RESTARTS   AGE
nginx-pod   1/1     Running   0          7m36s

E in generale si può usare la meravigliosa utility jq per compiere queries su stringhe JSON:

kubectl get pods -o json | \
jq -r '.items[] | select(.metadata.annotations."build-info" | fromjson | .commit=="abcd123") | .metadata.name'
nginx-pod

Terminare l'Esercizio

kubectl delete -f ~/scripts/annotations.yml

Controllo di Sanità e Probes

Kubernetes fornisce due tipi di sonde (probes) per testare l'usabilità di un applicativo: Liveness Probes e Readiness Probes.

Un Liveness Probe testa se l'applicativo è vivo e raggiungibile. Se non lo è, il pod che lo implementa viene terminato e ristartato.

Un Readiness Probe testa se l'applicativo può ricevere traffico del tipo programmato, Se non può, non viene inviato traffico al pod che lo implementa.

Vi sono tre tipi di probe; HTTP, Comando e TCP.

  • HTTP - Kubernetes accede ad un path HTTP del pod, e se il responso ha i codici tra il 200 e il 300 lo considera attivo. Un pod può implementare un miniserver HTTP a questo solo scopo.
  • Command - viene eseguito un comando nel pod, e se ritorna un codice di ritorno 0 viene considerato attivo.
  • TCP - viene aperta una connessione al pod, e se ha successo il pod è considerato attivo.

Command Liveness

Esempio di Liveness Probe:

vim ~/scripts/liveness.yml
apiVersion: v1
kind: Pod
metadata:
  labels:
    test: liveness
  name: liveness-exec
spec:
  containers:
  - name: liveness
    image: registry.k8s.io/busybox
    args:
    - /bin/sh
    - -c
    - touch /tmp/healthy; sleep 40; rm -f /tmp/healthy; sleep 600
    livenessProbe:
      exec:
        command:
        - cat
        - /tmp/healthy
      initialDelaySeconds: 5
      periodSeconds: 5

I parametri sono:

  • initialDelaySeconds - quanto tempo attendere prima di inviare il primo probe
  • periodSeconds - ogni quanto tempo inviare i probe

Sottomettere il manifest:

kubectl apply -f ~/scripts/liveness.yml

Ispezionare gli eventi del pod:

kubectl describe pod liveness-exec

Kubernetes inizia subito ad inviare liveness probes, anche quando il pod non è in stato di Running. Quindi possono esservi degli eventi di fallimento iniziali. Possono essere gestiti aumentando il parametro initialDelaySeconds.

Dopo un certo periodo di tempo il pod è stabile e il rapporto da:

kubectl get pod
NAME            READY   STATUS    RESTARTS      AGE
liveness-exec   1/1     Running   7 (78s ago)   20m

In caso di fallimento si vede un esempio di questo tipo:

  Warning  Unhealthy  7m17s (x19 over 16m)    kubelet            Liveness probe failed: cat: can't open '/tmp/healthy': No such file or directory
  Warning  BackOff    2m27s (x22 over 9m17s)  kubelet            Back-off restarting failed container liveness in pod liveness-exec_default(0726f57d-8dd9-489d-b0c5-ff352bfdc084)

E il pod è:

kubectl get pod
NAME            READY   STATUS             RESTARTS        AGE
liveness-exec   0/1     CrashLoopBackOff   7 (5m11s ago)   17m

Terminare il pod:

kubectl delete -f ~/scripts/liveness.yml

HTTP Liveness

Esempio:

vim ~/scripts/http-liveness.yml
apiVersion: v1
kind: Pod
metadata:
  labels:
    test: liveness
  name: liveness-http
spec:
  containers:
  - name: liveness
    image: registry.k8s.io/liveness
    args:
    - /server
    livenessProbe:
      httpGet:
        path: /healthz
        port: 8080
        httpHeaders:
        - name: Custom-Header
          value: Awesome
      initialDelaySeconds: 3
      periodSeconds: 3

L'immagine registry.k8s.io/liveness è appositamente didattica.

E' stata scritta in linguaggio Go e contiene il route handler:

http.HandleFunc("/healthz", func(w http.ResponseWriter, r *http.Request) {
    duration := time.Now().Sub(started)
    if duration.Seconds() > 10 {
        w.WriteHeader(500)
        w.Write([]byte(fmt.Sprintf("error: %v", duration.Seconds())))
    } else {
        w.WriteHeader(200)
        w.Write([]byte("ok"))
    }
})

Il responso è appositamente accurato per i primi 10 secondi, ma poi fallisce. Con un Liveness Probe il pod viene ristartato.

Sottomettiamo il manifest:

kubectl apply -f ~/scripts/http-liveness.yml

E ben presto abbiamo una situazione di questo tipo:

kubectl get pod
NAME            READY   STATUS    RESTARTS      AGE
liveness-http   1/1     Running   2 (13s ago)   75s

Con l'evento dedotto da:

kubectl describe pod liveness-http
.....
  Warning  Unhealthy  1s (x4 over 28s)   kubelet            Liveness probe failed: HTTP probe failed with statuscode: 500

Terminiamo l'esercizio:

kubectl delete -f ~/scripts/http-liveness.yml

HTTP Readiness

Una leggera modifica alle specifiche di liveness:

vim ~/scripts/http-readiness.yml
apiVersion: v1
kind: Pod
metadata:
  labels:
    test: readiness
  name: readiness-http
spec:
  containers:
  - name: readiness
    image: registry.k8s.io/liveness
    args:
    - /server
    readinessProbe:
      httpGet:
        path: /healthz
        port: 8080
        httpHeaders:
        - name: Custom-Header
          value: Awesome
      initialDelaySeconds: 3
      periodSeconds: 3

Sottomettere il manifest:

kubectl apply -f ~/scripts/http-readiness.yml

Inizialmente il pod si comporta come prima, e dopo 10 secondi fallisce. Kubernetes lo dichiara fallito e non lo ristarta.

Il rapporto di eventi è:

kubectl describe pod readiness-http
.....
  Warning  Unhealthy  18s (x21 over 75s)  kubelet            Readiness probe failed: HTTP probe failed with statuscode: 500

E il pod:

kubectl get pod
NAME             READY   STATUS    RESTARTS   AGE
readiness-http   0/1     Running   0          2m5s

Pulire l'esercizio:

kubectl delete -f ~/scripts/http-readiness.yml

TCP Liveness e Readiness

Prepariamo un manifest:

vim ~/scripts/tcp-liveness-readiness.yml
apiVersion: v1
kind: Pod
metadata:
  name: goproxy
  labels:
    app: goproxy
spec:
  containers:
  - name: goproxy
    image: registry.k8s.io/goproxy:0.1
    ports:
    - containerPort: 8080
    readinessProbe:
      tcpSocket:
        port: 8080
      initialDelaySeconds: 15
      periodSeconds: 10
    livenessProbe:
      tcpSocket:
        port: 8080
      initialDelaySeconds: 15
      periodSeconds: 10

Ha entrambi i tipi di probe, a scopo didattico per notare che non vi è differenza sintattica ma solo di comportamento. Basta uno o l'altro probe.

Kubernetes prova un'apertura di connessione alla porta 8080. Se ha successo il pod è alive/ready.

Sottomettiamo il manifest:

kubectl apply -f ~/scripts/tcp-liveness-readiness.yml

Non vi sono eventi di errore dopo tempo ragguardevole. Il pod sta bene.

Terminiamo l'esercizio:

kubectl delete -f ~/scripts/tcp-liveness-readiness.yml

Sicurezza e Controllo di Accesso

Modello

Una tipica rcichiesta API dal client kubectl allo API Server di Kubernetes passa possibilmente attraveso alcune fasi di sicurezza:

Auth01

Le fasi sono:

  • Autenticazione
  • Autorizzazione RBAC
  • Controllo di Accesso

RBAC è il modello di autorizzazjone usato da Kubernetes: Role Based Access Control.

Questo modello implementa il Principio del Minimo Privilegio: tutte le azioni sono proibite a meno che esista una regola che le consenta.

Esempio: L'utente pippo vuole creare un Deployment app1 nel namespace util.

  • il modulo di autenticazione determina se pippo è un utente reale o un impostore
  • il modulo di autorizzazione determina se pippo ha il permesso di creare un Deloyment nel namespace util
  • il controllo di accesso applica le policy di creazione di Deployment

Autenticazione

Livello indicato anche come AuthN (auth-enne).

Le richieste API includono credenziali e il modulo le verifica. Se la verifica fallisce, viene ritornato un errore HTTP 401.

Il modulo non è parte di Kubernetes, che non si occupa di gestione account. Questa è fornita dall'esterno, possibilmente con un plugin, p.es. il Cloud Provider fornisce il plugin e le credenziali di autenticazione.

Le credenziali di autenticazione sono mantenute nel file di kubeconfig, che nel caso di Linux è $HOME/.kube/config.

less ~/.kube/config
...
users:
- name: kind-kind
  user:
    client-certificate-data: ...
    client-key-data: ...
...

Si può vedere la configurazione più concisamente col comando:

kubectl config view

Nel nostro caso è un Certificato Client, supportato da ogni versione di Kubernetes.

Questo certificato è stato prodotto da Kind in fase di installazione del cluster. Alternativamente si possono usare strumenti esterni per generarlo, e inserirlo nel file di configurazione.

Si può ispezionare il certificato degli utenti installando l'utility jq:

sudo apt install jq

E dando il comando:

kubectl config view --raw -o json \
| jq ".users[] | select(.name==\"$(kubectl config current-context)\")" \
| jq -r '.user["client-certificate-data"]' \
| base64 -d | openssl x509 -text

Autorizzazione

Abbreviato in AuthZ (auth-zi).

Si occupa di tre aspetti:

  1. users
  2. actions
  3. resuorces

Descrive quali users possono compire quali actions su che tipo di resources.

Un esempio può essere dato dalla seguente tabella:

User (subject)ActionResource
BaocreatePods
KalilalistDeployments
JoshdeleteServiceAccounts

RBAC si basa su due concetti:

  • Role - definisce un insieme di permessi
  • RoleBinding - concede i permessi a utenti

Role

Un esempio di Manifest che definisce un Role:

apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
  namespace: shield
  name: read-deployments
rules:
- apiGroups: ["apps"]
  resources: ["deployments"]
  verbs: ["get", "watch", "list"]

Notare la particolare apiVersion.

Notare le direttive:

  • namespace - si applica a questo spazio nomi
  • apiGroups - indica la componente gruppo del campo apiVersion
    • per esempio sappiamo che i Deployments hanno apiVersion apps/v1
    • se apiGroups è omesso o se apiGroups: [""], allora è lo apiGroup core, p.es. Services che ha solo apiVersion: v1
  • resources - quali oggetti Kubernetes sono influiti
  • verbs - quali comandi di kubectl si possono usare

Per avere una lista dei verbi disponibili per ogni risorsa:

kubectl api-resources --sort-by name -o wide

Nei campi apiGroups, resources e verbs si può mettere un asterisco (*) che indica tutti i valori.

Per esempio:

...
rules:
- apiGroups: ["*"]
  resources: ["*"]
  verbs: ["*"]
...

RoleBinding

Un Role deve essere collegato ad un utente con un RoleBinding.

Esempio:

apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
  name: read-deployments
  namespace: shield
subjects:
- kind: User
  name: sky
# This is the authenticated user
  apiGroup: rbac.authorization.k8s.io
roleRef:
  kind: Role
  name: read-deployments
# This is the Role to bind to the user
  apiGroup: rbac.authorization.k8s.io

L'utente autenticato "sky" può compiere un comando come: kubectl get deployments -n shield.

Con adeguata progettazione dei Role e RoleBinding si ottiene che certi utenti autenticati possano compiere solo certe azioni solo su certi oggetti di un determinato spazio nomi.

Roles01

Cluster Level

Vi sono in realtà 4 oggetti RBAC:

  • Roles
  • ClusterRoles
  • RoleBindings
  • ClusterRoleBindings

Roles e RoleBindings sono specifici di namespace.

ClusterRoles e ClusterRoleBindings si applicano all'intero cluster.

ClusterRole

Esempio:

apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
  name: read-deployments
rules:
- apiGroups: ["apps"]
  resources: ["deployments"]
  verbs: ["get", "watch", "list"]

NOTA

Questo è solo un esempio. In generale non è una buona idea modificare i ClusterRole o ClusterRoleBinding se non si procede com molta cautela

Una serie di ClusterRole sono definiti nel cluster. Per vederli:

kubectl get clusterrole

Molti sono creati all'atto dell'installazione del cluster. Alcuni sono stati aggiunti da Manifests.

Per vedere i dettagli di un ClusterRole, p.es.:

kubectl describe clusterrole metallb-system:controller

ClusterRoleBinding

Vedere tutti i ClusterRoleBinding:

kubectl get vlusterrolebinding

Vedere i dettagli di uno, p.es.:

kubectl describe clusterrolebinding system:basic-user
Name:         system:basic-user
Labels:       kubernetes.io/bootstrapping=rbac-defaults
Annotations:  rbac.authorization.kubernetes.io/autoupdate: true
Role:
  Kind:  ClusterRole
  Name:  system:basic-user
Subjects:
  Kind   Name                  Namespace
  ----   ----                  ---------
  Group  system:authenticated  

Sequenza

Il cluster ispeziona il certificato dell'utente che accede, ricercando il campo "Subject:". Lo possiamo vedere da:

kubectl config view --raw -o json \
| jq ".users[] | select(.name==\"$(kubectl config current-context)\")" \
| jq -r '.user["client-certificate-data"]' \
| base64 -d | openssl x509 -text | grep "Subject:"
        Subject: O = system:masters, CN = kubernetes-admin

Ne estrae la proprietà O che ha valore system:masters. Questo è un gruppo predefinito che equivale agli amministratori globali di sistema.

Il ClusterRoleBinding corrispondente, predefinito, è cluster-admin. Lo si può vedere con:

kubectl describe clusterrolebinding cluster-admin
Name:         cluster-admin
Labels:       kubernetes.io/bootstrapping=rbac-defaults
Annotations:  rbac.authorization.kubernetes.io/autoupdate: true
Role:
  Kind:  ClusterRole
  Name:  cluster-admin
Subjects:
  Kind   Name            Namespace
  ----   ----            ---------
  Group  system:masters

Il ClusterRole associato è cluster-admin. Possiamo ispezionarlo con:

kubectl describe clusterrole cluster-admin
Name:         cluster-admin
Labels:       kubernetes.io/bootstrapping=rbac-defaults
Annotations:  rbac.authorization.kubernetes.io/autoupdate: true
PolicyRule:
  Resources  Non-Resource URLs  Resource Names  Verbs
  ---------  -----------------  --------------  -----
  *.*        []                 []              [*]
             [*]                []              [*]

Questo equivale in pratica a tutti i permessi su tutti gli spazi nomi.

Controllo Accesso

E' basato su una serie di Policy ed è implementato da degli Admission Controllers.

Vi sono due tipi di Admission Controllers:

  • validating - controllano la validità di una richiesta ma non possono cambiarla
  • mutating - possono modificare una richiesta

Gli Admission Controllers di tipo Mutating sono eseguiti per primi.

Un'implementazione di cluster ha un numero considerevole di Admission Controllers. Sono controllati da un numero considerevole di settaggi nei Manifest delle risorse gestite.

Per esempio, il settaggio .spec.containers.imagePullPolicy corrisponde al controller AlwaysPullImages.

La possibilità di loro gestione è determinata dall'implementazione del cluster, ed è di solito eseguita col comando kube-apiserver.

Nell'implementazione cluster Kind questo comando non è direttamente usabile ma è interno al pod kube-apiserver-kind-control-plane nello spazio nomi kube-system.

Per vedere i Controllers installati in Kind, il comando può essere:

kubectl exec kube-apiserver-kind-control-plane \
-n kube-system -- kube-apiserver --help | \
grep enable-admission-plugins | grep enabled | \
cut -d. -f2

Sfortunatamente il controllo fine può essere eseguito solo modificando il codice sorgente di Kind.


Esempi

Il Manifest di creazione del servizio di Metallb fornisce numerosi esempi d'uso di controllo di accesso.

Creazione di ServiceAccount

apiVersion: v1
kind: ServiceAccount
metadata:
  labels:
    app: metallb
  name: controller
  namespace: metallb-system

Definizione di ClusterRole

Il ClusterRole è globale di cluster.

Una serie di regole, ciascuna contraddistinta da apiGroups/resources.

Quali sono i verbi, cioè le azioni, che chi possiede questo ClusterRole può compiere sugli apiGroups/resources.

apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
  labels:
    app: metallb
  name: metallb-system:controller
rules:
- apiGroups:
  - ''
  resources:
  - services
  verbs:
  - get
  - list
  - watch
- apiGroups:
  - ''
  resources:
  - services/status
  verbs:
  - update
- apiGroups:
  - ''
  resources:
  - events
  verbs:
  - create
  - patch
- apiGroups:
  - policy
  resourceNames:
  - controller
  resources:
  - podsecuritypolicies
  verbs:
  - use

Definizione di Role

Locale di namespace. Serie di regole serie di regole, ciascuna contraddistinta da apiGroups/resources.

Quali sono i verbi, cioè le azioni, che chi possiede questo ClusterRole può compiere sugli apiGroups/resources.

apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
  labels:
    app: metallb
  name: config-watcher
  namespace: metallb-system
rules:
- apiGroups:
  - ''
  resources:
  - configmaps
  verbs:
  - get
  - list
  - watch

Chi possiede il ruolo config-watcher, solo nel namespace metallb-system: su tutti i ConfigMaps può dare i comandi kubectl get, kubectl list e kubectl watch.

In realtà non sono comandi client di kubectl, sono verbi della API REST che vengono inviati allo API Server.

Definizione di RoleBinding

Associazione tra ServiceAccount e Role.

Questo RoleBinding si chiama config-watcher, che è anche il nome di un Role: non confondersi, sono due oggetti diversi.

apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
  labels:
    app: metallb
  name: config-watcher
  namespace: metallb-system
roleRef:
  apiGroup: rbac.authorization.k8s.io
  kind: Role
  name: config-watcher
subjects:
- kind: ServiceAccount
  name: controller
- kind: ServiceAccount
  name: speaker

DaemonSet

Finalmente il Daemonset contiene specifiche di chi può operare.

Contiene anche molti altri elementi interessanti, utili per imparare seguendo gli esempi.

Prima l'intera specifica:

apiVersion: apps/v1
kind: DaemonSet
metadata:
  labels:
    app: metallb
    component: speaker
  name: speaker
  namespace: metallb-system
spec:
  selector:
    matchLabels:
      app: metallb
      component: speaker
  template:
    metadata:
      annotations:
        prometheus.io/port: '7472'
        prometheus.io/scrape: 'true'
      labels:
        app: metallb
        component: speaker
    spec:
      containers:
      - args:
        - --port=7472
        - --config=config
        - --log-level=info
        env:
        - name: METALLB_NODE_NAME
          valueFrom:
            fieldRef:
              fieldPath: spec.nodeName
        - name: METALLB_HOST
          valueFrom:
            fieldRef:
              fieldPath: status.hostIP
        - name: METALLB_ML_BIND_ADDR
          valueFrom:
            fieldRef:
              fieldPath: status.podIP
        # needed when another software is also using memberlist / port 7946
        # when changing this default you also need to update the container ports definition
        # and the PodSecurityPolicy hostPorts definition
        #- name: METALLB_ML_BIND_PORT
        #  value: "7946"
        - name: METALLB_ML_LABELS
          value: "app=metallb,component=speaker"
        - name: METALLB_ML_SECRET_KEY
          valueFrom:
            secretKeyRef:
              name: memberlist
              key: secretkey
        image: quay.io/metallb/speaker:v0.12.1
        name: speaker
        ports:
        - containerPort: 7472
          name: monitoring
        - containerPort: 7946
          name: memberlist-tcp
        - containerPort: 7946
          name: memberlist-udp
          protocol: UDP
        livenessProbe:
          httpGet:
            path: /metrics
            port: monitoring
          initialDelaySeconds: 10
          periodSeconds: 10
          timeoutSeconds: 1
          successThreshold: 1
          failureThreshold: 3
        readinessProbe:
          httpGet:
            path: /metrics
            port: monitoring
          initialDelaySeconds: 10
          periodSeconds: 10
          timeoutSeconds: 1
          successThreshold: 1
          failureThreshold: 3
        securityContext:
          allowPrivilegeEscalation: false
          capabilities:
            add:
            - NET_RAW
            drop:
            - ALL
          readOnlyRootFilesystem: true
      hostNetwork: true
      nodeSelector:
        kubernetes.io/os: linux
      serviceAccountName: speaker
      terminationGracePeriodSeconds: 2

La linea:

      serviceAccountName: speaker

specifica che questo DaemonSet opera come account Speaker, definito sopra, che ha sicuramente un RoleBinding ad un Role, e quest'ultimo lista le azioni che può compiere sulle varie risorse.

Le linee:

      nodeSelector:
        kubernetes.io/os: linux

richiedono che il sistema operativo sottostante sia Linux.

Le linee:

      tolerations:
      - effect: NoSchedule
        key: node-role.kubernetes.io/master
        operator: Exists

richiedono che il pod del DaemonSet corrente non venga schedulato sul Master del cluster.

Il Security Context del contenitore del pod è particolarmente interessante:

        securityContext:
          allowPrivilegeEscalation: false
          capabilities:
            add:
            - NET_RAW
            drop:
            - ALL
          readOnlyRootFilesystem: true

L'account che opera i pod di questo DaemonSet, che è speaker:

  • non può dare comandi equivalenti a sudo che compiano un Privilege Escalation
  • se usa il filesystem di root (quello del container), è in sola lettura
  • tutti i capabilities sono tolti eccetto NET_RAW, evidentemente il programma inserito nell'immagine usa solo un socket 'Raw', come fa ad esempio ICMP.

Notare le linee:

        - name: METALLB_ML_BIND_ADDR
          valueFrom:
            fieldRef:
              fieldPath: status.podIP

Sono l'assegnazione di valore ad una variabile d'ambiente, prese dal campo status.podIP. Quando il pod è creato e allocato ad un nodo, il suo indirizzo IP è già stato assegnato.

L'interessante è che questo campo non esiste nel Manifest, che descrive il desired state, ma è parte del current state del pod appena creato.

Notare anche i probe Liveness e Readiness.

Deployment

Il Deployment è anch'esso interessante:

apiVersion: apps/v1
kind: Deployment
metadata:
  labels:
    app: metallb
    component: controller
  name: controller
  namespace: metallb-system
spec:
  revisionHistoryLimit: 3
  selector:
    matchLabels:
      app: metallb
      component: controller
  template:
    metadata:
      annotations:
        prometheus.io/port: '7472'
        prometheus.io/scrape: 'true'
      labels:
        app: metallb
        component: controller
    spec:
      containers:
      - args:
        - --port=7472
        - --config=config
        - --log-level=info
        env:
        - name: METALLB_ML_SECRET_NAME
          value: memberlist
        - name: METALLB_DEPLOYMENT
          value: controller
        image: quay.io/metallb/controller:v0.12.1
        name: controller
        ports:
        - containerPort: 7472
          name: monitoring
        livenessProbe:
          httpGet:
            path: /metrics
            port: monitoring
          initialDelaySeconds: 10
          periodSeconds: 10
          timeoutSeconds: 1
          successThreshold: 1
          failureThreshold: 3
        readinessProbe:
          httpGet:
            path: /metrics
            port: monitoring
          initialDelaySeconds: 10
          periodSeconds: 10
          timeoutSeconds: 1
          successThreshold: 1
          failureThreshold: 3
        securityContext:
          allowPrivilegeEscalation: false
          capabilities:
            drop:
            - all
          readOnlyRootFilesystem: true
      nodeSelector:
        kubernetes.io/os: linux
      securityContext:
        runAsNonRoot: true
        runAsUser: 65534
        fsGroup: 65534
      serviceAccountName: controller
      terminationGracePeriodSeconds: 0

Vi sono due securityContext, uno a livello container e uno a livello pod. Quest'ultimo è:

      securityContext:
        runAsNonRoot: true
        runAsUser: 65534
        fsGroup: 65534
      serviceAccountName: controller

L'utente controller, che gestisce questo deployment, se ha interazioni con il Linux sottostante, è mappato all'utente di sistema 65534, gruppo di sistema 65534.

Le annotazioni:

      annotations:
        prometheus.io/port: '7472'
        prometheus.io/scrape: 'true'

vengono usate dall'utility Prometheus se esistente.

Ingress

Un LoadBalancer usa un indirizzo pubblico dal pool di indirizzi per ogni servizio configurato.

In un LoadBalancer implementato localmente gli indirizzi del pool possono essere insufficienti. Se il LoadBalancer è fornito dal Cloud Provider, ogni sua istanza ha un costo.

Ingress espone servizi multipli tramite un unico Load Balancer.

Ingress01

Ha due componenti:

  • Ingress controller
  • Ingress object

Molte installazioni di cluster Kubernetes, come Kind, non forniscono un Ingress Controller di serie, quindi va installato.

Per l'uso di un cluster Kubernetes di uno specifico Cloud Controller, leggere la documentazione appropriata.

Ingress su Kind

Kind supporta tre Ingress Controllers:

  • Contour
  • Ingress Kong
  • Ingress Nginx

Useremo qui Ingress Nginx-

Modifica del Cluster Kind

Occorre modificare la costruzione del cluster.

Cancellare il cluster corrente con la nostra procedura shell:

cd
./teardown,sh

Creiamo una nuova procedura shell di setup del cluster chiamata ingress.sh:

vim ingerss.sh
#!/bin/sh
set -o errexit

# crea il contenitore del registry se non esiste
reg_name='kind-registry'
reg_port='5000'
if [ "$(docker inspect -f '{{.State.Running}}' "${reg_name}" 2>/dev/null || true)" != 'true' ]; then
  docker run \
    -d --restart=always -p "127.0.0.1:${reg_port}:5000" --name "${reg_name}" \
    -v $HOME/.docker/registry:/var/lib/registry registry:2
fi

# crea un cluster con il registry locale abilitato in containerd
cat <<EOF | kind create cluster --image kindest/node:v1.24.0 --config=-
kind: Cluster
apiVersion: kind.x-k8s.io/v1alpha4
containerdConfigPatches:
- |-
  [plugins."io.containerd.grpc.v1.cri".registry.mirrors."localhost:${reg_port}"]
    endpoint = ["http://${reg_name}:5000"]
nodes:
- role: control-plane
  kubeadmConfigPatches:
  - |
    kind: InitConfiguration
    nodeRegistration:
      kubeletExtraArgs:
        node-labels: "ingress-ready=true"
  extraPortMappings:
  - containerPort: 80
    hostPort: 80
    protocol: TCP
  - containerPort: 443
    hostPort: 443
    protocol: TCP
  extraMounts:
    - hostPath: /data
      containerPath: /data
- role: worker
  extraMounts:
    - hostPath: /data
      containerPath: /data
- role: worker
  extraMounts:
    - hostPath: /data
      containerPath: /data
EOF

# connette il registry alla rete del cluster se non connesso
if [ "$(docker inspect -f='{{json .NetworkSettings.Networks.kind}}' "${reg_name}")" = 'null' ]; then
  docker network connect "kind" "${reg_name}"
fi

# Documenta il local registry
cat <<EOF | kubectl apply -f -
apiVersion: v1
kind: ConfigMap
metadata:
  name: local-registry-hosting
  namespace: kube-public
data:
  localRegistryHosting.v1: |
    host: "localhost:${reg_port}"
    help: "https://kind.sigs.k8s.io/docs/user/local-registry/"
EOF

La sezione aggiunta, dopo la linea - role: control-plane è:

  kubeadmConfigPatches:
  - |
    kind: InitConfiguration
    nodeRegistration:
      kubeletExtraArgs:
        node-labels: "ingress-ready=true"
  extraPortMappings:
  - containerPort: 80
    hostPort: 80
    protocol: TCP
  - containerPort: 443
    hostPort: 443
    protocol: TCP

Rendiamola eseguibile:

chmod +x ingress.sh

Copiamo la procedura di setup in una nuova:

cp setup.sh setup-ingress.sh

Modifichiamo la nuova procedura setup-ingress.sh sostituendo le linee:

echo "~/std.sh"
~/std.sh

con

echo "~/ingress.sh"
~/ingress.sh

Lanciamo il nuovo cluster che supporta Ingress:

./setup-ingress.sh

Deployment di Ingress

Copiamo dalla rete il Deployment del controller di Ingress e poniamolo nella directory ~/scripts:

wget https://raw.githubusercontent.com/kubernetes/ingress-nginx/main/deploy/static/provider/kind/deploy.yaml
mv deploy.yaml ~/scripts/ingress-deploy.yml

Eseguiamo il deployment e attendiamo che sia pronto:

kubectl apply -f ~/scripts/ingress-deploy.yml
echo Waiting up to 120s for nginx-ingress pod ...
kubectl wait --namespace ingress-nginx \
  --for=condition=ready pod \
  --selector=app.kubernetes.io/component=controller \
  --timeout=120s

Test di Ingress

Prepariamo un Manifest di test:

vim ~/scripts/nginx-test.yml
kind: Pod
apiVersion: v1
metadata:
  name: foo-app
  labels:
    app: foo
spec:
  containers:
  - command:
    - /agnhost
    - netexec
    - --http-port
    - "8080"
    image: registry.k8s.io/e2e-test-images/agnhost:2.39
    name: foo-app
---
kind: Service
apiVersion: v1
metadata:
  name: foo-service
spec:
  selector:
    app: foo
  ports:
  # Default port used by the image
  - port: 8080
---
kind: Pod
apiVersion: v1
metadata:
  name: bar-app
  labels:
    app: bar
spec:
  containers:
  - command:
    - /agnhost
    - netexec
    - --http-port
    - "8080"
    image: registry.k8s.io/e2e-test-images/agnhost:2.39
    name: bar-app
---
kind: Service
apiVersion: v1
metadata:
  name: bar-service
spec:
  selector:
    app: bar
  ports:
  # Default port used by the image
  - port: 8080
---
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: example-ingress
  annotations:
    nginx.ingress.kubernetes.io/rewrite-target: /$2
spec:
  rules:
  - http:
      paths:
      - pathType: ImplementationSpecific
        path: /foo(/|$)(.*)
        backend:
          service:
            name: foo-service
            port:
              number: 8080
      - pathType: ImplementationSpecific
        path: /bar(/|$)(.*)
        backend:
          service:
            name: bar-service
            port:
              number: 8080

Lo sottomettiamo:

kubectl apply -f ~/scripts/nginx-test.yml

E attendiamo che i pod vengano creati.

Connessioni di prova:

curl localhost/foo/hostname
foo-app
curl localhost/bar/hostname
bar-app

Al termine cancelliamo dal manifest:

kubectl delete -f ~/scripts/nginx-test.yml

Altro Esempio

Il seguente esempio è preso dal libro The Kubernetes Book di Nigel Poulton.

Abbiamo un Manifest con due pod e due servizi:

mkdir -p ~/scripts/ig
vim ~/scripts/ig/app.yml
apiVersion: v1
kind: Service
metadata:
  name: svc-shield
spec:
  type: ClusterIP
  ports:
  - port: 8080
    targetPort: 8080
  selector:
    env: shield
---
apiVersion: v1
kind: Service
metadata:
  name: svc-hydra
spec:
  type: ClusterIP
  ports:
  - port: 8080
    targetPort: 8080
  selector:
    env: hydra
---
apiVersion: v1
kind: Pod
metadata:
  name: shield
  labels:
    env: shield
spec:
  containers:
  - image: nigelpoulton/k8sbook:shield-ingress
    name: shield-ctr
    ports:
    - containerPort: 8080
    imagePullPolicy: Always
---
apiVersion: v1
kind: Pod
metadata:
  name: hydra
  labels:
    env: hydra
spec:
  containers:
  - image: nigelpoulton/k8sbook:hydra-ingress
    name: hydra-ctr
    ports:
    - containerPort: 8080
    imagePullPolicy: Always

Sottomettiamo il manifest:

kubectl apply -f ~/scripts/ig/app.yml

Abbiamo due possibilitè per Ingress:

  • Host Based Routing - il routing al servizio avviene sulla base dello host di destinazione
  • Path Based Routing - il routing al servizio avviene sulla base del Path nella URL

Nell'esercizio la situazione è:

  • Host-based: shield.mcu.com >> svc-shield
  • Host-based: hydra.mcu.com >> svc-hydra
  • Path-based: mcu.com/shield >> svc-shield
  • Path-based: mcu.com/hydra >> svc-hydra

Ingroute

L'oggetto Ingress viene specificato dal manifest:

vim ~/scripts/ig/ig-all.yml
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: mcu-all
  annotations:
    nginx.ingress.kubernetes.io/rewrite-target: /
spec:
  ingressClassName: nginx
  rules:
  - host: shield.mcu.com
    http:
      paths:
      - path: /
        pathType: Prefix
        backend:
          service:
            name: svc-shield
            port:
              number: 8080
  - host: hydra.mcu.com
    http:
      paths:
      - path: /
        pathType: Prefix
        backend:
          service:
            name: svc-hydra
            port:
              number: 8080
  - host: mcu.com
    http:
      paths:
      - path: /shield
        pathType: Prefix
        backend:
          service:
            name: svc-shield
            port:
              number: 8080
      - path: /hydra
        pathType: Prefix
        backend:
          service:
            name: svc-hydra
            port:
              number: 8080

Note

L'annotazione nginx.ingress.kubernetes.io/rewrite-target: / e' indispensabile per permettere a Ingress il reindirizzamento della richiesta.

E' un esempio di Annotation interpretata da un Controller specifico.

La specifica ingressClassName: nginx non è indispensabile se sul nostro cluster abbiamo solo lo Nginx Ingress Controller.

Qualora vi fossero più Ingress controller dovremmo configurare una classe per ciascuno, per esempio col Manifest:

apiVersion: networking.k8s.io/v1
kind: IngressClass
metadata:
  name: igc-nginx
spec:
  controller: nginx.org/ingress-controller

Sottoponiamo il manifest:

kubectl apply -f ~/scripts/ig/ig-all.yml

Ispezione degli oggetti Ingress:

kubectl get ing

Descrizione di un oggetto Ingress:

kubectl describe ing mcu-all

Notare la sezione Regole:

Rules:
  Host            Path  Backends
  ----            ----  --------
  shield.mcu.com  
                  /   svc-shield:8080 (10.244.1.10:8080)
  hydra.mcu.com   
                  /   svc-hydra:8080 (10.244.2.5:8080)
  mcu.com         
                  /shield   svc-shield:8080 (10.244.1.10:8080)
                  /hydra    svc-hydra:8080 (10.244.2.5:8080)

Notare anche il campo Address: localhost

Modificare il file /etc/hosts aggiungendo le linee:

127.0.0.1 shield.mcu.com
127.0.0.1 hydra.mcu.com
127.0.0.1 mcu.com

Aprire un browser e testare i seguenti URL:

  • shield.mcu.com
  • hydra.mcu.com
  • mcu.com/shield
  • mcu.com/hydra

Al termine ripulire:

kubectl delete -f ~/scripts/ig/ig-all.yml
kubectl delete -f ~/scripts/ig/app.yml

Rimettere anche a posto /etc/hosts.

Storaggio

Kubernetes supporta numerosi tipi di storaggio: iSCSI, SMB, NFS, object storage blobs, ecc. I fornitori (Provisioner) di storaggio possono essere in-house o nel cloud.

E' necessario che Kubernetes installi un plugin per supportare lo storaggio specifico, fornito dal Provisioner.

Il plugin deve essere conforme allo standard CSI - Container Storage Interface.

Csiplugin

Purtroppo i cluster Kind e Minikube non supportano questa funzionalità e sono limitati allo storaggio Standard - locale, o NFS visto come directory locale.

Per esempio un cluster è implementato su AWS e l'amministratore di AWS ha creato un volume di 25GB chiamato ebs-vol. L'amministratore di Kubernetes crea un PV chiamato k8s-vol collegato al volume ebs-vol dal plugin kubernetes.io/aws-ebs.

Ebsplugin

Sistema Persistent Volume

Le tre risorse principlali del sistema Persistent Volume sono:

  • Persistent Volume (pv)
  • Persistent Volume Claim (pvc)
  • Storage Class (sc)

I passi in Kubernetes sono:

  1. Creare il PV
  2. Creare il PVC
  3. Definire il volume nelle specifiche di un Pod
  4. Montarlo in un container

Esempio di creazione di PV.

vim gke-volume.yml
apiVersion: v1
kind: PersistentVolume
metadata:
  name: pv1
spec:
  accessModes:
  - ReadWriteOnce
  storageClassName: test
  capacity:
    storage: 10Gi
  persistentVolumeReclaimPolicy: Retain
  gcePersistentDisk:
    pdName: user-disk

Il PV è altamente specifico al Provisioner. Per esempio l'attributo gcePersistentDisk si riferisce a Gookle e ha bisogno della presenza del plugin GCE. Il volume user-disk deve essere stato creato prima da GCE.

Anche la storage class di nome test è creata altrove.

Questo è solo un esempio illustrativo, non funziona col nostro cluster.

L'effetto graficamente espresso sarebbe:

Userdisk

La proprietò .spec.persistentVolumeReclaimPolicy dice a Kubernetes cosa farne del disco PV quando non è più in uso. Due possibilità:

  • Delete
  • Retain

Retain ne conserva i dati, ma non sono più accessibili da un altro PVC in futuro. Il PV deve essere rimosso a mano.

Un PVC corrispondente a questo PV sarebbe ad esempio:

vim gke-pvc.yml
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: pvc1
spec:
  accessModes:
  - ReadWriteOnce
  storageClassName: test
  resources:
    requests:
      storage: 10Gi

Vi sono alcune proprietà che devono essere in corrispondenza:

Corrispond

Un pod che usa questo PVC potrebbe essere:

vim volpod.yml
apiVersion: v1
kind: Pod
metadata:
  name: volpod
spec:
volumes:
  - name: data
    persistentVolumeClaim:
      claimName: pvc1
containers:
  - name: ubuntu-ctr
    image: ubuntu:latest
    command:
    - /bin/bash
    - "-c"
    - "sleep 60m"
    volumeMounts:
    - mountPath: /data
      name: data

Dynamic Provisioning

Il Dynamic Provisioning è la fornitura da parte del Provider di spazio disco on-demand, quando c'è un Pod che effettivamente lo usa.

Questa funzionalità è basata sul concetto di Storage Class.

Storage Class

Una StorageClass è una risorsa definita nel gruppo API storage.k8s.io/v1. Si riferisce ad un tipo di storaggio offerto da un Provisioner.

Esempio.

vim fast-sc.yml
kind: StorageClass
apiVersion: storage.k8s.io/v1
metadata:
  name: fast
provisioner: kubernetes.io/aws-ebs
parameters:
  type: io1
  zones: eu-west-1a
  opsPerGB: "10"

Sottomettere il manifest:

kubectl apply -f fast-sc.yml

Si visionano le storage classes con:

kubectl get sc
NAME                 PROVISIONER             RECLAIMPOLICY   VOLUMEBINDINGMODE      ALLOWVOLUMEEXPANSION   AGE
fast                 kubernetes.io/aws-ebs   Delete          Immediate              false                  19s
standard (default)   rancher.io/local-path   Delete          WaitForFirstConsumer   false                  42h

La storage class viene creata anche se il plugin non è disponibile. E' solo al momento di creazione del PVC e Pod che si presentano dei problemi.

Si possono avere molte Storage Classee configurate. I parametri di ciascuna dipendono dal plugin del provisioner.

Per esempio:

vim sc-secure.yml
kind: StorageClass
apiVersion: storage.k8s.io/v1
metadata:
  name: portworx-db-secure
provisioner: kubernetes.io/portworx-volume
parameters:
  fs: "xfs"
  block_size: "32"
  repl: "2"
  snap_interval: "30"
  io-priority: "medium"
  secure: "true"

Una StorageClass è un oggetto immutabile. Non si può modificare, solo cancellare e ricreare.

Le fasi d'uso sono:

  1. Creare il cluster Kubernetes
  2. Installare i necessari plugin per lo storage
  3. Creare una StorageClass
  4. Creare un PVC che si riferisce alla StorageClass
  5. Creare un Pod che usa un volume basato sul PVC

Non occorre quindi creare un oggetto PV, che è sostituito dalla StorageClass.

Esercizio Demo

Creare una StorageClass

Preparare la directory e creare il file di manifest:

mkdir -p ~/ex/sc
cd ~/ex/sc
vim google-sc.yml
kind: StorageClass
apiVersion: storage.k8s.io/v1
metadata:
  name: slow
provisioner: kubernetes.io/gce-pd
parameters:
  type: pd-standard
reclaimPolicy: Retain

Applicare il file di manifest:

kubectl apply -f google-sc.yml

Verifica:

kubectl get sc
kubectl describe sc slow

Creare un PVC

Il file di manifest:

vim google-pvc.yml
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: pv-ticket
spec:
  accessModes:
  - ReadWriteOnce
  storageClassName: slow
  resources:
    requests:
      storage: 25Gi

Applicare il file di manifest:

kubectl apply -f google-pvc.yml

Verificare:

kubectl get pvc
NAME        STATUS    VOLUME   CAPACITY   ACCESS MODES   STORAGECLASS   AGE
pv-ticket   Pending                                      slow           3s

Notare lo stato Pending. Non abbiamo il driver CSI installato ed è in attesa che venga installato.

Ctreare un Pod

Il file fi manifest:

vim google-pod.yml
apiVersion: v1
kind: Pod
metadata:
  name: class-pod
spec:
  volumes:
  - name: data
    persistentVolumeClaim:
      claimName: pv-ticket
  containers:
  - name: ubuntu-ctr
    image: ubuntu:latest
    command:
    - /bin/bash
    - "-c"
    - "sleep 60m"
    volumeMounts:
    - mountPath: /data
      name: data

Applicare il file di manifest:

kubectl apply -f google-pod.yml

Verifica:

kubectl get pod
NAME        READY   STATUS    RESTARTS   AGE
class-pod   0/1     Pending   0          56s

Anch'esso è nello stato Pending.

Ispezionandolo con:

kubectl describe pod class-pod

Notiamo che:

Events:
  Type     Reason            Age   From               Message
  ----     ------            ----  ----               -------
  Warning  FailedScheduling  3m5s  default-scheduler  0/3 nodes are available: 3 pod has unbound immediate PersistentVolumeClaims. preemption: 0/3 nodes are available: 3 Preemption is not helpful for scheduling.

Di più non possiamo fare con Kind.

Ripoliamo l'esercizio:

kubectl delete -f google-pod.yml
kubectl delete -f google-pvc.yml
kubectl delete -f google-sc.yml

Autoscaling

Per questo esercizio occorre Minikube.

Lanciare Minikube:

minikube start --base-image='gcr.io/k8s-minikube/kicbase:v0.0.35'

Abilitare lo add-on Metrics Server:

minikube addons enable metrics-server

Controllare gli add-on:

minikube addons list
mkdir -p ~/ex/auto
wget https://k8s.io/examples/application/php-apache.yaml
mv php-apache.yaml ~/ex/auto/php-apache.yml
cd ~/ex/auto

Applichiamo il deployment:

kubectl apply -f php-apache.yml

L'immagine scaricata compie del lavoro intensivo di CPU:

<?php
  $x = 0.0001;
  for ($i = 0; $i <= 1000000; $i++) {
    $x += sqrt($x);
  }
  echo "OK!";
?>

Creiamo uno Horizontal Pod Autoscaler:

kubectl autoscale deployment php-apache --cpu-percent=50 --min=1 --max=10

Verifichiamo gli HPA:

kubectl get hpa

Per aumentare il carico, in un'altra finestra:

kubectl run -it --rm load-generator --image=busybox /bin/sh

E all'interno del contenitore lanciamo il comando:

while true; do wget -q -O- http://php-apache; done

Sul primo terminale:

kubectl get hpa -w
kubectl get pods

Torniamo sul secondo terminale e interrompiamo con Ctrl-C.

Sul primo terminale ripetiamo:

kubectl get hpa -w
kubectl get pods

Canccellare lo autoscaler:

kubectl delete hpa php-apache

Pulizia:

kubectl delete -f php-apache.yml

Helm Fondamenti

Helmlogo

Helm è un progetto open source che facilità l'impacchettamento, il deployment e l'amministrazione di applicativi su Kubernetes.

Ha il concetto di chart, una collezione di file che descrivono un insieme di risorse Kubernetes-

I chart sono descrizioni accurate, complete e funzionali di un progetto Kubernetes e sono mantenuti in repositories simili a quelli di Docker.

Gli aspetti coperti da questa sezione sono:

  1. Installazione di Helm
  2. Comandi di base di Helm - gestione di base di chart esistenti
  3. Creazione di un chart
  4. Customizzazione di un chart

Informazioni ufficiali e tutorials su Helm si trovano su https://helm.sh/docs/.

Architettura e Installazione

Componenti di Helm

  • Client - strumento da linea di comando che interagisce con l'ambiente Helm
  • Chart - collezione di files che definiscono il deployment di un applicativo su Kubernetes
  • Repository - collezioni di chart su un server, può essere pubblico o privato

Installazione del Client Helm

Le ultime release di Helm si trovano a https://github.com/helm/helm/releases.

Scaicarlo per la architettura appropriata, estrarlo e spostarlo in una directory del PATH.

Alternativamente usare i seguenti comandi:

curl -fsSL -o get_helm.sh \
https://raw.githubusercontent.com/helm/helm/master/scripts/get-helm-3
chmod 700 get_helm.sh
./get_helm.sh

Helm deve avere visibilità del cluster Kubernetes, quindi lo installeremo con il secondo metodo.

L'installazione di Helm pone i suoi componenti in locazioni specifiche del sistema corrente, e registra un numero di variabili per raggiungere i componenti.

Queste variabili sono visibili col comando:

helm env

Comandi di Base

Aggiunta df un Repository

Aggiungere un repository:

helm repo add bitnami https://charts.bitnami.com/bitnami

Listare i repositories:

helm repo list

Ricerca del chart drupal:

helm search repo drupal

Ricerca dei chart di Content Management System:

helm search repo content

Ricerca delle versioni disponibili di chart drupal:

helm search repo drupal --versions

La lista può essere lunga.

CHART VERSION è la versione del chart di Helm. APP VERSION è la versione dell'applicativo pacchettizzato nel chart.

Installazione della Release drupal

E' necessario avere un Load Balancer attivo. Noi abbiamo Metallb.

Installazione:

helm install mysite bitnami/drupal
NAME: mysite
LAST DEPLOYED: Sat Feb 10 16:18:03 2024
NAMESPACE: default
STATUS: deployed
REVISION: 1
TEST SUITE: None
NOTES:
CHART NAME: drupal
CHART VERSION: 17.3.4
APP VERSION: 10.2.3** Please be patient while the chart is being deployed **

1. Get the Drupal URL:

  NOTE: It may take a few minutes for the LoadBalancer IP to be available.
        Watch the status with: 'kubectl get svc --namespace default -w mysite-drupal'

  export SERVICE_IP=$(kubectl get svc --namespace default mysite-drupal --template "{{ range (index .status.loadBalancer.ingress 0) }}{{ . }}{{ end }}")
  echo "Drupal URL: http://$SERVICE_IP/"

2. Get your Drupal login credentials by running:

  echo Username: user
  echo Password: $(kubectl get secret --namespace default mysite-drupal -o jsonpath="{.data.drupal-password}" | base64 -d)

Vi sono istruzioni utili per l'utente o amministratore, e conviene leggerle.

E' possibile installare più applicativi uguali nello stesso cluster, a patto che siano in namespace separati. Per esempio:

kubectl create ns first
helm install --namespace first mysite bitnami/drupal

Listare le installazioni:

helm list
helm list --all-namespaces

Documentazione

La documentazione del particolare chart che stiamo installando è a https://artifacthub.io/packages/helm/bitnami/drupal.

ArtifactHub è un sito con documentazione su una svariata quantità di software, in vari formati di pacchettizzazione.

Oltre ad esempi, descizioni e raccomandazioni, vi è una lunga lista di parametri di configurazione.

Rimuovere una Release

helm uninstall mysite

Configurazione di Release

Preparare un file Yaml di configurazione in una directory appropriata:

mkdir -p ~/scripts/drupal
vim ~/scripts/drupal/values.yml
drupalUsername: admin
drupalEmail: admin@example.com
mariadb:
  db:
    name: "my-database"

Installare una release Helm con il file di configurazione:

helm install mysite bitnami/drupal \
  --values ~/scripts/drupal/values.yml

Si puà anche non avere un file di configurazione e modificare parametri di configurazione sulla linea di comando:

helm install mysite bitnami/drupal --set drupalUsername=admin

Per le sottosezioni usare parametri con notazione punto, ad esempio --set mariadb.db.name=my-database.

Si possono avere più opzioni --set.

Si possono avere entrambi il file di configurazione e parametri passati con l'opzione --set. In caso di collisione l'ultimo valore vince.

Upgrade di Installazione

Una release è una particolare combinazione di configurazione e versione di chart per una installazione.

L'installazione di un chart produce una release iniziale.

Un upgrade produce una nuova release della stessa installazione.

Upgrade di Configurazione

Esempio. Un upgrade che cambia solo un parametro di configurazione:

helm upgrade mysite bitnami/drupal --set ingress.enabled=true

In realtà non è così facile. L'errore risultante è esplicativo dei parametri mancanti al comando.

Il nostro applicativo usa tre files di secrets:

  • per la password dell'utente drupal
  • per la password dell'amministratore del database engine maria-db
  • per la password dell'utente che accede al database my-database

Questi tre files vanno preservati durante l'upgrade e quindi le tre password vanno anch'esse settate, invariate. Per scoprirle si caricano i loro valori correnti in variabili d'ambiente con:

export DRUPAL_PASSWORD=$(kubectl get secret \
  --namespace "default" mysite-drupal \
  -o jsonpath="{.data.drupal-password}" | base64 -d)
export MARIADB_ROOT_PASSWORD=$(kubectl get secret \
  --namespace "default" mysite-mariadb \
  -o jsonpath="{.data.mariadb-root-password}" | base64 -d)
export MARIADB_PASSWORD=$(kubectl get secret \
  --namespace "default" mysite-mariadb \
  -o jsonpath="{.data.mariadb-password}" | base64 -d)

E quindi il comando di upgrade è:

helm upgrade mysite bitnami/drupal --set ingress.enabled=true \
  --set drupalPassword=$DRUPAL_PASSWORD \
  --set mariadb.auth.rootPassword=$MARIADB_ROOT_PASSWORD \
  --set mariadb.auth.password=$MARIADB_PASSWORD

Il comando ha così successo.

Anzichè lo upgrade di un solo parametro si può passare un file di configurazione Yaml, come con il caso di installazione, naturalmente con gli stessi problemi di mantenimento dei Secrets.

helm upgrade mysite bitnami/drupal \
  --values ~/scripts/drupal/values.yml \
  --set drupalPassword=$DRUPAL_PASSWORD \
  --set mariadb.auth.rootPassword=$MARIADB_ROOT_PASSWORD \
  --set mariadb.auth.password=$MARIADB_PASSWORD

Upgrade di Versione

E' opportuno prima compiere un update di tutti i repositories configurati, per rinfrescare la lista dei chart disponibili:

helm repo update

Per compiere l'upgrade all'ultima versione disponibile:

helm upgrade mysite bitnami/drupal

Naturalmente si possono usare le opzioni --values e/o --set.

Listare le versioni disponibili:

helm search repo drupal --versions

Compiere l'upgrade as una versione data:

helm upgrade mysite bitnami/drupal --version 12.2.10

Si deve dare la versione del chart, non dell'applicativo.

Rimozione di una Release

Dal namespace di default:

helm uninstall mysite

Da un namespace specifico:

helm uninstall mysite --namespace first

La rimozione può richiedere del tempo, mentre Kubernetes elimina tutte le risorse. Durante questo tempo non è possibile compiere un'altra installazione dello stesso applicativo.

Informazioni di Release

Le informazioni sulle varie release sono mantenute come Secrets:

kubectl get secrets
NAME                           TYPE                 DATA   AGE
mysite-drupal                  Opaque               1      64m
mysite-mariadb                 Opaque               2      64m
sh.helm.release.v1.mysite.v1   helm.sh/release.v1   1      64m
sh.helm.release.v1.mysite.v2   helm.sh/release.v1   1      59m
sh.helm.release.v1.mysite.v3   helm.sh/release.v1   1      37m
sh.helm.release.v1.mysite.v4   helm.sh/release.v1   1      18m
sh.helm.release.v1.mysite.v5   helm.sh/release.v1   1      9m11s

Questo rende possible il rollback ad una revisione precedente.

La rimozione di un chart rimuove anche tutte le informazioni di Secrets connesse.

Altri Comandi Helm

Fasi di Esecuzione

Fasi di esecuzione di una installazione (o di un upgrade) di un chart di Helm:

  1. Caricamento del chart e delle sue dipendenze
  2. Attualizzazione dei parametri e dei valori
  3. Esecuzione dei templati e generazione dei file di specifiche Yaml
  4. Verifica dei dati di generazione di oggetti Kubernetes
  5. Invio al cluster Kubernetes

Ciascuna di queste fasi può generare errori.

Se nella fase 5 il server Kubernetes accetta i files Yaml, Helm dichiara il successo dell'operazione.

Questo non vuol dire che l'applicativo sia pronto, poichè i pod possono impiegare tempo considerevole a scaricare le immagini.

Alcune opzioni di installazione aiutano:

helm install mysite bitnami/drupal \
  --wait --timeout 120

Helm compie allora ripetute queries a Kubernetes e attende che tutti i pod dell'applicativo installato siano running.

E' buona prassi includere una opzione --timeout espressa in secondi, per non attendere rventualmente per sempre. Al termine del periodo di timeout lo stato dell'installazione è failed. Questo può dare delle inconsistenze se il periodo non è abbastanza lungo.

Un'alternativa è:

helm install mysite bitnami/drupal \
  --atomic --timeout 120

E'simile a --wait ma in caso di fallimento Helm compie un rollback automatico all'ultima versione di successo.

Dry Run

helm install mysite bitnami/drupal \
  --values ~/scripts/drupal/values.yml \
  --set drupalEmail=foo@example.com \
  --dry-run

Non genera l'applicativo descritto nel chart. Non invia le specifiche al cluster Kubernetes.

Produce molte informazioni di debugging.

  • identificativi della release
  • lista di tutti i file di specifiche Yaml generati
  • note e istruzioni per l'utente

Anche se non viene generato l'applicativo sul cluster, un dry run può contattare il cluster più volte durante le sue fasi, e generare files di specifiche legati al particolare cluster.

Helm Template

Il comando helm template genera solo l'identificativo della release e la lista dei file di specifiche Yaml. Non contatta mai il server Kubernetes.

helm template mysite bitnami/drupal \
  --values ~/scripts/drupal/values.yml \
  --set drupalEmail=foo@example.com

Non compie validazione dei file Yaml generati.

Ha praticamente le stesse opzioni di helm install.

Release

Partendo da una situazione pulita, installiamo la prima release di drupal:

helm install mysite bitnami/drupal \
  --values ~/scripts/drupal/values.yml \
  --set drupalEmail=foo@example.com

Vengono generati dei Secrets:

kubectl get secret
NAME                           TYPE                 DATA   AGE
mysite-drupal                  Opaque               1      13s
mysite-mariadb                 Opaque               2      13s
sh.helm.release.v1.mysite.v1   helm.sh/release.v1   1      13s

Helm mantiene le informazioni di release nel secret sh.helm.release.v1.mysite.v1.

L'ultimo è il numero di versione di upgrade. Ad ogni nuovo upgrade compare un nuovo secret con versione v2, v2, ecc. Vengono mantenute al massimo 10 versioni, le più vecchie sono rimosse.

Si può ispezionare il secret di versione con:

kubectl get secret sh.helm.release.v1.mysite.v1 -o json

La sezione maggiore, data, contiene informazioni per ricreare la release, compresse e trascodificate con Base64.

Si possono estrarre queste informazioni con dei comandi.

helm get notes

Note finali per l'utente:

helm get notes mysite

helm get values

Valori parametrici della release.

Valori forniti nel comando di unstallazione o upgrade:

helm get values mysite

Tutti i valori:

helm get values mysite --all

helm get manifest

Tutti i file di specifiche Yaml:

helm get manifest mysite

Helm Upgrade e History

Upgrade

Compiamo un upgrade alla release.

Prepariamo le variabili d'ambiente necessarie:

export DRUPAL_PASSWORD=$(kubectl get secret \
  --namespace "default" mysite-drupal \
  -o jsonpath="{.data.drupal-password}" | base64 -d)
export MARIADB_ROOT_PASSWORD=$(kubectl get secret \
  --namespace "default" mysite-mariadb \
  -o jsonpath="{.data.mariadb-root-password}" | base64 -d)
export MARIADB_PASSWORD=$(kubectl get secret \
  --namespace "default" mysite-mariadb \
  -o jsonpath="{.data.mariadb-password}" | base64 -d)

Compiamo l'upgrade:

helm upgrade mysite bitnami/drupal --set ingress.enabled=true \
  --set drupalPassword=$DRUPAL_PASSWORD \
  --set mariadb.auth.rootPassword=$MARIADB_ROOT_PASSWORD \
  --set mariadb.auth.password=$MARIADB_PASSWORD

History

Ispezioniamo ora la storia delle release:

helm history mysite
REVISION  UPDATED                  STATUS     CHART         APP VERSION  DESCRIPTION     
1         Sun Feb 11 09:37:53 2024  superseded  drupal-17.3.4 10.2.3      Install complete
2         Sun Feb 11 10:20:18 2024  deployed    drupal-17.3.4 10.2.3      Upgrade complete

Lo status può essere:

  • pending-install
  • deployed
  • pending-upgrade
  • superseded
  • pending-rollback
  • uninstalling
  • uninstalled
  • failed

Verifichiamo i valori della release:

helm get values mysite -o json
{"drupalPassword":"dcC54IVRFH","ingress":{"enabled":true},"mariadb":{"auth":{"password":"RzY3j10Azv","rootPassword":"3CVLN2SHFV"}}}

Restart dei Pod

Un upgrade è il meno invasivo possibile: Helm richiede il restart di pods solo se ve n'è bisogno. Si può forzare il restart di tutti i pod con l'opzione --force.

Cleanup

Un'installazione o upgrade fallita può lasciare lo stato di Kubernetes inconsistente. Per esempio possono essere stati creati dei Secrets prima del fallimento.

Questo è più probabile se è stata usata l'opzione --wait o --atomic.

Con l'opzione --cleanup-on-fail viene richiesta la rimozione di tutti gli oggetti della release in caso di fallimento.

Questo però non aiuta nell'eventuale debugging.

Rollback

Compiamo un rollback alla versione 1:

helm rollback mysite 1

Verifichiamo che i valori sono ora quelli della versione 1:

helm get values mysite -o json
{"drupalEmail":"foo@example.com","drupalUsername":"admin","mariadb":{"db":{"name":"my-database"}}}

Ispezioniamo la storia:

helm history mysite
REVISION  UPDATED                   STATUS      CHART         APP VERSION DESCRIPTION     
1         Sun Feb 11 09:37:53 2024  superseded  drupal-17.3.4 10.2.3       Install complete
2         Sun Feb 11 10:20:18 2024  superseded  drupal-17.3.4 10.2.3      Upgrade complete
3         Sun Feb 11 10:37:54 2024  deployed    drupal-17.3.4 10.2.3      Rollback to 1

Si possono compiere queries di un'altra revisione della storia, per esempio:

helm get values mysite --revision 2

E in modo simile per notes e manifest.

Mantenere la Storia

Si può disinstallare una release, ma mantenere la sua storia:

helm uninstall mysite --keep-history

La storia è ora:

helm history mysite
REVISION  UPDATED                   STATUS      CHART         APP VERSION DESCRIPTION            
1         Sun Feb 11 09:37:53 2024  superseded  drupal-17.3.4 10.2.3      Install complete       
2         Sun Feb 11 10:20:18 2024 superseded  drupal-17.3.4 10.2.3      Upgrade complete       
3        Sun Feb 11 10:37:54 2024 uninstalled drupal-17.3.4 10.2.3      Uninstallation complete

Con la storia presente, si può compiere un rollback ad una previa versione anche se quella corrente non è installata.

Generazione di un Chart

Creazione di un Chart

Helm fornisce un chart di default basato sull'applicativo Nginx.

Questo si chiama uno Starting Point.

Nginx è un buon starting point per applicativi stateless.

Helm fornisce degli starter packs per la creazione di altri modelli di applicativi.

La generazione di un chart di default è:

helm create anvil

Viene creata una sottodirectory anvil con la struttura:

tree anvil
anvil
├── Chart.yaml
├── charts
├── templates
│   ├── NOTES.txt
│   ├── _helpers.tpl
│   ├── deployment.yaml
│   ├── hpa.yaml
│   ├── ingress.yaml
│   ├── service.yaml
│   ├── serviceaccount.yaml
│   └── tests
│       └── test-connection.yaml
└── values.yaml

Nella directory anvil:

  • Chart.yaml - semplicemente descrive i numeri di versione del chart e dellìapplicativo
  • values.yaml - fornisce valori di default per la configurazione dell'applicativo. E' qui che viene configurato di default l'applicativo nginx.

Nella sottodirectory templates vi sono templati per la generazione di file Yaml dell'applicativo:

  • NOTES.txt - le note per l'utente
  • _helpers.tpl - spezzoni di templati che vengono inclusi in altri templati
  • deploymeny.yaml - per il deployment
  • hpa.yaml - per lo HorizontalPodAutoscaler
  • ingress.yaml - per Ingress
  • service.yaml - per il servizio
  • serviceaccount.yaml - per il ServiceAccount

La sintassi dei Templates è quella del linguaggio Go, quando con tale linguaggio si preparano applicativi web.

La sottodirectory tests contiene il file test-connection.yaml, che genera un pod per il test di connessione a Nginx.

Tutti questi files sono editabili, per indicare e personalizzare il nostro applicativo al posto di Nginx.

Installazione del Chart

Il chart prototipo è direttamente installabile. Posizionarsi della directory a monte di anvil e dare il comando:

helm install myapp anvil
NAME: myapp
LAST DEPLOYED: Sun Feb 11 12:53:52 2024
NAMESPACE: default
STATUS: deployed
REVISION: 1
NOTES:
1. Get the application URL by running these commands:
  export POD_NAME=$(kubectl get pods --namespace default -l "app.kubernetes.io/name=anvil,app.kubernetes.io/instance=myapp" -o jsonpath="{.items[0].metadata.name}")
  export CONTAINER_PORT=$(kubectl get pod --namespace default $POD_NAME -o jsonpath="{.spec.containers[0].ports[0].containerPort}")
  echo "Visit http://127.0.0.1:8080 to use your application"
  kubectl --namespace default port-forward $POD_NAME 8080:$CONTAINER_PORT

Il servizio installato nell'applicativo myapp è di tipo ClusterIP:

kubectl get svc
NAME          TYPE        CLUSTER-IP      EXTERNAL-IP   PORT(S)   AGE
kubernetes    ClusterIP   10.96.0.1       <none>        443/TCP   41h
myapp-anvil   ClusterIP   10.96.253.188   <none>        80/TCP    4m53s

Ciò vuol dire che il servizio non è direttamente accessibile dall'esterno. Le Note forniscono un workaround, per il testing, con i comandi:

export POD_NAME=$(kubectl get pods --namespace default \
  -l "app.kubernetes.io/name=anvil,app.kubernetes.io/instance=myapp" \
  -o jsonpath="{.items[0].metadata.name}")
export CONTAINER_PORT=$(kubectl get pod --namespace default \
  $POD_NAME -o jsonpath="{.spec.containers[0].ports[0].containerPort}")
echo "Visit http://127.0.0.1:8080 to use your application"
kubectl --namespace default \
  port-forward $POD_NAME 8080:$CONTAINER_PORT

L'ultimo comando è bloccante. Aprire un'altro terminale collegato al contenitore kub e dare il comando di test:

curl http://127.0.0.1:8080

Vi sono quattro tipi di servizio previsti:

  • ClusterIP - default in questo applicativo
  • NodePort
  • LoadBalancer
  • Ingress

Disinstallare al termine l'applicativo con il comando:

helm delete myapp

Files del Chart

Chart.yaml

Si possono aggiungere numerose altre proprietà oltre a quelle presenti.

Per esempio modificarla in:

apiVersion: v2
name: anvil
description: A surprise to catch something speedy.
version: 0.1.0
appVersion: 9.17.49
icon: https://wile.example.com/anvil.svg
keywords:
  - road runner
  - anvil
home: https://wile.example.com/
sources:
  - https://github.com/Masterminds/learning-helm/tree/main/chapter4/anvil
maintainers:
  - name: ACME Corp
    email: maintainers@example.com
  - name: Wile E. Coyote
    email: wile@example.com

La proprietà type: application può essere omessa poichè è il default. L'alternativa è type: library.

Package di Chart

La struttura gerarchica di un package viene trasformata in un archivio tar compresso col comando:

helm package anvil

In luogo di anvil si può dare il percorso, assoluto o relativo, della directory di testa del chart gerarchico.

Il risultato viene posto nella directory corrente ove si da il comando, e in questo caso è anvil-0.1.0.tgz.

La versione numerica è quella del chart, descritta nel file Chart.yaml.

Sono previste delle opzioni:

  • --dependency-update (-u) - compie un update dei chart dipendenti
  • --destination (-d) - setta la locazione dell'archivio se diversa dalla directory corrente
  • --app-version - cambia la versione dell'applicativo dal default descritto da Charts.yaml
  • --version - cambia la versione del chart dal default descritto da Charts.yaml

Vi sono anche delle opzioni per la firma crittografica del chart, usando PGP (Pretty Good Privacy). I comandi Helm install e upgrade hanno corrispondenti opzioni per la verifica della firma.

Il file eventiale ,helmignore nella directory corrente specifica files e directories da non inserire nell'archivio del chart. La sua struttura è uguala a quella di .gitignore.

Lint di Chart

Lint è uno strumento di analisi statica del chart, ed è utile eseguirlo prima della generazione del pacchetto, per trovare errori ed inconsistenze.

helm lint anvil

Si può anche eseguire su un chart raccolto in archivio:

helm lint anvil-0.1.0.tgz

Lint può generare notifiche di livello INFO, WARNING o ERROR.

Il livello ERROR fa si che il comando lint generi un codice di ritorno diverso da zero, utile quando inserito in una procedura shell.

Con l'opzione --strict anche WARNIBG genera un codice di ritorno diverso da zero.

Helm Avanzato

Helmtempl

Helm è un ambiente complesso.

Gli aspetti coperti da questa sezione sono:

  1. Templati per i chart - sintassi di costruzione del templato di un applicativo Kubernetes
  2. Dipendenze dei chart - carts che dipendono da altri charts
  3. Hooks di Helm - azioni ayyivate a punti specifici del ciclo di vita di un chart
  4. Repositories privati - setup e amministrazione

Esempio di Chart

Desideriamo produrre un Chart di Helm che automatizzi l'installazione di un nostro precedente applicativo: Wordpress e MySQL.

L'esercizio presume che le immagini verranno scaricate dal registry locale.

Se non presenti dare i comandi:

docker pull mysql:5.6
docker tag mysql:5.6 localhost:5000/mysql:5.6
docker push localhost:5000/mysql:5.6
docker pull wordpress:4.8-apache
docker tag wordpress:4.8-apache localhost:5000/wordpress:4.8-apache
docker push localhost:5000/wordpress:4.8-apache

Scaffolding del Chart

Un Chart non è altro che una directory con una determinata struttura.

Preparare lo scaffolding:

mkdir -p wordpress/charts wordpress/templates/tests
touch wordpress/Chart.yaml wordpress/values.yaml
touch wordpress/templates/01pvpvc-mysql.yaml
touch wordpress/templates/02mysql.yaml 
touch wordpress/templates/03pvpvc-wpress.yaml
touch wordpress/templates/04wpress.yaml

Tutti i file contenuti nella directory templates sono considerati dei manifest a meno che il loro nome inizi con _ (underscore).

Vengono inviati allo API Server in ordine alfabetico. Per indicare l'ordine, come trucco diamo ai nomi dei template un numero progressivo.

I PV e PVC gestiti da Helm sono delicati. Meglio avere un manifest che si occupa solo di loro.

tree wordpress
wordpress
├── charts
├── Chart.yaml
├── templates
│   ├── 01pvpvc-mysql.yaml
│   ├── 02mysql.yaml
│   ├── 03pvpvc-wpress.yaml
│   ├── 04wpress.yaml
│   └── tests
└── values.yaml

Elementi del Chart

Per prima cosa preparare le specifiche del chart:

vim wordpress/Chart.yaml
apiVersion: v2
name: wordpress
description: Wordpress app with MySQL backend

type: application

version: 0.1.0

appVersion: "1.0.0"

Inserire il primo template, della parte MySQL - namespace, PV e PVC:

vim wordpress/templates/01pvpvc-mysql.yaml
kind: Namespace
apiVersion: v1
metadata:
  name: mysql-db
  labels:
    name: mysql-db
---
apiVersion: v1
kind: PersistentVolume
metadata:
  name: my-pv-volume
spec:
  storageClassName: standard
  capacity:
    storage: 20Gi
  accessModes:
    - ReadWriteOnce
  hostPath:
    path: /data/my/
---
kind: PersistentVolumeClaim
apiVersion: v1
metadata:
  name: my-pv-claim
  namespace: mysql-db
spec:
  volumeName: my-pv-volume
  accessModes:
    - ReadWriteOnce
  resources:
    requests:
      storage: 10Gi

Preparare il secondo template, della parte MySQL - secret, deployment e servizio:

vim wordpress/templates/02mysql.yaml
apiVersion: v1
kind: Secret
metadata:
  namespace: mysql-db
  name: mysql-pass
type: Opaque
data:
# secret
  root-password: c2VjcmV0
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: mysql
  namespace: mysql-db
  labels:
    app: wordpress
spec:
  selector:
    matchLabels:
      app: wordpress
      tier: mysql
  strategy:
    type: Recreate
  template:
    metadata:
      labels:
        app: wordpress
        tier: mysql
    spec:             
      containers:     
      - image: localhost:5000/mysql:5.6
        name: mysql     
        env:            
        - name: MYSQL_ROOT_PASSWORD
          valueFrom:               
            secretKeyRef:          
              name: mysql-pass     
              key: root-password   
        ports:                     
        - containerPort: 3306      
          name: mysql              
        volumeMounts:              
        - name: mysql-persistent-storage
          mountPath: /var/lib/mysql     
      volumes:                          
      - name: mysql-persistent-storage  
        persistentVolumeClaim:          
          claimName: my-pv-claim
---
apiVersion: v1
kind: Service
metadata:
  name: mysql
  namespace: mysql-db
  labels:
    app: wordpress
spec:
  type: LoadBalancer
  ports:
    - protocol: TCP
      port: 3306
      targetPort: 3306
  selector:
    app: wordpress
    tier: mysql

La terza è la parte WordPress - PV e PVC:

vim wordpress/templates/03pvpvc-wpress.yaml
apiVersion: v1
kind: PersistentVolume
metadata:
  name: wp-pv-volume
spec:
  storageClassName: standard
  capacity:
    storage: 5Gi
  accessModes:
    - ReadWriteOnce
  hostPath:
    path: /data/wp/
---
kind: PersistentVolumeClaim
apiVersion: v1
metadata:
  name: wp-pv-claim 
spec:                
  volumeName: wp-pv-volume
  accessModes:            
    - ReadWriteOnce
  resources:       
    requests:
      storage: 5Gi

La quarta è la parte Wordpress - secret, deployment e servizio:

vim wordpress/templates/04wpress.yaml
apiVersion: v1
kind: Secret
metadata:
  name: mysql-pass   
type: Opaque      
data:       
  root-password: c2VjcmV0
---
apiVersion: apps/v1   
kind: Deployment      
metadata:                                        
  name: wordpress                                
  labels:                                        
    app: wordpress                               
spec:                                            
  selector:                                      
    matchLabels:                                 
      app: wordpress                             
      tier: frontend                             
  strategy:                                      
    type: Recreate                               
  template:                                      
    metadata:                                    
      labels:                                    
        app: wordpress                           
        tier: frontend                           
    spec:                                        
      containers:                                
      - image: localhost:5000/wordpress:4.8-apache
        name: wordpress
        env:                                     
        - name: WORDPRESS_DB_HOST                
          value: mysql.mysql-db                  
        - name: WORDPRESS_DB_PASSWORD            
          valueFrom:                             
            secretKeyRef:                        
              name: mysql-pass                   
              key: root-password                 
        ports:                                   
        - containerPort: 80                      
          name: wordpress                        
        volumeMounts:                            
        - name: wordpress-persistent-storage     
          mountPath: /var/www/html               
      volumes:                                   
      - name: wordpress-persistent-storage       
        persistentVolumeClaim:                   
          claimName: wp-pv-claim
---
apiVersion: v1
kind: Service
metadata:
  name: wordpress
  labels:
    app: wordpress
spec:
  type: LoadBalancer
  ports:      
    - port: 8080        
      targetPort: 80 
  selector:         
    app: wordpress  
    tier: frontend

Installazione del Chart

Con questi elementi dovremmo essere subito in grado di installare il chart:

helm install myword wordpress
NAME: myword
LAST DEPLOYED: Tue Feb 27 17:01:51 2024
NAMESPACE: default
STATUS: deployed
REVISION: 1
TEST SUITE: None

Verifichiamo gli oggetti nello spazio di default:

kubectl get all
NAME                             READY   STATUS    RESTARTS   AGE
pod/wordpress-5cdb8f4c8f-ghbs5   1/1     Running   0          74s

NAME                 TYPE           CLUSTER-IP   EXTERNAL-IP      PORT(S)          AGE
service/kubernetes   ClusterIP      10.96.0.1    <none>           443/TCP          22h
service/wordpress    LoadBalancer   10.96.5.6    172.18.255.201   8080:31037/TCP   76s

NAME                        READY   UP-TO-DATE   AVAILABLE   AGE
deployment.apps/wordpress   1/1     1            1           75s

NAME                                   DESIRED   CURRENT   READY   AGE
replicaset.apps/wordpress-5cdb8f4c8f   1         1         1       75s

Verifichiamo gli oggetti nello spazio nomi `mysql-db``:

kubectl get all -n mysql-db
NAME                         READY   STATUS    RESTARTS   AGE
pod/mysql-5d5db54ccd-fpmhh   1/1     Running   0          3m38s

NAME            TYPE           CLUSTER-IP     EXTERNAL-IP      PORT(S)          AGE
service/mysql   LoadBalancer   10.96.149.15   172.18.255.200   3306:30001/TCP   3m39s

NAME                    READY   UP-TO-DATE   AVAILABLE   AGE
deployment.apps/mysql   1/1     1            1           3m38s

NAME                               DESIRED   CURRENT   READY   AGE
replicaset.apps/mysql-5d5db54ccd   1         1         1       3m38s

Verifichiamo i PV:

kubectl get pv
NAME           CAPACITY   ACCESS MODES   RECLAIM POLICY   STATUS   CLAIM                  STORAGECLASS   REASON   AGE
my-pv-volume   20Gi       RWO            Retain           Bound    mysql-db/my-pv-claim   standard                5m49s
wp-pv-volume   5Gi        RWO            Retain           Bound    default/wp-pv-claim    standard                5m49s

I PVC nello spazio nomi di default:

kubectl get pvc
NAME          STATUS   VOLUME         CAPACITY   ACCESS MODES   STORAGECLASS   AGE
wp-pv-claim   Bound    wp-pv-volume   5Gi        RWO            standard       7m24s

I PVC nello spazio nomi mysql-db:

kubectl get pvc -n mysql-db
NAME          STATUS   VOLUME         CAPACITY   ACCESS MODES   STORAGECLASS   AGE
my-pv-claim   Bound    my-pv-volume   20Gi       RWO            standard       8m57s

Tutto è presente.

Listiamo il chart installato:

helm list
NAME  	NAMESPACE	REVISION	UPDATED                                	STATUS  	CHART          	APP VERSION
myword	default  	1       	2024-02-27 17:01:51.254489827 +0100 CET	deployed	wordpress-0.1.0	1.0.0      

Proviamo il collegamento con un browser a 172.18.255.201:8080.

Vediamo l'accesso a WordPress.

Wpress

Sembra funzionare.

Possiamo disinstallare il chart:

helm uninstall myword

Verifichiamo poi che tutti gli elementi dell'applicativo siano stati tolti.

Pacchettizzazione

Pacchettizziamo ora il chart:

helm package wordpress

Questo genera il file di package wordpress-0.1.0.tgz.

Lo potremo porre nel nostro repository personale quando ne avremo uno.

Il chart pacchettizzato è anche scaricabile al link wordpress-0.1.0.tgz.

Note

Il nostro chart non è per niente parametrico: non usa il linguaggi di templating. E' puramente un primo esercizio dimostrativo.

Volendo parte ora una lunga sequenza di Refactoring a passi successivi: a ciacun passo aggiungiamo un cero grado di parametrizzazione.

Diviene utile usare un ambiente di Controllo Versione, come Git, e un chain di CD/CI.

Ma questo è fuori dallo scopo del corso.

Templati

La specifica sintassi dei templati proviene dalla libreria standard dei templati del linguaggio Go, in cui Helm è scritto.

La stessa sintassi si usa anche in kubectl, Hugo e tanti altri applicativi scritti in Go.

Non è però necessario conoscere il linguaggio Go per usarla (anche se è un linguaggio di programmazione moderno, performante e bello da usare).

Azioni

Lo switch dentro il templating engine avviene quando si incontrano due graffe aperte, {{. Lo switch fuori dal templating engine avviene quando si incontrano due graffe chiuse, }}.

Ciò che si trova entro le graffe sono azioni, e possono essere comandi, strutture dati, costrutti di controllo, sottotemplati o funzioni.

Un trattino, -, prima o dopo le graffe, separato con spazio dal contenuto, sopprime spazi nel testo risultante. Per esempio:

{{ "Hello" -}} , {{- "World" }}

genera

Hello,World

Informazioni Esterne

Informazioni acquisite nel templating engine dall'ambiente esterno sono rappresentate come .Nome, oppure .Nome,nome oppure .Nome.nome.nome, ecc.

Tutte le informazioni iniziano col carattere punto, .. Per motivi derivanti da Go il primo Nome deve essere con iniziale maiuscola. In Go i simboli che iniziano con la maiuscola sono di visibilità pubblica, non privata.

La struttura è gerarchica e il separatore di gerarchia è il carattere punto, .. Corrispondono alla gerarchia presente nei files Yaml di specifica.

Le informazioni possono provenire dal file values.yaml, tantissimi parametri potenziali, e hanno il nome .Values.xxx.

Possono provenire da informazioni sulla release, col nome .Release.xxx:

  • .Release.Name
  • .Release.Namespace
  • .Release.IsInstall
  • .Release.IsUpgrade
  • .Release.Service - il servizio che genera la release, settato a "Helm" nel nostro caso

Possono provenire dal file Chart.yaml, col nome .Chart.xxx:

  • .Chart.Name
  • .Chart.Version
  • .Chart.AppVersion
  • .Chart.Annotations - una lista di annotazioni in formato key=value

Possono provenire da informazioni ricevute dal cluster Kubernetes, col nome .Capabilities.xxx:

  • .Capabilities.APIVersions
  • .Capabilities.KubeVersion.Version
  • .Capabilities.KubeVersion.Major
  • .Capabilities.KubeVersion.Minor

Possono provenire da files dopo una direttiva di inclusione. col nome .Files.xxx.

Possono riferirsi al template in corso di processamento, col nome .Template.xxx:

  • .Template.Name
  • .Template.BasePath

Pipelines e Tipi

Una pipeline è una sequenza di comandi, funzioni e variabili concatenati, separati dal simbolo di pipe |-

Il funzionamento è molto simile a quello standard di Unix/Linux: l'output del comando precedente la pipe è passato come input al comando seguente. Per esempio:

character: {{ .Values.character | default "Sylvester" | quote }}

Il valore del parametro .Values.character è passato alla funzione default. Questa lo controlla e se è la stringa vuota lo sostituisce con la stringa "Sylvester", quidi lo passa alla funzione quote. Tale funzione racchiude la stringa tra doppi apici.

Attenzione nella specifica di parametri, poichè il linguaggio sottostante Go è fortemente tipizzato e le funzioni lavorano solo su tipi prestabiliti.

Per esempio se .Values.character vale 127, questo è un numero intero; se vale ab112 può venir interpretato come intero esadecimale. Solo se vale "ab112" è una stringa.

Non scordarsi i doppi apici per i valori stringa nei vari files sottoposti al templating engine.

Funzioni

Le funzioni provengono dalla libreria Go chiamata Sprig, e mantenuta a https://github.com/Masterminds/sprig. Sono più di 100.

La documentazione delle funzioni è a https://helm.sh/docs/chart_template_guide/function_list/.

Esempio:

{{- toYaml .Values.podSecurityContext | nindent 8 }}
  • viene preso il parametro podSecurityContext dal file values.yaml (o passato tramite l'opzione --set)
  • vengono tolti eventuali spazi all'inizio, dalla funzione -
  • la stringa risultante viene convertita a formato Yaml, dalla funzione toYaml
  • la funzione nindent indenta la stringa di 8 spazi

Go ha tre tipi di collezioni semplici: array (liste immutabili), slice (liste mutabili) e maps (coppie chiave-valore).

Se la funzione nindent riceve in input una collezione, opera su ogni elemento della collezione su nuova riga.

Metodi

In Go un metodo è una funzione che si applica ad un certo tipo di oggetto.

La notazione è (oggetto).metodo.

Se oggetto è una collezione, metodo si applica ad ogni suo elemento.

Vi sono le funzioni:

  • .Files.Get name - ritorna il contenuto del file il cui nome è name, espresso come percorso relativo alla directory radice del chart. Il contenuto è una slice di caratteri, quella che si chiamerebbe una stringa.
  • .Files.GetBytes name - ritorna il contenuto del file il cui nome è name, espresso come percorso relativo alla directory radice del chart. Il contenuto è una slice di byte, espressa in Go come []byte
  • .Files.Glob pattern - ritorna il contenuto dei file conformi allo schema pattern, come stringa (slice di caratteri). Nel pattern si usano gli stessi simboli ? e * della shell di Unix.

Go tratta normalmente le stringhe usando la codifica Unicode UTF-8. Ciò vuol dire che ogni carattere (in Go chiamato runa) è potenzialmente rappresentato da più bytes. Il Go ha corrispondentemente funzioni che operano su stringhe (lunghezza, sottostringhe, ecc.) e funzioni che operano su bytes (JSON, Base64, ecc.).

Queste tre funzioni soprs ritornano collezioni.

Vi sono dei metodi che si applicano alle collezioni risultanti:

  • .Files.AsConfig - trasforma l'input per renderlo compatibile ad un manifest di ConfigMap
  • .Files.AsSecrets - trasforma l'input per renderlo compatibile ad un manifest di Secrets, codificato Base64
  • .Files.Lines - ritorna il contenuto dell'input come array, compiendo uno split degli elementi col carattere \n

Esempio. La stringa di template:

{{ (.Files.Glob "config/*").AsSecrets | indent 2 }}

potrebbe generare:

  jetpack.ini: ZW5hYmxlZCA9IHRydWU=
  rocket.yaml: ZW5hYmxlZDogdHJ1ZQ==

Funzione lookup

Compie una query al cluster Kubernetes alla ricerca di risorse.

Per esempio il seguente ricerca un D chiamato runner nel namespace anvil, e ne ritorna le annotazioni nella sezione metadati:

{{ (lookup "apps/v1" "Deployment" "anvil" "runner").metadata.annotations }}

Gli argomenti di lookup sono:

  • versione API
  • tipo di oggetto
  • namespace
  • nome dell'oggetto

Nell'esempio seguente viene ritornata la lista di tutti i ConfigMap nel namespace anvil:

{{ (lookup "v1" "ConfigMap" "anvil" "").items }}

La lista è iterabile in un loop.

Naturalmente lookup non funziona in un Dry Run o col comando helm template quando non viene contattato il server Kubernetes.

Costrutto if/else

Con solo la branca if:

{{- if .Values.ingress.enabled -}}
...
{{- end }}

Con anche la branca else:

{{- if .Values.ingress.enabled -}}
...
{{- else -}}
# Ingress not enabled
{{- end }}

and e or

Non sono operatori, ma funzioni che prendono i due argomenti da paragonare.

Esempi:

{{- if and .Values.characters .Values.products -}}
...
{{- end }}
{{- if or (eq .Values.character "Wile E. Coyote") .Values.products -}}
...
{{- end }}

Costrutto with

Simile ad un foreach di altri linguaggi.

Esempio:

{{- with .Values.ingress.annotations }}
annotations:
  {{- toYaml . | nindent 4 }}
{{- end }}

Se ad un'iterazione l'argomento corrente del with è vuoto, il blocco viene saltato e si passa all'argomento successivo.

Variabili

Si possono creare delle variabili. Il loro nome inizia col carattere $.

Hanno un tipo, che è dedotto in fase di assegnazione dal tipo del valore.

L'operatore di creazione ed assegnazione - la prima volta - è :=. Il cambiamento di valore quando la variabile esiste già è =.

Esempi:

{{ $var := .Values.character }}
{{ $var = "Tweety" }}

Lo scopo delle variabili è il templato in cui vivono, dal punto di assegnazione in avanti.

Il riferimento ad una variabile che non esiste è un errore.

Costrutto range

Opera in loop su una collezione, che può essere una lista o ona map (anche chiamata un dict).

Esempio:

Date le collezioni:

# An example list in YAML
characters:
  - Sylvester
  - Tweety
  - Road Runner
  - Wile E. Coyote
# An example map in YAML
products:
  anvil: They ring like a bell
  grease: 50% slippery
  boomerang: Guaranteed to return

Iterando sulla lista si può avere:

characters:
{{- range .Values.characters }}
  - {{ . | quote }}
{{- end }}

che fornisce in output:

characters:
  - "Sylvester"
  - "Tweety"
  - "Road Runner"
  - "Wile E. Coyote"

Il punto, ., è l'argomento corrente del loop di range.

Iterando sul dict si può avere:

products:
{{- range $key, $value := .Values.products }}
  - {{ $key }}: {{ $value | quote }}
{{- end }}

che produce in output:

products:
  - anvil: "They ring like a bell"
  - boomerang: "Guaranteed to return"
  - grease: "50% slippery"

$key e $value sono variabili predefinite per l'utilizzo di dict.

Notare che, derivato dal Go, è possibile la doppia assegnazione: $key, $value := .Values.products.


Template con Nome

E' possibile definire un template dandogli un nome, e così richiamarlo da un altro template.

Esempio.

{{/*
Selector labels
*/}}
{{- define "anvil.selectorLabels" -}}
app.kubernetes.io/name: {{ include "anvil.name" . }}
app.kubernetes.io/instance: {{ .Release.Name }}
{{- end -}}
  • {{/* ... */}} racchiudono un commento
  • {{- define "anvil.selectorLabels" -}} è il nome che si da al template corrente. Può essere un nome gerarchico, separato da ., e questo è utile per evitare collizioni di nomi
  • segue il corpo del template
  • {{- end -}} termina la definizione di template

include

Richiama un template specificato, includendolo in quello corrente.

Esempio:

{{- include "anvil.selectorLabels" . | nindent 8 }}
  • il primo argomento è il nome del template chiamato
  • il secondo argomento è un oggetto da passare al template. Il carattere . indica l'oggetto globale, cioè tutti gli oggetti che sono in scopo al punto della chiamata. E' l'uso più comune.
  • l'output del template richiamato è eventualmente, come qui, passato alla pipeline

Refactoring del Chart

Organizzazione

Vogliamo incrementalmente migliorare il chart prodotto, introducendo della parametrizzazione.

Il controllo versioni che usiamo è elementare:

cp -rf wordpress wordpress.1
  • salviamo la versione stabile corrente in wordpress.1
    • poi sarà wordpress.2, wordpress.3, ecc.
  • modifichiamo wordpress
  • rimaniamo sempre posizionati nella directory corrente, dando percorsi relativi

Generiamo anche un chart anvil, quello di esempio standard, da cui trarre spunto:

helm create anvil

Il file di templati viene copiato:

cp anvil/templates/_helpers.tpl wordpress/templates

OIn tale file, ogni occorrenza della stringa anvil deve essere sostituita con wordpress.

Il file wordpress/values.yaml invece lo generiamo passo a passo.

Note

Il chart di esempio presume che il nostro applicativo abbia un solo servizio, invece ne ha due: mysql e wordpress.

Ma possiamo considerare wordpress come servizio primario.

Prima Iterazione: Note per l'Utente

NOTES.txt

Copiamo il file dall'esempio standard e modifichiamolo:

cp anvil/templates/NOTES.txt wordpress/templates

Ogni occorrenza della stringa anvil deve essere sostituita con wordpress.

Per risolvere i riferimenti .Values del file editiamo e inseriamone i valori in values.yaml:

vim wordpress/values.yaml
service:
  type: LoadBalancer
  port: 8080

ingress:
  enabled: false

Occorre anche editare il Manifest del service di wordpress:

vim wordpress/templates/04wpress.yaml

E cambiare la sezione Service in :

apiVersion: v1
kind: Service
metadata:
  name: {{ include "wordpress.fullname" . }}
  labels:
    app: wordpress
spec:
  type: {{ .Values.service.type }}
  ports:
    - port: {{ .Values.service.port }}
      targetPort: 80
  selector:
    app: wordpress
    tier: frontend

Installazione e prova:

helm install myword wordpress

Escono i NOTES.

I comandi shell suggeriti funzionano:

export SERVICE_IP=$(kubectl get svc --namespace default myword-wordpress --template "{{ range (index .status.loadBalancer.ingress 0) }}{{.}}{{ end }}")
echo http://$SERVICE_IP:8080
http://172.18.255.201:8080

Diamo il comando di verifica:

kubectl get services

Notiamo che il servizio si chiama ora myword-wordpress e non semplicemente wordpress.

Apriamo un browser a http://172.18.255.201:8080 per verificare se funziona.

Oggetti Built-in

Helm ha una serie di oggetti predefiniti, come ad esempio {{ .Release.Name }} che abbiamo visto in NOTES.txt.

Sezione Release:

  • Release.Name
  • Release.Namespace
  • Release.IsUpgrade
  • Release.IsInstall
  • Release.Revision
  • Release.Service - sempre Helm

Sezione Values: oggetti definiti in values.yaml secondo la gerarchia Yaml e con notazione punto.

Sezione Chart: oggetti definiti in Chart.yaml secondo la gerarchia Yaml e con notazione punto.

Sezione Capabilities:

  • Capabilities.APIVersions - insieme di versioni
  • Capabilities.APIVersions.Has $version - indica se la versione $version è disponibile
  • Capabilities.KubeVersion - di Kubernetes
  • Capabilities.KubeVersion.Major
  • Capabilities.KubeVersion.Minor
  • Capabilities.HelmVersion
  • Capabilities.HelmVersion.GoVersion - versione del compilatore Go usata

Sezione Template:

  • Template.Name - il nome completo del Template corrente, incluso il percorso
  • Template.BasePath - solo il nome del Template corrente

Ha inoltre una serie di funzioni di gestione dei file:

  • Files.Get - ritorna il contenuto del file specificato come caratteri UTF-8 (rune in Go)
  • Files.GetBytes - ritorna il contenuto del file specificato come array di bytes
  • Files.Glob - ritorna una lista di files specificata da caratteri jolly
  • Files.Lines - ritorna un file linea per linea, utile nelle iterazioni
  • Files.AsSecrets - ritorna il contenuto di un file codificato in Base64
  • Files.AsConfig - ritorna un file codificato come mappa Yaml

NOTA

Una piccola guida all'arte di scrivere templati in Helm si trova a https://helm.sh/docs/chart_template_guide/

La documentazione generale è a https://helm.sh/docs/

Helm Starters

Uno Starter è simile ad uno Helm Chart ma serve come template di nuovi charts.

In uno Starter tutte le occorrenze esplicite del nome del chart in tutti i files, sono sostituite dalla stringa <CHARTNAME>. Questa viene attualizzata al nome specifico del chart quando il chart viene creato sulla base dello Starter.

Creazione di uno Starter

Per creare uno Starter di nome wpstart a partire dal nostro chart wordpress possiamo procedere come segue.

Copiamo tutti i file di wordpress nella nuova directory wpstart:

cp -rf wordpress wpstart

Modifichiamo tutte le occorrenze della sringa wordpress con <CHARTNAME> in tutti i file:

cd wpstart
find . -type f -exec sed -i 's/wordpress/<CHARTNAME>/g' {} \;
cd ..

Lo starter è pronto.

Uso dello Starter

Creiamo un chart mxwp a partire dallo starter wpstart:

helm create --starter wpstart mxwp

La directory wpstart è anch'essa sotto la directory corrente.

Possiamo ispezionare i fles di mxwp per verificare che cntengono il nome del chart corrente.

Solitamente gli Starter più usati vengono posti in una directory dell'ambiente di Helm, $HELM_DATA_HOME/starters. Questa directory può ancora non esistere.

mkdir -p ~/.local/share/helm/starters
cp -rv wpstart ~/.local/share/helm/starters

Ora si può creare un chart basato sullo Starter da ovunque nel filesystem:

helm create -p wpstart mywp

Charts Complessi

Dipendenze

Un chart può dipendere da altri chart.

Dipendenza lasca

Corrisponde ad un'installazione manuale del chart dipendente nella directory charts.

Dipendenza stretta

Formalizza la dipendenza e la gestisce tramite comandi Helm.

Le dipendenze sono specificate nel file Chart.yaml.

Esempio:

dependencies:
  - name: booster
    version: ^1.0.0
    repository: https://raw.githubusercontent.com/Masterminds/learning-helm/main/chapter6/repository/

Per la versione si usa comunemente un range espresso da simboli.

La notazione è la corrente:

SintassiEquivalente a
^1.2.3>= 1.2.3 < 2.0.0
^1.2.x>= 1.2.0 < 2.0.0
^2.3>= 2.3 < 3
^2.x>= 2.0.0 < 3
^0.2.3to >= 0.2.3 < 0.3.0
^0.2>= 0.2.0 < 0.3.0
^0.0.3to >= 0.0.3 < 0.0.4
^0.0>= 0.0.0 < 0.1.0
^0= 0.0.0 < 1.0.0
~1.2.3to >= 1.2.3 < 1.3.0
~1= 1 < 2
~2.3>= 2.3 < 2.4
~1.2.xto >= 1.2.0 < 1.3.0
~1.x>= 1 < 2

Il repository può essere:

  • una URL
  • un repository di Helm aggiunto con helm repo add, prefissoda un @, p.es. "@myrepo"

Update di Dipendenze

Col comando:

helm dependency update .

Si ottiene un risultato simile a:

Saving 1 charts
Downloading booster from repo https://raw.githubusercontent.com/Masterminds/learning-helm/main/chapter6/repository/
Deleting outdated charts

Durante l'update:

  • Vieme generato il file Chart.lock
  • la dipendenza viene copiata nells sottodirectory charts

In luogo dell'argomento . (punto), si può passare come parametro un oggetto, ma non è quasi mai usato.

Dipendenze Condizionali

E' fornita dalla prorietà condition nella descrizione di dipendenza.

Per esempio nel file Charts.yaml:

dependencies:
  - name: booster
    version: ^1.0.0
    condition: booster.enabled
    repository: https://raw.githubusercontent.com/Masterminds/learning-helm/main/chapter6/repository/

Controllata dal file values.yaml

booster:
  enabled: false

Importare Valori

A volte si desiderano importare valori da un chart dipendente (child) nel chart corrente (parent).

Proprietà exports

Esempio. Nel file values.yaml del child:

exports:
  types:
    foghorn: rooster

Nel file Charts.yaml del parent:

dependencies:
  - name: example-child
    version: ^1.0.0
    repository: https://charts.example.com/
    import-values:
      - types

Questo è l'equivalente di aver settato, in values.yaml del parent:

foghorn: rooster

Import Diretto

Se il child ha nel file values.yaml:

types:
  foghorn: rooster

Il parent ha nel file Charts.yaml:

dependencies:
  - name: example-child
    version: ^1.0.0
    repository: https://charts.example.com/
    import-values:
      - child: types
        parent: characters

Questo è l'equivalente di aver settato, in values.yaml del parent:

characters:
  foghorn: rooster

Librerie di Charts

Le librerie di charts sono concettualmente simili a librerie software Forniscono funzionalità riusabili e sono importate da altri charts.

Le librerie di charts non sono esse stesse installabili.

Creazione di un Library Chart

  • Creare un chart normalmente col comando helm create
  • Rimuovere il file values,yaml
  • Rimuovere il contenuto della directory templates
  • Modificare il file Chart.yaml inserendo il parametro type: library

Per esempio:

apiVersion: v2
name: mylib
type: library
description: an example library chart
version: 0.1.0

I files implementativi della libreria si trovano nella directory templates e il loro nome inizia con _ (underscore). Helm non genera templates da nomi che iniziano con _.

Le loro estensioni sono .tpl o .yaml.

Un esempio può essere _configmap.yaml:

{{- define "mylib.configmap.tpl" -}}
apiVersion: v1
kind: ConfigMap
metadata:
  name: {{ include "mylib.fullname" . }}
  labels:
    {{- include "mylib.labels" . | nindent 4 }}
data: {}
{{- end -}}
{{- define "mylib.configmap" -}}
{{- template "mylib.util.merge" (append . "mylib.configmap.tpl") -}}
{{- end -}}

Il template mylib.util.merge deve essere altrove definito come:

{{- /*
mylib.util.merge will merge two YAML templates and output the result.
This takes an array of three values:
- the top context
- the template name of the overrides (destination)
- the template name of the base (source)
*/ -}}
{{- define "mylib.util.merge" -}}
{{- $top := first . -}}
{{- $overrides := fromYaml (include (index . 1) $top) | default (dict ) -}}
{{- $tpl := fromYaml (include (index . 2) $top) | default (dict ) -}}
{{- toYaml (merge $overrides $tpl) -}}
{{- end -}}

Il seguente esempio, da un chart mychart, illustra l'uso di questa funzione di libreria:

{{- include "mylib.configmap" (list . "mychart.configmap") -}}
{{- define "mychart.configmap" -}}
data:
  myvalue: "Hello Bosko"
{{- end -}}

La prima linea include il template mylib.configmap dalla chart di libreria. L'argomento passato è una lista con l'oggetto corrente e il nome del template i cui contenuti compiranno un override di quelli del template di libreria.

L'output prodotto è:

apiVersion: v1
kind: ConfigMap
metadata:
  labels:
    app.kubernetes.io/instance:example
    app.kubernetes.io/managed-by: Helm
    app.kubernetes.io/name: mychart
    helm.sh/chart: mychart-0.1.0
  name: example-mychart
data:
  myvalue: Hello Bosko

Schema per Values

E' possibile fornire uno schema per controllare con lint la validità del file values.yaml.

Il file di schema si chiama values.schema.json, nella directory principale del chart ove si trova anche values.yaml. ed è scritto in JSON.

Esempio. Se values.yaml è il seguente:

image:
  repository: ghcr.io/masterminds/learning-helm/anvil-app
  pullPolicy: IfNotPresent
  tag: ""

Uno schema corrispondente potrebbe essere:

{
  "$schema": "http://json-schema.org/schema#",
  "type": "object",
  "properties": {
    "image": {
      "type": "object",
      "properties": {
        "pullPolicy": {
          "type": "string",
          "enum": ["Always", "IfNotPresent"]
        },
        "repository": {
          "type": "string"
        },
        "tag": {
          "type": "string"
        }
      }
    }
  }
}

Esempio d'uso con un errore:

helm lint . --set image.pullPolicy=foo

Output prodotto:

==> Linting .
[ERROR] templates/: values don't meet the specifications of the schema(s) in the
following chart(s):
booster:
- image.pullPolicy: image.pullPolicy must be one of the following: 
  "Always", "IfNotPresent"

Error: 1 chart(s) linted, 1 chart(s) failed

Helm Hooks

Azioni compiute ad un certo stadio del ciclo di vita dell'oggetto Kubernetes.

Si specificano nella proprietà annotations e possono avere le seguenti chiavi:

  • "helm.sh/hook" - azione durante il ciclo di vita dell'oggetto. Vi possono essere più azioni, separate da virgola e senza spazi
  • "helm.sh/hook-weight" - specifica l'annotazione precedente; deve sempre esseere un numero intero, positivo, negativo o zero (default), ma espresso come stringa. Helm sortizza gli hooks in ordine ascendente, dal più piccolo al più grande.
  • "helm.sh/hook-delete-policy" - se cancellare l'oggetto Kubernetes trattato relativamente allo hook

Gli hook possono essere:

NomeQuando viene eseguito
pre-installprima dell'invio a Kubernetes
post-installdopo l'invio a Kubernetes
pre-deleteprima della cancellazione
post-deletedopo la cancellazione
pre-upgradedurante un upgrade, prima dell'invio a Kubernetes
post-upgradedurante un upgrade, dopo l'invio a Kubernetes
pre-rollbackprima di un rollback
post-rollbackdopo un rollback
testdurante il comando helm test

Le policy di cancellazione, relative ad una risorsa di Kubernetes,possono essere:

PolicyDescrizione
before-hook-creationcancellata prima dello hook (default)
hook-succeededcancellata se lo hook ha avuto successo
hook-failedcancellata se lo hook è fallito

Esempio del templato di un pod con uno hook di tipo post-install:

apiVersion: v1
kind: Pod
metadata:
  name: "{{ include "mychart.fullname" . }}-post-install"
  labels:
    {{- include "mychart.labels" . | nindent 4 }}
  annotations:
    "helm.sh/hook": post-install
    "helm.sh/hook-weight": "-1"
    "helm.sh/hook-delete-policy": before-hook-creation,hook-succeeded
spec:
  containers:
    - name: wget
      image: busybox
      command: ["/bin/sleep","{{ default "10" .Values.sleepTime }}"]
  restartPolicy: Never

L'esecuzione degli hooks durante un comando Helm, può essere saltata con l'opzione --no-hooks.

Helm Test

Helm fornisce di serie il comando di linea helm test.

I templati di test risiedono nella directory del chart templates/tests.

Reinstalliamo il nostro chart d'esercizio:

helm install myapp anvil

Il templato di test è anvil/templates/tests/test-connection.yaml:

apiVersion: v1
kind: Pod
metadata:
  name: "{{ include "anvil.fullname" . }}-test-connection"
  labels:
    {{- include "anvil.labels" . | nindent 4 }}
  annotations:
    "helm.sh/hook": test
spec:
  containers:
    - name: wget
      image: busybox
      command: ['wget']
      args: ['{{ include "anvil.fullname" . }}:{{ .Values.service.port }}']
  restartPolicy: Never

Che sia un test è specificato da:

  annotations:
    "helm.sh/hook": test

Proviamo il test:

helm test myapp
NAME: myapp
LAST DEPLOYED: Thu Feb 15 12:07:55 2024
NAMESPACE: default
STATUS: deployed
REVISION: 1
TEST SUITE:     myapp-anvil-test-connection
Last Started:   Thu Feb 15 12:08:31 2024
Last Completed: Thu Feb 15 12:08:58 2024
Phase:          Succeeded
NOTES:
.....

Gestione di Repositories

I charts sviluppati in casa devono essere storati in un Chart Repository per la distribuzione a terze parti.

Lo scenario più comune è un progetto aziendale, quindi i charts devono essere su repository privati.

Vi sono varie soluzioni:

  1. Installare un web server tradizionale e adattarlo a Chart Repository. Esempi tipici: Apache, Nginx.
  2. Sviluppare un semplice web server con un linguaggio di programmazione come Python o Go. Avrà delle limitazioni.
  3. Usare un'offerta commerciale di un Provider, sicuramente a pagamento.
  4. Usare GitHub Pages. Solo i repository pubblici sono gratis, quelli privati sono a pagamento.
  5. Usare un prodotto Open Source specifico che offre un Chart Repository

Standard OCI

UCI sta per Open Container Initiative, ed è una struttura aperta di governance per la determinazione di standard aperti, tesi alla gestione di formati di container e ambienti runtime.

Nelle specifiche OCI vi è anche la specifica di distribuzione, una API per la distribuzione di immagini di contenitori, charts di Helm e simili.

A partire da Helm 3.0 è stato aggiunto un supporto per il push e il pull di charts da un registry OCI.

ChartMuseum

Chartmuseum

E' un progetto Open Source disponibile a https://github.com/helm/chartmuseum.

E' uno Helm Chart Repository scritto in linguaggio Go.

Oltre che storaggio locale, supporta molti tipi di storaggio forniti da Cloud Provider, inclusi Google Cloud Storage, Amazon S3, Microsoft Azure Blob Storage, Alibaba Cloud OSS Storage, Openstack Object Storage, Oracle Cloud Infrastructure Object Storage, Baidu Cloud BOS Storage, Tencent Cloud Object Storage, DigitalOcean Spaces, Minio, ed etcd.

Installazione

curl https://raw.githubusercontent.com/helm/chartmuseum/main/scripts/get-chartmuseum | bash

Preparare una directory che corrisponderà alla rootdir dello storaggio di chartmuseum:

mkdir ~/helmrepo

Lanciare chartmuseum:

chartmuseum --storage local --storage-local-rootdir ~/helmrepo --port 8123

Abbiamo specificato la porta 8123 per evitare collisioni con altri servizi su porte più comuni.

Blocca la finestra corrente.

I comandi seguenti vanno in un'altra finestra.

ChartMuseum in Docker

Sy può istanziare un'immagine Docker, per esempio:

docker run --rm -it \
  -p 8123:8080 \
  -e DEBUG=1 \
  -e STORAGE=local \
  -e STORAGE_LOCAL_ROOTDIR=/charts \
  -v $(pwd)/charts:/charts \
  ghcr.io/helm/chartmuseum:v0.14.0

Nello host mapping la directory locale $(pwd)/charts deve avere i permessi di lettura e scrittura per chi esegue i comandi rivolti a ChartMuseum.

Preparazione di ChartMuseum

Occorre generare un primo chart e trasferirlo a chartmuseum per generare un primo index.

cd /tmp
helm create mychart
helm package mychart

Trasferimento al repository ChartMuseum:

curl --data-binary "@mychart-0.1.0.tgz" http://localhost:8123/api/charts

Ora possiamo usare il repository chartmuseum:

helm repo add chartmuseum http://localhost:8123
helm repo update
helm repo list

Quando si usano chart repositories, meglio usarne uno per volta.

Listare i repositories:

helm repo list

E rimuovere quelli che non vogliamo, per esempio:

helm repo remove bitnami

Ricerca di chart:

help search repo mychart

Per compiere il push a chartmuseum occorre installare un plugin:

helm plugin install https://github.com/chartmuseum/helm-push

Configurazione Complessa

Chartmuseum ammette molti parametri ed opzioni di configurazione, storabili in un file. Sono soprattutto necessari per interfacciare i servizi forniti da un Cloud Provider.

Per ulteriori informazioni si rimanda al sito web del progetto, su GitHub.

Upload di Chart

Avevamo precedentemente prodotto il Chart wordpress. Compiamone lo upload su Chartmuseum.

Charmuseum deve essere attivo e deve avere installato il plugin cm-push.

cd ~/scripts
helm cm-push wordpress/ chartmuseum
helm repo update

Fatto.

Listare i charts disponibili su Chartmuseum, e tutti gli altri repositories configurati:

helm search repo

HashiCorp Vault

Lo storaggio e l'uso di Secrets, che sono solo trascodificati Base64, è un potenziale problema di sicurezza.

Occorrerebbe accedere ai Secrets in modo più sicuro, autenticato e crittografato.

La ditta HashiCorp fornisce il prodotto Vault, un ambiente generico di storaggio ed accesso di informazioni private, incluso i Secrets di Kubernetes.

La versione Kubernetes del prodotto Vault è disponibile in versione base come chart di Helm.

Versioni più complete e complesse sono soggette a licenza e a pagamento.

Il seguente esempio illustra l'utilizzo di Vault per Kubernetes.

Installazione di Vault

Creare la directory di lavoro:

mkdir -p ~/vault
cd ~/vault

Aggiungere il repository helm della HashiCorp:

helm repo add hashicorp https://helm.releases.hashicorp.com

Compiere un upfate:

helm repo update

Ricercare i chart di Vault:

helm search repo hashicorp/vault

Generate un file di configurazione per Vault:

cat > helm-vault-raft-values.yml <<EOF
server:
   affinity: ""
   ha:
      enabled: true
      raft: 
         enabled: true
         setNodeId: true
         config: |
            cluster_name = "vault-integrated-storage"
            storage "raft" {
               path    = "/vault/data/"
            }

            listener "tcp" {
               address = "[::]:8200"
               cluster_address = "[::]:8201"
               tls_disable = "true"
            }
            service_registration "kubernetes" {}
EOF

Installare il chart di Vault:

helm install vault hashicorp/vault --values helm-vault-raft-values.yml

Listare i pods:

kubectl get pods

Occorre un tempo anche considerevole perchè la situazione si stabilizzi. Il risultato finale è:

NAME                                    READY   STATUS    RESTARTS   AGE
vault-0                                 0/1     Running   0          2m12s
vault-1                                 0/1     Running   0          2m12s
vault-2                                 0/1     Running   0          2m12s
vault-agent-injector-56b65c5cd4-k7lbt   1/1     Running   0          2m13s

Configurazione di Vault

Inizializzare vault-0 con un key share e un key threshold:

kubectl exec vault-0 -- vault operator init \
    -key-shares=1 \
    -key-threshold=1 \
    -format=json > cluster-keys.json

Il file cluster-keys.json contiene le chiavi di accesso.

Questo è solo un esempio, ed è un procedimento insicuro. Il file delle chiavi dovrebbe immediatamente essere crittografato.

Visualizzare la chiave di sblocco:

jq -r ".unseal_keys_b64[]" cluster-keys.json

Porla in una variabile:

VAULT_UNSEAL_KEY=$(jq -r ".unseal_keys_b64[]" cluster-keys.json)

Sbloccare il Vault sul pod vault-0:

kubectl exec vault-0 -- vault operator unseal $VAULT_UNSEAL_KEY

Unire vault-1 e vault-2 al cluster Raft.

kubectl exec -ti vault-1 -- vault operator raft join http://vault-0.vault-internal:8200
kubectl exec -ti vault-2 -- vault operator raft join http://vault-0.vault-internal:8200

Usare la chiave di sblocco, digitata a mano a richiesta, per sbloccare vault-1 e vault-2

kubectl exec -ti vault-1 -- vault operator unseal $VAULT_UNSEAL_KEY
kubectl exec -ti vault-2 -- vault operator unseal $VAULT_UNSEAL_KEY

Porre un Secret nel Vault

Visualizzare il root token:

jq -r ".root_token" cluster-keys.json

Collegarsi al pod vault-0:

kubectl exec --stdin=true --tty=true vault-0 -- /bin/sh

Login al Vault. Viene richiesto il root token:

vault login

Se ha successo, l'autenticazione è cached e non c'è più bisogno di compiere il login.

Abilitare il secrets engine di integrazione con Kubernetes:

vault secrets enable -path=secret kv-v2

Creare un Secret ad un dato percorso e porvi dei dati:

vault kv put secret/webapp/config username="static-user" password="static-password"

Verificare la registrazione del Secret:

vault kv get secret/webapp/config

Uscire da vault-0:

exit

Autenticazione da Kubernetes

Vault fornisce un metodo di autenticazione Kubernetes che permette ai client di autenticarsi con un Service Account Token di Kubernetes.

Collegarsi al pod vault-0:

kubectl exec --stdin=true --tty=true vault-0 -- /bin/sh

Abilitare l'autenticazione Kubernetes:

vault auth enable kubernetes

Configurare che il metodo di autenticazione Kubernetes usi la locazione dell'API Kubernetes:

vault write auth/kubernetes/config \
    kubernetes_host="https://$KUBERNETES_PORT_443_TCP_ADDR:443"

Scrivere una policy di nome webapp che dia la possibilità read dei Secret al percorso secret/data/webapp/config.

vault policy write webapp - <<EOF
path "secret/data/webapp/config" {
  capabilities = ["read"]
}
EOF

Creare un Role chiamato webapp che connette il service account name vault di Kubernetes alla policy webapp:

vault write auth/kubernetes/role/webapp \
        bound_service_account_names=vault \
        bound_service_account_namespaces=default \
        policies=webapp \
        ttl=24h

Uscire dal pod vault-0:

exit

Applicativo che Usa il Vault

Deployment

Creare il deployment di un applicativo con il manifest deployment-01-webapp.yml:

vim deployment-01-webapp.yml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: webapp
  labels:
    app: webapp
spec:
  replicas: 1
  selector:
    matchLabels:
      app: webapp
  template:
    metadata:
      labels:
        app: webapp
    spec:
      serviceAccountName: vault
      containers:
        - name: app
          image: hashieducation/simple-vault-client:latest
          imagePullPolicy: Always
          env:
            - name: VAULT_ADDR
              value: 'http://vault:8200'
            - name: JWT_PATH
              value: '/var/run/secrets/kubernetes.io/serviceaccount/token'
            - name: SERVICE_PORT
              value: '8080'
  • VAULT_ADDR - indirizzo del servizio di VAULT
  • JWT_PATH - setta il percorso del JSON Web Token fornito da Kubernetes
  • SERVICE_PORT - per le richieste HTTP in arrivo

Sottomettere il nostro manifest:

kubectl apply --filename deployment-01-webapp.yml

Test

In un altro terminale compiere il port forward di tutte le richieste compiute a `` alla porta 80 del pod della webapp.

kubectl port-forward \
    $(kubectl get pod -l app=webapp -o jsonpath="{.items[0].metadata.name}") \
    8888:8080

Usiamo la porta 8888 per evitare collisioni con la porta 8080, possibilmente già in uso.

Nel terminale originale provare il collegamento:

curl http://localhost:8888
password:static-password username:static-user

Cosa Succede

L'applicativo che gira alla porta 8080 del pod webapp:

  • si autentica con il token del service account di Kubernetes
  • riceve un token da Vault che gli permette di leggere il percorso secret/data/webapp/config path
  • recupera il Secret dal percorso secret/data/webapp/config
  • ne compie il display in formato JSON

Pulizia Finale

Al termine compiere pulizia dell'esercizio.

Interrompere il Port Forwarding

Rimuovere l'applicativo tramite il suo Manifest

Disinstallare il chart di Vault

Questo ne rimuove tutti i pod e naturalmente i dati di configurazione e i Secrets storati vanno persi.

Ulteriori Possibilità

  • Settare Vault con storaggio integrato nel Google Kubernetes Engine.
  • Usare Annotations nell'applicativo per interagire con il servizio Vault Injector.
  • Persistere i dati di Vault su volumi.
  • Usare il servizio Vault fornito dalla stessa HashiCorp, a licenza.

Ulteriori dettagli documentativi si trovano sul sito Vault della Hashicorp.

Conclusione