Userò il cluster creato nel mio precedente articolo per effettuare i seguenti step:

  1. 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 😀
  2. Installare un cluster Vault a 3 repliche tramite Helm.
  3. 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.

  1. Prendere nota della key dell’utente che si vuole usare. Tale chiave sarà poi necessaria per la configurazione della StorageClass.
  2. Attenzione se executor è “mock” e se siete distratti come l’autore di questo post!
  3. 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:

  1. La storageClass è gluster-heketi.
  2. Replicas è 3 perchè mi aspetto un cluster Vault composto da 3 pod.
  3. 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, …)