kubernetesをローカルの開発に活用する

できる限りプロダクションのクラスタ設定をそのままローカルの開発にも使いたいなー、と思って色々と試行錯誤して、ようやく形になってきたので書いておく。

なぜローカルでkubernetesを動かしたいのか

最近ではInfrastructure as Code、Immutable Infrastructureの考え方と共に、コンテナの上でアプリケーションの環境の構築、運用、開発をすることが増えてきた。

少し前までは、Dockerでローカルの開発環境の構築は楽になったけど、本番にデプロイするのにはハードルがある印象が個人的にはあった。だけど、kubernetesの登場によってそのハードルは大きく下がった。

最近はマイクロサービスアーキテクチャへの注目と共に、様々なコンテナが協調してサービスを形作る構成が増えてきたように思う。kubernetesはこの全てのコンテナを管理する。

kubernetesは最初に学ばなければならないことは多いけれど、一度その概念を掴めばどれだけ楽に構成を管理できるか実感できる。

2年前ぐらい前のものだけど、全体の概念を掴むのはこの動画が良かった。

www.youtube.com

全ての設定は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に修正が発生した時に更新が少し面倒になる。

なので開発用のタグもバージョン管理した方が良いかもしれないけど、ローカルのソースコードのマウントのところで後述する通り、kubernetesyamlプログラマブルにするのは少し面倒なので、複雑性とのトレードオフになる。

人数が多くて変更が発生した際に周知が大変な場合には、バージョン管理する価値はあると思う。

ローカルのソースコードkubernetes上のコンテナにマウントする

kubernetesにはvolumesにhostPathを指定することができる。

Volumes - Kubernetes

これを用いればホストのソースコードをマウントすることは可能だが、ここは絶対パスで指定する必要があることに注意が必要だ。

さらにkubernetesはDeclarativeであることを崩さないため、templateの機能は提供されておらず、ここでhostの環境変数で設定を置き換えることはできない。

Feature: Support using environment variables inside deployment yaml file · Issue #52787 · kubernetes/kubernetes · GitHub

そこで、置き換えには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-mackubernetesを使う

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の選択肢があるようだ。

Install Minikube - Kubernetes

VirtualBoxを用いると、ファイルシステムの違いにより、inotifyイベントを検知できないためファイルの変更の監視が難しい。 ファイルの変更を検知してビルドの必要がある場合などに、これは致命的だ。

github.com

docker-for-macは体感として少し遅いが、これは今後改善されていくと信じたい。

おわり

まだ構築したばっかりなので、これから問題は出るかもしれない。 とりあえず今の所は便利に開発することができている。 今回紹介していないところで、細かいtips的なものもあるので、またそれは今度書こうと思う。