Userò il cluster creato nel mio precedente articolo per effettuare i seguenti step:
- Configurare un cluster GlusterFS + Heketi per usare l’apposita StorageClass. Il chart di Vault creerà dei PVC e non vorrei configurare i PV a mano 😀
 - Installare un cluster Vault a 3 repliche tramite Helm.
 - Eseguire una sessione minimale di chaos engineering.
 
L’architettura sarà la seguente.

Setup GlusterFS + Heketi
Installo GlusterFS su 2 worker Kubernetes k8s2 e k8s3 in modo da poter utilizzare la StorageClass con provisioner kubernetes.io/glusterfs.
Su k8s2 installo anche Heketi.
K8s1 vorrei fosse anche esso un nodo GlusterFS per avere un numero dispari di nodi, ma c’è già la control-plane di k8s per cui preferisco lasciarlo scarico.
 kind: StorageClass
 apiVersion: storage.k8s.io/v1
 metadata:
   name: gluster-heketi
 provisioner: kubernetes.io/glusterfs
 reclaimPolicy: Delete
 volumeBindingMode: Immediate
 allowVolumeExpansion: true
 parameters:
   resturl: "http://10.10.10.5:8080"
   restuser: "admin"
   restuserkey: "xxx"
   volumetype: "replicate:2"
   volumenameprefix: "k8s-dev"
   clusterid: "49d9119adfd9b5e9cbdea79e362938d4"
Il comportamento di Heketi è molto semplice, espone un’interfaccia REST per governare la creazione e la rimozione di volumi su GlusterFS.
Punti di attenzione sulla configurazione di Heketi
È molto importante definire bene la topologia del cluster GlusterFS da gestire.
Il seguente file di configurazione /etc/heketi/topology.json è molto semplice, ma attenzione avendo solo 2 nodi potrei avere problemi di split-brain (in produzione aumentare almeno a 3 il numero di membri del cluster).
[root@k8s2 vg_52bba7d0a906efef88e7b6e3a82395be]# cat /etc/heketi/topology.json
{
  "clusters": [
    {
      "nodes": [
        {
          "node": {
            "hostnames": {
              "manage": [
                "10.10.10.5"
              ],
              "storage": [
                "10.10.10.5"
              ]
            },
            "zone": 1
          },
          "devices": [
            "/dev/sdb"
          ]
        },
        {
          "node": {
            "hostnames": {
              "manage": [
                "10.10.10.6"
              ],
              "storage": [
                "10.10.10.6"
              ]
            },
            "zone": 1
          },
          "devices": [
            "/dev/sdb"
          ]
        }
      ]
    }
  ]
}
Il file di configurazione principale per Heketi ha qualche punto su cui stare attenti.
- Prendere nota della key dell’utente che si vuole usare. Tale chiave sarà poi necessaria per la configurazione della StorageClass.
 - Attenzione se executor è “mock” e se siete distratti come l’autore di questo post!
 - La parte ssh è facile, chiave privata e pubblica. Quest’ultima va inserita a bordo dei nodi del cluster. Heketi lancia comandi su GlusterFS, direttamente sui nodi, tramite ssh.
 
[root@k8s2 heketi]# cat /etc/heketi/heketi.json
{
  "_jwt": "Private keys for access",
  "jwt": {
    "_admin": "Admin has access to all APIs",
    "admin": {
      "key": "ivd7dfORN7QNeKVO"
    },
    "_user": "User only has access to /volumes endpoint",
    "user": {
      "key": "gZPgdZ8NtBNj6jfp"
    }
  },
...
.....
  "_glusterfs_comment": "GlusterFS Configuration",
  "glusterfs": {
    "_executor_comment": [
      "Execute plugin. Possible choices: mock, ssh",
      "mock: This setting is used for testing and development.",
      "      It will not send commands to any node.",
      "ssh:  This setting will notify Heketi to ssh to the nodes.",
      "      It will need the values in sshexec to be configured.",
      "kubernetes: Communicate with GlusterFS containers over",
      "            Kubernetes exec api."
    ],
    "executor": "ssh",
    "_sshexec_comment": "SSH username and private key file information",
    "sshexec": {
      "keyfile": "/etc/heketi/heketi_key",
      "user": "root",
      "port": "22",
      "fstab": "/etc/fstab"
    },
...
.....
  }
}
Load Cluster Topology
Una volta che Heketi è configurato carico la topologia del mio cluster.
[root@k8s2 heketi]#  heketi-cli topology load --user admin --secret ivd7dfORN7QNeKVO --json=/etc/heketi/topology.json
	Found node 10.10.10.5 on cluster 98a80b7163c124b8c3d956775bd61e35
		Adding device /dev/sdb ... OK
	Found node 10.10.10.6 on cluster 98a80b7163c124b8c3d956775bd61e35
		Adding device /dev/sdb ... OK
