kubernetesをローカルの開発に活用する
できる限りプロダクションのクラスタ設定をそのままローカルの開発にも使いたいなー、と思って色々と試行錯誤して、ようやく形になってきたので書いておく。
なぜローカルでkubernetesを動かしたいのか
最近ではInfrastructure as Code、Immutable Infrastructureの考え方と共に、コンテナの上でアプリケーションの環境の構築、運用、開発をすることが増えてきた。
少し前までは、Dockerでローカルの開発環境の構築は楽になったけど、本番にデプロイするのにはハードルがある印象が個人的にはあった。だけど、kubernetesの登場によってそのハードルは大きく下がった。
最近はマイクロサービスアーキテクチャへの注目と共に、様々なコンテナが協調してサービスを形作る構成が増えてきたように思う。kubernetesはこの全てのコンテナを管理する。
kubernetesは最初に学ばなければならないことは多いけれど、一度その概念を掴めばどれだけ楽に構成を管理できるか実感できる。
2年前ぐらい前のものだけど、全体の概念を掴むのはこの動画が良かった。
全ての設定はyamlで記述することになるので、構成管理がコード化できるところも良い。(ともすればyaml地獄とも揶揄されるけど)
複数のプロセスを立ち上げて開発をしている場合には、各プロセスを協調させるために、ローカル専用のプロキシサーバを立てるケースもある。
しかし各コンテナのインターナルな通信や、環境変数の設定をkubernetesに任せていると、これがローカルでそのまま動けばプロキシサーバなどいらずにプロダクションと同等の動作をさせることができるのでは、という気持ちになった。
ローカル開発とプロダクションとの差異を解決するためのkustomize
なるべくプロダクションの構成をそのまま使いたいとはいえ、どうしても差異はある。
そもそもプロダクションとローカルではデプロイするイメージは確実に違うし、Webアプリケーション開発においてはクライアントサイドのjsやhtmlなどの静的なファイルをビルドするためのサーバを立てなければいけなかったりもする。
この差異を解決するのには kustomize というツールを使うことにした。
kustomizeにより、環境ごとの差分のみを記述する
kustomizeを用いると、プロダクションの設定とローカルの設定は差分のみを記述するだけで済むようになる。 kustomizeが用意する基本的なレイヤーは、ベースとオーバレイの2つである。
各サービスの環境(プロダクション、ステージング、ローカルなど)ごとの設定は差分のみを記述した上でオーバレイのレイヤーに配置しておき、適用の際にベースの設定にパッチを当てる。
具体的には各環境にkustomization.yamlというファイルを用意し、そこがエントリポイントになる。
kustomizeのリポジトリのファイル配置の例を転記する。
~/someApp ├── base │ ├── deployment.yaml │ ├── kustomization.yaml │ └── service.yaml └── overlays ├── development │ ├── cpu_count.yaml │ ├── kustomization.yaml │ └── replica_count.yaml └── production ├── cpu_count.yaml ├── kustomization.yaml └── replica_count.yaml
例えば、overlays/development/kustomization.yamlの内容は以下のようになるだろう。
namespace: dev-someApp commonLabels: stage: development bases: - ../base resources: - cpu_count.yaml - replica_count.yaml
basesセクションに記述したものはdevelopment, productionの両方から参照され、その差分のあるファイルは各オーバレイのレイヤーにおいておく。 この例ではファイルごと変えているが、ファイルの中で部分的に差分を適用したい場合には、patchesというセクションを使えば可能だ。
patches: - patches/deployment.yaml
環境変数の管理
kubernetesを用いて環境変数を管理するには、SecretsとConfigMapを用いるのが一般的だ。 基本的に機密性の高い情報の管理にはSecrets、それ以外ではConfigMapを使う。
kubernetesを使うとリリースごとにリビジョンをつけることができるので、問題がおきた時にロールバックが容易になる。
しかし、リリースごとに環境変数を変えている場合にはこれもロールバックしなければいけないことになり、直接SecretsやConfigMapをいじってしまうと少し厄介になる。
これを解決するために、kustomizeにより作成されたSecret、ConfigMapにはそれぞれリソースに一意な名前となるようにサフィックスが自動で付与され、それを参照することになる。
つまり、デプロイするたびにSecretsやConfigMapは増えていく。
試しに見てみる。
$ kubectl get secrets -n dev-application NAME TYPE DATA AGE default-token-5lctz kubernetes.io/service-account-token 3 1d secrets-environment-4khckhf9c7 Opaque 27 5h secrets-environment-9hg6tc462d Opaque 29 3h secrets-environment-bb4k2t7cd9 Opaque 26 5h
kustomizeを利用しない場合にこの仕組みを実現する方法は、@yuyatさんの以下の記事が大変参考になった。
Kubernetes の ConfigMap を Immutable に管理する | Born Too Late
また、secretsGenerator、configMapGeneratorという仕組みも用意されていて、ここにはリテラルだけでなく、コマンドの出力結果も適用することができる。
僕は環境変数をAWS SystemManagerのParameterStore(ssm)で管理しているので、以下のように記述している。(get_env_from_ssmはaws-cliをラップして、ssmから値を取り出して標準出力するだけの簡単なシェルスクリプトです。)
secretGenerator: - name: server-environment commands: BAR_API_KEY: "../../scripts/get_env_from_ssm /dev/BAR_API_KEY" FOO_SECRET: "../../scripts/get_env_from_ssm /dev/FOO_SECRET" type: Opaque
ローカルで利用するソースコードのチェックアウトとイメージのビルド
ローカル環境のソースコードのパスは開発者ごとに違うのが普通だけど、後述のソースコードのコンテナへのマウントや、イメージの一括のビルドスクリプトを簡易にするため、今回は決められたパスに関連ソースコードをチェックアウトし、イメージをビルドするスクリプトを用意した。Dockerfileは各リポジトリに置いている。
工夫すればソースコードのパスは可変にはできると思う。けど結構苦労すると思う。
具体的には以下のようなスクリプトになった。
- scripts/checkout_all
#! /bin/bash mkdir -p ${HOME}/awesomeApps echo "local setup start..." if [ -d "${HOME}/awesomeApps/bar" ]; then echo "bar already exists." else # git clone ... fi if [ -d "${HOME}/awesomeApps/foo" ]; then echo "foo already exists." else # git clone fi # ... # ... echo "finished."
- scripts/build_all_images
#! /bin/bash docker build -t foo:dev ${HOME}/awesomeApps/foo docker build -t bar:dev ${HOME}/awesomeApps/bar # ... # ...
開発用のbuildのイメージのタグをバージョン管理せずにdevのみで作っていると、Dockerfileに修正が発生した時に更新が少し面倒になる。
なので開発用のタグもバージョン管理した方が良いかもしれないけど、ローカルのソースコードのマウントのところで後述する通り、kubernetesのyamlをプログラマブルにするのは少し面倒なので、複雑性とのトレードオフになる。
人数が多くて変更が発生した際に周知が大変な場合には、バージョン管理する価値はあると思う。
ローカルのソースコードをkubernetes上のコンテナにマウントする
kubernetesにはvolumesにhostPathを指定することができる。
これを用いればホストのソースコードをマウントすることは可能だが、ここは絶対パスで指定する必要があることに注意が必要だ。
さらにkubernetesはDeclarativeであることを崩さないため、templateの機能は提供されておらず、ここでhostの環境変数で設定を置き換えることはできない。
そこで、置き換えにはenvsubstを用いることにした。 具体的には以下のようなコマンドでデプロイすることになる。
kustomize build ./someApp/overlays/development/kustomization.yaml build | envsubst | kubectl apply -f -
別にenvsubstでなくても、置き換えられればいいだけなのでsedでも良い。 ここで1ステップ追加されてしまうのは僕の調べた限りでは回避できなかった。 具体的なDeploymentのファイルは以下のようになる。
apiVersion: extensions/v1beta1 kind: Deployment metadata: labels: service: server name: server spec: template: spec: volumes: - name: src hostPath: path: "${HOME}/awesomeApps/bar" type: DirectoryOrCreate containers: - name: server args: - yarn - run - dev image: bar:dev volumeMounts: - mountPath: /opt/app name: src workingDir: /opt/app
Macではminikubeではなくdocker-for-macのkubernetesを使う
windowsの人はdocker-for-windowsになる。linuxの人はminikubeを使ったとしても、driverにVirtualBoxを使わなければ良い。
Macユーザなので試した訳ではないけど、以下の議論がある。
More documentation around vm-driver=none for local use · Issue #2575 · kubernetes/minikube · GitHub
MacでもVirtualBoxを使わなければいけるのかもしれない。
docsをみると、VirtualBoxの他にVMware Fusion, HyperKitの選択肢があるようだ。
VirtualBoxを用いると、ファイルシステムの違いにより、inotifyイベントを検知できないためファイルの変更の監視が難しい。 ファイルの変更を検知してビルドの必要がある場合などに、これは致命的だ。
docker-for-macは体感として少し遅いが、これは今後改善されていくと信じたい。
おわり
まだ構築したばっかりなので、これから問題は出るかもしれない。 とりあえず今の所は便利に開発することができている。 今回紹介していないところで、細かいtips的なものもあるので、またそれは今度書こうと思う。