Skaffoldを使ってGROWI on K8s用のElasticsearchとHackMDをビルド＆デプロイ

本記事は「[Kubernetes2 Advent Calendar 2020](https://qiita.com/advent-calendar/2020/kubernetes2)」の8日目のエントリになります。よろしくお願いします。

内容としては、「[Rook/Cephのブロックストレージとオブジェクトストレージを使ってGROWI WikiをKubernetesで動かす](https://qiita.com/zaki-lknr/items/fb5b361d5facb05a6b93)」の続き(機能追加)になります。

# TL;DR

GROWI with "Elasticsearch and HackMD" on KubernetesをSkaffoldを使ってビルド＆デプロイしてみました。

[zaki-lknr/growi-on-k8s: GROWI Wiki on Kubernetes](https://github.com/zaki-lknr/growi-on-k8s)

# 前置き

先日、[Rookと仲間たち、クラウドネイティブなストレージの Advent Calendar 2020](https://qiita.com/advent-calendar/2020/rook-friends-cns)の3日目のエントリで以下の記事を作成しました。

[Rook/Cephのブロックストレージとオブジェクトストレージを使ってGROWI WikiをKubernetesで動かす - Qiita](https://qiita.com/zaki-lknr/items/fb5b361d5facb05a6b93)

この記事でのGROWIは、Elasticsearch連携は(~~`vm.max_map_count`のエラーを解消することができずに~~オブジェクトストレージの記事としては蛇足かと思い)オプションということもあり省略していたので、改めてElasticsearchも設定して検索が使えるGROWI on Kubernetesをデプロイしてみよう、という内容になります。

# GROWI with Elasticsearch

[Docker Compose版のGROWIで使用されるElasticsearch](https://github.com/weseek/growi-docker-compose/blob/6854f2373eef26e21d3a7b111dc6cfb7b10e13cf/docker-compose.yml#L45-L59)の定義ベースにすすめます。

## 日本語プラグイン導入

GROWIで使うために、以下の日本語プラグインを導入します。

- analysis-kuromoji
- analysis-icu

Helmチャートの設定でどうにかできたらベストだったんですが、[READMEのプラグインインストールの項](https://github.com/elastic/helm-charts/tree/master/elasticsearch#how-to-install-plugins)を見る限り、イメージをビルドしないといけないみたいなので、ビルドします。

※ [bitnamiのチャートだと、プラグインを設定で入れられるっぽい](https://github.com/bitnami/charts/blob/715f8e80d703e99090b082586ac3dde1f5369a31/bitnami/elasticsearch/values.yaml#L120-L123)のでビルド不要で使えるかもしれないけど未確認

### Dockerfile

ベースは[GROWIのリポジトリにあるDocker Composeで使っているDockerfile](https://github.com/weseek/growi-docker-compose/blob/3f201d20f3b3057dd6fe8c4b47b6af936adbbe43/elasticsearch/Dockerfile)を使用します。
ただし、この内容でビルドしたイメージをそのままKubernetesにデプロイしても下記エラーで起動に失敗します。

```
ERROR: [1] bootstrap checks failed
[1]: max virtual memory areas vm.max_map_count [65530] is too low, increase to at least [262144]
```

ポイントは、[コンテナ起動時のボリューム設定で挿入している`elasticsearch.yml`ファイル](https://github.com/weseek/growi-docker-compose/blob/6854f2373eef26e21d3a7b111dc6cfb7b10e13cf/docker-compose.yml#L59)による設定。
Kubernetesではコンテナ起動時にこの芸当は(NFSとかで事前に用意したファイルをpvとしてマウントしない限りはたぶん)できないので、イメージに組み込んでビルドします。

よって、Dockerfileはこんな感じ。

```dockerfile:Dockerfile
FROM docker.elastic.co/elasticsearch/elasticsearch:6.8.10

RUN bin/elasticsearch-plugin install analysis-kuromoji
RUN bin/elasticsearch-plugin install analysis-icu

COPY elasticsearch.yml /usr/share/elasticsearch/config/elasticsearch.yml
```

イメージに`COPY`する[`elasticsearch.yml`はこちら](https://github.com/weseek/growi-docker-compose/blob/423613668e94373db10f26b3fa80021009d3f049/elasticsearch/config/elasticsearch.yml)。

```yaml:elasticsearch.yml
http.host: 0.0.0.0

# for elasticsearch-head
http.cors.enabled: true
http.cors.allow-origin: "*"
```

### Skaffoldを使ってbuild & push & deploy (devモード)

Dockerイメージを手元でビルドし、Docker Hubへpushし、Kubernetesクラスタでpullします。
手作業でこれを繰り返すとまぁまぁ面倒なので、今回はこれらを全部一気にやってくれる[Skaffold](https://skaffold.dev/)というビルドツールを使ってみました。
(ずーーっと前から「Skaffoldを試す」というタスクに積んだままだったので、ついでにここで試し見ようというやつです)

[Google発のコンテナアプリケーション開発支援ツール「Skaffold」や「Kaniko」を使ってみる | さくらのナレッジ](https://knowledge.sakura.ad.jp/15321/)

#### Skaffold設定

[Quickstart | Skaffold](https://skaffold.dev/docs/quickstart/)

インストールは(Linuxなら)バイナリをダウンロードしてパスの通ったパス(`/usr/local/bin`など)に置けばOKです。  
Skaffoldコマンドを使うための準備としては

1. `docker login`しておき、`skaffold`実行ユーザアカウントでコンテナレジストリへ`push`ができる状態にする
1. `$HOME/.kube/config`を設定しておき、`skaffold`実行ユーザアカウントで`kubectl`を使ったクラスタへのマニフェストのデプロイができる状態にする

の2点です。(2については`helm`や`istioctl`と同じ要領。たぶん)

`docker push`を行うので、コンテナレジストリにDocker Hubを使用する場合はアカウント作成と、push先のリポジトリを用意しておく必要があります。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/256859/081f4212-1430-a22a-b875-2c2796463fbb.png)

#### Skaffoldビルド・デプロイ設定

前述の`Dockerfile`と`Dockerfile`から参照(イメージへコピー)する外部ファイル`elasticsearch.yml`と同じディレクトリ(←構成を簡単にするため)に以下の「デプロイ用Kubernetesマニフェストファイル」と「Skaffold定義ファイル」を作成します。

デプロイ用Kubernetesマニフェストファイルは特に通常通りなので省略。
[例えばDeploymentを使ってこんな感じ(あとでStatefulSetに変更しました)](https://github.com/zaki-lknr/growi-on-k8s/blob/bce2c6814794db110652fac7948761b2a0281fb6/elasticsearch/k8s-deploy.yaml)。

`skaffold.yaml`については、一つのイメージをbuild/push/deployする最小限な記述としてはこんな感じ。

```yaml:skaffold.yaml
apiVersion: skaffold/v1
kind: Config
build:
  artifacts:
  - image: zakihmkc/growi-elasticsearch
    context: ./
    docker:
      dockerfile: ./Dockerfile
deploy:
  kubectl:
    manifests:
    - ./k8s-deploy.yaml
```

[詳細はリファレンス](https://skaffold.dev/docs/references/yaml/)を確認してください。

[skaffold.yaml | Skaffold](https://skaffold.dev/docs/references/yaml/)

ポイントは`build`セクションの`artifacts`で、ビルドするイメージの名前==`push`先のリポジトリ名を設定しておくことで、`docker build`から`docker push`まで一気に行われます。
ビルドの`context`と`docker.dockerfile`はDocker Composeでビルドするときと同じでディレクトリの指定と`Dockerfile`の指定。「開発」「ステージング」「本番」のようにファイルを分けるときに使うと良いです。

また、`deploy`セクションでpushされたイメージをクラスターへデプロイするためのマニフェストファイルを指定することで、pushに成功した後にデプロイまで処理されます。

devモードで実行するには、サブコマンド`dev`を指定して実行します。
同じdevという名前にして分かりにくいけど、ビルドするイメージにテンポラリのタグを付与するために`-t dev`を追加。

```
[zaki@cloud-dev elasticsearch]$ ls
Dockerfile  elasticsearch.yml  k8s-deploy.yaml  skaffold.yaml
[zaki@cloud-dev elasticsearch]$ skaffold dev -t dev 
Listing files to watch...
 - zakihmkc/growi-elasticsearch
Generating tags...
 - zakihmkc/growi-elasticsearch -> zakihmkc/growi-elasticsearch:dev
Checking cache...
 - zakihmkc/growi-elasticsearch: Not found. Building
Building [zakihmkc/growi-elasticsearch]...
Sending build context to Docker daemon  3.072kB
Step 1/4 : FROM docker.elastic.co/elasticsearch/elasticsearch:6.8.10
 ---> ffa00077159c
Step 2/4 : RUN bin/elasticsearch-plugin install analysis-kuromoji
 ---> Using cache
 ---> 6966b4d5e5bf
Step 3/4 : RUN bin/elasticsearch-plugin install analysis-icu
 ---> Using cache
 ---> 7e8711478ef2
Step 4/4 : COPY elasticsearch.yml /usr/share/elasticsearch/config/elasticsearch.yml
 ---> 11556383ec5f
Successfully built 11556383ec5f
Successfully tagged zakihmkc/growi-elasticsearch:dev
The push refers to repository [docker.io/zakihmkc/growi-elasticsearch]
06e6281e2c86: Preparing
c2255a3188da: Preparing

[... snip ...]

edf3aa290fb3: Layer already exists
06e6281e2c86: Pushed
dev: digest: sha256:c260d4c6f595b15163c9f7a3d88fcb6f790995f9f63ac519baaa94af49bc5bf2 size: 2421
Tags used in deployment:
 - zakihmkc/growi-elasticsearch -> zakihmkc/growi-elasticsearch:dev@sha256:c260d4c6f595b15163c9f7a3d88fcb6f790995f9f63ac519baaa94af49bc5bf2
Starting deploy...
 - deployment.apps/elasticsearch created
 - service/elasticsearch created
Waiting for deployments to stabilize...
 - deployment/elasticsearch: creating container elasticsearch
    - pod/elasticsearch-76d9c6d6f6-nqtjs: creating container elasticsearch
 - deployment/elasticsearch is ready.
Deployments stabilized in 5.595258289s
Press Ctrl+C to exit
Watching for changes...
[elasticsearch] [2020-12-04T14:10:06,213][INFO ][o.e.e.NodeEnvironment    ] [TQOy5aT] using [1] data paths, mounts [[/ (rootfs)]], net usable_space [42.1gb], net total_space [58.9gb], types [rootfs]
[elasticsearch] [2020-12-04T14:10:06,215][INFO ][o.e.e.NodeEnvironment    ] [TQOy5aT] heap size [1gb], compressed ordinary object pointers [true]
[elasticsearch] [2020-12-04T14:10:06,218][INFO ][o.e.n.Node               ] [TQOy5aT] node name derived from node ID [TQOy5aTYQuKGZ35Crwr6Iw]; set [node.name] to override
[elasticsearch] [2020-12-04T14:10:06,218][INFO ][o.e.n.Node               ] [TQOy5aT] version[6.8.10], pid[1], build[default/docker/537cb22/2020-05-28T14:47:19.882936Z], OS[Linux/3.10.0-1127.13.1.el7.x86_64/amd64], JVM[AdoptOpenJDK/OpenJDK 64-Bit Server VM/14.0.1/14.0.1+7]
[elasticsearch] [2020-12-04T14:10:06,218][INFO ][o.e.n.Node               ] [TQOy5aT] JVM arguments [-Xms1g, -Xmx1g, -XX:+UseG1GC, -XX:G1ReservePercent=25, -XX:InitiatingHeapOccupancyPercent=30, -Des.networkaddress.cache.ttl=60, -Des.networkaddress.cache.negative.ttl=10, -XX:+AlwaysPreTouch, -Xss1m, -Djava.awt.headless=true, -Dfile.encoding=UTF-8, -Djna.nosys=true, -XX:-OmitStackTraceInFastThrow, -XX:+ShowCodeDetailsInExceptionMessages, -Dio.netty.noUnsafe=true, -Dio.netty.noKeySetOptimization=true, -Dio.netty.recycler.maxCapacityPerThread=0, -Dlog4j.shutdownHookEnabled=false, -Dlog4j2.disable.jmx=true, -Djava.io.tmpdir=/tmp/elasticsearch-16478887452689113828, -XX:+HeapDumpOnOutOfMemoryError, -XX:HeapDumpPath=data, -XX:ErrorFile=logs/hs_err_pid%p.log, -Xlog:gc*,gc+age=trace,safepoint:file=logs/gc.log:utctime,pid,tags:filecount=32,filesize=64m, -Djava.locale.providers=COMPAT, -XX:UseAVX=2, -Des.cgroups.hierarchy.override=/, -Des.path.home=/usr/share/elasticsearch, -Des.path.conf=/usr/share/elasticsearch/config, -Des.distribution.flavor=default, -Des.distribution.type=docker]
[elasticsearch] [2020-12-04T14:10:09,582][INFO ][o.e.p.PluginsService     ] [TQOy5aT] loaded module [aggs-matrix-stats]
[elasticsearch] [2020-12-04T14:10:09,582][INFO ][o.e.p.PluginsService     ] [TQOy5aT] loaded module [analysis-common]
[elasticsearch] [2020-12-04T14:10:09,582][INFO ][o.e.p.PluginsService     ] [TQOy5aT] loaded module [ingest-common]
[elasticsearch] [2020-12-04T14:10:09,582][INFO ][o.e.p.PluginsService     ] [TQOy5aT] loaded module [ingest-geoip]
[elasticsearch] [2020-12-04T14:10:09,582][INFO ][o.e.p.PluginsService     ] [TQOy5aT] loaded module [ingest-user-agent]
[elasticsearch] [2020-12-04T14:10:09,583][INFO ][o.e.p.PluginsService     ] [TQOy5aT] loaded module [lang-expression]
[elasticsearch] [2020-12-04T14:10:09,583][INFO ][o.e.p.PluginsService     ] [TQOy5aT] loaded module [lang-mustache]
[elasticsearch] [2020-12-04T14:10:09,583][INFO ][o.e.p.PluginsService     ] [TQOy5aT] loaded module [lang-painless]
[elasticsearch] [2020-12-04T14:10:09,584][INFO ][o.e.p.PluginsService     ] [TQOy5aT] loaded module [mapper-extras]
[elasticsearch] [2020-12-04T14:10:09,584][INFO ][o.e.p.PluginsService     ] [TQOy5aT] loaded module [parent-join]
[elasticsearch] [2020-12-04T14:10:09,584][INFO ][o.e.p.PluginsService     ] [TQOy5aT] loaded module [percolator]
[elasticsearch] [2020-12-04T14:10:09,584][INFO ][o.e.p.PluginsService     ] [TQOy5aT] loaded module [rank-eval]
[elasticsearch] [2020-12-04T14:10:09,584][INFO ][o.e.p.PluginsService     ] [TQOy5aT] loaded module [reindex]
[elasticsearch] [2020-12-04T14:10:09,584][INFO ][o.e.p.PluginsService     ] [TQOy5aT] loaded module [repository-url]
[elasticsearch] [2020-12-04T14:10:09,584][INFO ][o.e.p.PluginsService     ] [TQOy5aT] loaded module [transport-netty4]
[elasticsearch] [2020-12-04T14:10:09,584][INFO ][o.e.p.PluginsService     ] [TQOy5aT] loaded module [tribe]
[elasticsearch] [2020-12-04T14:10:09,584][INFO ][o.e.p.PluginsService     ] [TQOy5aT] loaded module [x-pack-ccr]
[elasticsearch] [2020-12-04T14:10:09,584][INFO ][o.e.p.PluginsService     ] [TQOy5aT] loaded module [x-pack-core]
[elasticsearch] [2020-12-04T14:10:09,584][INFO ][o.e.p.PluginsService     ] [TQOy5aT] loaded module [x-pack-deprecation]
[elasticsearch] [2020-12-04T14:10:09,584][INFO ][o.e.p.PluginsService     ] [TQOy5aT] loaded module [x-pack-graph]
[elasticsearch] [2020-12-04T14:10:09,585][INFO ][o.e.p.PluginsService     ] [TQOy5aT] loaded module [x-pack-ilm]
[elasticsearch] [2020-12-04T14:10:09,585][INFO ][o.e.p.PluginsService     ] [TQOy5aT] loaded module [x-pack-logstash]
[elasticsearch] [2020-12-04T14:10:09,585][INFO ][o.e.p.PluginsService     ] [TQOy5aT] loaded module [x-pack-ml]
[elasticsearch] [2020-12-04T14:10:09,585][INFO ][o.e.p.PluginsService     ] [TQOy5aT] loaded module [x-pack-monitoring]
[elasticsearch] [2020-12-04T14:10:09,585][INFO ][o.e.p.PluginsService     ] [TQOy5aT] loaded module [x-pack-rollup]
[elasticsearch] [2020-12-04T14:10:09,585][INFO ][o.e.p.PluginsService     ] [TQOy5aT] loaded module [x-pack-security]
[elasticsearch] [2020-12-04T14:10:09,585][INFO ][o.e.p.PluginsService     ] [TQOy5aT] loaded module [x-pack-sql]
[elasticsearch] [2020-12-04T14:10:09,585][INFO ][o.e.p.PluginsService     ] [TQOy5aT] loaded module [x-pack-upgrade]
[elasticsearch] [2020-12-04T14:10:09,586][INFO ][o.e.p.PluginsService     ] [TQOy5aT] loaded module [x-pack-watcher]
[elasticsearch] [2020-12-04T14:10:09,595][INFO ][o.e.p.PluginsService     ] [TQOy5aT] loaded plugin [analysis-icu]
[elasticsearch] [2020-12-04T14:10:09,595][INFO ][o.e.p.PluginsService     ] [TQOy5aT] loaded plugin [analysis-kuromoji]
[elasticsearch] [2020-12-04T14:10:16,226][INFO ][o.e.x.s.a.s.FileRolesStore] [TQOy5aT] parsed [0] roles from file [/usr/share/elasticsearch/config/roles.yml]
[elasticsearch] [2020-12-04T14:10:17,251][INFO ][o.e.x.m.p.l.CppLogMessageHandler] [TQOy5aT] [controller/79] [Main.cc@109] controller (64 bit): Version 6.8.10 (Build a5f7163bca0250) Copyright (c) 2020 Elasticsearch BV
[elasticsearch] [2020-12-04T14:10:17,994][INFO ][o.e.d.DiscoveryModule    ] [TQOy5aT] using discovery type [zen] and host providers [settings]
[elasticsearch] [2020-12-04T14:10:18,943][INFO ][o.e.n.Node               ] [TQOy5aT] initialized
[elasticsearch] [2020-12-04T14:10:18,943][INFO ][o.e.n.Node               ] [TQOy5aT] starting ...
[elasticsearch] [2020-12-04T14:10:19,069][INFO ][o.e.t.TransportService   ] [TQOy5aT] publish_address {127.0.0.1:9300}, bound_addresses {127.0.0.1:9300}
[elasticsearch] [2020-12-04T14:10:19,086][WARN ][o.e.b.BootstrapChecks    ] [TQOy5aT] max virtual memory areas vm.max_map_count [65530] is too low, increase to at least [262144]
[elasticsearch] [2020-12-04T14:10:22,134][INFO ][o.e.c.s.MasterService    ] [TQOy5aT] zen-disco-elected-as-master ([0] nodes joined), reason: new_master {TQOy5aT}{TQOy5aTYQuKGZ35Crwr6Iw}{mDuQTY1KS1q1Es7e0DDjLA}{127.0.0.1}{127.0.0.1:9300}{ml.machine_memory=8181829632, xpack.installed=true, ml.max_open_jobs=20, ml.enabled=true}
[elasticsearch] [2020-12-04T14:10:22,138][INFO ][o.e.c.s.ClusterApplierService] [TQOy5aT] new_master {TQOy5aT}{TQOy5aTYQuKGZ35Crwr6Iw}{mDuQTY1KS1q1Es7e0DDjLA}{127.0.0.1}{127.0.0.1:9300}{ml.machine_memory=8181829632, xpack.installed=true, ml.max_open_jobs=20, ml.enabled=true}, reason: apply cluster state (from master [master {TQOy5aT}{TQOy5aTYQuKGZ35Crwr6Iw}{mDuQTY1KS1q1Es7e0DDjLA}{127.0.0.1}{127.0.0.1:9300}{ml.machine_memory=8181829632, xpack.installed=true, ml.max_open_jobs=20, ml.enabled=true} committed version [1] source [zen-disco-elected-as-master ([0] nodes joined)]])
[elasticsearch] [2020-12-04T14:10:22,197][INFO ][o.e.h.n.Netty4HttpServerTransport] [TQOy5aT] publish_address {10.244.127.102:9200}, bound_addresses {0.0.0.0:9200}
[elasticsearch] [2020-12-04T14:10:22,197][INFO ][o.e.n.Node               ] [TQOy5aT] started
[elasticsearch] [2020-12-04T14:10:22,223][WARN ][o.e.x.s.a.s.m.NativeRoleMappingStore] [TQOy5aT] Failed to clear cache for realms [[]]
[elasticsearch] [2020-12-04T14:10:22,262][INFO ][o.e.g.GatewayService     ] [TQOy5aT] recovered [0] indices into cluster_state
[elasticsearch] [2020-12-04T14:10:22,874][INFO ][o.e.c.m.MetaDataIndexTemplateService] [TQOy5aT] adding template [.watches] for index patterns [.watches*]
[elasticsearch] [2020-12-04T14:10:22,941][INFO ][o.e.c.m.MetaDataIndexTemplateService] [TQOy5aT] adding template [.watch-history-9] for index patterns [.watcher-history-9*]
[elasticsearch] [2020-12-04T14:10:22,964][INFO ][o.e.c.m.MetaDataIndexTemplateService] [TQOy5aT] adding template [.triggered_watches] for index patterns [.triggered_watches*]
[elasticsearch] [2020-12-04T14:10:22,992][INFO ][o.e.c.m.MetaDataIndexTemplateService] [TQOy5aT] adding template [.monitoring-logstash] for index patterns [.monitoring-logstash-6-*]
[elasticsearch] [2020-12-04T14:10:23,044][INFO ][o.e.c.m.MetaDataIndexTemplateService] [TQOy5aT] adding template [.monitoring-es] for index patterns [.monitoring-es-6-*]
[elasticsearch] [2020-12-04T14:10:23,092][INFO ][o.e.c.m.MetaDataIndexTemplateService] [TQOy5aT] adding template [.monitoring-alerts] for index patterns [.monitoring-alerts-6]
[elasticsearch] [2020-12-04T14:10:23,255][INFO ][o.e.c.m.MetaDataIndexTemplateService] [TQOy5aT] adding template [.monitoring-beats] for index patterns [.monitoring-beats-6-*]
[elasticsearch] [2020-12-04T14:10:23,310][INFO ][o.e.c.m.MetaDataIndexTemplateService] [TQOy5aT] adding template [.monitoring-kibana] for index patterns [.monitoring-kibana-6-*]
[elasticsearch] [2020-12-04T14:10:23,479][INFO ][o.e.l.LicenseService     ] [TQOy5aT] license [93d6e9a7-978d-4035-8052-e26c27e49afe] mode [basic] - valid
```

ここで表示が止まってますが、これ、`kubectl logs -f`のログが出ています。
別のターミナルで`kubectl get pod`すれば以下の通り。

```
[zaki@cloud-dev elasticsearch]$ kubectl get pod
NAME                             READY   STATUS    RESTARTS   AGE
elasticsearch-76d9c6d6f6-nqtjs   1/1     Running   0          5m41s
```

ちゃんと動いています。

ログの最後じゃないので分かりづらいけど、多分このログが起動成功の目印だと思う。(多分)

```
[elasticsearch] [2020-12-04T14:10:22,197][INFO ][o.e.h.n.Netty4HttpServerTransport] [TQOy5aT] publish_address {10.244.127.102:9200}, bound_addresses {0.0.0.0:9200}
[elasticsearch] [2020-12-04T14:10:22,197][INFO ][o.e.n.Node               ] [TQOy5aT] started
```

実行中のイメージのtagはこの通りだけど、ハッシュ値が付与されている状態。

```
[zaki@cloud-dev elasticsearch]$ kubectl get pod elasticsearch-76d9c6d6f6-nqtjs -o jsonpath='{.spec.containers[].image}'; echo
zakihmkc/growi-elasticsearch:dev@sha256:c260d4c6f595b15163c9f7a3d88fcb6f790995f9f63ac519baaa94af49bc5bf2
```

ビルドしたイメージが正常に動作するのが確認できたので`Ctrl-c`で停止します。

```
[elasticsearch] [2020-12-04T14:10:23,479][INFO ][o.e.l.LicenseService     ] [TQOy5aT] license [93d6e9a7-978d-4035-8052-e26c27e49afe] mode [basic] - valid
^CCleaning up...
 - deployment.apps "elasticsearch" deleted
 - service "elasticsearch" deleted
There is a new version (1.17.1) of Skaffold available. Download it from:
  https://github.com/GoogleContainerTools/skaffold/releases/tag/v1.17.1

```

するとpodも停止します。
便利ですね。

```
[zaki@cloud-dev elasticsearch]$ kubectl get pod
NAME                             READY   STATUS        RESTARTS   AGE
elasticsearch-76d9c6d6f6-nqtjs   1/1     Terminating   0          11m
```

pushされたイメージはこの通り。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/256859/e2b219f8-0066-3371-7ab7-fbf30a2b25d4.png)

そして、Skaffoldのdevモードが更に便利なところは、`Ctrl-c`で停止するまの待機状態の間は、Dockerfileやデプロイ用マニフェストを変更すると、ファイルの変更を検知してリビルドから再デプロイまで自動でやってくれるため、開発中のトライアルアンドエラーの最中はデプロイ作業に時間を割かずに済むため、開発に集中できます。

### runモードでbuild & push & deploy

引数を`dev`から`run`に変更します。
ついでに、tagもリリースバージョンっぽくしてみます。

```
[zaki@cloud-dev elasticsearch]$ skaffold run -t 6.8.10-ja
Generating tags...
 - zakihmkc/growi-elasticsearch -> zakihmkc/growi-elasticsearch:6.8.10-ja
Checking cache...
 - zakihmkc/growi-elasticsearch: Found. Tagging
Tags used in deployment:
 - zakihmkc/growi-elasticsearch -> zakihmkc/growi-elasticsearch:6.8.10-ja@sha256:c260d4c6f595b15163c9f7a3d88fcb6f790995f9f63ac519baaa94af49bc5bf2
Starting deploy...
 - deployment.apps/elasticsearch created
 - service/elasticsearch created
Waiting for deployments to stabilize...
 - deployment/elasticsearch is ready.
Deployments stabilized in 3.40209576s
You can also run [skaffold run --tail] to get the logs
There is a new version (1.17.1) of Skaffold available. Download it from:
  https://github.com/GoogleContainerTools/skaffold/releases/tag/v1.17.1

[zaki@cloud-dev elasticsearch]$ 
```

今度はバックグラウンドで処理が行われるため、プロンプトが返ってきます。

```
[zaki@cloud-dev elasticsearch]$ kubectl get pod
NAME                            READY   STATUS    RESTARTS   AGE
elasticsearch-89688ff58-tw969   1/1     Running   0          13s
```

新しいtagもpushされています。(中身は変わっていないので`dev`と同じ)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/256859/823f6bd0-82d1-6fcd-e90c-131ebd23ed35.png)

### runモードでデプロイしたpod類の削除

停止・削除するには、`skaffold delete`を実行します。

```
[zaki@cloud-dev elasticsearch]$ skaffold delete 
Cleaning up...
 - deployment.apps "elasticsearch" deleted
 - service "elasticsearch" deleted
```

### StatefulSetバージョン

最初からそうすればよかったのにと思いつつ、ワークロードを[StatefulSetに変更](https://github.com/zaki-lknr/growi-on-k8s/commit/6fff9b4fad9d5dac60cb94b2d324afab8ac3251e)してデプロイ。

また、実行時に`-n <namespace>`を付与して、デプロイ先をGROWI本体と同じネームスペースを指定しています。

```
[zaki@cloud-dev elasticsearch (features/elasticsearch)]$ skaffold run -t 6.8.10-ja -n growi-on-k8s
Generating tags...
 - zakihmkc/growi-elasticsearch -> zakihmkc/growi-elasticsearch:6.8.10-ja
Checking cache...
 - zakihmkc/growi-elasticsearch: Found Remotely
Tags used in deployment:
 - zakihmkc/growi-elasticsearch -> zakihmkc/growi-elasticsearch:6.8.10-ja@sha256:c260d4c6f595b15163c9f7a3d88fcb6f790995f9f63ac519baaa94af49bc5bf2
Starting deploy...
 - statefulset.apps/elasticsearch created
 - service/elasticsearch created
Waiting for deployments to stabilize...
Deployments stabilized in 10.508539ms
You can also run [skaffold run --tail] to get the logs
There is a new version (1.17.1) of Skaffold available. Download it from:
  https://github.com/GoogleContainerTools/skaffold/releases/tag/v1.17.1

[zaki@cloud-dev elasticsearch (features/elasticsearch)]$ kc get pod,svc,pvc -n growi-on-k8s 
NAME                            READY   STATUS              RESTARTS   AGE
pod/elasticsearch-0             0/1     ContainerCreating   0          9s
pod/growi-app-fd6fcdd9d-vsdjj   1/1     Running             14         7d
pod/growi-mongo-0               1/1     Running             3          7d

NAME                    TYPE           CLUSTER-IP       EXTERNAL-IP     PORT(S)          AGE
service/elasticsearch   ClusterIP      10.99.155.247    <none>          9200/TCP         9s
service/growi-app       LoadBalancer   10.96.23.173     192.168.0.185   3000:32644/TCP   7d
service/growi-mongo     ClusterIP      10.106.230.143   <none>          27017/TCP        7d

NAME                                               STATUS   VOLUME                                     CAPACITY   ACCESS MODES   STORAGECLASS      AGE
persistentvolumeclaim/es-data-elasticsearch-0      Bound    pvc-f052985f-b3a9-408f-8343-d09b1cbf8ff7   8Gi        RWO            rook-ceph-block   9s
persistentvolumeclaim/mongo-config-growi-mongo-0   Bound    pvc-6b29acb4-bad6-4b01-9135-4302bf1f35d3   256Mi      RWO            rook-ceph-block   7d1h
persistentvolumeclaim/mongo-db-growi-mongo-0       Bound    pvc-21ea3319-5487-4b10-83d3-a9c076d30b28   4Gi        RWO            rook-ceph-block   7d1h
```

## GROWI WikiのElasticsearch設定

Elasticsearchの設定が無い場合はこの通り。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/256859/fb598455-935c-3c2e-76e2-cba5d93062a1.png)

GROWIにElasticsearchを組み込むには、[Docker Compose版の`docker-compose.yml`の通り](https://github.com/weseek/growi-docker-compose/blob/6854f2373eef26e21d3a7b111dc6cfb7b10e13cf/docker-compose.yml#L18)、環境変数`ELASTICSEARCH_URI`にElasticsearchのURLを設定します。
Kubernetes環境ではService経由のpod間通信になるので、Service名(elasticsearch)を記述します。

[マニフェストはこんな感じ](https://github.com/zaki-lknr/growi-on-k8s/blob/features/elasticsearch/manifests/growi-on-k8s.yaml#L24-L25)

```yaml:growi-on-k8s.yaml抜粋
      containers:
      - image: weseek/growi:4
        name: growi-app
        env:
          - name: MONGO_URI
            value: mongodb://growi-mongo:27017/growi
          - name: ELASTICSEARCH_URI
            value: http://elasticsearch:9200/growi
          - name: PASSWORD_SEED
            value: changeme
```

このマニフェストでデプロイし直せば、この通り画面上部に検索フィールドが表示されます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/256859/3fd10b2f-0eed-5763-2c28-f7f877d12248.png)

設定のメニューの「全文検索管理」を表示すると、この通りElasticsearchが有効になっています。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/256859/8d6f56ec-3f63-dccf-e148-07cd5420bf29.png)

これで、GROWIとElasticsearchが連携設定され、検索が使えるようになりました。

# GROWI with HackMD

GROWIのWikiはデフォルトでは同じページを複数ユーザーで同時編集は出来ません。
(やってしまった場合は、後から保存しようとしたユーザー側でconflictとなり、手動でマージする必要があります)

ただし、HackMDと連携することで、同時編集ができるようにする機能が用意されているので、(これはイメージのビルドが必要だけど日本語プラグイン入りElasticsearchでイメージビルドしてるのでそのままのノリで)GROWI on Kubernetesでもやってみます。

- [HackMD で同時多人数編集を利用する | GROWI Docs](https://docs.growi.org/ja/guide/features/hackmd.html)
- [HackMD(CodiMD)連携 | GROWI Docs](https://docs.growi.org/ja/admin-guide/admin-cookbook/integrate-with-hackmd.html)

HackMD連携も、[公式のDocker Compose](https://github.com/weseek/growi-docker-compose/tree/master/examples/integrate-with-hackmd)が公開されています。
現在の[docker-compose.yml](https://github.com/weseek/growi-docker-compose/blob/18d8f135d8afbcfd6d7cf73b4d6c47a9bca64981/examples/integrate-with-hackmd/docker-compose.override.yml)の内容を整理すると、以下の通り。

1. [MariaDBをデプロイ](https://github.com/weseek/growi-docker-compose/blob/18d8f135d8afbcfd6d7cf73b4d6c47a9bca64981/examples/integrate-with-hackmd/docker-compose.override.yml#L24-L34)
2. [GROWI用HackMDイメージをビルド](https://github.com/weseek/growi-docker-compose/blob/054eab1bb92f6843cdaf20b6e8c8b30b1a1ba47d/hackmd/Dockerfile)
3. GROWI用HackMDをデプロイ
    - [環境変数は以下の通り](https://github.com/weseek/growi-docker-compose/blob/18d8f135d8afbcfd6d7cf73b4d6c47a9bca64981/examples/integrate-with-hackmd/docker-compose.override.yml#L11-L14)
        - GROWI本体のブラウザからアクセスする際のURL
        - MariaDB接続先
    - HackMD単体でブラウザからアクセスできるようにしておく
4. GROWI本体の環境変数追加
    - GROWI本体(pod)から見たHackMDのURL
    - ブラウザからアクセスする際のHackMDのURL

## MariaDB

Helmとかで別途入れてもいい気もするけど、GROWIのdocker-compose.ymlだとcharactersetの引数設定があるので、これベースにマニフェスト作成。

```yaml:docker-compose.override.yml抜粋
  mariadb:
    image: mariadb:10.3
    command: mysqld --character-set-server=utf8 --collation-server=utf8_general_ci
    environment:
      - MYSQL_USER=hackmd
      - MYSQL_PASSWORD=hackmdpass
      - MYSQL_DATABASE=hackmd
      - MYSQL_RANDOM_ROOT_PASSWORD=true
```

ただしこれをそのままKubernetes用マニフェストに書き換えると、以下のエラーでデプロイに失敗します。

```
2020-12-05  0:45:49 0 [Note] mysqld (mysqld 10.3.27-MariaDB-1:10.3.27+maria~focal) starting as process 1 ...
mysqld: Please consult the Knowledge Base to find out how to run mysqld as root!
2020-12-05  0:45:49 0 [ERROR] Aborting
```

ポイントは`command`の部分。

[Kubernetesのマニフェストでは`command`でなく`args`を使う](https://github.com/zaki-lknr/growi-on-k8s/blob/features/hackmd/hackmd/k8s-deploy.yaml#L22-L25)とうまく動作します。

```yaml:mariadbマニフェスト抜粋
    spec:
      containers:
      - image: mariadb:10.3
        name: hackmd
        args:
          - mysqld
          - --character-set-server=utf8
          - --collation-server=utf8_general_ci
        env:
          - name: MYSQL_USER
            value: hackmd
          - name: MYSQL_PASSWORD
```

## GROWI用HackMDイメージビルドとデプロイ

これもSkaffoldを使います。
Dockerfileや、ビルド時にイメージに組み込む設定ファイル類は公式で公開されているものをそのまま使用できます。

<https://github.com/weseek/growi-docker-compose/tree/master/hackmd>

このファイル類に加えて、Elasticsearchのビルドと同様に以下のファイルを準備。

- [Kubernetesのマニフェスト](https://github.com/zaki-lknr/growi-on-k8s/blob/64b3a0662c444d0023c2299834acf9e5b93af949/hackmd/k8s-deploy.yaml)
- [skaffol.yaml](https://github.com/zaki-lknr/growi-on-k8s/blob/64b3a0662c444d0023c2299834acf9e5b93af949/hackmd/skaffold.yaml)

[環境変数にGROWI本体の(ブラウザからアクセスする際の)URL設定が必要](https://github.com/zaki-lknr/growi-on-k8s/blob/features/hackmd/hackmd/k8s-deploy.yaml#L88-L89)なので、設定しておきます。

build/push/deployの手順・内容はElasticsearchと同じなので省略します。
[作成したファイル一式はGitHub参照](https://github.com/zaki-lknr/growi-on-k8s/tree/64b3a0662c444d0023c2299834acf9e5b93af949/hackmd)してください。

デプロイに成功しtype:LoadBalancerなどを使って外部公開すると、以下のようにブラウザでアクセスできます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/256859/aa7b8ed6-ce68-b018-f515-41e8d2638a41.png)

GROWIの設定時に、ブラウザからアクセスするためのURLが必要なので確認しておきます。

## GROWI WikiのHackMD設定

前述の通り、[GROWI側にもHackMDがどこにいるかの設定](https://github.com/zaki-lknr/growi-on-k8s/blob/features/hackmd/manifests/growi-on-k8s.yaml#L28-L31)が必要なので、環境変数を追加します。
`HACKMD_URI_FOR_SERVER`はElasticsearchと同じく、GROWIのpodからHackMDのpodへのpod間通信のためService名を記述します。
`HACKMD_URI`はブラウザからアクセスする際のHackMDのURLを設定します。

```yaml:環境変数設定抜粋
      containers:
      - image: weseek/growi:4
        name: growi-app
        env:
          - name: MONGO_URI
            value: mongodb://growi-mongo:27017/growi
          - name: ELASTICSEARCH_URI
            value: http://elasticsearch:9200/growi
          - name: PASSWORD_SEED
            value: changeme
          - name: HACKMD_URI_FOR_SERVER
            value: http://hackmd:3000
          - name: HACKMD_URI
            value: http://192.168.0.186:3000
```

この設定でGROWI本体を再デプロイしてブラウザでアクセス、ページ編集画面の隣にある「HackMD」ボタンを押下、

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/256859/caac08f4-0a15-19c1-a730-662b828a6f0f.png)

すると以下のように、HackMDを使って編集開始するためのボタンが表示されます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/256859/3c7e2df2-9637-b701-f54e-3def3a5dee51.png)

ボタン押下すると、

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/256859/d62edaaf-8f53-82bf-9e00-92466b8010a9.png)

あれ……？

残念ながらこれ原因が分かりませんでしたが、ChromeとEdgeだとダメでしたが、Firefoxだとアクセスできました。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/256859/7f8c50f5-1ec7-7973-a412-5b0b723f1914.png)

もう一つブラウザを起動してアクセスすると、画面右上のオンライン数が2になり、それぞれのブラウザでの編集状態(カーソルなど)が表示されます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/256859/7c9c7be7-cee8-e982-e427-b65bf0f06f57.png)

ちなみにDocker Compose版だとChromeでもアクセスできるので、今回のtype:LoadBalancerを使用したKubernetes環境だと、GROWI本体とHackMDがブラウザから見ると別のIPアドレス(ホスト)にデプロイされていてかつHTTPSでもないので、Chromeのセキュリティ機能的な制限かなと思ってますが、このあたりで力尽きました。
(GROWIもHackMDも`kubectl logs`で確認する限りそれっぽいエラーログは何も出ていない)

Chromeのデベロッパーツールで見るとこんな感じで、`AUTH failed: No cookie transmitted.`というエラーが出ています。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/256859/b1beb631-63bf-4406-36ee-1bb3240185f7.png)

# まとめと課題

Skaffoldを使うことで、コンテナイメージのbuild/push/deployを一度に出来るので、大変効率的になります。
特にdevモードの場合は、ファイルの変更を検知して自動でイメージのリビルドからクラスタへのデプロイまで行われるので、コードの作成に集中できます。

GROWI on Kubernetesについても、ElasticsearchおよびHackMDをKubernetesへデプロイすることで、検索および同時編集が可能になりました。
今回はtype:LoadBalancerを使ってGROWI本体とHackMDを外部公開したため、Chromeアクセス時に期待する動作を確認できませんでしたが、Ingressなど別の方法で実装すると改善されるかもしれません。

またtype:LoadBalancerの欠点として、デプロイするまで外部アクセス用のアドレスが分からないため、GROWI本体とHackMDでお互いの外部アクセス用アドレスを初回デプロイ前は設定することができません。
これも改善の余地があり、Ingressでできるかもしれないし、[ExternalDNS](https://github.com/kubernetes-sigs/external-dns)も使えるかもしれないので検討してみたいです。

[自宅 k8s クラスタのサービスに FQDN で繋がるようにした - ののし.log](https://nnstt1.hatenablog.com/entry/2020/11/13/070000)

---

# (参考) エラーパターン

## Elasticsearch

```
[elasticsearch] ERROR: [1] bootstrap checks failed
[elasticsearch] [1]: max virtual memory areas vm.max_map_count [65530] is too low, increase to at least [262144]
```

このエラーは

```Dockerfile:Dockerfile
FROM docker.elastic.co/elasticsearch/elasticsearch:6.8.10

RUN bin/elasticsearch-plugin install analysis-kuromoji
RUN bin/elasticsearch-plugin install analysis-icu
```

この内容のDockerfileで日本語プラグインがインストールされたカスタムイメージをKubernetesにデプロイすると発生しました。

以下のファイルを`/usr/share/elasticsearch/config/elasticsearch.yml`置いておくと回避できます。

```yaml:/usr/share/elasticsearch/config/elasticsearch.yml
http.host: 0.0.0.0

# for elasticsearch-head
http.cors.enabled: true
http.cors.allow-origin: "*"
```

## MariaDB

Docker Composeで`command`を使ってデプロイすると、元々のイメージで指定されていた`docker-entrypoint.sh`と`command`指定の引数がセットで適用されるけれど、Kubernetesのpodのパラメタで`command`を使うと、どうやら`docker-entrypoint.sh`の情報が上書きされて`mysqld`を直接実行して起動に失敗します。

用意されているDocker Composeで正常動作しているMariaDBのコンテナを`docker inspect`でパラメタ類を確認すると以下の通り。

```json:docker-composeでデプロイしたmariadbコンテナのdocker-inspect抜粋
{
            "Cmd": [
                "mysqld",
                "--character-set-server=utf8",
                "--collation-server=utf8_general_ci"
            ],
            "Image": "mariadb:10.3",
            "Volumes": {
                "/var/lib/mysql": {}
            },
            "WorkingDir": "",
            "Entrypoint": [
                "docker-entrypoint.sh"
            ],
}
```

Kubernetes上で`command`指定でcharset等の指定を行った場合のエラーメッセージ

```
2020-12-05  0:45:49 0 [Note] mysqld (mysqld 10.3.27-MariaDB-1:10.3.27+maria~focal) starting as process 1 ...
mysqld: Please consult the Knowledge Base to find out how to run mysqld as root!
2020-12-05  0:45:49 0 [ERROR] Aborting
```

上記のメッセージでググるとヒットする(基本的に非コンテナ環境だと思われる)回避策の`--user=root`を追加すると

```
2020-12-05  0:46:37 0 [ERROR] Could not open mysql.plugin table. Some plugins may be not loaded
2020-12-05  0:46:37 0 [ERROR] Can't open and lock privilege tables: Table 'mysql.servers' doesn't exist
2020-12-05  0:46:37 0 [Note] Server socket created on IP: '::'.
2020-12-05  0:46:37 0 [ERROR] Fatal error: Can't open and lock privilege tables: Table 'mysql.user' doesn't exist
```

となり、同様に回避策をググって

```
          - --user=mysql
          - --datadir=/var/lib/mysql
```

に設定変更しても改善せず。

Docker Composeでの動作状態と比較してみると`command`を使うと`docker-entrypoint.sh`が使われずに起動しようとするので失敗していた、という状態でした。

これは、Docker単体でも、`mysqld`を単体で実行すると同じエラーで起動失敗します。

```
$ docker run -it --rm --name mariadb mariadb:10.3 bash
root@b8a277c49a9d:/# mysqld --character-set-server=utf8 --collation-server=utf8_general_ci
2020-12-05 11:27:44 0 [Note] mysqld (mysqld 10.3.27-MariaDB-1:10.3.27+maria~focal) starting as process 8 ...
mysqld: Please consult the Knowledge Base to find out how to run mysqld as root!
2020-12-05 11:27:44 0 [ERROR] Aborting

```

# 参考情報

- GROWI
  - [weseek/growi-docker-compose: growi-docker-compose - The fastest way to boot All-in-One GROWI](https://github.com/weseek/growi-docker-compose)
- HackMD
  - [growi-docker-compose/examples/integrate-with-hackmd at master · weseek/growi-docker-compose](https://github.com/weseek/growi-docker-compose/tree/master/examples/integrate-with-hackmd)
  - [growi-docker-compose/hackmd at master · weseek/growi-docker-compose](https://github.com/weseek/growi-docker-compose/tree/master/hackmd)