Apply della StorageClass
Niente di più semplice…
[root@k8s1 ~]# cat  storageclass.yaml
 kind: StorageClass
 apiVersion: storage.k8s.io/v1
 metadata:
   name: gluster-heketi
 provisioner: kubernetes.io/glusterfs
 reclaimPolicy: Delete
 volumeBindingMode: Immediate
 allowVolumeExpansion: true
 parameters:
   resturl: "http://10.10.10.5:8080"
   restuser: "admin"
   restuserkey: "xxxxxxx"
   volumetype: "replicate:2"
   volumenameprefix: "k8s"
   clusterid: "98a80b7163c124b8c3d956775bd61e35"
Punti di attenzione
restuserkey => è quella che avevo dichiarato nel file heketi.json. Potete usare anche un Secret Kubernetes, ma a me al momento non serve.
clusterid => Lo rimediate con "heketi-cli cluster list  http://localhost:8080 --user admin --secret  xxxxxxx" lanciato a bordo del server dove è stato installato Heketi e quindi dove è presente la sua CLI.
resturl => L'endpoint http REST di Heketi.
Debug creazione volumi
Creo un PVC con storageclass gluster-heketi.
[root@k8s1 ~]# cat claim.yaml
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: task-pv-claim
spec:
  storageClassName: gluster-heketi
  accessModes:
    - ReadWriteOnce
  resources:
    requests:
      storage: 1Gi
kubectl create -f claim.yaml
Creo un pod con persistenza.
[root@k8s1 ~]# cat pod.yaml
apiVersion: v1
kind: Pod
metadata:
  name: mypod
spec:
    containers:
    - name: mysql
      image: mysql
      env:
      - name: MYSQL_ROOT_PASSWORD
        value: "rootpasswd"
      volumeMounts:
      - mountPath: /var/lib/mysql
        name: site-data
        subPath: mysql
    - name: php
      image: php:7.0-apache
      volumeMounts:
      - mountPath: /var/www/html
        name: site-data
        subPath: html
    volumes:
    - name: site-data
      persistentVolumeClaim:
        claimName: task-pv-claim
kubectl create -f pod.yaml
Verifico che il PVC esista e che il pod sia running.
[root@k8s1 ~]# kubectl get pvc
NAME            STATUS   VOLUME                                     CAPACITY   ACCESS MODES   STORAGECLASS     AGE
task-pv-claim   Bound    pvc-af23091a-347b-4ea5-bdac-c57450a54ecf   1Gi        RWO            gluster-heketi   17m
[root@k8s1 ~]# kubectl get pods
NAME    READY   STATUS    RESTARTS   AGE
mypod   2/2     Running   0          16m
Ecco dove andare a guardare se non funziona nulla 😀
1. [Sui nodi GlusterFS] Un tail dei log in /var/log/glusterfs
2. [Sul nodo Heketi] journalctl -xe -u heketi 
3. [Sul nodo dove piloto k8s] kubectl get events
Installazione di Hashicorp Vault
Prendo spunto dalla doc ufficiale di Hashicorp, soprattutto da https://learn.hashicorp.com/tutorials/vault/kubernetes-raft-deployment-guide, per capire come modificare al meglio il mio values per Helm.
È uscito questo:
# Vault Helm Chart Value Overrides
global:
  enabled: true
  tlsDisable: true
