chartを作りながらhelmの概要を理解する

## 目的と対象読者

helmについて今まで理解していなかったのですが、[Document](https://docs.helm.sh/)を読んでキャッチアップしてみました。しかしDocumentは網羅性がある一方で概要を把握するには情報過多で、それができるチュートリアルもうまく見つからなかったので、備忘録も兼ねて記事にすることにしました。

以下のような方を対象にして書いているつもりです。

- helmのアーキテクチャーや用語を理解したい
- helmの基本的な使い方を広く浅く理解したい

また以下の前提で動作を確認していきます。

- kubernetes(以下k8s)クラスタが構築されている
- helmやtillerがインストールされている
- helmのバージョンはv2である

## helmの構成要素

helmとはk8sのパッケージマネジャーのことです。
パッケージとはk8sのマニフェスト（リソースの設定ファイル）の集合であり、helmはそれらを管理するためのツールと言えます。

c.f https://github.com/helm/helm#helm
> Helm is a tool for managing Kubernetes charts. Charts are packages of pre-configured Kubernetes resources.

このパッケージマネジャーは具体的には、以下のような機能を提供しています。
c.f https://docs.helm.sh/architecture/

- パッケージを共有、取得、管理する
- パッケージを作成、更新する
- パッケージをk8sクラスタに適用、巻き戻し、削除

これらの機能があることで、誰かが作ったマニフェストと同じようなものを作る必要がなくなったり、誰がいつどんな目的でk8sクラスタにこのマニフェストを適用したのか分からなくなることを防いだり、あるタイミングでapplyした内容に巻き戻したりすることができます。

まずは、helmに登場する用語を整理しましょう。
用語は[Document](https://docs.helm.sh/glossary/)で解説されており、詳しくはそちらを参照してください。

|用語|説明|
|:-----|:-----|
|helm|k8sのパッケージマネジャー|
|chart|パッケージ（マニフェストの集合）|
|repository|chartを保管したりダウンロードするサーバー|
|release|k8sクラスタに適用されたchartで履歴管理用に保管される|
|tiller|chartをk8sクラスタに適用したりreleaseを管理するサーバー|

これだけだとイメージが湧かないと思うので、図にすると以下のようになります。

![helm-architecture.png](https://qiita-image-store.s3.amazonaws.com/0/68018/5daa6109-0af7-35b3-0cba-b855e76b6685.png)

これらのことから、helmはchartを見つけたり作成してrepositoryに保管し、chartの内容からk8sリソースをクラスタ上に作成し、その度にreleaseを作ることができる一連の利用シーンが理解できるかと思います。そして、releaseが作られることで、適用した内容をバージョン管理できるので巻き戻しや更新などの管理も可能になります。

c.f https://docs.helm.sh/using_helm/#three-big-concepts


## chartを作る

### chartの構成要素

上述の通り、chartとはパッケージであり、k8sリソースの設定ファイルの集合です。

c.f https://docs.helm.sh/developing_charts/#charts
> Helm uses a packaging format called charts. A chart is a collection of files that describe a related set of Kubernetes resources.

概要について説明できたので次は作りながらchartをどのように作るかを見ていきたい思います。まず、chartは以下のような構成要素から成り立ちます。

c.f https://docs.helm.sh/developing_charts/#the-chart-file-structure

|構成要素|機能|
|:-----|:-----|
|Chart.yaml|chartに関するメタ情報が記述される（nameなど）|
|requirements.yaml|依存する外部のchartが指定される|
|values.yaml|templateに当てはめる値が記述される|
|charts/|現在のchartが依存するchartの一覧|
|templates/|values.yamlの値を嵌め込むことができるk8sマニフェストを作る雛形の一覧|

実際にchartを作ってみます。まず、chartをk8sクラスタにデプロイするには、tillerサーバーを初期化させる必要があります。

`kubectl config view --minify`で現在接続しているクラスタを確認し、そのクラスタの`kube-system` namespace内にtillerのpodを作成します。

```shell
$ kubectl config view --minify
$ helm init
$ kubectl --namespace kube-system get pods --selector name=tiller
NAME                             READY   STATUS    RESTARTS   AGE
tiller-deploy-7995854f67-jzx4d   1/1     Running   0          55m
```

次に、以下のコマンドでmychartというchartを作成します。
c.f https://docs.helm.sh/chart_template_guide/#a-starter-chart

```shell
$ helm create mychart
Creating mychart
```

実際に、上記のchartの構成要素となるファイルが生成されていることが確認できます。

```shell
$ ls mychart
Chart.yaml      charts          templates       values.yaml
```

### template

まずはtemplateを作ります。templateは、k8sマニフェストの雛形で後述のvalues.yamlやコマンド引数で渡される値を嵌め込み、tiller経由でk8sクラスタに適用されます。

c.f https://docs.helm.sh/chart_template_guide/#charts
> The templates/ directory is for template files. When Tiller evaluates a chart, it will send all of the files in the templates/ directory through the template rendering engine. Tiller then collects the results of those templates and sends them on to Kubernetes.

> The values.yaml file is also important to templates. This file contains the default values for a chart. These values may be overridden by users during helm install or helm upgrade

ここではconfigmapのtemplateを作ってみますが、先ほど生成されたデフォルトのtemplateは利用しないので削除します。

```shell
$ rm -rf mychart/templates/*
```

その上で、`mychart/templates/configmap.yaml`に以下のconfigmapのマニフェストを作成します。これはtemplateですが、値は既に嵌め込まれた状態です。

```yaml
# mychart/templates/configmap.yaml
apiVersion: v1
kind: ConfigMap
metadata:
  name: mychart-configmap
data:
  myvalue: "Hello World"
```

`helm install [chart-name]`で先ほどのtemplateをtiller経由でデプロイしてみます。

```shell
$ helm install ./mychart
NAME:   hardy-grizzly
LAST DEPLOYED: Sat Feb  9 19:23:20 2019
NAMESPACE: default
STATUS: DEPLOYED

RESOURCES:
==> v1/ConfigMap
NAME               DATA  AGE
mychart-configmap  1     0s
```

実際に、`hardy-grizzly`という名前のchartが以下のマニフェストとしてデプロイされたようです。
c.f https://docs.helm.sh/chart_template_guide/#debugging-templates

```shell
$ helm get manifest hardy-grizzly

---
# Source: mychart/templates/configmap.yaml
apiVersion: v1
kind: ConfigMap
metadata:
  name: mychart-configmap
data:
  myvalue: "Hello World"
```

実際にconfigmapが作成されていることも確認できました。

```shell
$ kubectl get cm
NAME                DATA   AGE
mychart-configmap   1      47m
```

また、templateは値を嵌め込むだけではなく、値に応じた条件分岐（`if`）や利用する値のスコープの制御（`with`）、ループ（`range`）などを使ってtemplateを作ることもできます。
c.f https://docs.helm.sh/chart_template_guide/#flow-control


### values

**templateに値を渡す**

templateを作成したので、次はtemplateに値を渡してみましょう。
templateに値を提供する方法は主に二つあります。

c.f https://docs.helm.sh/developing_charts/#templates-and-values

1. values.yamlを開発者が用意する
2. chartの利用者が helm install コマンド時に値を渡す（values.yamlを上書きすることができる）

実際にvalues.yamlからtemplateに値を渡してみます。以下のように、values.yamlを書き換えます。

```yaml
# mychart/values.yaml
favoriteDrink: coffee
```

先ほどのconfigmapのtemplateを以下のように書き換えます。

```yaml
# mychart/templates/configmap.yaml
apiVersion: v1
kind: ConfigMap
metadata:
  name: {{ .Release.Name }}-configmap
data:
  myvalue: "Hello World"
  drink: {{ .Values.favoriteDrink }}
```


以下の`--dry-run --debug`フラグを使うことで、実際にはデプロイせずに、デプロイされるchartの内容を確認できます。
c.f https://docs.helm.sh/chart_template_guide/#adding-a-simple-template-call

> When you want to test the template rendering, but not actually install anything, you can use helm install --debug --dry-run ./mychart. This will send the chart to the Tiller server, which will render the templates. But instead of installing the chart, it will return the rendered template to you so you can see the output:


```shell
$ helm install --dry-run --debug ./mychart
NAME:   wishing-porcupine
REVISION: 1
RELEASED: Sat Feb  9 19:39:31 2019
CHART: mychart-0.1.0
USER-SUPPLIED VALUES:
{}

COMPUTED VALUES:
favoriteDrink: coffee

HOOKS:
MANIFEST:

---
# Source: mychart/templates/configmap.yaml
apiVersion: v1
kind: ConfigMap
metadata:
  name: wishing-porcupine-configmap
data:
  myvalue: "Hello World"
  drink: coffee
```

values.yamlで指定した値がtemplateで利用されていることが確認できます。

また、`--set`フラグでvalues.yamlの値をinstall時に上書きすることもできます。

```shell
$ helm install --dry-run --debug --set favoriteDrink=slurm ./mychart
NAME:   jittery-aardvark
REVISION: 1
RELEASED: Sat Feb  9 19:41:04 2019
CHART: mychart-0.1.0
USER-SUPPLIED VALUES:
favoriteDrink: slurm

COMPUTED VALUES:
favoriteDrink: slurm

HOOKS:
MANIFEST:

---
# Source: mychart/templates/configmap.yaml
apiVersion: v1
kind: ConfigMap
metadata:
  name: jittery-aardvark-configmap
data:
  myvalue: "Hello World"
  drink: slurm
```

**subchartの値を操作する**

次にsubchart（現在のchartが依存するchart）を作ってみます。

```shell
$ helm create mychart/charts/mysubchart
Creating mysubchart
$ rm -rf mysubchart/templates/*
```

subchartのtemplateに以下のファイルを追加します。

```yaml
# mychart/charts/mysubchart/templates/configmap.yaml
apiVersion: v1
kind: ConfigMap
metadata:
  name: {{ .Release.Name }}-cfgmap2
data:
  dessert: {{ .Values.dessert }}
```

そして、subchartのvalues.yamlを以下のように書き換えます。

```yaml
# mychart/charts/mysubchart/values.yaml
dessert: cake
```

top levelのchart（mychart）はsubchart（mychart/charts/mysubchart）のvalues.yamlも利用することができます。その場合は対象のsubchartの名前を挟む必要があります。

c.f https://docs.helm.sh/developing_charts/#scope-dependencies-and-values
> Charts at a higher level have access to all of the variables defined beneath


以下のように、対象のsubchartの名前を指定して、そのsubchartのvalues.yamlの値をtop levelのtemplateから参照できます。

```yaml
# mychart/templates/configmap.yaml
apiVersion: v1
kind: ConfigMap
metadata:
  name: {{ .Release.Name }}-configmap
data:
  myvalue: "Hello World"
  drink: {{ .Values.favoriteDrink }}
  dessert: {{ .Values.mysubchart.dessert }}
```

```shell
$ helm install --dry-run --debug ./mychart
---
# Source: mychart/charts/mysubchart/templates/configmap.yaml
apiVersion: v1
kind: ConfigMap
metadata:
  name: imprecise-chimp-cfgmap2
data:
  dessert: cake
---
# Source: mychart/templates/configmap.yaml
apiVersion: v1
kind: ConfigMap
metadata:
  name: imprecise-chimp-configmap
data:
  myvalue: "Hello World"
  drink: coffee
  dessert: cake
```

またsubchartのtemplateに渡される値をtop level のvalues.yamlから上書きすることもできます。

subchartでは、以下のように値が指定されています。

```yaml
# mychart/charts/mysubchart/values.yaml
dessert: cake
```

これをtop levelのvalues.yamlで上書きしてみます。

```yaml
# mychart/values.yaml
favoriteDrink: coffee
mysubchart:
  dessert: ice cream
```

`cake`ではなく、`ice cream`に値が上書きされていることが確認できます。

```shell
$ helm install --dry-run --debug ./mychart
---
# Source: mychart/charts/mysubchart/templates/configmap.yaml
apiVersion: v1
kind: ConfigMap
metadata:
  name: illocutionary-seal-cfgmap2
data:
  dessert: ice cream
---
# Source: mychart/templates/configmap.yaml
apiVersion: v1
kind: ConfigMap
metadata:
  name: illocutionary-seal-configmap
data:
  myvalue: "Hello World"
  drink: coffee
  dessert: ice cream
```

**値の上書きの優先順位**

実際は以下のような優先順位でtemplateに渡す値を上書きすることができます。
c.f https://docs.helm.sh/chart_template_guide/#values-files

1. --set フラグでkey-valueを指定
2. -f フラグで利用するvalues.yamlを指定
3. top levelのvalues.yaml
4. subchartのvalues.yaml

このような前提を成り立たせるためにsubchartはそれだけで独立（親のchartに依存しない）する必要があります。なぜなら親が必ずsubchartで定義した値を上書きするとは限らないからです。

c.f https://docs.helm.sh/chart_template_guide/#subcharts-and-global-values

**global values**

ここまでvalues.yamlの機能を説明してきましたが、グローバルな値を定義することも可能です。
c.f https://docs.helm.sh/chart_template_guide/#global-chart-values

> Global values are values that can be accessed from any chart or subchart by exactly the same name. 
> The Values data type has a reserved section called Values.global where global values can be set. 

今まで特定のsubchartのvalues.yamlを上書きするには、subchartの名前（Chart.yamlのname）を指定する必要がありました。

```yaml
# values.yaml
mysubchart_1:
  dessert: ice cream
mysubchart_2:
  dessert: ice cream
```

しかし、global valuesだとsubchartの名前の指定なく全てのsubchartの`{{ .Values.global.dessert }}`の値を上書きできます。

```yaml
# values.yaml
global:
  dessert: ice cream
```

### 書くための規則

chartを今まで作成してきましたが、書く上でのベストプラクティスが存在します。

**yamlのフォーマット**

c.f https://docs.helm.sh/chart_best_practices/#formatting-yaml

- tabではなく、2 spaceでインデントする

**chart**

c.f https://docs.helm.sh/chart_best_practices/#chart-names

- 小文字で始まる名前をつける
- 大文字やアンダースコア, ドットは名前に使うべきではない

**template**

c.f https://docs.helm.sh/chart_best_practices/#templates

- camelCaseではなくdashed notationでファイルを名付ける（e.g my-example-configmap.yaml）
- resourceの種類を反映したファイル名前にする（e.g my-service.yaml）

**values**

c.f https://docs.helm.sh/chart_best_practices/#values

- 小文字で始まるcamelCaseで名付ける（e.g `chickenNoodleSoup: true`）
- 丁寧にnestして値を定義した方がいい
  - nestしていると、それぞれの名前空間が存在するかチェックされるので安全
  - 名前空間が区切られていた方が分かりやすい

## release管理をする

releaseとは、k8sクラスタにデプロイされたマニフェストの集合（chart）です。
したがって、`helm install [chart-name]`でtillerがk8sクラスタにapplyするとreleaseが作成されます。

```shell
$ helm ls
NAME            REVISION        UPDATED                         STATUS          CHART           APP VERSION     NAMESPACE
hardy-grizzly   1               Sat Feb  9 19:23:20 2019        DEPLOYED        mychart-0.1.0   1.0             default
```

`helm delete [release-name]`でクラスタからreleaseの内容のマニフェストを削除することができます。
しかし削除されても履歴管理のためにrelease自体は残されます。
c.f https://docs.helm.sh/using_helm/#uninstall-a-release

> Because Helm tracks your releases even after you’ve deleted them, you can audit a cluster’s history, and even undelete a release (with helm rollback).

```shell
$ helm delete hardy-grizzly
release "hardy-grizzly" deleted

$ helm ls --deleted
NAME            REVISION        UPDATED                         STATUS  CHART           APP VERSION     NAMESPACE
hardy-grizzly   1               Sat Feb  9 19:23:20 2019        DELETED mychart-0.1.0   1.0             default
```

これを以下のように巻き戻すこともできます。

```shell
$ helm rollback hardy-grizzly 1
Rollback was a success! Happy Helming!

$ helm ls
NAME            REVISION        UPDATED                         STATUS          CHART           APP VERSION     NAMESPACE
hardy-grizzly   2               Sat Feb  9 20:27:44 2019        DEPLOYED        mychart-0.1.0   1.0             default
```

また以下のように履歴を確認することもできます。
c.f https://docs.helm.sh/helm/#helm-history

```shell
$ helm history hardy-grizzly
REVISION        UPDATED                         STATUS          CHART           DESCRIPTION
1               Sat Feb  9 20:46:21 2019        SUPERSEDED      mychart-0.1.0   Deletion complete
2               Sat Feb  9 20:47:09 2019        DEPLOYED        mychart-0.1.0   Rollback to 1
```

`--purge`フラグでreleaseも含めて完全に削除することができます。

```shell
$ helm delete --purge hardy-grizzly
release "hardy-grizzly" deleted
```

## 最後に

ちなみに、`tiller`はv3からは無くなる予定のようです。
c.f https://github.com/helm/community/blob/master/helm-v3/000-helm-v3.md#summary

> Tiller is gone, and there is only one functional component (helm)