Kubernetes prakticky: předávání konfigurací aplikacím v kontejnerech

Kontejnerový image s aplikací je immutable a obraz vybudovaný v rámci Continuous Integration pro vývoj a testování má být naprosto identický jako ten, který se přes staging dostane až do produkce například v rámci Continuous Deployment procesu. Pak je tedy potřeba mít schopnost ovlivnit aplikační nastavení mimo samotný kontejnerový image – connection stringy, klíče pro API, certifikáty, feature flagy a tak podobně. Jak je do kontejneru dostat při používání Kubernetes?

Environmentální proměnné v definici Podu

První nejjednodušší způsob jak předat kontejnerům nějaké vstupní parametry je použití environmentálních proměnných přímo v definici Podu:

kind: Pod
apiVersion: v1
metadata:
  name: envPod
spec:
  containers:
    - name: ubuntu
      image: ubuntu
      command: ["tail"]
      args: ["-f", "/dev/null"]
      env:
        - name: mykey
          value: myvalue
        - name: mykey2
          value: myvalue2

Pustíme si Pod.

kubectl apply -f envPod.yaml

Následně do něj skočíme a vypíšeme si env.

kubectl exec env -- env
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
HOSTNAME=ubuntu
mykey2=myvalue2
mykey=myvalue
...

Jednoduché a na začátek účinné. Přesto ne ideální.

ConfigMap

Předchozí příklad trpí některými obtížemi.

Co jsme udělali je to, že jsme zcela spojili životní cyklus parametrů s definicí výpočetního zdroje, což není ideální praxe. Znamená to, že definici Podu musíme modifikovat mezi jednotlivými nasazeními. Řešením by bylo nepoužívat přímo Kubernetes soubory, ale přejít na Helm šablony (o tom podrobně jindy). Ty výše uvedený soubor mohou parametrizovat a hodnoty env můžeme měnit vstupními parametry Helm šablony.  Přesto ale zůstává, že pro modifikaci parametrů musíme provést redeployment i zdrojů (Podu).

Druhá potíž je v tom, že environmentální proměnné se hodí pro jednoduché hodnoty typu feature flag nebo connection string, ale ne pro složité parametry. Pokud jich potřebujete předávat třeba 30, začne to být nepřehledné. Navíc často potřebujete pro middleware či aplikaci přechroupat tyto hodnoty do komplexnějšího konfiguračního souboru třeba my.cnf pro MySQL. Museli bychom pak složitě uvnitř kontejneru vybudovat takový soubor z env. Jednodušší by byla schopnost předat celý obsah my.cnf jako takový, ale to přes env neuděláme díky omezením na použité znaky a délku.

Třetí možný problém je v tom, že tyto proměnné se aplikují pouze při startu kontejneru. Pokud je změníme, toto se v kontejneru nebude reflektovat, takže nějaké změny konfigurace za živa nejsou možné.

Čtvrtá starost je v tom, že parametry nejsou z bezpečnostního hlediska nijak chráněné. V systému jsou plně viditelné a v clusteru jsou uložené v plain textu. Stejně tak bychom mohli potřebovat rozdělit odpovědnosti, tedy nechť jeden člověk má na starost vytváření hesel a někdo jiný deployment. Pokud se nám potkávají v jednom zdroji (definici Podu), nemůžeme použít RBAC na oddělení práv.

První tři potíže pojďme vyřešit použitím ConfigMap.

Použijme tuto ConfigMapu:

apiVersion: v1
kind: ConfigMap
metadata:
  name: myconfigmap
data:
  mycfgkey: myvalue
  mycfgkey2: myvalue2
  configfile.ini: |
    [default]
    something=azure
    somethigelse=false

Všimněte si, že definujeme key/value páry, ale u configfile.ini jsme s využitím symbolu | vložili několik řádek celého konfiguračního souboru aplikace.

Založme objekt ConfigMap v Kubernetes.

kubectl apply -f configMap.yaml

Použijeme teď následující definici Podu. Env nebudeme definovat uvnitř, ale načteme si celý obsah ConfigMap jako env dovnitř kontejneru.

kind: Pod
apiVersion: v1
metadata:
  name: cfg1
spec:
  containers:
    - name: ubuntu
      image: ubuntu
      command: ["tail"]
      args: ["-f", "/dev/null"]
      envFrom:
      - configMapRef:
          name: myconfigmap

Podívejme se, co to udělá.

kubectl exec cfg1 -- env

kubectl exec cfg1 -- env
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
HOSTNAME=cfg1
configfile.ini=[default]
something=azure
somethigelse=false

mycfgkey=myvalue
mycfgkey2=myvalue2
...

