kro で Google Cloud のリソースを管理する
はじめに
この記事は #GauDev Advent Calendar 2025 の 9 日目の記事です。
こんにちは、株式会社Gaudiy にて SRE をしている児玉です。
今回は、ちょっと今更感はありますが、kro (Kube Resource Orchestrator) (発音は crow ) という AWS, Microsoft Azure, Google Cloud 共同プロジェクトでもある、Kubernetes リソース管理ツールを紹介できればと思います。
先日の AWS re:Invent 2025 に合わせて発表された EKS Capabilities でも対象ツールとして入っているみたいですね。
kro とは
kro は「任意の Kubernetes リソース を 1 つのカスタム API として定義できる」ツールになります。Crossplane と同じように、プラットフォームチームがインフラの複雑さを抽象化した上で、アプリケーションチームにシンプルな (カスタム) API を提供するという、いわゆるプラットフォームエンジニアリングが主なユースケースになってくるのかなと思っています。
ここにさらに、ACK、ASO、KCC などの Kubernetes アドオンを組み合わせて使うことで、Kubernetes リソースだけではなく、クラウドプロバイダのリソースについても、間接的に kro で管理することができるようになります。
せっかくなので、簡単に Crossplane と kro の仕組みについて見てみたいと思います。

Crossplane では Core (わかりやすさのため core と呼んでいます) がリソースの抽象化と、抽象化したリソースの API としての提供を、Provider がクラウドサービスとのやり取りを担当しています。
Core と Provider どちらも、Crossplane エコシステムの一部として提供されています。

kro を使う場合、kro がリソースの抽象化と、抽象化したリソースの API としての提供を、Addons (図中では KCC を例としています) がクラウドサービスとのやり取りを担当しています。
kro はあくまで任意の Kubernetes リソースの抽象化と、抽象化したリソースの API としての提供をスコープとするので、図中の Addon や ArgoCD の Application , 自作 CRD といったカスタムリソースや、Deployment や Service などのビルドインリソースを抽象化して API として提供することができます。
Crossplane でも provider-kubernetes を利用することで任意の Kubernetes リソースを抽象化できますが、Provider が必要ないという点で、任意の Kubernetes リソースを抽象化するという場合においては kro の方がシンプルな構成になりそうです。
また、以下にもう少しだけ細かく流れを書いてみたので、興味がある方は見てみてください。
crossplane

kro + kcc

使ってみた
構成
以下の通り、マイクロサービスごとにシンプルな Web App を持つ、という構成で試してみたいと思います。単一クラスタで、サービスごとに NodePool を用意するという形にしています。

