この記事はOpenSaaS Studio Advent Calendar 2019の2日目の記事。今日も@stormcat24 が書きます。
背景
弊部署で開発しているSaaSプロダクトは基本的にOpenSaaS戦略に則り開発されている。OpenSaaSとはManagedのSaaSとして提供するという面と、OSSとして公開し、技術コミュニティの力を借りながらもその技術ドメインのデファクトの一つを目指すという考え方である。代表的なモデルとしてはKubernetesであったり、mongoDBやRedis、最近だとGrafanaもそうだ。彼らはOpenSaaSという言葉を標榜してはいないが、基本的に目指す方向は同じと思ってもらっていい。
OpenSaaSを標榜していると、マルチプラットフォーム(パブリッククラウド・プライベートクラウド問わず)対応からは逃れることはできない。とはいえ最初からマルチクラウドでやるのは開発リソース上無理なので、GCPが対応できたら次はAWS、その次はAzure・・・みたいな感じになる。
今取り組んでいるプロダクトのほとんどはKubernetesベースで構築されているため、様々なプラットフォームとワークロードに対応したマニフェストを上手く管理するのが重要になる。我々中の人の運用効率を確保することと、OSSとして利用しやすくするための適度な抽象化が求められる。
Kustomize
マニフェスト管理を助けるツールといえばHelmとKustomize。
Helmはテンプレートを記述して、値を埋め込んだり分岐して描画したり、historyを持てたりとなかなか強力である。また、サーバコンポーネントであるTillerが必要であり、Tiller自身を管理する必要があった(これは、Helm3でtillerが廃止されたため解消されたと言ってよい)。
Helmは強力であるが、「Docker/Kubernetes 実践コンテナ開発入門」においても解説がそこそこのボリュームになるくらいは難しい。
そこで最近はKustomizeを活用している。
KustomizeはHelmのようにテンプレートを用意するわけではなく、複数のKubernetesのマニフェストをマージしたり、パッチ的に上書きしたりして一つの完成形のマニフェストを構築するというものである。Kustomizeはマニフェストの構築までが役割であり、成果物を kubectl apply
するだけで良いというシンプルさも良い。
マニフェストのディレクトリ構成
今取り組んでいるプロダクトはmonorepoで開発している。Microservices構成であるが、1つのリポジトリで複数のサービスのDockerイメージを構築していて、ベースとなるマニフェストもこのリポジトリで管理している。リポジトリ名はとりあえず nekotan
として説明しよう。
nekotan -- manifests --- base
|- local (baseにlocal固有の設定を上書き・追加したもの)
|- gcp (baseにgcp固有の設定を上書き・追加したもの)
|- aws (baseにaws)固有の設定を上書き・追加したもの
各Microservicesであったり、その他アプリケーションが依存する横断的なコンポーネント(SealedSecretやIstioの設定)も含めてbase
に配置する。baseに配置するものは、基本的にはプラットフォームを問わず必要になるものと考えていい。
例えばRedisの利用が必須で、この接続情報をSecretで管理していて環境変数で渡すのであれば、baseディレクトリに配置するアプリケーションのマニフェストは次のようになる。
apiVersion: apps/v1
kind: Deployment
metadata:
name: account
spec:
template:
spec:
containers:
- name: server
image: asia.gcr.io/your-project/nekotan-account:latest
env:
- name: REDIS_HOST
valueFrom:
secretKeyRef:
name: redis-credentials
key: host
- name: REDIS_PASSWORD
valueFrom:
secretKeyRef:
name: redis-credentials
key: password
このアプリケーションではプラットフォームを問わずRedisを必須としているため、より具象的なマニフェスト群である、local
, gcp
, aws
でこれを定義する必要はなくなる。
プラットフォーム固有のものがある
プラットフォームごとに分ける必要あるんかいなーと思う人もいるかもしれないが、残念ながらプラットフォーム固有な要素も多い。
例えばGCPでRDBをCloudSQLで運用するような場合、RDBにアクセスするためのProxyとなるサイドカーコンテナが必要となる(Cloud SQL Proxy)。この場合、gcp
ディレクトリに配置されるbaseを上書きするマニフェストは次のようになる(ブログのために簡略化しているので雰囲気を感じ取ってくれ)。
apiVersion: apps/v1
kind: Deployment
metadata:
name: account
spec:
template:
spec:
containers:
- name: server
args:
- server
- --port=8080
// hogehoge...
- name: cloudsql-proxy
image: gcr.io/cloudsql-docker/gce-proxy:1.16
command:
- "/cloud_sql_proxy"
- "-instances=$(RDB_ADDRESS)"
- "-credential_file=/secrets/cloudsql/credentials.json"
securityContext:
runAsUser: 2
allowPrivilegeEscalation: false
env:
- name: RDB_ADDRESS
valueFrom:
secretKeyRef:
name: account-db-credentials
key: address
volumeMounts:
- name: cloudsql-instance-credentials
mountPath: /secrets/cloudsql
readOnly: true
volumes:
- name: gcp-credentials
secret:
secretName: account-gcp-credentials
- name: cloudsql-instance-credentials
secret:
secretName: cloudsql-instance-credentials
このマニフェストファイルを、次のようにkustomization.yaml
でbase
を上書きするようにし、patchesStrategicMerge
でマージする。
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
bases:
- ../../../base/apps/account
patchesStrategicMerge:
- deployment.yaml
サイドカーとして追加されたのがcloudsql-proxy
である。完全にGCP固有であり、server
コンテナはcloudsql-proxy
経由でRDBにアクセスする。server
側にはDBのユーザーとパスワードを渡す必要があるが、これはRedisのときと同様にbase
で抽象化し、Secretで渡すという方法が通用する。
細かい制御はやっぱり難しい
「クラウドプラットフォームレベルの抽象化」という意味であればこの管理でも上手くいくかもしれない。しかし、Helmではテンプレートと分岐によって細かく制御できる仕組みがあるが、overlayしていくKustomizeでは辛みがある。例えばフラグ制御。
アプリケーションのフラグは配列で定義するので、フラグで色々出し分けするケースは辛みがある。
args:
- server
- --port=8080
// hogehoge...
この場合はargs配列をまるっと上書きするか、次のようなpatches
という仕組みを使う。
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
bases:
- ../../../base/apps/account
patchesStrategicMerge:
- deployment.yaml
patches:
- path: server-args.yaml
target:
group: apps
version: v1
kind: Deployment
name: account
- op: add
path: /spec/template/spec/containers/0/args/-
value: --log-level=debug
args
配列に1つフラグを追加するという定義だ。追加は確かに可能だが、ベースのものを更新や削除はできないので厳しみが強い。
オプショナルな制御をしようとすればするほど厳しくなってくる。例えばメトリクスでDatadogとPrometheus両方対応しような!みたいなことを安易に考えてしまいがちだが、そもそもPush型とPull型でメトリクスの収集方法が違うため、マニフェストの構造も大きく変わってしまう。ああ、茨の道が見える。
結論
Helm3やっていこうな!でもターゲットがシンプルであればKustomizeはいいぞ!