HelmのChartに少しだけ手を入れたいあなたへ

# はじめに

この記事の内容は個人の見解です。
また、ここに書かれている手段が唯一の解決策だという意図もなく、特定のツールを中傷するつもりもありません。

# このブログポストのオチ

* ksonnetを使えばHelm Chartをカスタマイズできる
* ただし、hook機構を利用したChartはだめ（stable/spinnakerなど)

# Helm は便利なツール、だけど

Kubernetesとお友達の皆さん、Helm使ってますか？使ってますよね、この記事を読んでいるんですから。
色々なChartが公開されていて便利です、Helm。

Nginx IngressやPromethes、GrafanaからMinecraftのサーバまで、手軽にデプロイできて、とてもとても助かります。

素敵ツールのHelmですが、しょっちゅう、これでもかと、耳にタコができるほど言われるのが、**「カスタマイズが辛い」**だと思います。
「ちょっとDeploymentのあそこをごにょごにょしたい」、「DaemonSetのあれをこれしたい」なんてときに、取れる手段は「Chartを落としてきて手で直す」です。
めんどくさいですね。とっても。ぶっちゃけやりたくない。

# 解決手段っぽいもの

辛い思いをしている我々に救いの手を差し伸べるためかどうかは知りませんが、先日リリースされたあるツールの最新版でこんな機能が追加されました。

> Added Helm Registry support where charts operate as ksonnet parts（意訳:Helm Chartをksonnetのパーツとして使えるよう、Helm Registryサポートを追加した)

