Table of Contents

Laboratorul 07 - Secrete, variabile de mediu și persistența datelor în Kubernetes

În laboratoarele anterioare am folosit variabilele de mediu direct în fișierele YAML ale pod-urilor. Evident, în cazul datelor sensibile, nu este o idee bună pentru ca oricine are acces la fișierele de configurare YAML să aibă posibilitatea de a vedea variabilele de mediu definite. Problema apare în momentul în care în variabilele de mediu definim nume de utilizatori și parole.

Cum putem pasa datele sensibile în variabilele de mediu ale unui pod într-un mod sigur? Prin intermediul obiectelor Secrets din Kubernetes.

Secrete în Kubernetes

Exemplu de secret în Kubernetes

apiVersion: v1
data:
  POSTGRES_DB: bGlicmFyeQ==
  POSTGRES_PASSWORD: c3R1ZGVudA==
  POSTGRES_USER: c3R1ZGVudA==
kind: Secret
metadata:
  name: db-secret

Fișierul de mai sus este un secret care reprezintă variabilele de mediu folosite în podul DB. Puteți observa că perechile cheie-valoare nu sunt în totalitate clare. Mai exact, cheia este vizibilă, dar câmpul de valoare este encodat în format BASE64. Atenție, BASE64 nu este criptare, nu asigură siguranța.

Pentru a crea fișierul pentru un secret fără a converti manual string-urile în BASE64, puteți folosi comanda următoare:

kubectl create secret generic db-secret --from-literal=POSTGRES_USER=student --from-literal=POSTGRES_PASSWORD=student --from-literal=POSTGRES_DB=library --dry-run=client -o yaml > db-secret.

Creați acest secret:

kubectl apply -f db-secret.yaml

Incercati sa vizualizati datele din acest secret:

kubectl get secret db-secret
kubectl describe secret db-secret

Variabile de mediu în Kubernetes

Cum injectăm aceste date în variabilele de mediu ale podului nostru DB? Să ne uităm pe următorul exemplu:

apiVersion: v1
kind: Pod
metadata:
  creationTimestamp: null
  labels:
    run: db
  name: db
spec:
  containers:
  - image: axonedge/lab-k8s-database
    name: db
    envFrom:
      - secretRef:
          name: db-secret

Creați pod-ul și verificați variabilele de mediu. Expuneti pod-ul de DB ca la laboratorul anterior, printr-un serviciu de tipul clusterIP.

Serviciul de RabbitMQ

Serviciul de RabbitMQ, fiind unul basic, îl rulăm în continuare ca la laboratorul anterior.

kubectl run rabbitmq --image=rabbitmq:3 --port=5672

și îl expunem printr-un serviciu de tipul ClusterIP:

apiVersion: v1
kind: Service
metadata:
  name: rabbitmq-cluster-ip-service
spec:
  ports:
  - port: 5672
    protocol: TCP
    targetPort: 5672
  selector:
    run: rabbitmq

Serviciul Procesator

Asa cum știm, pod-ul de procesator are mai multe variabile de mediu, le puteți vedea mai jos:

    env:
    - name: PORT
      value: "8000"
    - name: PGHOST
      value: db-cluster-ip-service
    - name: PGDATABASE
      value: library
    - name: PGPORT
      value: "5432"
    - name: PGUSER
      value: student
    - name: PGPASSWORD
      value: student
    - name: AMQPURL
      value: amqp://rabbitmq-cluster-ip-service

Cum putem să injectăm aceste variabile fără să le avem în text clar în fișierul YAML?

Putem rezolva această problemă fie prin configMaps, fie prin secrets. Asa cum știm deja, perechile cheie-valoare dintr-un secret nu pot fi văzute din interiorul clusterului, deci secret rămâne cea mai bună opțiune pentru datele sensibile (PGDATABASE, PGUSER, PGPASSWORD). În plus, putem folosi configMap pentru variabilele care nu sunt sensibile: PGHOST, PGPORT, AMQPURL.

Așa cum am văzut anterior, creăm un secret cu datele mai sus menționate:

kubectl create secret generic proc-secret --from-literal=PGUSER=student --from-literal=PGPASSWORD=student --from-literal=PGDATABASE=library --dry-run=client -o yaml > proc-secret.yaml

Fișierul YAML este următorul:

apiVersion: v1
data:
  PGDATABASE: bGlicmFyeQ==
  PGPASSWORD: c3R1ZGVudA==
  PGUSER: c3R1ZGVudA==
kind: Secret
metadata:
  creationTimestamp: null
  name: proc-secret

Creați un configMap pentru restul variabilelor (urmăriți exemplul din documentație, este foarte similar cu cel de la secret):

