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, …)