Kubernetes e MongoDb Replica Set

di Andrea Zani, in Docker,

Lo avevo promesso ed eccomi qua per un breve post su Kubernetes. In quello precedente avevo creato un esempio con Docker Swarm in cui mostravo la configurazione di un Replica Set con MongoDb composto da tre macchine con due web application che lo utilizzavano. E' ora di fare lo stesso con Kubernetes per vedere le differenze con il rivale(!?) Docker Swarm.

Un passo alla volta. Il primo è creare un ambiente di test locale dove fare i propri test. Se con Docker Swarm il tutto si limita ad una linea di comando - docker swarm init - con Kubernetes le cose si fanno leggermente più complesse. Niente di trascendentale, anzi, in locale è disponibile il tool Minikube con il quale si possono testare configurazioni anche complesse. Se si ha necessità di un ambiente di test più complesso si possono creare più macchine le quali dovranno essere configurate. La struttura è simile a quella di Docker Swarm: si deve creare uno o più nodi manager (che gestiranno il tutto) e quanti nodi worker necessari alle proprie esigenze (un'altra piccola differenza è che con la configurazione di default, in Kubernetes non è possibile fare girare le proprie applicazioni sui server manager, cosa invece fattibile, ma sconsigliata, in Docker Swarm). Kubernetes mette a disposizione appunto Minikube che crea l'infrastruttura di base necessaria per i propri test; una volta avviato crea un nodo unico che fa da manager e worker. I limiti sono appunto l'impossibilità di aggiungere più nodi alla propria rete e altri limiti riguardanti i Service e i Volume. Per avere la piena potenza di Kubernetes è possibile utilizzare servizi sui vari cloud dove danno già tutto predisposto (EKS su AWS, Azure Kubernetes, Google Cloud, ecc...), oppure creare una piccola rete con macchine virtuali dove configurare il tutto (minimo due macchine: un nodo manager e un nodo worker).

Senza entrare troppo nel dettaglio, visto che la documentazione ufficiale è molto chiara in merito, in Kubernetes sono disponibili questi oggetti:

Pod: è un contenitore di Container, i quali condividono la rete ed eventuali dischi. E' possibile creare un Pod per ogni Container senza problemi. L'avvio di più Container all'interno di un Pod è consigliato, come appena scritto, per la condivisione di eventuali risorse. Il Pod è l'oggetto principale in Kubernetes, è la base degli oggetti più complessi come:

ReplicaSet: è un oggetto che permette l'avvio e la gestione di più Pod in replica contenenti la nostra applicazione; deciso il numero di Pod che vogliamo che girino contemporaneamente sarà il Replica Set a garantire che quel determinato numero venga eseguito.

Deployment è un oggetto più ad alto livello il cui sottostante è un ReplicaSet. Viene utilizzato per il deployment di un ReplicaSet, per eventuali aggiornamenti e rollback dello stato del ReplicaSet, ed è inoltre in grado di scalarne il numero anche in modo automatico.

StatefulSet: è un Deployment specifico per quei servizi che hanno bisogno di unicità nella creazione e nella definizione del loro nome (con la possibilità di gestire i volume per un legame uno a uno con un Pod).

DaemonSet: come il ReplicaSet, lavora a livello di macchine: avvia un Pod per ogni macchina presente nella rete di Kubernetes.

Job: avvia uno o più Pod che dovranno eseguire delle operazioni e poi terminare. Inoltre mette a disposizione la possibilità di avviare un Job a intervalli regolari con una Cron Expression.

Esistono altri oggetti utili come Service, di cui scriverò a breve, i Volume per la gestione dei volumi all'interno dei Pod, oppure i Namespace per suddividere la struttura che vogliamo creare in Kubernetes in più gruppi virtuali, i ConfigMap per il passaggio di informazioni come le configurazioni, i Secret per il passaggio informazioni da tenere nascosti come le credenziali e altro; di default tutti Pod che girano in Kubernetes possono vedere tutti gli altri, se c'è la necessità di maggiore sicurezza è possibile usare le Pod Security Policy.

Gli esempi seguenti saranno sempre fatti con Minikube. Senza dettagli sulla sua installazione, una volta avviato con:

minikube start

La prima volta sarà cercato e utilizzato VirtualBox per la creazione di una macchina virtuale con all'interno tutto il necessario, altrimenti, se si vuole utilizzare l'installazione di Docker eventualmente già presente nella macchina dove si fanno i test, si dovrà specificare il driver apposito:

minikube start --vm-driver=none

Personalmente il secondo metodo sono riuscito a farlo eseguire solo su una macchina Linux - non ho investigato oltre su una macchina con Windows 10 perché non me ne importava nulla.

Ora, così come in un ambiente completo di Kubernetes, si avrà a disposizione il comando kubectl per eseguire le varie operazioni - questo comando si connette alle API interne di Kubernetes sulla/e macchina/e manager che poi eseguiranno materialmente il tutto. Qui l'interessante struttura interna di Kubernetes. kubectl è la nostra porta per Kubernetes. Per vedere se Minikube ha avviato il tutto correttamente:

< kubectl get componentstatus
> NAME                STATUS   MESSAGE            ERROR
> scheduler           Healthy  ok                 
> controller-manager   Healthy   ok
> etcd-0              Healthy   {"health":"true"}
< kubectl cluster-info
> Kubernetes master is running at https://192.168.99.100:8443
> KubeDNS is running at https://192.168.99.100:8443/api/v1/namespaces/kube-system/services/kube-dns:dns/proxy
> 
> To further debug and diagnose cluster problems, use 'kubectl cluster-info dump'.

Per vedere la struttura della nostra rete:

< kubectl get nodes
> NAME       STATUS  ROLES    AGE   VERSION
> minikube   Ready   master   41d   v1.17.0

Usando Minikube viene visualizzato un unico nodo. Per vederne i dettagli:

kubectl describe node minikube

Che visualizza informazioni estese e dettagliate sulla macchina in questione (cpu, quantità ram, log e altro).

Questi due comandi possono essere utilizzati con gli altri oggetti di Kubernetes. Per vedere i Pod:

kubectl get pods

Per vedere il dettaglio di un Pod:

kubectl describe pod nomepod

La lista dei service:

kubectl get services

Dettagli di un service?

kubectl descrive service nomeservice

Voglio cancellare un Pod e un Service?

kubectl delete pod nomepod

kubectl delete service nomeserivce

E così via...

Con kubectl è possibile creare anche un Pod direttamente da linea di comando:

kubectl run mongodbtest --image=sbraer/mongodbtest:v1 --port=5000 --generator=run-pod/v1

Ora controllo che il Pod sia stato creato:

< kubectl get pods
> NAME         READY   STATUS   RESTARTS  AGE
> mongodbtest   1/1    Running   0          53s

Anche qui c'è il describe:

kubectl describe pod mongodbtest

Che ha come output:

Name:         mongodbtest
Namespace:    default
Priority:     0
Node:        
minikube/192.168.99.100
Start Time:   Sat, 01 Feb 2020 15:34:37 +0100
Labels:       run=mongodbtest
Annotations:  <none>
Status:       Running
IP:          
172.17.0.6
IPs:
  IP:  172.17.0.6
Containers:
  mongodbtest:
    Container ID:  docker://8d7c8b5d9ba2e92481d9b037520dcd5a8bc613ea04e1472e687d9c5123701570
   Image:         sbraer/mongodbtest:v1
    Image ID:      docker-pullable://sbraer/mongodbtest@sha256:59cae48284a417990bbaea2f15a3376cb22d274b64c8b2d284a98301aefb15a8
   Port:          5000/TCP
    Host Port:      0/TCP
   State:         Running
     Started:      Sat, 01 Feb 2020 15:34:38 +0100
   Ready:          True
    Restart Count:  0
    Environment:    <none>
    Mounts:
     /var/run/secrets/kubernetes.io/serviceaccount from default-token-br5jl (ro)
Conditions:
 Type             Status
 Initialized       True 
 Ready            True 
  ContainersReady   True 
  PodScheduled      True 
Volumes:
  default-token-br5jl:
    Type:        Secret (a volume populated by a Secret)
    SecretName:  default-token-br5jl
    Optional:    false
QoS Class:       BestEffort
Node-Selectors:  <none>
Tolerations:    node.kubernetes.io/not-ready:NoExecute for 300s
                node.kubernetes.io/unreachable:NoExecute for 300s
Events:
  Type    Reason    Age  From              Message
  ----    ------     ----  ----              -------
  Normal  Scheduled  2m   default-scheduler  Successfully assigned default/mongodbtest to minikube
  Normal  Pulled    2m    kubelet, minikube  Container image "sbraer/mongodbtest:v1" already present on machine
  Normal  Created    2m   kubelet, minikube  Created container mongodbtest
  Normal  Started    119s  kubelet, minikube  Started container mongodbtest

Ricomincio. Cancello quanto fatto:

kubectl delete pod mongodbtest

Ma per questo post utilizzerò file esterni per la creazione degli oggetti in Kubernetes. Tra i formati accettati c'è lo YAML, ed ecco un file di esempio per la creazione dello stesso Pod creato prima:

apiVersion: v1

kind: Pod
metadata:
    name: mongodbtest
    labels:
      app: mongodbtestapp # label for selector
spec:
   containers:
   - name: mongodbtest
     image: sbraer/mongodbtest:v1
     imagePullPolicy: IfNotPresent # or Always
     ports:
      - containerPort: 5000
        protocol: TCP

Prima di creare il Pod due parole su questo file. In image viene specificata quale immagine Docker utilizzare e con imagePullPolicy specifichiamo con IfNotPresent che Kubernetes dovrà scaricarla solo se non è già presente, con Always sarà sempre scaricata. Questo comportamento cambia nel caso non venga specificato imagePullPolicy: in questo caso se l'immagine specifica il TAG (v1 qui sopra) di default Kubernetes utilizzerà come Pull Policy il valore IfNotPresent, altrimenti, specificando un'immagine senza TAG, sarà utilizzato il valore Always.

Ora creo il Pod:

kubectl create -f podfile.yml

Controllo che sia creato:

kubectl get pods

E cancello quanto fatto con il comando prima visto:

kubectl delete pod mongodbtest

Per la creazione dell'oggetto ho a disposizione due opzioni principali: create e apply (qui sopra ho usato create, più avanti userò il comando apply). Tra i due comandi non ci sono differenze se si crea una nuova risorsa in Kubernetes, la differenza tra le due opzioni la si ha con risorse già esistenti: in caso dell'uso di create la risorsa sarà completamente sovrascritta, mentre con apply, kubectl cercherà di inserire solo le modifiche, se possibile.

Se volessi creare lo stesso Pod in un ReplicaSet scriverei:

apiVersion: apps/v1
kind: ReplicaSet
metadata:
  name: myreplicaset1
  labels:
    app: mongodbtest
spec:
  replicas: 3
  selector:
    matchLabels:
      mylabel: test1 # selector reference
  template:
    metadata:
      labels:
        mylabel: test1 #selected
    spec:
      containers:
      - name: mongodbtest
        image: sbraer/mongodbtest:v1

Importante l'uso del selector che permette di legare gli oggetti un Kubernetes. Qui definisce per quel ReplicaSet quale template utilizzare (cercherò di spiegare più avanti l'uso dei selector). Uso lo stesso comando sopra:

kubectl create -f replicaset.yml

Ecco la lista dei ReplicaSet:

< kubectl get rs
> NAME            DESIRED   CURRENT  READY   AGE
> myreplicaset1   3         3         3       13s

Ed ecco la lista dei Pod:

< kubectl get pods
> NAME                  READY   STATUS    RESTARTS   AGE
> myreplicaset1-7smdf   1/1     Running   0          76s
> myreplicaset1-kgql5   1/1     Running   0          76s
> myreplicaset1-st875   1/1     Running   0          76s

Se cancellassi un Pod direttamente con il comando:

kubectl delete pod myreplicaset1-st875

Avrei una sorpresa:

< kubectl delete pod myreplicaset1-st875
> pod "myreplicaset1-st875" deleted
< kubectl get pods
> NAME                  READY   STATUS    RESTARTS   AGE
> myreplicaset1-7smdf   1/1     Running   0          3m49s
> myreplicaset1-czgpj   1/1     Running   0          18s
> myreplicaset1-kgql5   1/1     Running   0          3m49s

Il ReplicaSet è impostato per creare tre istanze del Pod in questione, la cancellazione, anche se volontaria, fa in modo che il ReplicaSet si avvii e crei un nuovo Pod (myreplicaset1-czgpj nell'esempio).

Nella definizione del Pod è presente anche la porta cui l'app nel Container sta aspettando la richiesta, ma a differenza di Docker Swarm, i Container all'interno di Kubernetes non sono raggiungibili dall'esterno ma solo dall'interno degli stessi Pod del Cluster. Per esempio, con il comando seguente lancio il comando curl direttamente dall'interno del Container:

kubectl exec -it myreplicaset1-kgql5 -- curl localhost:5000/api/info

Che darebbe l'output:

{
  "Settings": {
    "BooksCollectionName": "MyTest",
    "DatabaseName": "MyDatabase",
    "ReplicaSet": "replicaTest"
  },
  "Environment": {
    "ASPNETCORE_Environment": "Production",
    "ASPNETCORE_URLS": "http://+:5000",
    "DOTNET_RUNNING_IN_CONTAINER": "true",
    "DOTNET_SYSTEM_GLOBALIZATION_INVARIANT": "true",
    "HOME": "/root",
    "HOSTNAME": "myreplicaset1-kgql5",
    "KUBERNETES_PORT": "tcp://10.96.0.1:443",
    "KUBERNETES_PORT_443_TCP": "tcp://10.96.0.1:443",
    "KUBERNETES_PORT_443_TCP_ADDR": "10.96.0.1",
    "KUBERNETES_PORT_443_TCP_PORT": "443",
    "KUBERNETES_PORT_443_TCP_PROTO": "tcp",
    "KUBERNETES_SERVICE_HOST": "10.96.0.1",
    "KUBERNETES_SERVICE_PORT": "443",
    "KUBERNETES_SERVICE_PORT_HTTPS": "443",
    "PATH": "/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"
  }

L'opzione exec si comporta come l'omonimo comando in Docker. Anche altri comandi Docker sono stati inseriti in kubectl, come il comando per vedere i messaggi di log dai Pod:

kubectl logs myreplicaset1-kgql5

Output:

info: Microsoft.Hosting.Lifetime[0]
      Now listening on: http://[::]:5000
info: Microsoft.Hosting.Lifetime[0]
      Application started. Press Ctrl+C
to shut down.
info: Microsoft.Hosting.Lifetime[0]
      Hosting environment: Production
info: Microsoft.Hosting.Lifetime[0]
      Content root path: /app

Docker Swarm per controllare che un Container sia attivo e dunque disponibile ad accettare richieste utilizzava l'healthcheck nativo di Docker.

healthcheck:
  test: ["CMD", "curl", "-f", "http://localhost"]
  interval: 1m30s
  timeout: 10s
  retries: 3
  start_period: 40s

Kubernetes migliora questo controllo all'interno dei Pod con Liveness, Readiness e Startup Probes. Liveness controlla lo stato del Pod e in caso lo riavvia, Readiness verifica quando il Pod è disponibile ad accettare richieste e solo allora lo pubblica e lo rende disponibile all'intero del Cluster, e Startup Probes imposta i parametri di avvio del Pod (oltre ai vari controlli via http è possibile farli anche per servizi che accettano richieste di tipo TCP). In questo post non tratterò questo argomento per i miei Pod (forse un futuro?) ma rimando alla dettaglia descrizione tecnica nella documentazione ufficiale.

Per esporre questi Container esternamente si deve utilizzare l'oggetto Service. Kubernetes mette a disposizione quattro tipi:

ClusterIP: (valore di default se non è specificato altro), espone il servizio solo all'interno del Cluster Kubernetes (utile, per esempio, per un database perché possa essere accessibile dagli altri Pod ma NON dall'esterno).

NodePort: espone il Pod con una porta su ogni macchina worker del Cluster; ovviamente non è possibile creare un altro Service che espone la stessa porta. Ha il limite di poter usare solo le porte dalla 30.000 alla 32.767.

LoadBalancer: questa tipologia di Service è utilizzato nei cloud (AWS, Azure, Google...) per connettere direttamente i servizi di Kubernetes ai balancer nativi del cloud, dunque è inutile parlarne in ora.

Ingress: versione avanzata di NodePort per i protocolli HTTP e HTTPS, con feature come il routing.

Ecco il file YML per la creazione di un service NodePort che mi permette di chiamare direttamente i Pod creati prima.

apiVersion: v1
kind: Service
metadata:
  name: my-service
spec:
  selector:
    mylabel: test1 # selector in myreplicaset della label nel template
  type: NodePort
  ports:
    - protocol: TCP
      port: 80
      targetPort: 5000
      nodePort: 30000

Creato il Service, Kubernetes assegnerà esso un IP e sarà inserito un suo nome nel DNS interno;  questo permetterà di richiamare il servizio che invierà le richieste al Pod o ai Pod sottostanti.

Notare ancora l'uso del selector. In Kubernetes è importantissimo l'uso delle label e dei selector per creare un legame tra i vari oggetti. In questo esempio il Service esporrà la porta configurata e bilancerà le richieste proveniente da essa a tutti i Pod collegati con quel selector. Ecco la lista dei Service attivi:

< kubectl get svc
> NAME         TYPE        CLUSTER-IP      EXTERNAL-IP   PORT(S)        AGE
> kubernetes   ClusterIP   10.96.0.1       <none>        443/TCP        4h6m
> my-service   NodePort    10.96.150.237   <none>        80:30000/TCP   6s

Tralasciando la prima voce che è utilizzata internamente da Kubernetes, ecco il servizio che mostrano il tipo di service, l'IP interno nel Cluster e altre informazioni. In Ports sono presenti due porte separate dai due punti, in cui il primo valore, 80, sarà la porta a cui risponderà all'interno del Cluster e il secondo valore, 30000, la porta al di fuori della macchina. E' sufficiente fare la prova per verificare questo sia vero da uno dei Pod:

< kubectl exec -it myreplicaset1-bvq7t -- curl 10.96.85.32/api/info
> {
>  "Settings": {
>    "BooksCollectionName": "MyTest",
>    "DatabaseName": "MyDatabase",
>    "ReplicaSet": "replicaTest"
> }, ...

Nella colonna sopra nell'elenco dei service è presente la colonna EXTERNAL IP valorizzata con none, dunque non possiamo contattare il servizio dall'esterno. Minikube nella versione che utilizza una macchina virtuale in VirtualBox non può esporre un servizio direttamente, e mette a disposizione un comando apposito per esporre un Service. Ora con il comando seguente si potrà chiamare il Pod esternamente:

Il browser si avvierà automaticamente mostrando una pagina bianca: l'app avviata non ha nulla nella root essendo una API Rest (è la stessa inserita nell'esempio con Docker Swarm nel post precedente); uso il link corretto: http://192.168.99.100:30000/api/info:

La pagina mostra i parametri inseriti direttamente nella app e tutte le environment variable. Si può notare che oltre a quelle della app e del sistema operativo, sono state inserite molte altre, come KUBERNETES_PORT_..., KUBERNETES_SERVICE_... e altre. Questa è una caratteristica di Kubernetes che inserisce in queste variabili anche l'elenco dei suoi servizi e di tutti gli altri servizi disponibili al momento (un Pod non inserirà nelle environment variable un Service avviato dopo il Pod). Personalmente non ho mai trovato occasione di utilizzare questa feature essendo i servizi esposti con il DNS interno del Cluster di Kubernetes, ma è sempre meglio sapere che esiste questa cosa.

Se si è abituati al mondo di Docker Swarm ci si potrebbe chiedere perché usare internamente un Service. In effetti con Docker Swarm, se da un Container io volessi accedere ad una API Rest dovrei solo chiamare il nome del Container come ho fatto nel post precedente, per esempio:

curl nomecontainer:5000/api/info

In Kubernetes il nome del Pod non può essere utilizzato in questo modo, il perché lo si può averlo immaginato vedendo l'esempio sopra per un ReplicaSet:

> myreplicaset1-7smdf   1/1     Running   0          3m 49s
> myreplicaset1-czgpj   1/1     Running   0          18s
> myreplicaset1-kgql5   1/1     Running   0          3m 49s

I nomi sono completamente casuali e in caso di cancellazione e aggiunta è impossibile sapere il nome che avrà il Pod/Container. Ovviamente questo non blocca l'accesso a questo Container da altri Container perché sapendo l'IP è possibile accedere. Per esempio, per il Pod myreplicaset1-7smdf, con il comando prima visto:

kubectl describe pod myreplicaset1-7smdf

Tra le informazioni:

...
Annotations:  <none>
Status:       Running
IP:           172.17.0.6
IPs:
  IP:  172.17.0.6
Containers:
...

Ora da un altro Pod:

kubectl exec -it myreplicaset1-czgpj -- curl 172.17.0.6:5000/api/info

Mostrerà il risultato correttamente. Ovviamente pure l'IP in caso di distruzione del Pod e di una sua nuova creazione può cambiare, ma questa informazione è utile per verificare eventuali problemi/funzionalità di un Pod in modo veloce.

Dopo questa veloce disamina delle basi di Kubernetes è ora di fare sul serio. Riprenderò quanto fatto nel post precedente con Docker Swarm e lo rifarò con Kubernetes. Riassumo: in Docker Swarm avevo creato una Replica Set con MongoDb composto da tre macchine alle quali accedevano per richiedere i dati una web app per la gestione della sua struttura e la web app appena mostrata come esempio che espone i metodi per richiedere, inserire, modificare e cancellare dati dalla collection books.

Parto dagli oggetti più semplici: i Secret dove inserirò le credenziali e il file di configurazione per MongoDb. Così come Docker Swarm è possibile inserire nei Pod (e nei Container) queste informazioni come file, in modo che possano essere lette dalle applicazioni. Innanzitutto creo i Secret sempre con dei file YML:

apiVersion: v1
kind: Secret
metadata:
  name: mysecret
type: Opaque
stringData:
  username: mongouser
  password: mycomplexpassword
data:
  keyfile: ZXhhbXBsZTEyMzQ1Ng==

Si possono inserire in chiaro, come ho fatto  per le credenziali, così come in base64 per il contenuto del file per l'autorizzazione per MongoDb (il cui contenuto in testo è "example123456"). Controllo la lista dei Secret:

< kubectl get secrets
> NAME                  TYPE                                  DATA   AGE
> default-token-bmhnc   kubernetes.io/service-account-token   3      9h
> mysecret              Opaque                                3      3s

A parte il primo che è interno, ecco mysecret appena creato. Nel dettaglio:

< kubectl describe secret mysecret
> Name:        
mysecret
> Namespace:    default
> Labels:       <none>
> Annotations:  
> Type:         Opaque
>
> Data
> ====
> keyfile:   13 bytes
> password:  17 bytes
> username:  9 bytes

E' arrivato il momento della creazione delle tre istanze di MongoDb in Replica Set. Si può immagine che sia l'omonimo oggetto di Kubernetes ad essere adatto per la loro configurazione, ma se si è notata la creazione dei Pod nei ReplicaSet, si sarà notato che sono inseriti con il nome del ReplicaSet, ma seguiti da un trattino e da alcune lettere casuali. Questo blocca qualsiasi utilizzo diretto dei Pod per la creazione di una stringa di connessione a MongoDb da parte delle applicazioni. Nella lista degli oggetti disponibili in Kubernetes ho anche accennato a StatefulSet che è stato creato proprio per questi scopi. Infatti, come il ReplicaSet, creerà tanti Pod quanti quelli richiesti, ma la differenza è avranno un nome facilmente identificabile seguito da un numero progressivo.

Faccio un esempio reale con questo oggetto e MongoDb (non funzionante):

apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: mongodb
spec:
  selector:
    matchLabels:
      app: mongodbservice # has to match .spec.template.metadata.labels
  serviceName: "mongodbservice"
  replicas: 3 # by default is 1
  template:
    metadata:
      labels:
        app: mongodbservice # has to match .spec.selector.matchLabels
    spec:
      containers:
      - name: mongodbservice
        image: mongo:4.2.1
        ports:
        - containerPort: 27017
          name: mongodb

Una volta creato il tutto con il comando:

< kubectl get statefulset
> NAME      READY   AGE
> mongodb   3/3     18s

Saranno creati questi Pod:

< kubectl get pods
> NAME        READY   STATUS    RESTARTS    AGE
> mongodb-0   1/1     Running   0          102s
> mongodb-1   1/1     Running   0          100s
> mongodb-2   1/1     Running   0          98s

Ora i nomi sono prevedibili essendo il nome dello StatefulSet seguito da un numero intero consecutivo. Ma anche la creazione di questi Pod è differente. Quando ad un ReplicaSet viene chiesto di creare venti Pod, questo li creerà tutti contemporanemente (distribuendoli, ovviamente, in tutti i nodo del Cluster di Kubernetes). Con StatefulSet no, sarà creato sempre uno alla volta; sarà creato il primo (con numero zero) e si passerà al numero successivo solo quando questo sarà avviato (questo non evita alcuni problemi che farò vedere in seguito). Nell'ipotesi qui sopra, se crashasse il Pod mongodb-0, non verrebbe sostituito da un nuovo Pod con nome mongodb-3, ma sarà creato un nuovo Pod con lo stesso nome di quello crashato. Questo permette di poter accedere direttamente ai singoli Pod per la creazione di stringhe di connessioni come devo fare proprio con il mio esempio (anche se manca un service per renderlo visibile e utilizzabile, ma questo ne parlerò tra poco).

Qui sopra si può vedere come le tre istanze sono state avviate correttamente, ma esse non sono in ReplicaSet (non confondere il Replica Set di MongoDb con il ReplicaSet di Kubernetes!!!). Come nel post precedente il tutto dev'essere configurato. Innanzitutto devono essere passate le credenziali che l'istanza di MongoDb dovrà usare per l'authentication, inoltre dovrà essere configurato il keyfile per il riconoscimento e l'accettazione della creazione del Replica Set. Come con Docker Swarm passerò queste informazioni direttamente come file all'interno dei Pod, ma questa volta con qualche riga di configurazione aggiuntiva. Innanzitutto cancello il SatefulSet appena creato:

> kubectl delete statefulset mongodb

Quindi ecco il nuovo file YML:

apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: mongodb
spec:
  selector:
    matchLabels:
      app: mongodbservice # has to match .spec.template.metadata.labels
  serviceName: "mongodbservice"
  replicas: 3 # by default is 1
  template:
    metadata:
      labels:
        app: mongodbservice # has to match .spec.selector.matchLabels
    spec:
      volumes:
      - name: secretvolume
        secret:
          secretName: mysecret
      containers:
      - name: mongodbservice
        image: mongo:4.2.1
        ports:
        - containerPort: 27017
          name: mongodb
        volumeMounts:
        - name: secretvolume
          readOnly: true
          mountPath: "/etc/secret-volume"

Una volta che i Pod sono stati creati verifico che questi filesiano stati creati e inseriti:

< kubectl get pods
> NAME        READY   STATUS    RESTARTS   AGE
> mongodb-0   1/1     Running   0          52s
> mongodb-1   1/1     Running   0          51s
> mongodb-2   1/1     Running   0          50s
< kubectl exec -it mongodb-0 -- ls -l /etc/secret-volume
> total 0
> lrwxrwxrwx 1 root root 14 Feb  1 20:22 keyfile -> ..data/keyfile
> lrwxrwxrwx 1 root root 15 Feb  1 20:22 password -> ..data/password
> lrwxrwxrwx 1 root root 15 Feb  1 20:22 username -> ..data/username

E il contenuto:

< kubectl exec -it mongodb-0 -- cat /etc/secret-volume/username
> mongouser

Perfetto, ora è il momento di definire per il Pod questi parametri per la creazione dell'istanza di MongoDb con tutti questi dati:

apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: mongodb
spec:
  selector:
    matchLabels:
      app: mongodbservice # has to match .spec.template.metadata.labels
  serviceName: "mongodbservice"
  replicas: 3 # by default is 1
  template:
    metadata:
      labels:
        app: mongodbservice # has to match .spec.selector.matchLabels
    spec:
      volumes:
      - name: secretvolume
        secret:
          secretName: mysecret
      containers:
      - name: mongodbservice
        image: mongo:4.2.1
        ports:
        - containerPort: 27017
          name: mongodb
        volumeMounts:
        - name: secretvolume
          readOnly: true
          mountPath: "/etc/secret-volume"
        args:
        - mongod
        - --port=27017
        - --bind_ip=0.0.0.0
        - --replSet=replicaTest
        - --keyFile=/etc/secret-volume/keyfile
        env:
        - name: MONGO_INITDB_ROOT_USERNAME_FILE
          value: /etc/secret-volume/username
        - name: MONGO_INITDB_ROOT_PASSWORD_FILE
          value: /etc/secret-volume/password

E creo il tutto in Kubectl (cancellando quanto fatto prima):

kubectl delete all --all 
kubectl create -f mongodbtest3.yml

E verifico che funzioni:

< kubectl get statefulset
> NAME      READY   AGE
> mongodb   0/3     78s

Ora c'è qualche problema:

< kubectl get pods
> NAME        READY   STATUS             RESTARTS   AGE
> mongodb-0   0/1     CrashLoopBackOff   4          113s
> mongodb-1   0/1     CrashLoopBackOff   4          96s

Kubernetes ha problemi ad avviare queste istanze di MongoDb (se si è letto il post precedente si saprà già perché). Qui si vede che Kubernetes ha creato un Pod e una volta avviato ha fatto partire subito il secondo, ma di seguito è nato il problema nell'avvio di MongoDb e il sistema ha cercato di riavviare i Container (non riuscendoci visto che in meno di due minuti sono stati fatti quattro riavvii). Controllo il log:

< kubectl logs mongodb-0
> about to fork child process, waiting until server is ready for connections.
> forked process: 30
> 2020-02-01T20:33:02.215+0000 I  CONTROL  [main] ***** SERVER RESTARTED *****
> 2020-02-01T20:33:02.219+0000 I  CONTROL  [main] Automatically disabling TLS 1.0, to force-enable TLS 1.0  specify --sslDisabledProtocols 'none'
> 2020-02-01T20:33:02.221+0000 I  ACCESS   [main] permissions on /etc/secret-volume/keyfile are too  open
> ERROR: child process failed, exited with error number 1
> To see additional information in this output, start without the "--fork" option.

Ecco il problema: il keyfile deve avere dei permessi più restrittivi altrimenti MongoDb si rifiuta di partire. Con Docker Swarm avevo risolto con le corrette impostazioni:

- source: MONGO_DB_KEYFILE
  uid: '999'
  gid: '999'
  mode: 0600

Il problema è che Kubernetes questo non è permesso (ad essere precisi è possibile impostare il mode per il file, ma non è possibile modificare il proprietario del file - è una feature richiesta da tempo, ma non è stata ancora inserita). Senza tirarla per le lunghe ecco una possibile con gli Init Containers. Nella definizione del template di un Pod è possibile inserire uno o più Container che avranno accesso alle risorse dello stesso Pod (volume e altro) ed eseguiti prima dell'avvio del Container principale. La soluzione è tutta qua: definire un volume che sarà condiviso nel Pod principale dove un Container Docker secondario preparerà i file per MongoDb con i giusti permessi.

Il primo passo per fare questo è definire un volume nel Pod:

apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: mongodb
spec:
  selector:
    matchLabels:
      app: mongodbservice # has to match .spec.template.metadata.labels
   serviceName: "mongodbservice"
  replicas: 3 # by default is 1
   template:
     metadata:
       labels:
        app: mongodbservice # has to match .spec.selector.matchLabels
     spec:
       volumes:
       - name: secretvolume
         secret:
          secretName: mysecret
       - name: workdir
         emptyDir:
          medium: Memory

Notare l'oggetto serviceName: questo nome è utilizzato poi nella creazione del Service. In volumes vengono specificati due volume: il primo, secretvolume, serve per leggere i Secret iniettati da Kubernetes, il secondo, workdir, sarà il volume dove un Container apposito copierà i Secret con i giusti permessi che il Container MongoDb saprà leggere correttamente (notare che, per maggiore sicurezza, il volume dove saranno salvate queste informazioni sarà creato in RAM - opzione medium. I volume dai Secret hanno quest'impostazione già attiva di default). Ora è il momento di introdurre l'Init Container:

      containers:
      - name: mongodbservice
        image: mongo:4.2.1
        ports:
        - containerPort: 27017
          name: mongodb
        args:
        - mongod
        - --port=27017
        - --bind_ip=0.0.0.0
        - --replSet=replicaTest
        - --keyFile=/tmp/secrets/keyfile
        env:
        - name: MONGO_INITDB_ROOT_USERNAME_FILE
          value: /tmp/secrets/username
        - name: MONGO_INITDB_ROOT_PASSWORD_FILE
          value: /tmp/secrets/password
        volumeMounts:
        - name: workdir
          mountPath: /tmp/secrets
        - name: datadir
          mountPath: /data/db
      # These containers are run during pod initialization
      initContainers:
      - name: install
        image: mongo:4.2.1
        command: ["/bin/sh", "-c"]
        args: 
        - cp /etc/secret-volume/keyfile /work-dir;
          chmod 0600 /work-dir/keyfile;
          chown 999:999 /work-dir/keyfile;
          cp /etc/secret-volume/username /work-dir;
          cp /etc/secret-volume/password /work-dir;
        volumeMounts:
        - name: workdir
          mountPath: "/work-dir"
        - name: secretvolume
          readOnly: true
          mountPath: "/etc/secret-volume"