はい、[ksonnet](https://ksonnet.io/)です。
2018/08/02にリリースされたv0.12.0([リリースノート](https://github.com/ksonnet/ksonnet/releases/tag/v0.12.0))でHelm Chartを使う機能が追加されました。

知名度がいまいちなksonnetですので、一旦さっくりと概要を説明します。

ksonnetはJsonnetの仕組みを使って、KubernetesのYAML地獄から逃れようというツールです。（jsonnetとはGoogleがjsonをなんやかんやするために作った~~なんか難しい~~テンプレート言語です。）

ksonnetにもHelmのChartのようなものがあり、それは**Prototype**と言います。これまたHelmのRepositoryのようなものは**Registry**といいます。
そのRegistryとして、HelmのRepositoryを登録できるようになったというものが、前述のリリースノートで書かれた新機能です。
単純にいうと、ksonnetでHelmのChartがデプロイできるということです。

概要はこの程度にしておいて、実際に使う方法を解説します。

# ksonnetでHelmのChartをデプロイする

とりあえず[stable/rocketchat](https://github.com/helm/charts/tree/master/stable/rocketchat)で[Rocket.Chat](https://rocket.chat/)をデプロイします。MySQLとか入れても面白くないので。

ksonnetでHelmのRepositoryを追加するのはksonnetのアプリケーションを作ったあとなので、ツールのインストール（Mac）〜ksonnetのアプリケーション作成までをやります。

```:ksonnetのアプリケーション作成まで
$ brew install ks　kubernetes-cli# ksonnetと、kubectlをインストール
$ ks version  # バージョン確認。ksonnet versionが0.12.0以上でなければならない。
ksonnet version: 0.12.0
jsonnet version: v0.11.2
client-go version: kubernetes-1.10.4
$ # なんとかしてkubernetesのAPIを叩けるようにしておく。~/.kube/configをなんとかしたり
$ ks init rocketchat # ksonnetアプリケーションの作成。この時点でKubernetesに接続できる必要あり
$ cd rocketchat
```

これでksonnetのアプリケーションができました。以下のようなファイルツリーになっています。

```:ksonnetのアプリケーションのファイルツリー
$ tree .
.
├── app.yaml
├── components
│   └── params.libsonnet
├── environments
│   ├── base.libsonnet
│   └── default
│       ├── globals.libsonnet
│       ├── main.jsonnet
│       └── params.libsonnet
├── lib
│   └── ksonnet-lib
│       └── v1.11.0
│           ├── k.libsonnet
│           ├── k8s.libsonnet
│           └── swagger.json
└── vendor

7 directories, 9 files
```

アプリケーションを作成したあとは以下の流れでHelmのChartをデプロイします。
1. HelmのRepositoryをRegistryとして追加
2. Prototypeを適用
3. パラメータを指定
4. デプロイ

## HelmのRepositoryをRegistryとして追加

`ks registry add --help`でヘルプを見ると、`ks registry add helm-stable https://kubernetes-charts.storage.googleapis.com`をたたけと書かれているので叩きます。stableのリポジトリのURLですね。

```:registryの追加
$ ks registry add helm-stable https://kubernetes-charts.storage.googleapis.com
$ ks registry list # helm-stableが追加されている。
NAME        OVERRIDE PROTOCOL URI
====        ======== ======== ===
helm-stable          helm     https://kubernetes-charts.storage.googleapis.com
incubator            github   github.com/ksonnet/parts/tree/master/incubator
```

追加されたRegistoryのPROTOCOLがhelmとなっていますね。

ちなみに、ファイルの変更点はapp.yamlのregistriesに数行追加されただけです。

```diff:app.yamlの差分
diff --git a/app.yaml b/app.yaml
index a1cff24..62f7ee1 100644
--- a/app.yaml
+++ b/app.yaml
@@ -9,6 +9,9 @@ environments:
 kind: ksonnet.io/app
 name: rocketchat
 registries:
+  helm-stable:
+    protocol: helm
+    uri: https://kubernetes-charts.storage.googleapis.com
   incubator:
     protocol: github
     uri: github.com/ksonnet/parts/tree/master/incubator
```

## Prototypeを適用

Prototypeを適用する前にまず**Package**をインストールする必要があります。ksonnetではPrototypeはかならずPackageに含まれているので、先にPackageをインストールします。

```:Packageインストール
$ ks pkg list # Packageの一覧を確認する。
REGISTRY    NAME                           VERSION                                  INSTALLED ENVIRONMENTS
========    ====                           =======                                  ========= ============
helm-stable acs-engine-autoscaler          2.2.0
helm-stable aerospike                      0.1.7
helm-stable anchore-engine                 0.2.0
...(すごく多いので略
helm-stable risk-advisor                   2.0.4
helm-stable rocketchat                     0.1.3
helm-stable rookout                        0.1.0
...(さらに略
incubator   redis                          40285d8a14f1ac5787e405e1023cf0c07f6aa28c
incubator   tomcat                         40285d8a14f1ac5787e405e1023cf0c07f6aa28c
$
$ ks pkg install helm-stable/rocketchat # helm-stableのrocketchatをインストール
```

Packageをインストールした段階で、vendorディレクトリにHelmのChartが追加され、app.yamlが修正されます。

```:Pkg追加後のファイルツリー
$ tree .
.
├── app.yaml
(略
└── vendor
    └── helm-stable
        └── rocketchat
            └── helm
                └── 0.1.3
                    └── rocketchat # HelmのChartがそのままここに追加されている。
                        ├── Chart.yaml
                        ├── charts
                        │   └── mongodb
                        │       ├── Chart.yaml
                        │       ├── README.md
                        │       ├── templates
                        │       │   ├── NOTES.txt
                        │       │   ├── _helpers.tpl
                        │       │   ├── deployment.yaml
                        │       │   ├── pvc.yaml
                        │       │   ├── secrets.yaml
                        │       │   └── svc.yaml
                        │       └── values.yaml
                        ├── requirements.lock
                        ├── requirements.yaml
                        ├── templates
                        │   ├── NOTES.txt
                        │   ├── _helpers.tpl
                        │   ├── deployment.yaml
                        │   ├── ingress.yaml
                        │   ├── pvc.yaml
                        │   ├── secrets.yaml
                        │   └── svc.yaml
                        └── values.yaml

16 directories, 29 files
```

```diff:app.yamlの差分
diff --git a/app.yaml b/app.yaml
index 62f7ee1..672b666 100644
--- a/app.yaml
+++ b/app.yaml
@@ -7,6 +7,11 @@ environments:
     k8sVersion: v1.11.0
     path: default
 kind: ksonnet.io/app
+libraries:
+  rocketchat:
+    name: rocketchat
+    registry: helm-stable
+    version: ""
 name: rocketchat
 registries:
   helm-stable:
```

これでPrototypeが使えるようになったので、適用します。

```:Prototypeの適用
$ ks prototype list #一覧を確認
NAME                                  DESCRIPTION
====                                  ===========
io.ksonnet.pkg.configMap              A simple config map with optional user-specified data
io.ksonnet.pkg.deployed-service       A deployment exposed with a service
io.ksonnet.pkg.helm-stable-rocketchat Helm Chart rocketchat from the helm-stable registry
io.ksonnet.pkg.namespace              Namespace with labels automatically populated from the name
io.ksonnet.pkg.single-port-deployment Replicates a container n times, exposes a single port
io.ksonnet.pkg.single-port-service    Service that exposes a single port
$
$ ks generate io.ksonnet.pkg.helm-stable-rocketchat -h # prototypeのパラメータを確認する
Usage of prototype-flags:
      --module string        Component module
      --name string          Name of the component
      --values string        Helm values (default "{}")
      --values-file string   Prototype values file (file returns a Jsonnet object)
  -v, --verbose count[=-1]   Increase verbosity. May be given multiple times.
      --version string       Version of the Helm chart. If blank, it will use latest installed version (default "0.1.3")
$ ks generate io.ksonnet.pkg.helm-stable-rocketchat my-app --name my-rocketchat # 適用。とりあえずは最低限だけ指定。
```

components/my-app.yamlが作成され、components/params.libsonnetが変更されました。my-app.yamlがデプロイするアプリケーション本体です。params.libsonnetはアプリケーションに設定するパラメータのデフォルト値が格納されます。`ks generate`時に指定したパラメータがparams.libsonnetに格納されています。

```:Prototype適用後のファイルツリー
$ tree .
.
├── app.yaml
├── components
│   ├── my-app.jsonnet
│   └── params.libsonnet
├── environments
│   ├── base.libsonnet
(略
```

```diff:components/params.libsonnetの差分
diff --git a/components/params.libsonnet b/components/params.libsonnet
index 4fe2a83..9603e7d 100644
--- a/components/params.libsonnet
+++ b/components/params.libsonnet
@@ -6,5 +6,10 @@
   components: {
     // Component-level parameters, defined initially from 'ks prototype use ...'
     // Each object below should correspond to a component in the components/ directory
+    "my-app": {
+      name: "my-rocketchat",
+      values: {},
+      version: "0.1.3",
+    },
   },
 }
```

## パラメータを指定

このままではChartにパラメータを指定しないのと変わりないので、values.yamlにあたる設定をします。
ksonnetでmy-appにvaluesを指定するときはcomponents/params.libsonnetのcomponents.my-app.valuesに値を設定します。
たとえば、Rocket.ChatのChartでIngressを有効にする場合は、ingress.enabledにtrueを設定します。これをksonnetで指定する場合、components.my-app.values.*ingress.enabled*にtrueを指定します。

```:パラメータの設定
$ ks param set my-app values.ingress.enabled true # ingress.enabledにtrueを設定する
$ ks param list # 確認
COMPONENT PARAM   VALUE
========= =====   =====
my-app    name    'my-rocketchat'
my-app    values  { ingress: {
  enabled: true,
} }
my-app    version '0.1.3'
```

```diff:components/params.libsonnetの差分
diff --git a/components/params.libsonnet b/components/params.libsonnet
index 34469d7..eccdbad 100644
--- a/components/params.libsonnet
+++ b/components/params.libsonnet
@@ -6,6 +6,9 @@
     "my-app": {
       name: 'my-rocketchat',
       values: {
+        ingress: {
+          enabled: true,
+        },
       },
       version: '0.1.3',
     },
```

これで、パラメータが設定出来ました。

## デプロイする

デプロイでは特に特殊なことはなく、ksonnetでデプロイするときのいつものやり方になります。

```:デプロイ
$ ks apply default
```

デプロイが完了したらポートフォワードを使って接続してみましょう

```:デプロイしたRocket.Chatにport-forwardで接続
$ kubectl rollout status deployment my-rocketchat-rocketchat # デプロイが完了するのを待つ
$ kubectl port-forward my-rocketchat-rocketchat-68558f8689-hsm7z 8888:3000 # pod名はの後半はランダムなので、自分の環境で出来たものを確認してください。
```

http://localhost:8888 に接続すると、こんな画面が出るはずです。

<img width="1082" alt="Rocket.Chatログイン画面.png" src="https://qiita-image-store.s3.amazonaws.com/0/69698/3ffc4dab-233f-6d10-ff79-130b7117a570.png">

これで、HelmのChartがksonnetで無事デプロイ出来ました。

# ksonnetでHelmのChartをカスタマイズする

さて、ここからが本番です。
HelmのChartをカスタマイズするわけですが、いい例が全く思い浮かばないので、すべてのリソースにmetadata.labels.maintainer=fujiokaを追加しようと思います。

```:変更後イメージ
$ kubectl get deployment -o=custom-columns=NAME:.metadata.name,MAINTAINER:.metadata.labels.maintainer
NAME                       MAINTAINER
my-rocketchat-mongodb      fujioka
my-rocketchat-rocketchat   fujioka
```

変更するファイルはcomponents/my-app.jsonnetです。`ks generate`で生成されたファイルですね。
カスタマイズするように修正したcomponents/my-app.jsonnetと、元ファイルとのdiffがこちら↓になります。

```jsonnet:components/my-app.jsonnet
local env = std.extVar("__ksonnet/environments");
local params = std.extVar("__ksonnet/params").components["my-app"];

local chart_org = std.prune(std.native("renderHelmChart")(
   // registry name
   "helm-stable",
   // chart name
   "rocketchat",
   // chart version
   params.version,
   // chart values overrides
   params.values,
   // component name
   params.name,
 ));

 [
  w + {
    metadata+: {
      labels+: {
        maintainer: "fujioka"
      },
    },
  }
  for w in chart_org
 ]
```

```diff:components/my-app.jsonnetの差分
diff --git a/components/my-app.jsonnet b/components/my-app.jsonnet
index f3eb80e..c8b1532 100644
--- a/components/my-app.jsonnet
+++ b/components/my-app.jsonnet
@@ -1,7 +1,7 @@
 local env = std.extVar("__ksonnet/environments");
 local params = std.extVar("__ksonnet/params").components["my-app"];
 
-std.prune(std.native("renderHelmChart")(
+local chart_org = std.prune(std.native("renderHelmChart")(
    // registry name
    "helm-stable",
    // chart name
@@ -12,4 +12,15 @@ std.prune(std.native("renderHelmChart")(
    params.values,
    // component name
    params.name,
- ))
+ ));
+
+ [
+  w + {
+    metadata+: {
+      labels+: {
+        maintainer: "fujioka"
+      },
+    },
+  }
+  for w in chart_org
+ ]
```

HelmのChartから生成されたManifestをchart_orgに代入し、あとからちょろっと改造しているだけです。

今回は簡単な変更しかしていませんが、Jsonnetでできる修正であれば何でも可能なので、あなたがカスタマイズしたいようにJsonnetを書けば、あとは好き放題ということになります。
これで、Helm Chartのかゆいところに手が届きますね。

# 終わりに

ksonnetを使わなければHelmのChartがカスタマイズできないというわけではありませんのでご注意くださいませ。
Tillerを介さずデプロイすることになるため、HelmのHistory機能が使えなくなっていますし、そもそもJsonnetを覚えなければなりません。
ぶっちゃけ`helm template`で生成したManifestをkustomizeでoverrideするだけでも事足りるし、簡単だと思います。（悔しいのでそのやり方はここでは書きません。）

# Misc

* ksonnetで作られるファイルはJsonnetで書かれているものなので、さっぱり内容がわからないという方はJsonnetのチュートリアル([https://jsonnet.org/learning/tutorial.html](https://jsonnet.org/learning/tutorial.html))を参考にすると良いでしょう。
    * 逆にJsonnetが読み書きできればかなりksonnetを使うのは楽だと思います。
* Jsonnetをググるときは`-Json.Net`をキーワードに追加したほうが良いと思います。
* VSCodeを使うとある程度補完が効いてManifestを楽に作れるようになるksonnetですが、HelmのChartをレンダリングする処理がksに入っているせいか、補完が効かなくなります。
<img width="987" alt="vscodeエラー.png" src="https://qiita-image-store.s3.amazonaws.com/0/69698/6d86480e-b86b-72d9-c5b1-dd3433c8b2e2.png">

* パラメータの値にnullを指定すると、`ks param set`がエラーを吐きますが、`ks apply`などは普通に出来ますので、ご安心を。

* HelmのHook機構（[これ](https://github.com/helm/helm/blob/master/docs/charts_hooks.md)）により起動されるものは、全て無視される。例えば、pre-installで動くJobがあった場合、そのJobは作られない。