この記事はenechain Advent Calendar 2023の20日目の記事です。
はじめに
こんにちは、enechain でエンジニアをしている青戸です。
本記事では Kubernetes マニフェストで GCP のリソースを管理できる Config Connector を実際のプロダクト開発に活用してみたので、enechain での活用事例や使ってみて感じたメリットなどを紹介します。
背景
enechain は eSquare
というエネルギーのトレーディングプラットフォームを提供しています。
現在、enechain ではアプリケーションをデプロイするための基盤として GKE を中心とした社内共通プラットフォームを構築しており、以前から存在するプロダクトは順次移行を進めているところです。創業期から稼働しているeSquare も共通的な構成には準拠しておらず、AppEngine をデプロイの基盤として利用していました。開発組織の規模が拡大するに連れて、従来の構成では開発効率やガバナンスの面で課題があったためGKEへの移行を行いました。
移行プロジェクトの中で、主に開発効率の向上を目的として Config Connector を試験的に導入してみましたので、どのようなケースに活用したのかと、実際に使ってみて感じたメリットや注意点についてご紹介していきたいと思います。
Config Connectorの概要
Config Connectorとは
Config Connector とは Kubernetes のアドオンであり、GCPリソースをCRD (Custom Resource Definition) として定義し、Kubernetesのビルトインリソースと同様のインターフェースで管理できるようにする仕組みです。簡単に言うと通常のKubernetesのリソースと同じようにマニフェストを書いてapplyするとGCPリソースが簡単に作れてしまうツールです。Config ConnectorをKubernetesクラスタにインストールすると、GCPリソースを管理するためのカスタムコントローラーがデプロイされ、カスタムリソース定義と実際のGCPリソースが一致した状態になるようにGCPのAPIへのリクエストが継続的に行われます。
例えば下記のようなYAMLファイルを作成し、kubectl apply
を実行することによって Cloud PubSub のトピックとサブスクリプションを作成できます。リソース間の参照は、通常のKubernetesマニフェストと同様ににリソース名(下記の例ではspec.topicRer.name
)によって指定します。
Config Connectorの管理対象となっていないリソースについては、 リソースID(下記の例ではspec.topicRef.external
)を直接指定することによって参照できます。
apiVersion: pubsub.cnrm.cloud.google.com/v1beta1 kind: PubSubTopic metadata: name: topic --- apiVersion: pubsub.cnrm.cloud.google.com/v1beta1 kind: PubSubSubscription metadata: name: subscription spec: topicRef: name: topic # or 管理外のリソースをIDで指定 # external: projects/project-id/topics/topic
公式ドキュメントにConfig Connector で管理できるリソースの一覧とマニフェストのスキーマの詳細がありますので詳しくはそちらを参照ください。
リソース定義が Kubernetes で記述できることによって、Kustomize などのツールを使って共通部分の定義を再利用しながら柔軟に複数環境の定義を管理できます(後ほど具体的なコード例を用いて紹介します)。
特徴
Terraform などのIaCツールも同じようにインフラをコード管理するためのツールですが、Config Connector には下記のような特徴があります。
単一のツールでインフラまで管理できる
Deployment や Service などの Kubernetes ビルトインリソースと同じようなインターフェースで GCP リソースの定義や適用が行えるため、複数のツールを使い分けることなく単一のツールでインフラも含めたデプロイを行えるようになります。Kubernetes に習熟したアプリケーション開発チームであれば、ツールを統一することによって 認知負荷を高めることなくセルフサービスでインフラの管理までを行いやすくなります。
インフラの管理をどのような役割分担で行うのかは組織のポリシーによりますが、アプリ開発チームの責任範囲が大きく、インフラ管理までチームで賄うような場合には認知負荷が下がることは大きなメリットになるかもしれません。
Reconciliation Loop による自動適用
Config Connector のもう一つの大きなメリットは、Reconciliation Loop による自動適用です。 Terraform は state ファイルで状態を管理しながら、差分となる変更を明示的に実行するようなPush型のパラダイムですが、Config Connector は Kubernetes のカスタムコントローラーが常に最新のリソースの状態を API 経由で取得し、コードの定義に合うように継続的に変更が行われるPull型のパラダイムです。Push型の IaC ツールでは、ツール外での変更によってコードと実際のリソースの状態が一致しなくなるドリフトがしばしば問題になりますが、Config Connector では Reconciliation Loop によってリソースが常に望まれた状態に保たれます。常にコードと実際のリソースの状態が一致していることが保証されるため、予期せぬ変更があっても自己修復を働かせることが可能だったり、問題が起こった際にインフラを含めて特定の状態にロールバックすることが容易になります。
KRMに準拠していることによってKubernetesツールチェインの恩恵を受けられる
KRM (Kubernetes Resource Model) に準拠したマニフェストでリソースの定義を行えることによって豊富にあるKubenetesツールチェインの恩恵を受けることができます。
例えば、Kubernetes の Dynamic Admission Control を利用してリソースが作られる前に、マニフェストの検証や変更を行うことができます。これによって、社内で定めたルールにしたがってリソースが作成されるようにガバナンスを効かせることが容易になります。open-policy-agent/gatekeeper が代表的なツールです。
GKE移行におけるConfig Connectorの活用
eSquare の GKE への移行において、Config Connector を導入したねらいや、利用方針、Config Connector を利用してどのようにリソースを定義していったのかを具体的なマニフェストを示しながら紹介します。
導入のねらい
従来の構成では、GCP リソースのプロビジョニングはすべて Terraform で行われていました。デプロイに伴うインフラの変更にも Terraform の知見が必要となるため、アプリケーション開発メンバーが複数のツールに精通する必要があり認知負荷が高い状態でした。構築手順も複雑であったため、新規に GCP リソースを含んだ環境を構築するコストが高く、オンボーディングや検証のために気軽に環境を準備できないという課題がありました。
そこで、Config Connector を利用することによってデプロイに必要なリソースの管理をKubernetesのインターフェースを通じて行えるようにして、普段の認知負荷の低減と環境構築の効率化を図ることをねらいとしました。
他のIaCツールとの棲み分け
Config Connector はマニフェストを書いて apply すればインフラが変更されるという手軽さがありそれはメリットにもなりますが、裏返すとむやみに変更されては困るようなリソースを誤って変更してしまいやすいデメリットにもなります。そのため、普段のデプロイであまり変更されないような基盤となるリソースは Terraform のように、変更点のレビューを事前に強制できるようなコンセプトのツールのほうが管理には適していると考えられます。
また、enechain では GCP プロジェクトや、権限設定、ログ転送のルールなど組織全体でガバナンスを効かせたいリソースについては SRE チームが Terraform モジュールを使って共通化を図っていますので、アプリの特定の機能に必要なデプロイの都度変更される必要があるリソースを Config Connector の管理対象とするのが適当だと考えました。
まとめると下記のとおりです。
- Terraform で管理: GCPプロジェクトの作成、グループへの権限設定やログ集約の設定など、全社でガバナンスを効かせたかったり、アプリケーションのデプロイの都度変更される可能性が低いリソース
- Config Connector で管理: アプリケーションのデプロイの都度変更される可能性があり、アプリ開発チームの裁量で変更できたほうが効率的なリソース(例えば、Cloud Pub/Sub や Cloud Storage など)
マニフェスト定義
下記では、検証用の環境(例えば開発者個人がオンボーディングや個人的な検証のために使える実環境をイメージしてください)を素早く立ち上げることができないという課題を解決するために、どのようにリソースの定義を構成したのかをマニフェストの内容を一部抜粋しながら紹介します。
下記が事前に行われている前提で説明していきます。
- Kubernetes クラスタに環境用の個別の namespace を用意する
- 複数環境の GCP リソースを管理するための共通の GCP プロジェクトを作成する (下記の例では
sandbox-project
とします)- GCP プロジェクトは Terraform で作成しました。検証用の環境ということもあり、厳密に環境ごとのリソース分離を行う必要はないことと、Config Connector のみで環境のセットアップを完結させて作業ステップを簡略化できるように、事前に作成しておいた共通の GCP プロジェクトにリソースを集約させる方針としました。
- Config Connector 用の IAM サービスアカウントを作成し、GCP リソースの作成権限付与と、事前定義されたKubernetes サービスアカウントへのバインドを行う
実際のものより簡略化していますが、マニフェストのディレクトリ構造は下記のとおりです。マニフェストの構造化には Kustomize を利用しています。
├── base // 共通のマニフェスト │ ├── app // Deployment, ServiceなどKubernetesビルトインリソースのマニフェスト │ ├── infra // Config Connectorのマニフェスト │ │ ├── kustomization.yaml │ │ ├── pubsub.yaml │ │ └── workload-identity.yaml │ └── kustomization.yaml └── overlays // 環境ごとの差分のマニフェスト ├── env-template // 環境のテンプレートとなるマニフェスト │ ├── app │ ├── infra │ │ └── kustomization.yaml │ └── kustomization.yaml ├── evn1 // テンプレートから作成された環境のマニフェスト └── ...
各ディレクトリの役割は下記のとおりです。
- base: 共通のマニフェスト
- app: Deployment や Service などの Kubernetes のビルトインリソースのマニフェスト
- infra: Config Connector のマニフェスト。今回は例としてサービスアカウントの権限設定と Cloud Pub/Sub のリソース定義を行うものとします
- overlays: 環境ごとの差分のマニフェスト
- env-template: 環境のテンプレートとなるマニフェスト。新規の環境を作成する際はこのディレクトリのマニフェストをもとに、必要な設定が施されたマニフェストを生成します。具体的な方法は後述しますが、各環境のアーキテクチャ変更への追従を容易にするため、マニフェス定義はできるだけ base に寄せて、環境ごとの差分を Kustomizeのトランスフォーマー で表現することによって、差分のコード量を最小限に留めるようにしました
次に Config Connector のリソース定義に関するマニフェストの内容を示します。(Deployment などのビルトインリソースの定義は割愛します)
base/infra/pubsub.yaml
apiVersion: pubsub.cnrm.cloud.google.com/v1beta1 kind: PubSubTopic metadata: name: topic spec: resourceID: topic # リソースのID。のちほど、環境ごとにユニークになるようにoverlaysで値を修正する --- apiVersion: pubsub.cnrm.cloud.google.com/v1beta1 kind: PubSubSubscription metadata: name: subscription spec: topicRef: name: topic resourceID: subscription # リソースのID。のちほど、環境ごとにユニークになるようにoverlaysで値を修正する
Cloud Pub/Sub のトピックとサブスクリプションを定義しています。特に指定がない場合、metadata.name
の値がリソースのIDとして利用されますが、同一プロジェクトに同一名称のリソースを複数作成しようとすると衝突してしまうためエラーとなります。リソース名は環境によらず一貫していたほうが管理が容易なので、環境ごとに異なるリソース名をつけることは避けたいです。このような場合には、リソース名とは別にリソースIDを付与することによって、マニフェストはシンプルに保ちながらリソースIDの衝突を防ぐ事ができます。上記のマニフェストでは、spec.resourceID
でリソースIDを明示的に指定しています。
詳細は後述しますが、Kustomize のトランスフォーマーによって環境に固有のリソースIDとなるように値を修正します。
base/infra/workload-identity.yaml
apiVersion: v1 kind: ServiceAccount metadata: name: service-account annotations: iam.gke.io/gcp-service-account: # のちほど、overlaysで値を設定する --- apiVersion: iam.cnrm.cloud.google.com/v1beta1 kind: IAMServiceAccount metadata: name: iam-service-account spec: resourceID: app-sa --- apiVersion: iam.cnrm.cloud.google.com/v1beta1 kind: IAMPolicyMember metadata: name: iampolicy-workload-identity spec: member: # のちほど、overlaysで値を設定する role: roles/iam.workloadIdentityUser resourceRef: apiVersion: iam.cnrm.cloud.google.com/v1beta1 kind: IAMServiceAccount name: iam-service-account --- apiVersion: iam.cnrm.cloud.google.com/v1beta1 kind: IAMPolicyMember metadata: name: iampolicymember-pubsub-publisher spec: memberFrom: serviceAccountRef: name: service-account role: roles/pubsub.publisher resourceRef: kind: PubSubTopic name: topic
上記はアプリが利用する Kubernetesサービスアカウントに Pub/Sub トピックにパブリッシュする権限を付与するマニフェストです。
下記のリソースを定義しています
- アプリが利用する Kubernetes のサービスアカウント
- GCP リソースに対する権限を付与する IAM サービスアカウント
- 両者をバインドするための Workload Identity の設定
- IAM サービスアカウントに対するロールの設定
ServiceAccount
のmetadata.annotations.iam.gke.io/gcp-service-account
と IAMPolicyMember
の spec.member
には環境ごとに固有の設定を行う必要があるため、Pub/Sub リソースと同様にトランスフォーマーで値を設定します。
base/infra/kustomization.yaml
apiVersion: kustomize.config.k8s.io/v1beta1 kind: Kustomization resources: - pubsub.yaml - workload-identity.yaml
Config Connectorのリソースを集約するKustomizationファイルです。
以上で基本となるマニフェストは定義できました。最後に環境感の差分を overlays に定義します。
overlays/env-template/infra/kustomization.yaml
apiVersion: kustomize.config.k8s.io/v1beta1 kind: Kustomization namespace: env-template resources: - ../../base/infra transformers: - |- # ① apiVersion: builtin kind: AnnotationsTransformer metadata: name: ProjectAnnotations annotations: cnrm.cloud.google.com/project-id: sandbox-project fieldSpecs: - kind: PubSubTopic path: metadata/annotations create: true - kind: PubSubSubscription path: metadata/annotations create: true - kind: IAMServiceAccount path: metadata/annotations create: true - kind: IAMPolicyMember path: metadata/annotations create: true - |- # ② apiVersion: builtin kind: PrefixSuffixTransformer metadata: name: PersonalPrefixer prefix: env-template- fieldSpecs: - kind: PubSubTopic path: spec/resourceID - kind: PubSubSubscription path: spec/resourceID - kind: IAMServiceAccount path: spec/resourceID patches: # ③ - target: kind: ServiceAccount name: service-account patch: |- - op: add path: /metadata/annotations/iam.gke.io~1gcp-service-account value: env-template-app-sa@sandbox-project.iam.gserviceaccount.com - target: kind: IAMPolicyMember name: iampolicy-workload-identity patch: |- - op: add path: /spec/member value: serviceAccount:cluster-project.svc.id.goog[env-template/service-account]
base のマニフェストをインポートし、環境ごとの差分を表現するための Kustomization ファイルです。トランスフォーマーの設定が大きく3つあり、それぞれ下記の設定を行っています。
- ①: Config Connector リソースに
project-id
アノテーションを付与してリソースが作成される GCP プロジェクトを指定しています。ネームスペースにアノテーションをつけることによりリソースのデフォルトプロジェクトを決めることができますが、今回のケースでは権限分離のためにネームスペースはすでに作成済みであるためリソースに対して指定しています。 - ②: Config Connector リソースの
spec.resourceID
に環境名-
というプレフィックスを付与し、GCP プロジェクト内で一意なリソースIDが割り当てられるようにしています。 - ③: Workload Identity を利用するための Kubernetes サービスアカウントと IAM サービスアカウントのバインドに必要なアノテーション、値を設定しています。
cluster-project
はKubernetes クラスタがホストされている GCP プロジェクトです。
デプロイ
以上でマニフェストの準備ができました。あとは kubectl apply
を実行するだけです。KubenetesビルトインリソースとともにConfig Connectorリソースが作成され、アプリケーションのデプロイとそれに必要なGCPリソースが作成されます。
$ kubectl apply -k overlays/env-template serviceaccount/service-account created iampolicymember.iam.cnrm.cloud.google.com/iampolicy-workload-identity created iampolicymember.iam.cnrm.cloud.google.com/iampolicymember-pubsub-publisher created iamserviceaccount.iam.cnrm.cloud.google.com/iam-service-account created pubsubsubscription.pubsub.cnrm.cloud.google.com/subscription created pubsubtopic.pubsub.cnrm.cloud.google.com/topic created $ kubectl get pubsubtopics.pubsub.cnrm.cloud.google.com NAME AGE READY STATUS STATUS AGE topic 15s True UpToDate 7s
環境やリソースの種類によって実際にリソースが作られるまでの時間は異なりますが、たとえば、Pub/Sub トピックであれば数秒で作成が完了します。
検証環境の構築を想定しているため、マニフェストの適用は手元から手動で行うこととしました。個別の環境を新たに立ち上げる際は、テンプレートマニフェストをコピーし環境名のプレースホルダー(例ではenv-template
)を置き換えて適用します。 Makefile で自動化すると下記のようなイメージです。
.PHONY: all all: create_manifests apply_manifests .PHONY: create_manifests create_manifests: cp -r env-template $(ENV_NAME) && \ cd $(ENV_NAME) && \ grep -lr env-template | xargs sed -i "" -e "s/env-template/$(ENV_NAME)/g" .PHONY: apply_manifests apply_manifests: cd $(ENV_NAME) && \ kubectl apply -k .
下記のように実行します。
ENV_NAME=env1 make
ツールが Kubernetes に統一できたことによって、上記のようなシンプルな仕組みを作るだけでワンコマンドで新たな環境を立ち上げられるようになりました。 従来の Terraform などの複数のツールを使って行っていた手順に比べると、格段に環境構築が簡単になりました。
今回紹介したマニフェスト構成は個人が使う検証環境を想定しているため、アーキテクチャ構成の変更があったとしてもあとから追従しやすいように、できるだけリソース定義を共通のマニフェストに寄せるようにしました。 一方、共通の開発環境やプロダクション環境では共通化よりも可読性や変更可能性を高めるためにトランスフォーマーは使わずに環境ごとにリソースの定義を行うようにしました。
実際に導入して感じたメリットと注意点
最後に実際に Config Connector を導入してみて感じたメリットと注意点についていくつか紹介したいと思います。
メリット
試行錯誤が容易で開発体験が良い
今回の GKE 移行プロジェクトでは結果的に大幅なアーキテクチャの見直しを行いました。その過程で様々な試行錯誤を行ったのですが、kubectl で素早くリソースの作成や変更を繰り返せるため非常に開発体験が良かったです。Terraform に切り替えて plan, apply を繰り返すフローに比べると1回のループに要する時間が短くなり検証を効率的に行うことができました。プロジェクト初期の仮説検証では特に恩恵を感じられるメリットなのではないでしょうか。
また config-connector という CLIツール を使えば既存の GCP リソースをマニフェストとしてエクスポートできることも便利で、検証のために手動で作成したリソースを簡単にコード化する事ができます。
アプリケーション開発者チームが自走しやすくなる
ツールを単一化して開発者の認知負荷を下げるということをメリットだと考え導入してみましたが、eSquare の開発メンバーは Kubernetes の利用経験者が多かったこともあり、以前よりもGCPリソースをメンテナンスできるメンバーが増え、Terraform に習熟しているメンバーに頼らなくてもリソース管理を開発メンバーで完結させやすくなりました。Kubernetes の利用者は今後もますます増えると予想されるため、このメリットはさらに重要になってくるのではと感じています。
繰り返しになりますが、インフラの管理をどのような役割で行うのが最適かは組織の状況によって変わりますので、アプリ開発チームがセルフサービスで開発をすすめたい場合は特に恩恵を感じられると思います。
注意点
Config Connectorのメリットについて上げてきましたが、最後に今後使う上で注意しておいた方が良いポイントを何点か紹介します。
意図しない変更や削除を防ぐ
マニフェストによる適用はスピーディに行えて開発体験も良いですが、Terraform のように適用前の差分確認が強制されないため意図しない変更を行ってしまうリスクがあります。削除や変更してはいけないリソースの管理には注意が必要です。変更を適用する際は、必ずdiffの内容をレビューし、承認を必要とするフローを確立させることが望ましいです。
アノテーションにcnrm.cloud.google.com/deletion-policy: abandon
を追加すると Config Connector リソースを削除してしまったとしてもGCPリソースは削除されないようになるため、意図しない削除のリスクを下げることができます。
リソースのサポート状況を確認する
Config Connector は比較的新しいツールということもあり、リソースそのものがサポートされていなかったり、APIが完全にサポートされていない場合があります。公式リファレンス に一覧がありますので、想定しているユースケースが Config Connector で実現可能かどうかよく確認しましょう。現時点では Terraform のほうがサポートされているリソースは多いため、場合によっては使い分けが必要となります。
過去のリリースノートを見ると、開発は活発に行われておりサポート対象が日々拡充されているようです。
おわりに
本記事ではConfig Connectorについて具体的なユースケースを交えて紹介しました。国内ではまだ活用事例が多くないようですので、これから利用を検討している方に少しでも参考になれば幸いです。まだまだ対応していないリソースがあったり、組織の役割分担によっては活用のメリットが少ない技術ではありますが、個人的には気軽に仮説検証を繰り返せるスピーディーな開発体験や、アプリケーションコードと同じようにGitOpsのアプローチでGCPリソースをデプロイできる仕組みが気に入っているため今後も期待の技術です。
Advent Calendar 21日目は yagi2
による「GoでGraceful ShutdownをFxを用いて実現する」が公開予定です。お楽しみに。
enechainでは日本のエネルギーの歴史を変えるプラットフォームを一緒につくってくれる仲間を募集しています!興味を持っていただけた方は下記のリンクからぜひご応募ください。