Highway to Helm
初めての方は、初めまして。そうでない方も、初めまして。クラウドエースのシステム開発部 SRE DivisionでProfessional Cooking Architectをしているzetaです。
INTRODUCTION
以前弊社の案件でHelmのカスタムチャートを使ったGKE(Google Kubernetes Engine)上のKubernetes(以後k8s)リソースの構築を行いました。今後Helmを初めて使うという方が筆者と同じところで詰まったりハマったりしないように知見を共有していきます。
INTENDED AUDIENCE
CKAD[1]レベルの知識があるとわかり良いと思います。
WHAT THIS ARTICLE DOESN'T PRESENT
- Kustomize等の他のミドルウェアとの比較
- インターネットで公開されているチャートをDLして使う方法
- Helm CLIのインストールの方法
- チャートの公開の方法
後ろ3つの項目については、Helmの公式ドキュメント[2]にチュートリアルがありますのでそちらを参考にしてください。
WHAT IS Helm ?
公式サイトの説明文がこちら
Helm は、Kubernetes アプリケーションの管理を支援します。Helm チャートは、最も複雑な Kubernetes アプリケーションの定義、インストール、およびアップグレードを支援します。
🤔
ようするにKubernetesの実装とか管理とかを楽にするためのミドルウェアです。
ちなみに英単語的には「舵」という意味で、Kubernetesが航海長だとか操舵手だとかいう意味らしいのでそういう人たちの使う道具というつもりで名付けていそうですね。
WHAT IS HARD ABOUT RAW k8s ?
そもそもなぜHelmのようなミドルウェアを使おうとするのでしょうか?生のk8s[3]ではどこに課題があるのでしょうか?
-
生のk8sはマニフェストファイルの数だけ
kubectl apply
を繰り返す必要があり、マニフェストファイルの数だけkubectl delete
を繰り返さなければならない- デプロイに手間がかかる
- オペミスの温床
-
生のk8sは95%設定が同じだが5%だけパラメータが違うというようなマニフェストファイル群が存在し得る
- dev / prd / stg環境があるとすると、ほとんど同じマニフェストファイル群を3セット書くことになり、冗長
- オペミスの温床
- パラメータ変更が発生した時に変更漏れを起こす等
こういった問題を解消しようという切なる願いがあって、ミドルウェアの導入を検討することになるわけです。
WHAT IS THE MOTIVATION FOR USING Helm ?
前項の問題群を解決する目的でHelmというミドルウェアに使うわけですが、Helmによって解決が見込めるのは以下のような要件です。
- あるひとまとまりのマニフェストファイル群に記述された一連のk8sリソースを、コマンド一発でデプロイし、コマンド一発で消し去りたい
- 変更を加えたマニフェストファイル群の差分をk8sリソースに適用する更新をコマンド一発でやりたい
- 環境毎に別々のマニフェストファイル群を作りたくない
では、具体的な使い方の解説に入っていきます。
WHAT IS THE Helm CHART ?
Helmではひとまとまりにした一連のk8sマニフェストファイル群・それらで使う変数を記したファイル・Helm版のREADMEのようなファイル等で構成されるチャートという単位を使ってk8sリソースをまとめます。このファイル群が格納されているディレクトリが、チャートとニアリーイコールと考えていいと思います。
HOW DIVEDE THE Helm CHARTS ?
小規模なシステムであれば1つのチャートで作ってしまってもいいですが、規模が大きくなるとコンポーネント毎にチャートを分けたくなってくることがあると思います。マイクロサービスごとに分けるといった具合に、システムの特性によって分け方は変わってくると思います。Helmのチャートはデプロイ・削除・更新をひとまとめにやる単位ですから、更新頻度が高いリソース群と低いリソース群という分け方をするのは一つの戦略です。
HOW CONFIGURE THE FILE STRUCTURE IN THE CHART ?
helm create <チャート名>
というコマンドを実行すると以下のようなディレクトリ構造が生成されます。templates/
の中にサンプルのマニフェストファイルなんかが生成されて、これらを改造して開発していくことも一応できますが、実際の開発現場において現実的な方法ではないと思うので消してしまっていいと思います。values.yamlの中身も同様です。NOTE.txtはチャートの備考を書いておくと後述のinstall / upgradeをするときに内容を表示してくれるものです(使わなかったので解説は略)。
sample_chart/ // ディレクトリ名がチャート名
┣ charts/ // サブチャートが入るディレクトリ。使わなかったので本記事では扱わない。
┣ templates/ // 本体。生のk8sで動くマニフェストファイルをここに突っ込んだらとりあえずは動くはず。多分。
┣ deployment.yaml
┣ service.yaml
...
┣ .helmignore // .gitignore のような役割のファイル
┣ Chart.yaml // README のような役割のファイル。チャート配布するときは書くほうがいいらしい。
┣ values.yaml // 変数を書く。環境毎に差が出る部分は env/ の中で書く。
sample_chart-0.1.0.tgz // **helm packageで生成される** これを使ってデプロイをする。 .gitignore に入れておこう
┣ env/ // **自分で作る** 環境毎に異なる値の入る変数はこの中のファイルに記述
┣ dev.yaml
┣ prd.yaml
/env以下に書く環境変数は、後述のデプロイ手順のオプションでvalues.yamlの値を上書きする使い方ができます。しかしながら、環境変数の扱いについてはHelmが公式に用意しているものではないので、このやり方がベストプラクティスかどうかは議論の余地がありそうです。
WHAT IS THE Helm IMPREMENTATION ?
そろそろ生のk8sとHelmの実装で実際の作業としてどういう差分があるのかということが気になってこのあたりまでスクロールしてきてると思うので、その辺りの解説やTipsを書いていきます。
WHAT IS ToDo BEFORE IMPREMENTATION ?
Helmのtemplate化をしたマニフェストファイルは、「このようなものはyaml構文の文法ではない」といった具合にエディタに怒られてエラーの赤線塗れにされます。シンタックスエラーと区別がつかないのでHelmのtemplate構文に対応したlinterを入れておきたいところ。
VSCodeの場合、Microsoftが出しているKubernetes[4]という拡張機能をインストールして、setting.json
に以下の記述を追加すると解決します。
{
"files.associations": {
"*helmfile*.yaml": "helm"
}
}
values.yaml: VARIAVLE DESCRIPTION
values.yaml
には変数を書きます。何をどこまで変数にするのかはシステムの特性にもよると思いますが、
- 後々変更が起きそうな値
- 繰り返し複数作られるリソースで使用する値
は変数化しておいた方が効率的です。
文章で説明されるよりコードを見る方が理解が早いでしょうからサンプルを貼ります。
environment: dev
serviceAccountName: sample-sa
namespace: sample
deployment:
labels:
app: sample
image: nginx
imageTag: latest
containerPort : 8080
env:
name: PORT
value: 8080
volumeMounts:
mountPath: /vol
name: sample_vol
service:
selector:
app: sample
ports:
port: 80
protocol: TCP
targetPort: 8080
backendconfig:
iap:
enabled: true
secretName: iap-secret
healthCheck:
type: HTTP
port: 8080
requestPath: /api/v1/health
後述のマニフェストファイルのtemplate化の工程で呼び出しが可能であればそれでいいので、values.yaml
は任意の構造で好きな変数名を名付けて作ることができます。しかしながら、わかりやすさ・使いやすさの観点でこういう構造にしておくと綺麗というのはあると思うので、いくつかポイントを挙げておきます。
- 基本的にリソースkind毎に変数群をまとめる
- 後述のtemplate化するときに、ややこしいことを考えなくていいのでミスしにくい
- リソースkindのネスト内でも、実際のマニフェストのブロックに合わせたネストをしておくほうが視覚的にわかりやすい
- namespaceのようなリソースkindを超越して使うような値は、リソースkindのネストの外に置いてグローバル変数のように使うと良い
- カスタムマニフェストなんかに登場する
node.attr.zone
のようなkeyは.
がエラー扱いになるので代わりの名前を考える必要がある
似たようなリソースを複数回作るときもポイントがありますが、それは後述のtemplate化のパートで紹介します。
TEMPLATIZATION
templates/
以下のマニフェストファイルはtemplate化を行うことで、前述のvalues.yaml
に記述した変数を代入して使用できます。また、値が一つだけ違うというような設定値がほぼ同じのリソースを、ループを使って定義するということもできます。
ここからはいろんなパターンで該当部分のvalues.yaml
とマニフェストファイルの抜粋を例示するという構成で紹介していきます。
基本的な変数の呼び方
values.yaml
が以下のようになっているとき
deployment:
image: nginx
templates/以下のマニフェストファイルでの呼び出し方は以下のようになります。
apiVersion: apps/v1
kind: Deployment
...
spec:
...
template:
spec:
containers:
- name: sample
image: {{ .Values.deployment.image }}
また、変数を複数つなげて使うこともできて、
image: nginx
imageTag: latest
image: {{ .Values.deployment.image }}:{{ .Values.deployment.imageTag }} //nginx:latest
といった感じでシンプルで直感的な方法で接続できます。
ローカル変数を使って環境毎の値を設定
templates/
以下のマニフェストファイル内で呼び出した変数をローカル変数として扱うことができます。これを活用して環境毎に異なる値に対応する書き方ができます。
environment: dev
serviceAccountName: sample-sa
{{ $env := .Values.environment }}
{{ $serviceAccountName := .Values.serviceAccountName }}
serviceAccountName: {{ $serviceAccountName }}-{{ $env }} //sample-sa-dev
ループを使って値がわずかに違うほとんど同じリソースを定義
マルチゾーンで冗長化してるリソースのような、値が一つ違うだけで他のスペックは同じリソースを作るといったことはよくあると思います。これに対してはループを使ってコードの冗長化を防ぎます。
es:
nodeSets:
- name: node-a
zone: asia-northeast1-a
- name: node-b
zone: asia-northeast1-b
- name: node-c
zone: asia-northeast1-c
...
このようなvalues.yamlがあった場合、ループにしたい部分をrangeとendで囲んでループで使いたい変数ブロックをセットします。
kind: ElasticSearch
...
spec:
nodeSets:
{{- range .Values.es.nodeSets }}
name: {{ .name }}
...
config:
node.attr.zone: {{ .zone }}
...
{{- end }}
ループ毎に変わる値と不変の値が混在する状態でループを回したい
実際開発をしているとこういった複雑なことをやらなければならない場面は出てくると思います。こういうパターンはドキュメントやブログなんかでも載ってなくて、かゆいところに手が届かないということもあると思います。
es:
nodeSets: //変動値
- name: node-a
zone: asia-northeast1-a
- name: node-b
zone: asia-northeast1-b
- name: node-c
zone: asia-northeast1-c
podSpec: //不変動値
requests:
memory: 2.0Gi
cpu: 1
limits:
memory: 2.0Gi
cpu: 1
この場合、podSpec
以下の値は各ループで不変です。この部分をマニフェストファイルでローカル変数にします。
{{ $podSpec := .Values.es.podSpec }}
kind: ElasticSearch
...
spec:
nodeSets:
{{- range .Values.es.nodeSets }}
name: {{ .name }}
...
config:
node.attr.zone: {{ .zone }}
...
podTemplate:
spec:
...
containers:
- name: sample_container
resources:
requests:
memory: {{ $podSpec.requests.memory }}
cpu: {{ $podSpec.requests.cpu }}
limits:
memory: {{ $podSpec.limits.memory }}
cpu: {{ $podSpec.limits.cpu }}
...
{{- end }}
環境毎に作ったり作らなかったりするリソース
これは開発環境では作らないが本番環境では必要なのだ(ギュッ)というリソースはあると思います。こういう場合は条件分岐をします。
ingress:
enabled: true
...
ingress:
enabled: false
{{- if .Values.ingress.enabled -}}
apiVersion: networking.k8s.io/v1
kind: Ingress
...
{{- end }}
環境ファイルの選択の方法は後述のデプロイ時に実行するコマンドの引数のパートで説明します。
一つの設定項目に複数値を設定するとき
これもループで書きます。
ingress:
annotations:
kubernetes.io/ingress.global-static-ip-name: sample-ip-name
kubernetes.io/ingress.class: "gce-internal"
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
...
annotations:
{{- toYaml .Values.ingress.annotations | nindent 4 }}
toYaml
とかnindent
とか突然出てきてなんだこれは?となると思いますが、これを書かないと2行目以降が左詰めになりyamlの構造が壊れてしまってちゃんと設定できないのでこの呪文を書いておく必要があるのです。ここはあんまりスマートじゃないところですね。
HOW TO DEPLOY Helm ?
マニフェストファイルのtemplate化が済んだらデプロイをします。デプロイはチャート単位で行います。なお、チャートのデプロイの方法には新規デプロイで使うインストールと、既存のデプロイを更新するアップグレードがあります。
ここではkubectlのコンテキスト取得や切り替えは済んでいる前提で省略します。
CREATE COMPRESSED CHART FILE
helm package <チャート名>
チャート名はhelm create
の際に名付けた名前です。チャートのディレクトリ名と一致しているはずです。
INSTALL CHART
先程の手順でチャートの圧縮ファイルが<チャート名>-0.1.0.tgz
というような名前でできているはずです。これを使ってデプロイを行います。新規デプロイの場合はインストールという方法を使います。基本的な構文は以下の形です。
helm install <release名> <Helmチャートの圧縮ファイル名>
環境ファイルの選択
環境毎に差がある値が書かれている設定ファイルを指定する場合の書式は以下の形です。
helm install <release名> <Helmチャートの圧縮ファイル名> -f <env設定ファイルのパス>
namespaceを指定する場合
namespaceはマニフェストファイル側で指定しているとは思いますが、Helmのチャート自体にもnamespaceを指定したいという場合に使うオプションです。基本的にkubectlの--namespace
ないし-n
と同じ使い方です。
helm install <release名> <Helmチャートの圧縮ファイル名> --namespace <namespace名>
デプロイ前にデプロイされるリソース設定が正しいかチェックしたい
kubectlでおなじみの--dry-run
オプションが使えます。kubectlと違って=client
とかつけなくてもいいです。
helm install <release名> <Helmチャートの圧縮ファイル名> --dry-run
別にパッケージ化しなくてもデプロイできるらしい
あとから知った......
helm install <release名> --values <envファイルとか> <チャートディレクトリ名>
UPGRADE CHART
既存のreleaseで使用したチャートに加えた修正を環境に反映したい場合、アップグレードというデプロイの方法をとります。installで使えるオプションはこっちでも使えるはずです。基本の構文は以下の形式。
helm upgrade <release名> <Helmチャートの圧縮ファイル名>
アップグレードコマンドでインストールもしたい
エンジニアは あたらしく helm upgradeを おぼえたい......
しかし エンジニアは コマンドを 4つ
おぼえるので せいいっぱいだ!
helm upgradeの かわりに
ほかの コマンドを わすれさせますか?
みたいな状況で使えるのが--install
オプションです。既に初回デプロイ済みの内容であればオプションは無視されるので、CI/CDをやる場合なんかは設定ファイルに書くコマンドは全部これでいいです。
helm upgrade --install <release名> <Helmチャートの圧縮ファイル名>
COMFIRMATION OF DEPLOYED RELEASES
helm list
を使います。全namespaceのreleaseの表示をしたい場合は-A
オプションを使います。もちろん-n
でnamespaceを指定する使い方もできます。
helm list -A
DELETE DEPLOYED RESOURCES
デプロイしたk8sリソースはチャートのrelease単位でまとめて削除することができます。チャートが不要になったときとか、検証環境のお掃除をするときとかはこれを使いましょう。
helm uninstall <release名>
OUTRODUCTION
今回はtemplate構文の書き方を中心にHelmを使ったk8sリソースの実装について解説しました。Helmのtemplate構文は結構な癖がある割に、ググっても良いExampleが得られなくて困ることもあると思います。この記事が皆さんのHelmの導入・実装を高速にするHighwayとなれば幸いです。
-
Linux Foundationが提供するKubernetesの公式認定試験であるCertified Kubernetes Application Developerの略称。主に実装作業能力が問われる。 ↩︎
-
この記事ではミドルウェア等が特に入っていないKubernetesをkubectlを使って操作するようなことを生のk8sと呼ぶ。 ↩︎
-
https://marketplace.visualstudio.com/items?itemName=ms-kubernetes-tools.vscode-kubernetes-tools ↩︎
Discussion