In volumeMounts del Pod principale ho definito un mountPath in /tmp/secret (con il nome workdir che fa riferimento al volume creato prima). Nel Container definito nell'init, ho usato l'immagine Docker Busybox per le sue dimensioni ridotte (~700KB) che ha i tre comandi che mi servono per la copia e l'assegnazione dei permessi dei file (in args). Da notare in questo Container come ho definito i due volumeMounts per l'accesso ai Secret e al volume workdir che è condivisa con il Container di MongoDb. Avviato ora il tutto in Kubernetes, saranno creati i tre Pod funzionanti.

In precedenza avevo mostrato come leggere i log di un Pod utile anche per verificarne i problemi (con il quale avevo capito il problema dei permessi sul file). In caso di initContainer, per vedere eventuali messaggi da questo Container, avrei dovuto scrivere:

kubectl logs mongodbservice install

Dove mongodbservice è il nome del Pod principale e install è il nome dell'init container.

E' ora di creare un Service per rendere questi tre Pod visibili e accessibili con un nome bloccato per creare la corretta stringa di connessione:

apiVersion: v1
kind: Service
metadata:
  name: mongodbservice
  labels:
    app: mongodbservice
spec:
  ports:
  - port: 27017
    name: mongodb
  clusterIP: None
  selector:
    app: mongodbservice

