Kubernetes v rámci Azure Container Service je skvělé řešení pro vaše kontejnerizované aplikace. Jenže co když ta se skládá z několika komponent ať už technologických (web, cache, databáze, ...) nebo s byznys logikou (mikroslužby)? Jak koordinovaně nasadit, upgradovat a rollbackovat celé aplikace bez nutnosti řešit každý dílek zvlášť? V Linuxu máte package manager jako je apt nebo yum. Existuje něco podobného pro Kubernetes? Ano a jmenuje se Helm. Vyzkoušejme si dnes.
Jak už jsem v úvodu psal moderní aplikace se typicky skládá z několika technologických i byznysových komponent. Kubernetes se dokáže parádně postarat o běh a deployment komponent (kontejnerů), ale jak koordinovaně řešit aplikaci jako celek? Open source firma Deis, která je dnes po akvizici součástí Microsoft, vyvynula řešení Helm - "package manager" pro Kubernetes. Celou aplikaci pak dokážete popsat (vytvořit Chart) a tu pak lze jednoduše nasadit i upgradovat.
Protože se mi nechce stavět si Kubernetes cluster sám, použiji Azure Container Service. Ta pro vás připraví cluster na základě best practice v Azure, je to plně open source řešení a celá služba je zdarma (platíte jen za použité VM zdroje). Cluster naběhne s řadou hotových integrací, například CNI pluginu pro Azure networking, takže z Kubernetes jednoduše ovládáte i Azure Load Balancer a nemusíte tunelovat provoz (napojíte se na Azure networking, respektive VNet).
Sestavme si Kubernetes cluster s využitím Azure CLI 2.0. Nejprve vytvoříme Resource Group.
az group create -n kube -l westeurope
Následně spustíme vytvoření clusteru Kubernetes. Já zvolím řešení z jedním masterem (nepotřebuji teď redundanci control plane, nicméně stačí zvolit číslo 3 a máte ji - postup je stejný) a trojicí agent nodů s Linux (Kubernetes v Azure podporuje i Windows nody, pokud chcete orchestrovat svět Windows kontejnerů). Použiji vlastní SSH klíč, ale můžete nechat ACS rovnou nějaké vygenerovat, pokud nemáte.
az acs create --orchestrator-type=kubernetes --resource-group kube --name=mujkubernetes --agent-count=3 --ssh-key-value "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDFhm1FUhzt/9roX7SmT/dI+vkpyQVZp3Oo5HC23YkUVtpmTdHje5oBV0LMLBB1Q5oSNMCWiJpdfD4VxURC31yet4mQxX2DFYz8oEUh0Vpv+9YWwkEhyDy4AVmVKVoISo5rAsl3JLbcOkSqSO8FaEfO5KIIeJXB6yGI3UQOoL1owMR9STEnI2TGPZzvk/BdRE73gJxqqY0joyPSWOMAQ75Xr9ddWHul+v//hKjibFuQF9AFzaEwNbW5HxDsQj8gvdG/5d6mt66SfaY+UWkKldM4vRiZ1w11WlyxRJn5yZNTeOxIYU4WLrDtvlBklCMgB7oF0QfiqahauOEo6m5Di2Ex" --dns-prefix mujkubernetes --agent-vm-size Standard_A1_v2 --admin-username tomas
Teď stačí jen čekat. Následně si nainstalujte kubectl, tedy příkazovou řádku pro Kubernetes. To můžete udělat přímo z Azure CLI.
sudo az acs kubernetes install-cli
Tím máme nainstalováné kubectl. Přímo ze své stanice můžeme ovládat Kubernetes cluster, stačí si stáhnout údaje o konektivitě a klíče. I to pro vás udělá Azure CLI.
az acs kubernetes get-credentials --resource-group=kube --name=mujkube
Pokud všechno dopadlo dobře, jste ve svém Kubernetes clusteru.
tomas@jump:~$ kubectl get nodes NAME STATUS AGE VERSION k8s-agent-6d417f1c-0 Ready 7m v1.6.6 k8s-agent-6d417f1c-1 Ready 6m v1.6.6 k8s-agent-6d417f1c-2 Ready 6m v1.6.6 k8s-master-6d417f1c-0 Ready,SchedulingDisabled 7m v1.6.6
Teď si můžeme stáhnout helm příkazovou řádku.
wget https://kubernetes-helm.storage.googleapis.com/helm-v2.5.0-linux-amd64.tar.gz tar -xvf helm-v2.5.0-linux-amd64.tar.gz sudo mv linux-amd64/helm /usr/bin/
Proveďme potřebnou inicializaci (Helm nainstaluje svou serverovou část) a updatujme repozitář.
helm init helm repo update
Vyzkoušejme si teď nějaký z veřejných Helm balíčků, například Wordpress. Ten se bude skládat z kontejneru s databází, který bude mít jen interní adresu. Dále s webovou částí, která si vezme externí adresu - tedy zažádá si o ni (Kubernetess Ingress) a Kubernetes díky Azure pluginu zavolá samotný Azure a vytvoří novou veřejnou IP adresu na balanceru. Součástí Helm mohou být i další vstupní parametry, v mém případě například heslo do blogu a jeho název.
tomas@jump:~$ helm install stable/wordpress --name mujwp --set wordpressUsername=tomas,wordpressPassword=Azure12345678,wordpressBlogName=Muj-super-blog NAME: mujwp LAST DEPLOYED: Mon Jun 26 08:44:43 2017 NAMESPACE: default STATUS: DEPLOYED RESOURCES: ==> v1/Secret NAME TYPE DATA AGE mujwp-mariadb Opaque 2 2s mujwp-wordpress Opaque 3 2s ==> v1/ConfigMap NAME DATA AGE mujwp-mariadb 1 2s ==> v1/PersistentVolumeClaim NAME STATUS VOLUME CAPACITY ACCESSMODES STORAGECLASS AGE mujwp-wordpress Bound pvc-b48a4780-5a4b-11e7-bfac-000d3a250d6e 10Gi RWO default 2s mujwp-mariadb Pending default 2s ==> v1/Service NAME CLUSTER-IP EXTERNAL-IP PORT(S) AGE mujwp-mariadb 10.0.65.253306/TCP 2s mujwp-wordpress 10.0.200.140 80:32674/TCP,443:32156/TCP 2s ==> v1beta1/Deployment NAME DESIRED CURRENT UP-TO-DATE AVAILABLE AGE mujwp-mariadb 1 1 1 0 2s mujwp-wordpress 1 1 1 0 2s NOTES: 1. Get the WordPress URL: NOTE: It may take a few minutes for the LoadBalancer IP to be available. Watch the status with: 'kubectl get svc --namespace default -w mujwp-wordpress' export SERVICE_IP=$(kubectl get svc --namespace default mujwp-wordpress -o jsonpath='{.status.loadBalancer.ingress[0].ip}') echo http://$SERVICE_IP/admin 2. Login with the following credentials to see your blog echo Username: tomas echo Password: $(kubectl get secret --namespace default mujwp-wordpress -o jsonpath="{.data.wordpress-password}" | base64 --decode)
Jak vidíme Helm nasadil dva kontejnery - jeden s webem a jeden s databází. Externí IP adresa je pending, takže musíme chvilku počkat, až se Kubernetes a Azure Load Balancer domluví. Po chvilce najdeme IP adresu takto:
tomas@jump:~$ kubectl get services NAME CLUSTER-IP EXTERNAL-IP PORT(S) AGE kubernetes 10.0.0.1443/TCP 1h mujwp-mariadb 10.0.65.25 3306/TCP 1h mujwp-wordpress 10.0.200.140 137.116.197.194 80:32674/TCP,443:32156/TCP 1h
Připojím se na tuto IP adresu a můj blog je nahoře.
Mohu se přihlásit údaji, které jsme specifikovali při spuštění a můžeme začít psát články.
Chcete si vytvořit vlastní Helm balíček? Dobrý způsob jak se to naučit je podívat se pod kapotu nějakému hotovému, jako například už vyzkoušený wordpress. Ten se nachází v Helm cache, tak si ho rozbalme a prozkoumejme jeho strukturu.
tar -xvf .helm/cache/archive/wordpress-0.6.6.tgz
Jednotlivé součástky Helmu se nazývají Chart. Wordpress Chart má dependency na Chart s mariadb. Tuto závislost najdeme v souboru requirements.yaml:
tomas@jump:~/wordpress$ cat requirements.yaml dependencies: - name: mariadb version: 0.6.3 repository: https://kubernetes-charts.storage.googleapis.com/
Tak například onen Chart pro databázi obsahuje v adresáři templates šablony pro Kubernetes, které Helm při deploymentu vyplní v závislosti na konfiguračních parametrech. Tak například takhle vypadá samotný deployment template:
tomas@jump:~/wordpress$ cat charts/mariadb/templates/deployment.yaml apiVersion: extensions/v1beta1 kind: Deployment metadata: name: {{ template "fullname" . }} labels: app: {{ template "fullname" . }} chart: "{{ .Chart.Name }}-{{ .Chart.Version }}" release: "{{ .Release.Name }}" heritage: "{{ .Release.Service }}" spec: template: metadata: labels: app: {{ template "fullname" . }} annotations: pod.alpha.kubernetes.io/init-containers: ' [ { "name": "copy-custom-config", "image": "{{ .Values.image }}", "imagePullPolicy": {{ .Values.imagePullPolicy | quote }}, "command": ["sh", "-c", "mkdir -p /bitnami/mariadb/conf && cp -n /bitnami/mariadb_config/my.cnf /bitnami/mariadb/conf/my_custom.cnf"], "volumeMounts": [ { "name": "config", "mountPath": "/bitnami/mariadb_config" }, { "name": "data", "mountPath": "/bitnami/mariadb" } ] } ]' spec: containers: - name: {{ template "fullname" . }} image: "{{ .Values.image }}" imagePullPolicy: {{ .Values.imagePullPolicy | quote }} env: - name: MARIADB_ROOT_PASSWORD valueFrom: secretKeyRef: name: {{ template "fullname" . }} key: mariadb-root-password - name: MARIADB_USER value: {{ default "" .Values.mariadbUser | quote }} - name: MARIADB_PASSWORD valueFrom: secretKeyRef: name: {{ template "fullname" . }} key: mariadb-password - name: MARIADB_DATABASE value: {{ default "" .Values.mariadbDatabase | quote }} - name: ALLOW_EMPTY_PASSWORD value: "yes" ports: - name: mysql containerPort: 3306 livenessProbe: exec: command: - mysqladmin - ping initialDelaySeconds: 30 timeoutSeconds: 5 readinessProbe: exec: command: - mysqladmin - ping initialDelaySeconds: 5 timeoutSeconds: 1 resources: {{ toYaml .Values.resources | indent 10 }} volumeMounts: - name: data mountPath: /bitnami/mariadb volumes: - name: config configMap: name: {{ template "fullname" . }} - name: data {{- if .Values.persistence.enabled }} persistentVolumeClaim: claimName: {{ .Values.persistence.existingClaim | default (include "fullname" .) }} {{- else }} emptyDir: {} {{- end -}}
Používá perzistentní volume, jehož template je zde:
tomas@jump:~/wordpress$ cat charts/mariadb/templates/pvc.yaml {{- if and .Values.persistence.enabled (not .Values.persistence.existingClaim) }} kind: PersistentVolumeClaim apiVersion: v1 metadata: name: {{ template "fullname" . }} labels: app: {{ template "fullname" . }} chart: "{{ .Chart.Name }}-{{ .Chart.Version }}" release: "{{ .Release.Name }}" heritage: "{{ .Release.Service }}" annotations: {{- if .Values.persistence.storageClass }} volume.beta.kubernetes.io/storage-class: {{ .Values.persistence.storageClass | quote }} {{- else }} volume.alpha.kubernetes.io/storage-class: default {{- end }} spec: accessModes: - {{ .Values.persistence.accessMode | quote }} resources: requests: storage: {{ .Values.persistence.size | quote }} {{- end }}
K databázi se přistupuje přes Kubernetes Service a i její template můžeme prozkoumat:
tomas@jump:~/wordpress$ cat charts/mariadb/templates/svc.yaml apiVersion: v1 kind: Service metadata: name: {{ template "fullname" . }} labels: app: {{ template "fullname" . }} chart: "{{ .Chart.Name }}-{{ .Chart.Version }}" release: "{{ .Release.Name }}" heritage: "{{ .Release.Service }}" spec: type: {{ .Values.serviceType }} ports: - name: mysql port: 3306 targetPort: mysql selector: app: {{ template "fullname" . }}
Velmi podobně se řeší template pro samotný Wordpress.
tomas@jump:~/wordpress$ cat templates/deployment.yaml apiVersion: extensions/v1beta1 kind: Deployment metadata: name: {{ template "fullname" . }} labels: app: {{ template "fullname" . }} chart: "{{ .Chart.Name }}-{{ .Chart.Version }}" release: "{{ .Release.Name }}" heritage: "{{ .Release.Service }}" spec: replicas: 1 template: metadata: labels: app: {{ template "fullname" . }} spec: containers: - name: {{ template "fullname" . }} image: "{{ .Values.image }}" imagePullPolicy: {{ default "" .Values.imagePullPolicy | quote }} env: - name: ALLOW_EMPTY_PASSWORD {{- if .Values.allowEmptyPassword }} value: "yes" {{- else }} value: "no" {{- end }} - name: MARIADB_ROOT_PASSWORD valueFrom: secretKeyRef: name: {{ template "mariadb.fullname" . }} key: mariadb-root-password - name: MARIADB_HOST value: {{ template "mariadb.fullname" . }} - name: MARIADB_PORT_NUMBER value: "3306" - name: WORDPRESS_DATABASE_NAME value: {{ default "" .Values.mariadb.mariadbDatabase | quote }} - name: WORDPRESS_DATABASE_USER value: {{ default "" .Values.mariadb.mariadbUser | quote }} - name: WORDPRESS_DATABASE_PASSWORD valueFrom: secretKeyRef: name: {{ template "mariadb.fullname" . }} key: mariadb-password - name: WORDPRESS_USERNAME value: {{ default "" .Values.wordpressUsername | quote }} - name: WORDPRESS_PASSWORD valueFrom: secretKeyRef: name: {{ template "fullname" . }} key: wordpress-password - name: WORDPRESS_EMAIL value: {{ default "" .Values.wordpressEmail | quote }} - name: WORDPRESS_FIRST_NAME value: {{ default "" .Values.wordpressFirstName | quote }} - name: WORDPRESS_LAST_NAME value: {{ default "" .Values.wordpressLastName | quote }} - name: WORDPRESS_BLOG_NAME value: {{ default "" .Values.wordpressBlogName | quote }} - name: SMTP_HOST value: {{ default "" .Values.smtpHost | quote }} - name: SMTP_PORT value: {{ default "" .Values.smtpPort | quote }} - name: SMTP_USER value: {{ default "" .Values.smtpUser | quote }} - name: SMTP_PASSWORD valueFrom: secretKeyRef: name: {{ template "fullname" . }} key: smtp-password - name: SMTP_USERNAME value: {{ default "" .Values.smtpUsername | quote }} - name: SMTP_PROTOCOL value: {{ default "" .Values.smtpProtocol | quote }} ports: - name: http containerPort: 80 - name: https containerPort: 443 livenessProbe: httpGet: path: /wp-login.php port: http initialDelaySeconds: 120 timeoutSeconds: 5 failureThreshold: 6 readinessProbe: httpGet: path: /wp-login.php port: http initialDelaySeconds: 30 timeoutSeconds: 3 periodSeconds: 5 volumeMounts: - mountPath: /bitnami/apache name: wordpress-data subPath: apache - mountPath: /bitnami/wordpress name: wordpress-data subPath: wordpress - mountPath: /bitnami/php name: wordpress-data subPath: php resources: {{ toYaml .Values.resources | indent 10 }} volumes: - name: wordpress-data {{- if .Values.persistence.enabled }} persistentVolumeClaim: claimName: {{ template "fullname" . }} {{- else }} emptyDir: {} {{ end }}
Opět najdete definici i pro perzistentní Volume a také Service. Navíc je tu definice Ingress, což je Kubernetes řešení pro získání externího přístupu na službu (Kubernetes se domluví s Azure Load Balancer):
tomas@jump:~/wordpress$ cat templates/ingress.yaml {{- if .Values.ingress.enabled -}} apiVersion: extensions/v1beta1 kind: Ingress metadata: name: {{ template "fullname" . }} labels: app: {{ template "fullname" . }} chart: "{{ .Chart.Name }}-{{ .Chart.Version }}" release: "{{ .Release.Name }}" heritage: "{{ .Release.Service }}" annotations: {{- range $key, $value := .Values.ingress.annotations }} {{ $key }}: {{ $value | quote }} {{- end }} spec: rules: - host: {{ .Values.ingress.hostname }} http: paths: - path: / backend: serviceName: {{ template "fullname" . }} servicePort: 80 {{- if .Values.ingress.tls }} tls: {{ toYaml .Values.ingress.tls | indent 4 }} {{- end -}} {{- end -}}
Nechme Helm vytvořit jednoduchou kostru pro Chart, který v rámci příkladu bude nginx kontejner.
helm create mujtest
Prohlédněte si strukturu, zejména jednotlivé templaty. Následně pojďme tento Chart nainstalovat s tím, že si zapneme Ingress, tedy požádáme Kubernetes o přiřazení externí balancované public IP z Azure Load Balancer.
tomas@jump:~$ cd mujtest/ tomas@jump:~/mujtest$ helm install . --name mujtest --set ingress.enabled=true,service.type=LoadBalancer NAME: mujtest LAST DEPLOYED: Mon Jun 26 11:06:03 2017 NAMESPACE: default STATUS: DEPLOYED RESOURCES: ==> v1beta1/Ingress NAME HOSTS ADDRESS PORTS AGE mujtest-mujtest chart-example.local 80 1s ==> v1/Service NAME CLUSTER-IP EXTERNAL-IP PORT(S) AGE mujtest-mujtest 10.0.32.6280:30506/TCP 1s ==> v1beta1/Deployment NAME DESIRED CURRENT UP-TO-DATE AVAILABLE AGE mujtest-mujtest 1 1 1 0 1s NOTES: 1. Get the application URL by running these commands: NOTE: It may take a few minutes for the LoadBalancer IP to be available. You can watch the status of by running 'kubectl get svc -w mujtest-mujtest' export SERVICE_IP=$(kubectl get svc --namespace default mujtest-mujtest -o jsonpath='{.status.loadBalancer.ingress[0].ip}') echo http://$SERVICE_IP:80
Po chvilce si zjistíme veřejnou IP a zkusíme se připojit prohlížečem.
tomas@jump:~/mujtest$ kubectl get services NAME CLUSTER-IP EXTERNAL-IP PORT(S) AGE kubernetes 10.0.0.1443/TCP 2h mujtest-mujtest 10.0.32.62 40.114.150.4 80:30506/TCP 4m mujwp-mariadb 10.0.65.25 3306/TCP 2h mujwp-wordpress 10.0.200.140 137.116.197.194 80:32674/TCP,443:32156/TCP 2h
Vyzkoušejme si teď využít Kubernetes ConfigMap k tomu, abychom v rámci instalaci zajistili jednoduchý statický obsah pro webovky. Nejprve zrušte předchozí deployment Helmu.
helm delete mujtest --purge
Vytvořte tento soubor:
nano templates/configmap.yaml
Toto bude obsah našeho souboru. V zásadě říkáme, že obsah soubor index.html si chceme vzít z proměnné index v našem Values.yaml souboru (nebo z příkazové řádky při instalaci).
apiVersion: v1 kind: ConfigMap metadata: name: {{ template "fullname" . }} labels: heritage: {{ .Release.Service }} release: {{ .Release.Name }} chart: {{ .Chart.Name }}-{{ .Chart.Version }} app: {{ template "name" . }} data: index.html: {{ .Values.index | quote }}
Tuto konfigurační mapu může namountovat do kontejneru na místo, kam si nginx dává obsah webových stránek. Za tím účelem potřebujeme změnit deployment.yaml šablonu - nastavíme mountpoint a také specifikujeme volume. Celý soubor vypadá takhle:
apiVersion: extensions/v1beta1 kind: Deployment metadata: name: {{ template "fullname" . }} labels: app: {{ template "name" . }} chart: {{ .Chart.Name }}-{{ .Chart.Version | replace "+" "_" }} release: {{ .Release.Name }} heritage: {{ .Release.Service }} spec: replicas: {{ .Values.replicaCount }} template: metadata: labels: app: {{ template "name" . }} release: {{ .Release.Name }} spec: containers: - name: {{ .Chart.Name }} image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}" imagePullPolicy: {{ .Values.image.pullPolicy }} ports: - containerPort: {{ .Values.service.internalPort }} volumeMounts: - mountPath: /usr/share/nginx/html name: wwwdata-volume livenessProbe: httpGet: path: / port: {{ .Values.service.internalPort }} readinessProbe: httpGet: path: / port: {{ .Values.service.internalPort }} resources: {{ toYaml .Values.resources | indent 12 }} {{- if .Values.nodeSelector }} nodeSelector: {{ toYaml .Values.nodeSelector | indent 8 }} {{- end }} volumes: - name: wwwdata-volume configMap: name: {{ template "fullname" . }}
Teď už zbývá jen nainstalovat tento Chart a předat mu naše parametry.
helm install . --name dalsitest --set ingress.enabled=true,service.type=LoadBalancer,index="Tohle je moje webovka"
Zjistíme si veřejnou adresu.
tomas@jump:~/mujtest$ kubectl get services NAME CLUSTER-IP EXTERNAL-IP PORT(S) AGE dalsitest-mujtest 10.0.247.205 52.174.240.147 80:30206/TCP 1m kubernetes 10.0.0.1443/TCP 1d mujwp-mariadb 10.0.65.25 3306/TCP 1d mujwp-wordpress 10.0.200.140 137.116.197.194 80:32674/TCP,443:32156/TCP 1d
Připojte se na ni. Měli bychom zjistit, že se nám podařilo vytvořit Helm, který nainstaluje nginx, z Azure si Kubernetes získá veřejnou adresu a v kontejneru je nastrčen náš statický obsah.
Povedlo se!