# In produzione naturalmente tlsDisable deve essere false!
injector:
  enabled: false
  image:
    repository: "hashicorp/vault-k8s"
    tag: "latest"
  certs:.
    secretName: null
    caBundle: ""
    certName: tls.crt
    keyName: tls.key
server:
  # For HA configuration and because we need to manually init the vault,
  # we need to define custom readiness/liveness Probe settings
  readinessProbe:
    enabled: true
    path: "/v1/sys/health?standbyok=true&sealedcode=204&uninitcode=204"
  livenessProbe:
    enabled: true
    path: "/v1/sys/health?standbyok=true"
    initialDelaySeconds: 60
# Non mi occorrono gli audit logs...
  auditStorage:
    enabled: false
# Persistenza abilitata.
  dataStorage:
   enabled: true
   storageClass: gluster-heketi
   size: 1Gi
  standalone:
    enabled: false
  ha:
    enabled: true
    replicas: 3
    raft:
      enabled: true
      setNodeId: true
      config: |
        ui = true
        listener "tcp" {
          tls_disable = "true"
          address = "[::]:8200"
          cluster_address = "[::]:8201"
        }
        storage "raft" {
          path = "/vault/data"
            retry_join {
            leader_api_addr = "http://vault-0.vault-internal:8200"
          }
          retry_join {
            leader_api_addr = "http://vault-1.vault-internal:8200"
          }
          retry_join {
            leader_api_addr = "http://vault-2.vault-internal:8200"
          }
          autopilot {
            cleanup_dead_servers = "true"
            last_contact_threshold = "200ms"
            last_contact_failure_threshold = "10m"
            max_trailing_logs = 250000
            min_quorum = 1
            server_stabilization_time = "10s"
          }
        }
        service_registration "kubernetes" {}
# Vault UI
ui:
  enabled: true
  serviceType: "ClusterIP"
  serviceNodePort: null
  externalPort: 8200
Punti di attenzione:
- La storageClass è gluster-heketi.
 - Replicas è 3 perchè mi aspetto un cluster Vault composto da 3 pod.
 - tlsDisable: Per comodità, solo per questo laboratorio, disabilito il TLS.
 
Ricordo che https://learn.hashicorp.com/tutorials/vault/kubernetes-raft-deployment-guide è molto utile per i deploy di un cluster che usa RAFT come storage backend integrato.
helm install vault hashicorp/vault --namespace vault -f override-values.yml --version 0.19.0
Una volta che tutti i pod di Vault sono running prosso procedere con l’inizializzazione del cluster da cui ottengo 5 unseal key e il root token (che in ambienti produttivi va rimosso dopo la procedura di init).
[root@k8s1 ~]#  kubectl exec -n vault -ti vault-0 -- sh
/ $ vault operator init
Unseal Key 1: hTXhuYB3b97HvEp+8z3v2vMLgEIJCtX8BetLIVnCjgJO
Unseal Key 2: y1Rfq7XBAbuvhrV3OnDu/Sk5ttien8mtAHTA3RQoJeDY
Unseal Key 3: NKeDVhkCTep57X7RLTAypK/8nxUwBOuKFxFsXB9x9I9F
Unseal Key 4: jADp10O+Bhw4i7CK51AO/Bew390lNjqwhJ+SDt2CTCDQ
Unseal Key 5: UUCKAztAtdJWpI2mCAsfiPTQKV6SfXFND8cjkekppJgX
Initial Root Token: s.DFWAkgxGsOf88t0bxhl27OnV
Un po’ di chaos engineering
Non è una vera e propria sessione di chaos engineering ma preparo qualche “bashone” per vedere cosa succede se elimimo pod di Vault randomicamente tramite KubeInvaders.
Loop inifinito – Procedura di unseal
Non avendo impostato un meccanismo di auto unseal procedo con uno script molto minimale. Prendo indirizzo IP dei singoli pod e procedo con unseal via API.
Questo perchè se elimino pod randomicamente, c’è bisogno di effettuare la procedura di unseal alla crezione dei nuovi pod o quest’ultimi partirebbero non funzionanti.
[root@k8s1 ~]# cat unseal_loop.sh
#!/bin/bash
while :
do
  for i in $(kubectl get pods -n vault -o jsonpath="{.items[*].status.podIP}")
  do
    echo "unseal $i"
    curl  --connect-timeout 1 --request POST --data '{ "key": "mykey1" }' http://${i}:8200/v1/sys/unseal
    curl  --connect-timeout 1 --request POST --data '{ "key": "mykey2" }' http://${i}:8200/v1/sys/unseal
    curl  --connect-timeout 1 --request POST --data '{ "key": "mykey3" }' http://${i}:8200/v1/sys/unseal
  done
  sleep 1
