🐦‍⬛

kro で Google Cloud のリソースを管理する

に公開
2026/02/23

はじめに

この記事は #GauDev Advent Calendar 2025 の 9 日目の記事です。

https://adventar.org/calendars/11616

こんにちは、株式会社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 としては上がっているようです。

https://github.com/kubernetes-sigs/kro/issues/591
https://github.com/kubernetes-sigs/kro/issues/1060

RGD と Instance

手順に入る前に、簡単に kro の重要な要素である RGD (ResourceGraphDefinition) と Instance (ResourceGraphDefinition Instance) について説明したいと思います。

RGD は kro が提供する CRD で、Kubernetes リソースをまとめて抽象化し、新しい API として公開するための定義です。Crossplane でいうところの CompositeResourceDefinition (XRD) と Composition を組み合わせたようなものにあたるのかなと思います。

webapp-rgd.yaml
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 にあたるのかなと思います。

webapp-instance.yaml
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 の設定をしていきます。

configconnector.yaml
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 と設定してあげるのが良さそうです。

configconnectorcontext.yaml
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 へのルーティング)
webapp-rgd.yaml
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 リソースを作成できることになります。利用する側としてはかなりシンプルに、楽になりますね。

webapp-instance.yaml
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.

https://docs.cloud.google.com/config-connector/docs/how-to/managing-deleting-resources

setup.sh にて作成したリソースを RGD として定義します。

platform-bootstrap.yaml
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 を作成します。

platformbootstrap.yaml
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 さんです。

https://adventar.org/calendars/11616

GitHub

本記事にて利用したコードは以下にまとめてありますので、興味のある方は見てみてください。
https://github.com/ppluuums-jp/kro-google-cloud-lab

Gaudiy Engineers' Blog

Discussion