Questo creerà il service mongodbservice e si è usata una property aggiuntiva: clusterIp: None. Questa specifica che questo service sarà headless, questo vuol dire che non sarà assegnato un IP fisso a questo servizio ma tutte le richieste pervenute saranno rimandate agli IP dei Pod sottostanti (grazie al servizio DNS interno). Internamente Kubernetes avvia in tutti i nodi del suo Cluster un DNS interno per la risoluzione dei nomi dei suoi oggetti. Il nome di dominio creato di defaut è default.svc.cluster.local, dove default è il namespace di default (che fantasia) dove saranno create i vari oggetti. Per esempio, il primo Pod creato all'inizio di questo post avrebbe questo DNS name completo: mongodbtest.default.svc.cluster.local; se lo avessi creato nel namespace myblog: mongodbtest.myblog.svc.cluster.local. Gli oggetti nello stesso livello di dominio non hanno bisogno di specificarne il padre, se due Pod hanno questi nomi:

mongodbtest0.default.svc.cluster.local
mongodbtest1.default.svc.cluster.local

Per referenziarsi l'uno con l'altro potranno usare semplicemente mongodbtest1 da mongodbtest0 così come il contrario. Ma se i due Pod fossero stati:

mongodbtest0.default.svc.cluster.local
mongodbtest1.myblog.svc.cluster.local

