Helm Chartの実践的活用法

 

コンテナアプリケーションで人気のツール「Kubernetes」は、ギリシャ語の「パイロット」または「船を操縦する人」という意味にちなんで命名されています。どんな旅でも操縦士は地図と同じくらい重要なものです。

アプリケーションのHelm chartとは操縦士の地図、つまりK8sリソースの関連セットを記述するHelmリポジトリからデプロイできるファイルコレクションです。最適な方法でHelm chartを作成することができれば、Kubernatesが稼働環境でコンテナをデプロイする際に深みに嵌ることなく運用することができるでしょう。

しかし製品のデプロイ用に公開しているK8s chartを開発している時に私が見つけたように、他の方法で運用する方法もあります。プルリクエスト毎にHelmコミュニティからフィードバックをもらうことにより、コンテナの運用と更新の両方で最良の結果を得るHelm chartの実践的な活用方法を知ることができます。

コミュニティやユーザによって使用される本番環境のK8s chartを書く時に気をつけなければならないポイントを紹介します。特に気に留めておくべきことは以下のポイントです:

  • どの依存関係を定義する必要があるか?
  • アプリケーションは永続的な稼働が必要か?
  • 機密事項と許可を通してセキュリティーをどう取り扱っていくか?
  • 実行中のKubeletコンテナをどうやってコントロールするか?
  • アプリケーションが稼働中でリクエストを受信できるということをどうやって確認するか?
  • アプリケーションのサービスをどのように正式公開するか?
  • Chartをどのようにテストするか?

これらに注意することで、実際にK8sがコンテナアプリケーションをdockにスムーズに展開してHelm chartを構成および作成するときに役立ちます。

 

最初のステップ

始める前にHelm chartを開発する必須手順についておさらいしましょう。

このガイドではExpress.jsを使ってMongoデータベースに2層コミットの作成、読み取り、更新、削除(CRUD)アプリケーションのデプロイを推奨する実践方法に基づいてHelm chartを作成します。

GitHubのexpress-crudにサンプルアプリケーションのソースコードを公開していますので参照ください。

Helm chartの作成

Helmクライアントのcreateコマンドを使ってHelm chartのテンプレートを作成してみましょう。

$ helm create express-crud

これでexpress-crud Helm chartのディレクトリ構造が作成されます。

開始するには、作成したChart.yamlファイルのchartメタデータを更新します。appVersion(dockerイメージタグとして使用されるアプリケーションのバージョン)、説明、バージョン(SemVer 2 バージョン文字列)、ソース、メンテナーとアイコンに適切な情報を追加してください。

apiVersion: v1
appVersion: "1.0.0"
description: A Helm chart for express-crud application
name: express-crud
version: 0.1.0
sources:
- https://github.com/jainishshah17/express-mongo-crud
maintainers:
- name: myaccount
 email: myacount@mycompany.com
icon: https://github.com/mycompany17/mycompany.com/blob/master/app/public/images/logo.jpg
home: http://mycompany.com/

依存関係の定義

アプリケーションに依存関係がある場合はそれらを明記するHelm chartのディレクトリにrequirements.yamlファイルを作成しなければなりません。アプリケーションはmongodbデータベースが必要なので作成するrequirements.yamlファイルの依存関係リストに明記する必要があります。

例えば以下のようにrequirements.yamlファイルに記述します:

dependencies:
- name: mongodb
 version: 3.0.4
 repository: https://kubernetes-charts.storage.googleapis.com/
 condition: mongodb.enabled

requirements.yamlファイル作成後はHelm chartでdependency updateコマンドを実行してください:

$ helm dep update

デプロイメントファイルの作成

Helm chartのデプロイメントファイルは\templatesサブディレクトリにあり、K8sがコンテナアプリケーションをデプロイする方法を指定しています。

また、デプロイメントファイルを開発する際に決めておくべきいくつかの重要事項があります。

DeploymentオブジェクトかStatefulSetオブジェクトか

作成するdeploymentファイルはアプリケーションがK8sを使用してDeploymentオブジェクトまたはStatefulSetオブジェクトとして管理するかどうかで違ってきます。