apiVersion: v1
data:
  AMQPURL: amqp://rabbitmq-cluster-ip-service
  PGPORT: "5432"
  PGHOST: db-cluster-ip-service
kind: ConfigMap
metadata:
  name: proc-cmap

Injectați variabilele în pod:

apiVersion: v1
kind: Pod
metadata:
  labels:
    run: procesator
  name: procesator
spec:
  containers:
  - image: axonedge/lab-k8s-procesator
    name: procesator
    envFrom:
      - secretRef:
          name: proc-secret
    env:
      - name: AMQPURL
        valueFrom:
          configMapKeyRef:
            name: proc-cmap
            key: AMQPURL
      - name: PGPORT
        valueFrom:
          configMapKeyRef:
            name: proc-cmap
            key: PGPORT
      - name: PGHOST
        valueFrom:
          configMapKeyRef:
            name: proc-cmap
            key: PGHOST       

Observați modul în care am injectat variabilele din configMap. Am exemplificat acest mod pentru scopul didactic, ele pot fi injectate la fel ca mai sus la secret.

Pentru pod-ul de API, urmăm pașii făcuți pentru procesator. Putem să observăm că datele sensibile sunt aceleași, deci putem să folosim același secret creat la pasul anterior. Restul variabilelor de mediu sunt și ele identice, dar avem în plus variabila PORT. Pentru a exemplifica faptul că putem folosi envFrom din mai multe surse, o sa creăm un nou configMap:

apiVersion: v1
data:
  AMQPURL: amqp://rabbitmq-cluster-ip-service
  PGPORT: "5432"
  PGHOST: db-cluster-ip-service
  PORT: "8000"
kind: ConfigMap
metadata:
  name: api-cmap

Acest configMap este identic cu cel de mai sus, dar a fost adăugată variabila PORT. Pentru a injecta variabilele de mediu în pod folosim envFrom, ca in fișierul de mai jos:

apiVersion: v1
kind: Pod
metadata:
  labels:
    run: api
  name: api
spec:
  containers:
  - image: axonedge/lab-k8s-api
    name: api
    envFrom:
      - secretRef:
          name: proc-secret
      - configMapRef:
          name: api-cmap

Puteți observa ca am folosit două surse pentru envFrom, o sursă fiind un secret, iar cealaltă sursă fiind un configMap. Astfel, am exeplificat cele mai folosite două metode prin care pot fi injectate variabilele dintr-un configMap într-un pod.

Temă

Modificați deployment-ul astfel încât să nu folosim pod-uri, ci să folosim deployment-uri acolo unde este nevoie.

Persistența datelor

Adăugați câteva intrări în baza de date. Verificați ca acestea există. Ștergeți podul de DB, creați unul nou și executați o cerere de tip GET. Mai avem intrări în baza de date?

Pentru ca datele din baza de date să nu dispară odată cu ștergerea pod-ului, folosim PV (PersistentVolume) și PVC (PersistentVolumeClaims).

Un fișier de bază pentru a crea un PersistentVolume găsiti mai jos. Proprietățile pe care le folosim aici sunt destul de intuitive.

apiVersion: v1
kind: PersistentVolume
metadata:
    name: mypv
spec:
  accessModes:
    - ReadWriteMany
  capacity:
    storage: 1Gi
  hostPath:
    path: /tmp/data

Dupa ce creăm acest volum, verificăm că acesta există:

kubectl get pv # în continuare urmăriți câmpul de status

Creăm un PVC pe baza următorului fișier de configurare:

apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: mypvc
spec:
  accessModes:
    - ReadWriteMany
  resources:
    requests:
      storage: 1Gi

După ce am creat și un PVC, verificați din nou atât PV, cât și PVC:

kubectl get pv # urmăriți câmpul de status
kubectl get pvc

Cum folosim un volum într-un pod? Modificăm fișierul de configurare al pod-ului pentru baza de date astfel încât acesta să folosească noul volum:

apiVersion: v1
kind: Pod
metadata:
  creationTimestamp: null
  labels:
    run: db
  name: db
spec:
  containers:
  - image: axonedge/lab-k8s-database
    name: db
    envFrom:
      - secretRef:
          name: db-secret
    volumeMounts:
      - mountPath: /var/lib/postgresql/data
        name: mypd
 
 
  volumes:
  - name: mypd
    persistentVolumeClaim:
      claimName: mypvc

Ștergeți pod-ul de DB creat anterior și creați altul pe baza fișierului de mai sus. Adăugați câteva intrări în baza de date, ștergeți pod-ul și apoi creați altul nou. Putem observa că de data aceasta baza de date nu este goală.