Dal primo Pod, per accedere al secondo, dovrei usare il DNS name con il namespace: mongodbtest1.myblog.

Questa veloce ed essenziale premessa serve a spiegare come scriverò la stringa di connessione alle tre istanze di MongoDb. Se il servizio qui sopra fosse definito NON headless con questa DNS name:

mongodbservice.default.svc.cluster.local
mongodbservice.default.svc.cluster
mongodbservice.default.svc
mongodbservice.default
mongodbservice

Il DNS di Kubernetes mi avrebbe mandato ad una istanza casuale di MongoDb, mentre io voglio avere la possibilità di accedere alle singole istanze. Con headless attivo posso farlo con:

mongodb-0.mongodbservice
mongodb-1.mongodbservice
mongodb-2.mongodbservice

E' arrivato il momento di collegare le tre istanze di MongoDb. Nel post precedente ho risolto il problema creando un Container apposito che, attesa la creazione e l'avvio delle tre istanze di MongoDb, invia i corretti comandi per l'avvio del Replica Set. Per farlo con Kubernetes abbiamo un oggetto apposito, già citato ad inizio di questo post: job. In Docker Swarm avevo usato un Container apposito creato da me, e userò a continuare quel Container anche se o dovuto fare delle piccole modifiche ed ho creato una versione specifica (V2). Ecco la definizione del job:

apiVersion: batch/v1
 kind: Job
 metadata:
   name: mongodbreplicaset
 spec:
   backoffLimit: 1
   activeDeadlineSeconds: 600
   template:
     spec:
       volumes:
       - name: secretvolume
         secret:
          secretName: mysecret
       containers:
      - name: mongodbreplicaset
        image: sbraer/mongoreplicaset:v2
        imagePullPolicy: IfNotPresent #Always
         volumeMounts:
        - name: secretvolume
          readOnly: true
           mountPath: /etc/secretvolume
         env:
        - name: replicasetname
          value: replicaTest
        - name: MONGODB_CLUSTER_LIST
          value: 'mongodb-0.mongodbservice mongodb-1.mongodbservice mongodb-2.mongodbservice'
        - name: MONGODB_USERNAME_FILE
          value: /etc/secretvolume/username
        - name: MONGODB_PASSWORD_FILE
          value: /etc/secretvolume/password
       restartPolicy: Never

Se non fosse per la definizione in kind sembrerebbe la definizione di un ReplicaSet o di un StatefulSet (questo dettaglio l'ho apprezzato immediatamente in Kubernetes) e avere solo delle minime aggiunte per la definizione di compiti specifici dell'oggetto trattato. Per esempio, nel job definito qui sopra, è presente l'attributo activeDeadlineSeconds per specificare quanti secondi può restare in esecuzione il job, oppure backOffLimit che specifica il numero di riavvii del job in caso di errori. Inutile entrare nei dettagli perché la documentazione ufficiale vale più di qualsiasi mia parola.

Creando anche questo oggetto con kubectl come fatto più volte sopra, le tre istanze di MongoDb saranno collegate in ReplicaSet e utilizzabili dalle web application con la stringa di connessione prima spiegata nell'environment variable MONGODB_CLUSTER_LIST.

E' arrivato il momento di avviare le due web application di esempio. Quella per accedere a MongoDb userò un service collegato ad un ReplicaSet:

apiVersion: v1
kind: Service
metadata:
  name: showdb
spec:
  selector:
    app: showdb
  ports:
  - protocol: TCP
    port: 5002
    targetPort: 8081
    nodePort: 30164
  type: NodePort
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: showdb
  labels:
    app: showdb
spec:
  replicas: 1
  selector:
    matchLabels:
      app: showdb
  template:
    metadata:
      labels:
        app: showdb
    spec:
      volumes:
      - name: secretvolume
        secret:
          secretName: mysecret
      containers:
      - name: showdb
        image: mkucuk20/mongo-express
        imagePullPolicy: IfNotPresent
        env:
        - name: ME_CONFIG_MONGODB_ADMINUSERNAME_FILE
          value: /etc/secretvolume/username
        - name: ME_CONFIG_MONGODB_ADMINPASSWORD_FILE
          value: /etc/secretvolume/password
        - name: ME_CONFIG_MONGODB_SERVER
          value: mongodb-0.mongodbservice,mongodb-1.mongodbservice,mongodb-2.mongodbservice
        volumeMounts:
        - name: secretvolume
          readOnly: true
          mountPath: '/etc/secretvolume'

Qui sarà avviato mongoexpress e, non essendoci bisogno di permessi particolari, passerò i valori dei Secret nel modo tradizionale (con un singolo Volume). Notare, inoltre, come YAML permetta di poter inserire in un singolo file più sezioni dividendole con tre trattini "---".

Nel mio post precedente, per la mia web app, avevo fatto in modo che, usando l'oggetto global di Docker Swarm, venisse istanziata una copia per ogni nodo collegato al mio Cluster. Se volessi fare lo stesso in Kubernetes dovrei usare l'oggetto DaemonSet (di cui ne ho parlato prima) che ha proprio questo scopo:

apiVersion: v1
kind: Service
metadata:
  name: testdb
spec:
  selector:
    app: testdb2
  ports:
  - protocol: TCP
    port: 5000
    targetPort: 5000
    nodePort: 30163
  type: NodePort
---
apiVersion: apps/v1
kind: DaemonSet
metadata:
  name: testdb
  labels:
    app: testdb
spec:
  selector:
    matchLabels:
      app: testdb
  template:
    metadata:
      labels:
        app: testdb
    spec:
      volumes:
      - name: secretvolume
        secret:
          secretName: mysecret
      - name: datadir
        emptyDir: {}
      containers:
      - name: testdb
        image: sbraer/mongodbtest:v1
        imagePullPolicy: IfNotPresent
        resources:
          limits:
            memory: 200Mi
          requests:
            cpu: 100m
            memory: 200Mi
        ports:
        - containerPort: 5000
          hostPort: 5000
          protocol: TCP
        securityContext:
          runAsUser: 405
          runAsNonRoot: true
          readOnlyRootFilesystem: true
        env:
        - name: MONGODB_SERVER_USERNAME_FILE
          value: /etc/secretvolume/username
        - name: MONGODB_SERVER_PASSWORD_FILE
          value: /etc/secretvolume/password
        - name: MONGODB_SERVER_LIST
          value: mongodb-0.mongodbservice,mongodb-1.mongodbservice,mongodb-2.mongodbservice
        - name: MONGODB_REPLICA_SET
          value: replicaTest
        - name: MONGODB_DATABASE_NAME
          value: MyDatabase
        - name: MONGODB_BOOKS_COLLECTION_NAME
          value: MyTest
        - name: TMPDIR
          value: /tmp
        volumeMounts:
        - name: secretvolume
          readOnly: true
          mountPath: '/etc/secretvolume'
        - name: datadir
          mountPath: /tmp

In questo caso, solo altre info aggiuntive, ho inserito la sezione resources in cui si possono specificare i limiti del Container nell'uso di memoria e di cpu (questa sezione è disponibile anche in ReplicaSet, StatefullSet...).

Inserito tutto in un unico file (alla fine di questo post è presente un link al repository con tutti i file trattati) e lanciato con:

kubectl apply -f mongodb-minikube.yml

Sarà creato il tutto in circa un minuto (dipende dalla potenza delle macchine, se i Container Docker sono stati già scaricati etc...):

< kubectl get pods
> NAME                      READY   STATUS      RESTARTS   AGE
> mongodb-0                 1/1     Running     0          48s
> mongodb-1                 1/1     Running     0          42s
> mongodb-2                 1/1     Running     0          36s
> mongodbreplicaset-q7zsg   0/1     Completed   0          48s
> showdb-6979c7ddc-k598v    1/1     Running     2          48s
> testdb-pxpkx              1/1     Running     0          48s

Tutti i Pod sono avviati e hanno lo STATUS corretto (Running) tranne mongodbreplicaset-q7zsg che ha come STATUS il valore Completed - è corretto visto che questo è il Job che, concluso il suo compito, ha spento il Pod risparmiando così risorse delle macchine.

Controllo se sono stati creati anche i Service:

< kubectl get svc
> NAME             TYPE        CLUSTER-IP     EXTERNAL-IP   PORT(S)          AGE
> kubernetes       ClusterIP   10.96.0.1      <none>        443/TCP          2m14s
> mongodbservice   ClusterIP   None           <none>        27017/TCP        109s
> showdb           NodePort    10.96.97.30    <none>        5002:30164/TCP   109s
> testdb           NodePort     10.96.48.226   <none>        5000:30163/TCP   109s

Gli ultimi due Service, essendo di tipo NodePort, sono utilizzabili anche esternamente. Per farlo, come già detto, dobbiamo usare un comando apposito di Minikube:

< minikube service showdb

service in minikube

La web api ha come Service name testdb:

< minikube service testdb

Che aprirà il browser il cui url correggerò per richiedere le info del Container:

service in minikube

Nell'esempio del post precedente con Docker Swarm avevo usato anche una web application esterna per vedere la distribuzione dei Container nelle varie macchine. Kubernetes ha uno strumento simile ma più potente che Minikube mette a disposizione con un comando:

< minikube dashboard

dashboard in minikube

Qui sarà possibile vedere tutti gli oggetti in esecuzione in Kubernetes più eventuali dettagli.

Ok, basta. E' ora di spegnere tutto:

kubectl delete all --all

Che cancellerà tutti i Pod creati ma non altri oggetti come i Secret, le Network Security, i Volume non collegati direttamente ai Pod, i Namespace e altro. Interessante e utile in certi scenari anche le Network Security: di base Kubernetes avvia tutti i Pod senza restrizioni di accesso; per evitare questo è presente questo oggetto in cui si possono impostare le regole di connessione tra Pod in modo molto restrittivo: dal blocco di tutte le porte tranne quelle del servizio che vogliamo sia esposto fuori dal Pod, a quelle sul selector sui Pod ai quali vogliamo dare o negare accesso, alla selezione dei namespace etc... Non mostro qui nessun esempio a riguardo perché con l'installazione base di Minikube questo servizio non è presente e un'eventuale configurazione viene semplicemente ignorata. Non ho trattato nemmeno altri argomenti come l'uso dei Namespace, l'RBAC, oppure la gestione dei Volume, ma in questo caso, per mostrare le feature interessanti, avrei dovuto creare il tutto dentro un servizio di Cloud. Forse in un prossimo post? Chissà...

Questo è il link dove trovare i file utilizzati in questo blog, compreso un esempio con l'uso dei Storage Class e VolumeClaimTemplate con Minikube:

https://github.com/sbraer/kubernetes-blog

Commenti

Visualizza/aggiungi commenti

| Condividi su: Twitter, Facebook, LinkedIn

Per inserire un commento, devi avere un account.

Fai il login e torna a questa pagina, oppure registrati alla nostra community.

Nella stessa categoria
I più letti del mese