Deploymentオブジェクトはdeployment.yamlファイルで宣言され、kindパラメータをdeploymentとして指定するステートレスアプリケーションです。

Statefulオブジェクトは分散システムにおいて使用されるステートフルなアプリケーション用オブジェクトです。stateless.yamlファイルで宣言され、kindパラメータをstatefulとして指定します。

Deployment StatefulSet
Deploymentはステートレスな使用を目的としており比較的軽量です。 StatefulSetは状態を永続化する必要がある場合に使われます。従って永続的なボリュームでvolumeClaimTemplatesを使用し、コンポーネントの再起動後も状態を維持できるようにします。
アプリケーションがステートレスである場合や起動中にバックエンドシステムから構築できる場合はDeploymentを使います。 アプリケーションがステートフルである場合やKubernetes上にステートフルストレージをデプロイする場合はStatefulSetを使います。

 

このアプリケーションでは状態を永続化する必要がないためdeploymentオブジェクトを使用しています。
deployment.yamlファイルはhelm createコマンドで既に作成されています。

アプリケーションのDockerイメージタグとしてAppVersionを使用します。そうすることでChart.yamlで値を変更するだけでアプリケーションの新しいバージョンでHelm chartをアップグレードできます。

image: "{{ .Values.image.repository }}:{{ default .Chart.AppVersion .Values.image.tag }}"

SecretかConfigMapか

どの資格情報や設定データをSecretとして保存して、どれをConfigMapに格納できるかを決める必要があります。

SecretとはK8sが暗号化形式で保存するパスワードなどの機密情報です。

ConfigMapはアプリケーションによって共有される可能性のある構成情報を含むファイルです。ConfigMapの情報は暗号化されていないため機密情報を含めないでください。

Secret ConfigMap
情報をsecretに入れることはpod定義やdockerイメージに逐一格納していくよりも安全で柔軟です。 ConfigMapを使用するとアーティファクトの設定をイメージコンテンツから分離し、コンテナ化されたアプリケーションの移植性を維持できます。
機密データに使用 非機密データに使用
使用例:APIキー、パスワード、トークン、sshキー 使用例:ログローテータ、機密データなしの設定

 

この例ではHelmがイメージプルsecretを使用してプライベートdockerレジストリからdockerイメージをプルできるようにします。

この手順ではリポジトリマネジャのログイン資格情報を規定するKubernatesクラスタで利用可能なsecretを持つことに依存しています。このsecretはkubectlコマンドで以下のように作成できます:

Helm chartのvalues.yamlファイルでsecret名に値を渡すことができます:

$ kubectl create secret docker-registry regsecret --docker-server=$DOCKER_REGISTRY_RUL --docker-username=$USERNAME --docker-password=$PASSWORD --docker-email=$EMAIL

Helm chartのvalues.yamlファイルでsecret名に値を渡すことができます:

imagePullSecrets: regsecret

次にこのsecretを利用してHelmがdeployment.yamlの次の行を介してdockerレジストリにアクセスできるようにします:

{{- if .Values.imagePullSecrets }}
 imagePullSecrets:
 - name: {{ .Values.imagePullSecrets }}
{{- end }}

アプリケーションで使用可能なsecretについては、その情報を直接values.yamlに追加する必要があります。

例えば事前に作成したユーザとデータベースでmongodbにアクセスするようにアプリケーションを構成するにはvalues.yamlにその情報を追加します。

mongodb:
 enabled: true
 mongodbRootPassword:
 mongodbUsername: admin
 mongodbPassword: 
 mongodbDatabase: test

Helm chartにデフォルトの資格情報をハードコーディングしないようにご注意ください。–set flagもしくはvalues.yamlでパスワードが提供されない場合にはlogicを使用しランダムのパスワードを生成します。

deployment.yamlの以下の行で、secretを使用してmongodbの資格情報をアプリケーションに渡します。

env:
- name: DATABASE_PASSWORD
  valueFrom:
    secretKeyRef:
      name: {{ .Release.Name }}-mongodb
      key: mongodb-password

特別なInit ContainersContainer Lifecycle Hooksを通してkubeletコンテナ実行をコントロールできます。

