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?
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í.
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?
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 ...
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.
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.