管理クラスタとワークロードクラスタを分離するようなユースケースを想定し、以下のような構成を試そうかと思いましたが、現状 kro としてはマルチクラスタに対応しておらず、クラスタを跨いで Kubernetes リソースを管理するということはできないようでした。ただし、以下の通り、issue としては上がっているようです。
RGD と Instance
手順に入る前に、簡単に kro の重要な要素である RGD (ResourceGraphDefinition) と Instance (ResourceGraphDefinition Instance) について説明したいと思います。
RGD は kro が提供する CRD で、Kubernetes リソースをまとめて抽象化し、新しい API として公開するための定義です。Crossplane でいうところの CompositeResourceDefinition (XRD) と Composition を組み合わせたようなものにあたるのかなと思います。
apiVersion: kro.run/v1alpha1
kind: ResourceGraphDefinition
metadata:
name: webapp
spec:
# API スキーマの定義(Crossplane の XRD に相当)
schema:
apiVersion: v1alpha1
kind: WebApp # この Kind が CRD として登録される
spec: # Instance にて指定するフィールド
name: string | required=true
replicas: integer | default=2
status:
availableReplicas: ${deployment.status.availableReplicas}
# 作成するリソースを定義(Crossplane の Composition に相当)
# Instance を作成すると、ここにあるリソースが全て作成される
resources:
- id: deployment
template:
apiVersion: apps/v1
kind: Deployment
# ...
- id: service
template:
apiVersion: apps/v1
kind: Service
# ...
上記の RGD を適用すると WebApp という新しい CRD が登録され、kubectl get webapp のように扱えるようになります。
Instance は RGD から生成された CRD のカスタムリソース (実体) になります。Instance を作成すると、kro が RGD にて定義したリソース群を作成します。Instance は Crossplane でいうところの Composite Resource にあたるのかなと思います。
apiVersion: kro.run/v1alpha1
kind: WebApp # RGD の schema.kind で定義した Kind
metadata:
name: svc-a
spec: # RGD の schema.spec で定義したフィールド
name: svc-a
replicas: 1
簡単に使ってみたいと思います。まずは、RGD を適用してみます。
$ kubectl apply -f webapp-rgd.yaml
resourcegraphdefinition.kro.run/webapp created
$ kubectl get rgd webapp
NAME APIVERSION KIND STATE AGE
webapp v1alpha1 WebApp Active 5s
RGD が Active になると、kro が WebApp という新しい CRD を自動生成します。
$ kubectl get crd webapps.kro.run -o yaml | head -30
apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
labels:
kro.run/resource-graph-definition-name: webapp
name: webapps.kro.run
spec:
group: kro.run
names:
kind: WebApp
...
name: v1alpha1
schema:
openAPIV3Schema:
...
spec: # RGD にて定義した API のスキーマ
properties:
name:
type: string
replicas:
default: 2
type: integer
required:
- name
type: object
RGD で指定した通り name, replicas などが定義されていますね。
$ kubectl api-resources --api-group=kro.run | grep webapp
webapps kro.run/v1alpha1 true WebApp
そして、WebApp が API として登録されました。新しい CRD が登録されると kube-apiserver がこのように API として登録してくれます。
API (定義) の準備ができたので Instance (実体) を作成してみます。
$ kubectl apply -f webapp-instance.yaml
webapp.kro.run/svc-a created
$ kubectl get webapp
NAME STATE READY AGE
svc-a ACTIVE True 9s
$ kubectl get deployment,service -n svc-a
NAME READY UP-TO-DATE AVAILABLE AGE
deployment.apps/svc-a 1/1 1 1 18s
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
service/svc-a ClusterIP 10.2.8.153 <none> 80/TCP 18s
Instance を作成したことによって、Deployment と Service が作成されていますね。
手順
全ての手順を説明すると長くなってしまうので、ポイントに絞って説明できればと思います。
1. ブートストラップ
まずは、kro や KCC を動かすための GKE クラスタを作成する必要がありますが、ここは単純なコマンドラインなので省略とします。
次に KCC (Config Connector) をセットアップしていきます。KCC のインストール方法としては以下の通り大きく 3 つありますが、KCC のみインストールできれば良いので、手動インストールにしたいと思います。Config Controller によるインストールの場合、Policy Controller や Config Sync もインストールしてくれます。また、Addon 経由だと KCC バージョンがビハインドしてしまうようです。
$ gcloud storage cp gs://configconnector-operator/latest/release-bundle.tar.gz release-bundle.tar.gz && tar zxvf release-bundle.tar.gz
# NodePool を管理したいので Standard クラスタ
$ kubectl apply -f operator-system/configconnector-operator.yaml
続いて、KCC の設定をしていきます。
apiVersion: core.cnrm.cloud.google.com/v1beta1
kind: ConfigConnector
metadata:
name: configconnector.core.cnrm.cloud.google.com
spec:
mode: namespaced # KCC が利用する (Google Cloudのリソースを作成する) を SA を namespace ごとに分けるように。
stateIntoSpec: Absent # KCC で管理するリソースのパラメータをシンプルにするために Absent
特に spec.mode については開発チーム間で権限を分離するという意味で namespaced と設定してあげるのが良さそうです。
apiVersion: core.cnrm.cloud.google.com/v1beta1
kind: ConfigConnectorContext
metadata:
name: configconnectorcontext.core.cnrm.cloud.google.com
namespace: config-connector
spec:
googleServiceAccount: "config-connector@<project-id>.iam.gserviceaccount.com"
stateIntoSpec: Absent
こちらでは、KCC が利用する GSA を指定してあげます。
次に kro をインストールしていきます。
helm install kro oci://registry.k8s.io/kro/charts/kro \
--namespace kro \
--create-namespace
本記事内ではここまでとしますが、その他 GKE クラスタ・ネットワーク作成など、続きは setup.sh をご確認いただければと思います。
2. RGD デプロイ
続いて、先ほどの構成図で示したリソースを RGD として定義していきます。ここでは、開発者向けの WebApp を例としてみてみます。Nodepool や DB, LB などは別の RGD として定義し、プラットフォームエンジニアや DevOps, または各チームのインフラ担当が扱うような想定をしています。
WebApp では、以下のリソースを定義しています。
- ComputeSecurityPolicy (Cloud Armor ポリシー)
- Namespace (サービス専用の namespace)
- ConfigMap (nginx 設定)
- Deployment (アプリケーション Pod)
- Service (サービス)
- GCPBackendPolicy (Cloud Armor と Service の紐付け)
- HTTPRoute (Gateway へのルーティング)
apiVersion: kro.run/v1alpha1
kind: ResourceGraphDefinition
metadata:
name: webapp
spec:
# API スキーマの定義
schema:
apiVersion: v1alpha1
kind: WebApp
spec:
name: string | required=true
image: string | default="nginx:latest"
replicas: integer | default=2
port: integer | default=80
pathPrefix: string | required=true
allowedSourceRanges: "[]string | required=true" # Cloud Armor 用
status:
availableReplicas: ${deployment.status.availableReplicas}
# この API が作成するリソース群
resources:
# KCC (Google Cloud) リソース: Cloud Armor ポリシー
- id: securityPolicy
template:
apiVersion: compute.cnrm.cloud.google.com/v1beta1
kind: ComputeSecurityPolicy
metadata:
name: ${schema.spec.name}-security-policy
namespace: config-connector
spec:
rule:
- action: allow
match:
config:
srcIpRanges: ${schema.spec.allowedSourceRanges}
# ...
# Kubernetes ビルトインリソース: Namespace, Deployment, Service
- id: serviceNamespace
template:
apiVersion: v1
kind: Namespace
metadata:
name: ${schema.spec.name}
- id: deployment
template:
apiVersion: apps/v1
kind: Deployment
metadata:
name: ${schema.spec.name}
namespace: ${serviceNamespace.metadata.name} # 他リソースを参照
# ...
# Gateway API リソース: HTTPRoute
- id: httpRoute
template:
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
namespace: ${serviceNamespace.metadata.name}
spec:
parentRefs:
- name: ${schema.spec.gatewayName}
namespace: default
# ...
$ kubectl apply -f webapp-rgd.yaml
resourcegraphdefinition.kro.run/webapp configured
kro では ${resourceId.field} の形式で他のリソースを参照すると、自動的に依存関係として認識されます。kro が解決した作成順序 (Topological Order) を見てみます。
kubectl describe rgd webapp | grep -A 10 "Topological Order"
Topological Order:
securityPolicy (依存なし)
serviceNamespace (依存なし)
nginxConfig (serviceNamespace を参照)
deployment (serviceNamespace, nginxConfig を参照)
service (serviceNamespace を参照)
backendPolicy (securityPolicy, service を参照)
httpRoute (serviceNamespace, service を参照)
良さそうですね。例えば nginxConfig は ${serviceNamespace.metadata.name} を参照しているため、serviceNamespace の後に作成されることになっています。
本リソース全体は webapp.yaml をご確認いただければと思います。
3. Instance デプロイ
それでは最後に、RGD にて定義した API を利用してリソースを作成していきましょう。先ほどと同様に WebApp を例として見てみましょう。
このリソース 1 つで、先ほど挙げた計 7 リソースを作成できることになります。利用する側としてはかなりシンプルに、楽になりますね。
apiVersion: kro.run/v1alpha1
kind: WebApp
metadata:
name: svc-a
spec:
name: svc-a
image: nginx:latest
replicas: 2
port: 80
pathPrefix: /svc-a
allowedSourceRanges:
- "YOUR_IP" # Cloud Armor にて穴をあける IP
$ kubectl apply -f webapp-instance.yaml
webapp.kro.run/svc-a created
$ kubectl get webapp
NAME STATE READY AGE
svc-a ACTIVE True 1m
$ kubectl get namespace svc-a
NAME STATUS AGE
svc-a Active 65s
$ kubectl get deployment,service,configmap -n svc-a
NAME READY UP-TO-DATE AVAILABLE AGE
deployment.apps/svc-a 2/2 2 2 65s
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
service/svc-a ClusterIP * <none> 80/TCP 65s
NAME DATA AGE
configmap/svc-a-nginx-config 1 65s
$ kubectl get gcpbackendpolicy,httproute -n svc-a
NAME AGE
gcpbackendpolicy.networking.gke.io/svc-a-backend-policy 65s
NAME HOSTNAMES AGE
httproute.gateway.networking.k8s.io/svc-a-route 65s
$ kubectl get computesecuritypolicy -n config-connector | grep svc-a
svc-a-security-policy True UpToDate 65s
作成できました。RGD で定義されている通り計 7 つのリソースが作成されていますね。
作成順序も見てみましょう。
$ kubectl get namespace,configmap,deployment,service,gcpbackendpolicy,httproute -A -o custom-columns='KIND:.kind,NAME:.metadata.name,RV:.metadata.resourceVersion' | grep svc-a
Namespace svc-a 1765152416155679018
ConfigMap svc-a-nginx-config 1765152416556719019
Deployment svc-a 1765152424146751018
Service svc-a 1765152423577247009
GCPBackendPolicy svc-a-backend-policy 1765152471379903015
HTTPRoute svc-a-route 1765152606058143023
$ kubectl get computesecuritypolicy -n config-connector -o custom-columns='KIND:.kind,NAME:.metadata.name,RV:.metadata.resourceVersion' | grep svc-a
ComputeSecurityPolicy svc-a-security-policy 1765152442776895006
Topological Order: 実際の作成順 (RV順):
1. securityPolicy 1. serviceNamespace
2. serviceNamespace 2. nginxConfig
3. nginxConfig 3. service
4. deployment 4. deployment
5. service 5. securityPolicy
6. backendPolicy 6. backendPolicy
7. httpRoute 7. httpRoute
RGD で決定された作成順序 (Topological Order) の出力通りの順番ではなかったですが、依存関係は守られた上で作成されたようです。
おまけ
kro や KCC が載っているクラスタを kro と KCC で管理する
ブートストラップにて作成した GKE クラスタや VPC などの既存リソースを、後から KCC の管理下に置くことができます。これは KCC における取り込み (acquire) という機能で実現します。
やることはシンプルで、取り込みたいリソースと同じリソース名で KCC リソースを作成するだけです。以下の通り、KCC は同名リソースが存在していた場合、取り込みを行います。
If a resource with the same name already exists, Config Connector acquires that resource and starts managing it.
setup.sh にて作成したリソースを RGD として定義します。
apiVersion: kro.run/v1alpha1
kind: ResourceGraphDefinition
metadata:
name: platform-bootstrap
spec:
schema:
apiVersion: v1alpha1
kind: PlatformBootstrap
spec:
name: string | required=true
region: string | required=true
subnetRange: string | default="10.0.0.0/24"
podRange: string | default="10.1.0.0/16"
serviceRange: string | default="10.2.0.0/20"
resources:
# GKE Cluster (既存クラスタを acquire)
- id: cluster
template:
apiVersion: container.cnrm.cloud.google.com/v1beta1
kind: ContainerCluster
metadata:
name: ${schema.spec.name}-cluster # KCC リソース名を Google Cloud リソース名と合わせる
namespace: config-connector
annotations:
cnrm.cloud.google.com/state-into-spec: absent # Google Cloud リソース の値を spec に書き戻さない (管理対象としない)
cnrm.cloud.google.com/deletion-policy: abandon # KCC リソース削除時に Google Cloud リソースを残す
# VPC Network
- id: network
template:
apiVersion: compute.cnrm.cloud.google.com/v1beta1
kind: ComputeNetwork
spec:
# ...
# Subnet
- id: subnet
template:
apiVersion: compute.cnrm.cloud.google.com/v1beta1
kind: ComputeSubnetwork
# ...
# Cloud Router
- id: router
template:
apiVersion: compute.cnrm.cloud.google.com/v1beta1
kind: ComputeRouter
# ...
# Cloud NAT
- id: nat
template:
apiVersion: compute.cnrm.cloud.google.com/v1beta1
kind: ComputeRouterNAT
# ...
少なくとも、取り込みを行う間は、以下のラベルを貼ると良さそうかなと思いました。
metadata:
annotations:
cnrm.cloud.google.com/state-into-spec: absent # 'merge' または 'absent'. デフォルトは 'merge'. (1.113.0 以前. 1.114.0 以降は 'absent' のみ) 作成後変更不可.
cnrm.cloud.google.com/deletion-policy: abandon # 'none' または 'abandon'. デフォルトは 'none'.
state-into-spec を absent とすると、spec で指定していないフィールドについては管理しないことになります。もし以下のようなケースで、Google Cloud のリソース側に machineType: e2-standard-4 というフィールドがあった場合、spec には追加されず、machineType については KCC の管理対象外フィールドとなります。
spec:
location: asia-northeast1
# machineType は指定していない
逆に、state-into-spec を merge とすると、spec に machineType: e2-standard-4 というフィールドと値が追加され、KCC の管理下となります。最初に取り込むときは、小さく始めるという意味で、最小限のフィールドを管理下に置く方法が進めやすかなという印象でした。使用感は Terraform の ignore_changes に少し似ていますね。
次に、deletion-policy についてですが、abandon と指定することで、もし KCC リソースを削除してしまった場合でも、 Google Cloud リソースは削除しないようにすることができます。これで、取り込みの最中 (デバックなど) に KCC リソース側を綺麗にしたい、または、誤って消してしまった場合でも Google Cloud リソースは削除せずに残しておけるので、運用が安定しない間などは安全策として貼っておいても良いラベルかなと思いました。
$ kubectl apply -f platform-bootstrap.yaml
resourcegraphdefinition.kro.run/platform-bootstrap created
$ kubectl get rgd platform-bootstrap
NAME APIVERSION KIND STATE AGE
platform-bootstrap v1alpha1 PlatformBootstrap Active 5s
RGD を適用しました。
次に Instance を作成します。
apiVersion: kro.run/v1alpha1
kind: PlatformBootstrap
metadata:
name: pf
spec:
name: pf
region: asia-northeast1
subnetRange: "10.0.0.0/24"
podRange: "10.1.0.0/16"
serviceRange: "10.2.0.0/20"
$ kubectl apply -f infra/shared/bootstrap.yaml
platformbootstrap.kro.run/pf created
$ kubectl get computenetwork,computesubnetwork,computerouter,computerouternat,containercluster -n config-connector
NAME READY STATUS
computenetwork.compute.cnrm.cloud.google.com/pf-vpc True UpToDate
NAME READY STATUS
computesubnetwork.compute.cnrm.cloud.google.com/pf-subnet True UpToDate
NAME READY STATUS
computerouter.compute.cnrm.cloud.google.com/pf-router True UpToDate
NAME READY STATUS
computerouternat.compute.cnrm.cloud.google.com/pf-nat True UpToDate
NAME READY STATUS
containercluster.container.cnrm.cloud.google.com/pf-cluster True UpToDate
ステータスが UpToDate になれば、既存の Google Cloud リソースが KCC の管理下に入り、かつ、KCC で指定した状態 (spec に定義したフィールドの値) と実際の Google Cloud リソースの状態 (設定値) が一致したことになります。
感想
任意の Kubernetes リソースを 1 つのカスタム API として定義できるというシンプルなコンセプトで、使用感も良く、AWS、Microsoft Azure、Google Cloud の共同プロジェクトということもあり、今後が非常に楽しみなプロダクトだと思いました。
現時点では、API としてはまだ alpha であり、マルチクラスタサポートや RGD のバージョニング機能がないなど (現状は RGD を更新すると、その RGD から作成された全ての Instance に対しても、即座に更新が走ってしまうようでした)、これからのところもあるのかなと思います。
とはいえ、今後プラットフォームエンジニアリングの標準ツールとなり得る可能性を十分に感じることができ、非常に良かったです。
さいごに
長くなってしまいましたが、最後まで読んでいただきありがとうございました。
#GauDev Advent Calendar 2025、明日の担当は umirai さんです。
GitHub
本記事にて利用したコードは以下にまとめてありますので、興味のある方は見てみてください。
Gaudiy(ガウディ)グループ所属エンジニアの個人発信をまとめたPublicationです。 公式Tech Blog:techblog.gaudiy.com/
Discussion