InitContainers Container Lifecycle Hooks
InitContainersはアプリコンテナの前に実行される特殊なコンテナで、アプリイメージに存在しないユーティリティやセットアップスクリプトを含めることができます。 コンテナはContainer Lifecycle Hooksを使い管理サイクル中にイベントによってトリガーされるコードを実行します。
Podには1つ以上のInit Containersを含めることができ、アプリコンテナが開始される前に実行されます。

 

PodはPostStartまたはPreStopフックを1つだけ持つことができます。
PostStartフックはコンテナが作成された直後に実行されますが、コンテナのENTRYPOINTの前に実行されるという保証はありません。パラメータはハンドラに渡されません。例:ConfigMap/Secretsを使用してマウントされたファイルを別の場所に移動します。
PreStopフックはコンテナが終了する直前に呼び出されます。これはブロッキング、つまり非同期であり、コンテナを削除する呼び出しを送信する前に完了する必要があります。

例: アプリケーションを正常にシャットダウンします。

initContainers を使って待ち時間を追加し、依存するマイクロサービスが機能することを確認してから次に進むことができます。  Service IPで構成ファイルを更新するといったようにPostStartフックを使用して同じpodでファイルを更新します。

 

この例ではinitContainers仕様をdeployments.yamlに追加し、データベースが稼働するまでアプリケーションの起動をホールドします。

initContainers:
- name: wait-for-db
 image: "{{ .Values.initContainerImage }}"
 command:
 - 'sh'
 - '-c'
 - >
   until nc -z -w 2 {{ .Release.Name }}-mongodb 27017 && echo mongodb ok;
     do sleep 2;
   done

Readiness ProbeとLiveness Probeの追加

多くの場合においてアプリケーションの継続的な健全性を確認するためにReadiness probeやLiveness probeを追加して確認することは適切な対処です。そうしないとアプリケーションは実行されているように見えても、呼び出しやクエリに対して反応せず失敗している場合があります。

以下のように定期的な確認をするためにprobeをdeployment.yamlファイルに追加します:

livenessProbe:
 httpGet:
   path: '/health'
   port: http
 initialDelaySeconds: 60
 periodSeconds: 10
 failureThreshold: 10
readinessProbe:
 httpGet:
   path: '/health'
   port: http
 initialDelaySeconds: 60
 periodSeconds: 10
 failureThreshold: 10

RBACサポートの追加

アプリケーションで必要な場合にはロールベースのアクセス制御 (RBAC) サポートがchartに追加されます。

ステップ1:次のコンテンツをrole.yamlファイルに追加することでロールを作成します:
ロールは単一のネームスペース内のリソースへのアクセスを許可するためにのみ使用できます。

{{- if .Values.rbac.create }}
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
 labels:
   app: {{ template "express-crud.name" . }}
   chart: {{ template "express-crud.chart" . }}
   heritage: {{ .Release.Service }}
   release: {{ .Release.Name }}
 name: {{ template "express-crud.fullname" . }}
rules:
{{ toYaml .Values.rbac.role.rules }}
{{- end }}

ステップ2:次のコンテンツをrolebinding.yamlファイルに追加することでRoleBindingを作成します:
ClusterRoleはロールとして同じ権限を付与できますが、cluster-scopedであるため次のアクセス権を付与するためにも使用できます:

 

  • cluster-scopedリソース(ノードなど)
  • non-resourceエンドポイント(“/healthz”など)
  • すべてのネームスペースにおけるnamespacedリソース(podsなど)
{{- if .Values.rbac.create }}
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
 labels:
   app: {{ template "express-crud.name" . }}
   chart: {{ template "express-crud.chart" . }}
   heritage: {{ .Release.Service }}
   release: {{ .Release.Name }}
 name: {{ template "express-crud.fullname" . }}
subjects:
- kind: ServiceAccount
 name: {{ template "express-crud.serviceAccountName" . }}
roleRef:
 kind: Role
 apiGroup: rbac.authorization.k8s.io
 name: {{ template "express-crud.fullname" . }}
{{- end }}

ステップ3:次のコンテンツをserviceaccount.yamlファイルに追加することでServiceAccountを作成します:

サービスアカウントはPodで実行されるプロセスIDを提供します。

{{- if .Values.serviceAccount.create }}
apiVersion: v1
kind: ServiceAccount
metadata:
 labels:
   app: {{ template "express-crud.name" . }}
   chart: {{ template "express-crud.chart" . }}
   heritage: {{ .Release.Service }}
   release: {{ .Release.Name }}
 name: {{ template "express-crud.serviceAccountName" . }}
{{- end }}

ステップ4:helperテンプレートを使用してServiceAccount名を設定します。
_helpers.tplに次のコンテンツを追加することで設定します。

{{/*
Create the name of the service account to use
*/}}
{{- define "express-crud.serviceAccountName" -}}
{{- if .Values.serviceAccount.create -}}
{{ default (include "express-crud.fullname" .) .Values.serviceAccount.name }}
{{- else -}}
{{ default "default" .Values.serviceAccount.name }}
{{- end -}}
{{- end -}}

サービスの追加

次にサービスを介してアプリケーションを一般公開します。

サービスはIPアドレスを通してアプリケーション側でトラフィックを受信します。type指定することでサービスをさまざまな方法で公開できます:

ClusterIP サービスはクラスタ内から内部IPのみで確認できます。
NodePort サービスはNodeIPとNodePortを介してクラスタの外部からアクセスできます。
LoadBalancer LoadBalancerサービスは外部ロードバランサを介してクラスタの外部からアクセスできます。アプリケーションにIngressを適用できます。


次のコンテンツをservice.yamlに追加することで実現します:

apiVersion: v1
kind: Service
metadata:
 name: {{ template "express-crud.fullname" . }}
 labels:
   app: {{ template "express-crud.name" . }}
   chart: {{ template "express-crud.chart" . }}
   release: {{ .Release.Name }}
   heritage: {{ .Release.Service }}
spec:
 type: {{ .Values.service.type }}
 ports:
   - port: {{ .Values.service.externalPort }}
     targetPort: http
     protocol: TCP
     name: http
 selector:
   app: {{ template "express-crud.name" . }}
   release: {{ .Release.Name }}

上記ではサービスのtypeについてvalues.yamlの設定を参照しています:

service:
  type: LoadBalancer
  internalPort: 3000
  externalPort: 80

Values.yamlの概要

values.yamlでいろいろな設定を定義し試してみることはHelm chartをメンテナンスする上でも良いことです。

この例ではvalues.yamlファイルがどのように表示されるかを示しており、上記で説明した多くの機能に対して定義したさまざまな設定を表示しています:

# Default values for express-mongo-crud.
# This is a YAML-formatted file.
# Declare variables to be passed into your templates.

## Role Based Access Control
## Ref: https://kubernetes.io/docs/admin/authorization/rbac/
rbac:
 create: true
 role:
   ## Rules to create. It follows the role specification
   rules:
   - apiGroups:
     - ''
     resources:
     - services
     - endpoints
     - pods
     verbs:
     - get
     - watch
     - list

## Service Account
## Ref: https://kubernetes.io/docs/admin/service-accounts-admin/
##
serviceAccount:
 create: true
 ## The name of the ServiceAccount to use.
 ## If not set and create is true, a name is generated using the fullname template
 name:

## Configuration values for the mongodb dependency
## ref: https://github.com/kubernetes/charts/blob/master/stable/mongodb/README.md
##
mongodb:
 enabled: true
 image:
   tag: 3.6.3
   pullPolicy: IfNotPresent
 persistence:
   size: 50Gi
 # resources:
 #  requests:
 #    memory: "12Gi"
 #    cpu: "200m"
 #  limits:
 #    memory: "12Gi"
 #    cpu: "2"
 ## Make sure the --wiredTigerCacheSizeGB is no more than half the memory limit!
 ## This is critical to protect against OOMKill by Kubernetes!
 mongodbExtraFlags:
 - "--wiredTigerCacheSizeGB=1"
 mongodbRootPassword:
 mongodbUsername: admin
 mongodbPassword:
 mongodbDatabase: test
#  livenessProbe:
#    initialDelaySeconds: 60
#    periodSeconds: 10
#  readinessProbe:
#    initialDelaySeconds: 30
#    periodSeconds: 30