done
Loop inifinito – Lettura e scrittura dei secret
Punto all’IP del Service di Vault che bilancia sui vari pod. Uso l’ip ma è andrebbe bene anche FQDN del service. Questo codice scrive e legge un secret da Vault.
[root@k8s1 ~]# cat secrets_loop.sh
#!/bin/bash
while :
do
  time curl \
    -H "X-Vault-Token: xxxx" \
    -H "Content-Type: application/json" \
    -X POST \
    -d '{"data":{"value":"bar"}}' \
    http://10.233.4.227:8200/v1/kv/baz
  time curl \
    -H "X-Vault-Token: xxxx" \
    -X GET \
    http://10.233.4.227:8200/v1/kv/baz
sleep 0.5
done
Risultati
Inizio a “killare” pod all’incirca ogni 10 secondi tramite KubeInvaders e non ho errori se non qualche delay.
Naturalmente questo test andrebbe effettuato su hardware più prestante e con un numero di repliche maggiori. Riporto un subset dei risultati emersi durante il test:
{"request_id":"52ba7f27-5e6a-aaac-0077-a043505c6e9f","lease_id":"","renewable":false,"lease_duration":2764800,"data":{"data":{"value":"bar"}},"wrap_info":null,"warnings":null,"auth":null}
real	0m0.382s
user	0m0.006s
sys	0m0.003s
{"request_id":"44f2f0f5-b256-3a88-624c-08ebd0cc2b01","lease_id":"","renewable":false,"lease_duration":2764800,"data":{"data":{"value":"bar"}},"wrap_info":null,"warnings":null,"auth":null}
real	0m0.109s
user	0m0.004s
sys	0m0.005s
{"request_id":"7ef6640b-a980-288c-7302-f9fcbd1986b3","lease_id":"","renewable":false,"lease_duration":2764800,"data":{"data":{"value":"bar"}},"wrap_info":null,"warnings":null,"auth":null}
real	0m0.010s
user	0m0.004s
sys	0m0.004s
{"request_id":"84c61be0-da12-d695-cf33-00a29b565090","lease_id":"","renewable":false,"lease_duration":2764800,"data":{"data":{"value":"bar"}},"wrap_info":null,"warnings":null,"auth":null}
real	0m0.275s
user	0m0.003s
sys	0m0.005s
{"request_id":"66075c3c-a939-2946-a265-8d0a0f784003","lease_id":"","renewable":false,"lease_duration":2764800,"data":{"data":{"value":"bar"}},"wrap_info":null,"warnings":null,"auth":null}
real	0m0.097s
user	0m0.004s
sys	0m0.004s
{"request_id":"d3405205-54d1-26f4-a18c-c395eb365089","lease_id":"","renewable":false,"lease_duration":2764800,"data":{"data":{"value":"bar"}},"wrap_info":null,"warnings":null,"auth":null}
real	0m0.098s
user	0m0.003s
sys	0m0.006s
Non male direi. Considerando che…
Il cluster Kube è composto da 3 nodi 2gb di RAM e 1 core (Hypervisor Proxmox: Server KS-9 – Intel W3520 – 16GB DDR3 ECC 1333 MHz – 240GB SSD) ed oltre Vault sta eseguendo altri workload (vari blog, il custer GlusterFS, accolli.it, …)