Hodnoty jednoduchých klíčů se nám všechny načetly v pořádku. Takhle to tedy můžeme používat, ale config.ini je tam špatně. Obsahem proměnné prostředí nemůže být řádkování a Linux tomu neporozumí správně. K tomu se dostaneme.

Pojďme tedy do env natáhnout jen to, co dává smysl. Možná také budeme potřebovat změnit její název. V ConfigMap můžeme mít data uložena pod nějakým klíčem, ale v různých kontejnerech potřebujeme tyto hodnoty pod jiným názvem. Co uděláme takhle:

kind: Pod
apiVersion: v1
metadata:
  name: cfg2
spec:
  containers:
    - name: ubuntu
      image: ubuntu
      command: ["tail"]
      args: ["-f", "/dev/null"]
      env:
        - name: newnamekey1
          valueFrom:
            configMapKeyRef:
              name: myconfigmap
              key: mycfgkey
        - name: newnamekey2
          valueFrom:
            configMapKeyRef:
              name: myconfigmap
              key: mycfgkey2

Vytvoříme pod a koukneme se, jak to dopadlo.

kubectl apply -f cfg2Pod.yaml

kubectl exec cfg2 -- env
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
HOSTNAME=ubuntu
newnamekey1=myvalue
newnamekey2=myvalue2
...

Výborně. Z ConfigMap můžeme použít jen co nám dává smysl a ještě si to případně přejmenovat. Ale co s tím ini souborem?

Hodnoty můžeme do kontejneru také namapovat jako soubor. Funguje to tak, že do kontejneru připojíme Volume na nějakou cestu v soborovém systému a jednotlivé klíče pak budou název souboru a value jeho obsah. Můžeme samozřejmě vytahovat jen určité a ještě vytvářet stromovou strukturu. Pro zjednodušení tam pošlu všechno.

kind: Pod
apiVersion: v1
metadata:
  name: cfg3
spec:
  containers:
    - name: ubuntu
      image: ubuntu
      command: ["tail"]
      args: ["-f", "/dev/null"]
      env:
        - name: newnamekey1
          valueFrom:
            configMapKeyRef:
              name: myconfigmap
              key: mycfgkey
        - name: newnamekey2
          valueFrom:
            configMapKeyRef:
              name: myconfigmap
              key: mycfgkey2
      volumeMounts:
      - name: config-volume
        mountPath: /etc/config
  volumes:
    - name: config-volume
      configMap:
        name: myconfigmap

Vytvořme tento Pod, skočíme dovnitř a prohlédneme si soubory.

kubectl apply -f cfg3Pod.yaml

kubectl exec cfg3 -it -- bash
root@cfg3:/# ls /etc/config
configfile.ini  mycfgkey  mycfgkey2
root@cfg3:/# cat /etc/config/configfile.ini
[default]
something=azure
somethigelse=false

Je to tam! Tímto způsobem tedy můžeme přidat kompletní konfigurační soubor. Publikování přes souborový systém má ještě jednu výhodu. Na rozdíl od řešení přes proměnnou prostředí se tyto informace dokáží dynamicky aktualizovat, takže pokud si vaše aplikace z tohoto místa pravidelně čte, dozví se o změně (ne okamžitě, může to nějakou dobu trvat – obvykle tak do minuty).

Proveďme tedy změny v ConfigMap.

apiVersion: v1
kind: ConfigMap
metadata:
  name: myconfigmap
data:
  mycfgkey: CHANGEDmyvalue
  mycfgkey2: CHANGEDmyvalue2
  configfile.ini: |
    [default]
    something=azure
    somethigelse=false

Bez restartu Podu se připojíme a po krátké době budou v souborech nová data zatímco env se nezměnil.

kubectl apply -f configMap2.yaml
kubectl exec cfg3 -it -- bash
root@cfg3:/# env | grep key
newnamekey1=myvalue
newnamekey2=myvalue2

root@cfg3:/# cat /etc/config/mycfgkey
CHANGEDmyvalue

Pokud se jedná skutečně o konfigurační soubory, možná se vám nechce je přepisovat do YAML předpisu. Můžete ConfigMap založit přes kubectl a poslat ho buď na jednotlivé soubory nebo celý adresář.

kubectl create configmap novamapa --from-file=./configs/

Když si ConfigMap vypíšu, uvidím tam dva soubory, které jsem v adresáři měl.

kubectl describe cm novamapa

Name:         novamapa
Namespace:    default
Labels:       <none>
Annotations:  <none>

Data
====
config.ini:
----
[configs]
something=azure
somethingelse=false
otherconfig.cfg:
----
useSomething;
seeSomething;
name = "hopla";
Events:  <none>

První tři body jsme tedy vyřešili. Oddělili jsme konfiguraci od definice Podu, dokázali jsme předávat komplexní konfigurace a aktualizovat je bez restartu Podu. A jak s bezpečností předávaných hesel?