ingress:
 enabled: false
 annotations: {}
   # kubernetes.io/ingress.class: nginx
   # kubernetes.io/tls-acme: "true"
 path: /
 hosts:
   - chart-example.local
 tls: []
 #  - secretName: chart-example-tls
 #    hosts:
 #      - chart-example.local

initContainerImage: "alpine:3.6"
imagePullSecrets:
replicaCount: 1

image:
 repository: jainishshah17/express-mongo-crud
 # tag: 1.0.1
 pullPolicy: IfNotPresent

service:
 type: LoadBalancer
 internalPort: 3000
 externalPort: 80

resources: {}
 # We usually recommend not to specify default resources and to leave this as a conscious
 # choice for the user. This also increases chances charts run on environments with little
 # resources, such as Minikube. If you do want to specify resources, uncomment the following
 # lines, adjust them as necessary, and remove the curly braces after 'resources:'.
 # limits:
 #  cpu: 100m
 #  memory: 128Mi
 # requests:
 #  cpu: 100m
 #  memory: 128Mi

nodeSelector: {}

tolerations: []

affinity: {}

Helm Chartのテストとインストール

Helm lintコマンドを使用してHelm chartをテストすることは非常に重要です。

$ helm lint ./

## Output
==> Linting ./
Lint OK

1 chart(s) linted, no failures

Helm installコマンドを使用してKubernetesのhelm chartを使ったアプリケーションをデプロイします。

$ helm install --name test1 ./ 

## Output
NAME:   test1
LAST DEPLOYED: Sat Sep 15 09:36:23 2018
NAMESPACE: default
STATUS: DEPLOYED

RESOURCES:
==> v1beta1/Deployment
NAME           DESIRED  CURRENT  UP-TO-DATE  AVAILABLE  AGE
test1-mongodb  1        1        1           0          0s

==> v1beta2/Deployment
test1-express-crud  1  1  1  0  0s

==> v1/Secret
NAME           TYPE    DATA  AGE
test1-mongodb  Opaque  2     0s

==> v1/PersistentVolumeClaim
NAME           STATUS   VOLUME    CAPACITY  ACCESS MODES  STORAGECLASS  AGE
test1-mongodb  Pending  standard  0s

==> v1/ServiceAccount
NAME                SECRETS  AGE
test1-express-crud  1        0s

==> v1/Service
NAME                TYPE          CLUSTER-IP     EXTERNAL-IP  PORT(S)       AGE
test1-mongodb       ClusterIP     10.19.248.205         27017/TCP     0s
test1-express-crud  LoadBalancer  10.19.254.169      80:31994/TCP  0s

==> v1/Role
NAME                AGE
test1-express-crud  0s

==> v1/RoleBinding
NAME                AGE
test1-express-crud  0s

==> v1/Pod(related)
NAME                                READY  STATUS    RESTARTS  AGE
test1-mongodb-67b6697449-tppk5      0/1    Pending   0         0s
test1-express-crud-dfdbd55dc-rdk2c  0/1    Init:0/1  0         0s


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 test1-express-crud'
  export SERVICE_IP=$(kubectl get svc --namespace default test1-express-crud -o jsonpath='{.status.loadBalancer.ingress[0].ip}')
  echo http://$SERVICE_IP:80

上記のhelm installコマンドを実行するとロードバランサにExternal_IPが生成されます。そのIPを使ってアプリケーションを実行できます。

これは実行時にアプリケーションがどのように表示されるかを表しています:

最後に

この例からもわかるようにHelmはchartの構成と開発方法に高い柔軟性がある、非常に汎用性の高いシステムです。Helmコミュニティを活用することでHelm chartを公開して使用するプロセスが容易になり、アプリケーションを更新する際のメンテナンスがはるかに簡単になります。

このサンプルプロジェクトで完成したHelm chartはGitHubのexpress-crudリポジトリからダウンロードできますので、サンプルで機能を確認してどのように動くか理解してみてください。

Kubernetesに製品をデプロイするためのサンプルプロジェクトをもっと見つけるにはHelm chartのサンプルリポジトリを参照ください。