Posted at

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の簡単なサーバーをサンプルとして用意しました。


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を入力します。


Dockerfileの作成

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

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のマニフェストファイルになります。


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リソースも作成しておきます。


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にアクセスすると、以下のように表示されます。


ファイルを更新してみる

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

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

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

reload:

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

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

make reload


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

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

今回、変更の監視にはCompileDaemonを利用しました。

go get github.com/githubnemo/CompileDaemon

go install github.com/githubnemo/CompileDaemon

先ほどの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も試してみたいと思います。