Secrets

Pro poskytování tajností je v Kubernetes objekt Secret. V okamžiku vzniku u něj nebyl prakticky žádný implementační rozdíl oproti ConfigMap. Oddělení je žádoucí, protože procesy kolem konfigurací jsou zkrátka často jiné než kolem certifikátů a hesel. Kubernetes postupně přidával funkce a bude v tom jistě pokračovat. Někdy kolem verze 1.7 začalo být možné tyto informace v platformě ukládat zašifrované, takže pokud by někdo penetroval cluster nebudou snadno dostupné (musel by najít i místo, kde je uložen šifrovací klíč k tajnostem, což je o dost složitější). Ve verzi 1.10 se objevila možnost klíče šifrovat s využitím externích systémů jako je Azure Key Vault. Klíč je zašifrován a vložen do obálky, která je šifrovaná informací z externího zdroje jako je právě Azure Key Vault. Tato možnost je velmi čerstvá a v době psaní článku není v Azure Kubernetes Service ještě k dispozici, ale podle dostupných informací bude poměrně brzy.

Jak tedy fungují Secrets? Skoro stejně jako ConfigMap, jen se posílají v base64 kódování (což ale není šifra, považujte to za plain text). Tím, že je to odlišný objekt a API můžete oddělit práva pro vytváření ConfigMap od Secret.

Nejprve si zakódujeme secret do base64, což je dobré pro zabránění nějakých problémů s interpretací znaků apod.

echo -n 'Azure12345678' | base64
QXp1cmUxMjM0NTY3OA==

Následně base64 výsledek použijeme v definici.

apiVersion: v1
kind: Secret
metadata:
  name: secret1
type: Opaque
data:
  password: QXp1cmUxMjM0NTY3OA==

A pošleme do Kubernetes.

kubectl apply -f secret.yaml

Můžeme také vytvořit Secret přímo z jenoho a více souborů přes kubectl, který base64 kódování provede za nás.

echo -n 'Azure87654321' > ./heslo.txt
kubectl create secret generic secret2 --from-file=./heslo.txt

Přístup k heslům je podobný ConfigMap – buď přes env nebo soubory.

Vytvořme si Pod a namapujme do něj secrets z obou Secret.

kind: Pod
apiVersion: v1
metadata:
  name: sec
spec:
  containers:
    - name: ubuntu
      image: ubuntu
      command: ["tail"]
      args: ["-f", "/dev/null"]
      env:
        - name: SECRET1
          valueFrom:
            secretKeyRef:
              name: secret1
              key: password
        - name: SECRET2
          valueFrom:
            secretKeyRef:
              name: secret2
              key: heslo.txt

Pošleme do Kubernetes a ověříme, že je tam máme.

kubectl create -f secretPod.yaml
kubectl exec sec -- env

PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
HOSTNAME=sec
SECRET1=Azure12345678
SECRET2=Azure87654321
...

Dynamické konfigurace mimo Kubernetes platformu

V některých případech můžete chtít zcela oddělit konfiguraci systémů od jakýchkoli Kubernetes konstruktů tak, aby to fungovalo v kontejneru, ve VM nebo třeba platformní službě. Dobrým způsobem v takovém případě je vytažení konfigurací do centrálního repozitáře.

Často nasazovaným systémem, který používá i sám Kubernetes pro svoje potřeby, je Etcd nebo jeho alternativa Consul. Jde o distribuovaná řešení pro tzv. distribuovaný konsenzus. Jde v zásadě a silně konzistentní vysoce redundantní konfigurační databázi, kterou lze použít i pro další problémy distribuovaných systémů jako volba lídra, semafory či service discovery. Nadstavbou nad tím může být configd, což je vyráběč konfiguračních souborů. Sleduje jaké parametry jsou v Etcd a z nich montuje konfigurační soubory. Dokáže detekovat změny, upravit konfiguraci a případně otočit nějakou službu tak, aby si změny natáhla (například NGINX to potřebuje, ale třeba Envoy proxy už to dokáže sama).

Tyto řešení jsou na samotný článek, tak se k nim vraťme někdy později.

Ukládání tajností do trezoru mimo Kubernetes

Možná nechcete certifikáty a hesla vůbec vztahovat ke Kubernetes jako takovému. Scénář je opět takový, že musí jít o oddělený systém fungující pro kontejnery, VM i platformní služby. Perfektním řešením je Azure Key Vault. O něm už jsem na tomto blogu psal a psát ještě budu.

 

Při vytváření immutable kontejnerových obrazů se neobejdete bez předávání parametrů. Kubernetes na to myslí. Zapněte si Azure Kubernetes Service a vyzkoušejte si.

Podobné příspěvky: