経緯
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の簡単なサーバーをサンプルとして用意しました。
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のマニフェストファイルになります。
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リソースも作成しておきます。
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'`\"}}}}}"
echo
deploymentの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も試してみたいと思います。