Helm - váš package manager pro Kubernetes
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.
Deis, Helm a k čemu to je
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.
Sestavme si Kubernetes s Helm a vyzkoušejme
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.
Podívejme se Helmu pod kapotu
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 -}}
Zkusme jednoduchý Helm Chart
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!





