kubernetesのホットリロード開発環境をvolumeMountsでシンプルに実現

## 経緯

kubernetesのローカル開発環境といえば、skaffoldを使うのがポピュラーなのかと思います。まあ、あんまり他所の事情は知りませんが・・・
しかし、個人的に`skaffold dev`を使うのってあんまり好きじゃないんですよね・・・例えば以下のような理由です。

- 毎回build・Pod再起動するの時間がかかる
  - GoとかのImageならマシだが、Railsとかだと遅さが目立つ
- DockerImageがどんどん増える
  - 自分はDocker for Mac使ってるので、ローカルのDocker環境が汚染されるのもイヤ
  - 当然容量も圧迫する。特にRailsとかだと顕著。

DockerImage増える問題に関しては、kanikoを利用すれば問題にならないかもしれませんが、build・再起動に時間かかる問題に関してはどうしようもありません。それに落ち着いて考えると、コード変更が入るたびにdocker buildするのってどうなのって感じですし・・・

## 代替法の検討

TelepresenceとかのOSSを利用することも考えましたが、今回はよりシンプルな方法を自作しました。

方法として、特に捻りも何もないですが単にローカルマシンのディレクトリをPodに`VolumeMount`するだけです。

## 前提環境

- Goが使える環境
- k8sに対応したDocker for Mac

## 今回使用するサンプル

今回は例として、Goの簡単なサーバーをサンプルとして用意しました。

```go:main.go
package main

import (
	"fmt"
	"log"
	"net/http"
)

func main() {
	http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
		fmt.Fprintf(w, "Hello k8s")
	})
	log.Fatal(http.ListenAndServe(":8080", nil))
}
```

これをGoが動く適当なディレクトリに`main.go`というファイル名で置きます。
以降は、main.goを配置したディレクトリのパスを、`PATH`と表記します。

## Docker for Macの設定

Docker for Macのkubernetesで、先ほどのディレクトリを共有できるように設定します。`Preferences -> File Sharing`から、先ほど配置した`PATH`を入力します。

<img width="502" alt="スクリーンショット 2019-04-26 16.13.36.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/79302/4b224999-9432-f3cb-6c9d-50bba405e01a.png">


## Dockerfileの作成

以下のDockerfileを作成し、`PATH`に配置します。

```dockerfile
FROM golang:1.11-alpine as goapp
ENV APP_ROOT /go/src/app
WORKDIR $APP_ROOT
ADD main.go $APP_ROOT

CMD ["go", "run", "main.go"]
```

golangのDockerImageを作成する際は、マルチステージビルドを使って最終的に出来上がったバイナリだけを軽量なBaseImageに載せるみたいな工夫をすると思いますが、今回はあえて`go run`を使います。

とりあえずbuildも済ませておきます。

```
docker build -t echo:v0.1 .
```

## マニフェストファイルの作成

先ほどのDockerfileで作成したイメージを、deploymentリソースとしてデプロイします。
以下がdeploymentのマニフェストファイルになります。

```yaml:deployment.yaml
apiVersion: apps/v1beta1
kind: Deployment
metadata:
  name: echo
spec:
  replicas: 1
  selector:
    matchLabels:
      app: echo
  template:
    spec:
      containers:
        - name: echo
          image: echo:v0.1
          ports:
            - containerPort: 8080
          volumeMounts:
            - name: echo-volume
              mountPath: /go/src/app
      volumes:
        - name: echo-volume
          hostPath:
            path: PATH
```

先ほどDocker for MacでFile Sharingに登録した`PATH`をvolumsの`hostPath`に記述します。これで`PATH`のディレクトリと、Pod内の`/go/src/app`ディレクトリが共有されます。

deploymentの定義はこれでOKなので、k8sクラスタにデプロイします。

```
kubectl apply -f deployment.yaml
```

ついでに、動作検証用のloadbalancerリソースも作成しておきます。

```yaml:service.yaml
apiVersion: v1
kind: Service
metadata:
  name: echo-svc
spec:
  type: LoadBalancer
  selector:
    app: echo
  ports:
    - port: 8080
      targetPort: 8080
      protocol: TCP
```

デプロイします

```
kubectl apply -f service.yaml
```

これで`localhost:8080`にアクセスすると、以下のように表示されます。

<img width="441" alt="スクリーンショット 2019-04-26 16.31.05.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/79302/1bb8007e-2a20-0d11-6e52-90cce39244e6.png">


## ファイルを更新してみる

それでは、ファイルを更新してみます。`main.go`の11行目で"Hello k8s"としている部分を、"Updated hello k8s"に書き換えます。

```go
fmt.Fprintf(w, "Updated hello k8s")
```

これで、Pod内のファイルも書き換わります。Rails等のアプリケーションならこれだけでもOKなのですが、今回はGoなのでコンパイルし直す必要があります。Dockerfileのentrypointを`go run main.go`にしてあるので、Podを再起動すればOKです。再起動させるにはkubectlでPodを削除するのが手っ取り早いですが、毎回するのはしんどいので、[このページ](https://qiita.com/dtan4/items/9e0ab5dbe8c64ed6dd21)の方法を参考にしてmakefileを作りました。

```makefile
reload:
	@kubectl patch deployment echo -p "{\"spec\":{\"template\":{\"metadata\":{\"annotations\":{\"reloaded-at\":\"`date +'%Y%m%d%H%M%S'`\"}}}}}"
```

`echo`deploymentのannotationsを書き換えて無理やりRollingUpdateさせる感じですね。以下のコマンドで再起動できるようになりました。

```
make reload
```

## 変更を監視して自動でリロードする

せっかくなのでファイル変更が入ったら自動でリロードできるようにしてみます。

今回、変更の監視には[CompileDaemon](https://github.com/githubnemo/CompileDaemon)を利用しました。

```
go get github.com/githubnemo/CompileDaemon
go install github.com/githubnemo/CompileDaemon
```

先ほどのmakefileを書き換えます。

```makefile
hot_reload:
	CompileDaemon -command="$(MAKE) reload"
reload:
	kubectl patch deployment echo -p "{\"spec\":{\"template\":{\"metadata\":{\"annotations\":{\"reloaded-at\":\"`date +'%Y%m%d%H%M%S'`\"}}}}}"
```

以下のコマンドで、ファイル変更があった際には自動で`reload`が実行されるようになりました。

```
make hot_reload
```

## まとめ

今回はGoをアプリケーションを扱ったのでリロードの仕組みも作りましたが、Rails等のアプリケーションなら頻繁なリロードも必要ないのでもっと簡単ですね。
skaffoldを使う場合と比較して、良い点・悪い点いずれもあるかと思います。

### 良い点

- 反映が早い。特にImageが大きいほど有利。
- Docker Imageが毎回増えない

### 悪い点

- 開発環境用のマニフェストファイルが本番用と乖離しやすい
- 管理したいリソースが増えると、makefile等の自作設定ファイルの管理が必要

次は[Telepresence](https://www.telepresence.io)も試してみたいと思います。