KRO (Kube Resource Orchestrator) を AWS CDK EKS v2 L2 Constructs で試す
KRO (Kube Resource Orchestrator) を検証する機会があったので、AWS CDK EKS v2モジュールと組み合わせて試してみました。
@aws-cdk/aws-eks-v2-alpha module · AWS CDK
※ブログ執筆時点では KRO は alpha ステージ、AWS CDK EKS v2 Module は alpha モジュール(developer preview)です
目次
KRO とは
AWS が開発する OSS で Kubernetes のリソースを抽象化して扱うことができます。
なお発音は crow(クロー)とのことです。
概念としては大きく以下の2つです。
- ResourceGraphDefinition (以降 RG): Kubernetes のリソースを抽象化したテンプレート。AWS CDK でいうと L3 Construct のようなもの。
- ResourceGraphDefinition Instance (以降 Instance) : 上記のテンプレートをもとにデプロイする実体。
yamlによりパラメータを指定することができる。
Platform チームが RG を用意して抽象化、DevチームはInstance作成時に必要なパラメータのみを指定したYAMLを作成することで、簡単にアプリケーションをデプロイできるようになります。Platform Engineering で活用できそうですね。
※ 図は What is kro? より引用
AWS CDK EKS v2 L2 Constructs とは
v2.178.0 でリリースされたモジュールです。
また以下がリリースブログになります(ブログは 2025/6 ですが、モジュール自体は上記のバージョンがリリースされた 2025/2 から存在しています) aws.amazon.com
EKS の L2 Constructs が存在する中でv2 が作成された理由ですが、v1 の L2 Constructs はクラスターの作成にカスタムリソースが使用されていることが理由です。
当時カスタムリソースで実装された理由は、CloudFormation 経由で作成した EKS クラスターにアクセスできるロールが CloudFormation の実行ロールに限定される制約があったためです。しかしデプロイに時間がかかる、依存関係などL2 Constructs の実装が複雑になるという課題があります。
その後 EKS 側のアップデートにより、CloudFormation 経由の際に上記の制約を解消できるようになったため、CloudFormation のネイティブ API を使用して L2 Constructs を実装したものが v2 モジュールです。この辺りの詳細は以下の Issue に記載されています。
実際に試してみる
今回は以下のようなイメージで試してみます。
- Platform チームが 1. EKS クラスター w/KRO をデプロイ、2. RG をデプロイ
- Developer チームが 3. Instance をデプロイ

AWS CDK のコードや KRO 関連の YAML 配下にあります。 github.com
1. EKS クラスター w/KRO をデプロイ
ドキュメントのインストール手順相当のことを行い、EKS クラスター w/KRO を用意します。
まずは手順に従って KRO のバージョンを取得します(実行時点では 0.3.0 でした)
export KRO_VERSION=$(curl -sL \ https://api.github.com/repos/kro-run/kro/releases/latest | \ jq -r '.tag_name | ltrimstr("v")' )
その上で EKS クラスター w/KRO を用意していきます。EKS クラスターを用意 → コマンドで Helm を使用して KRO を設定とやっても良いですが、今回は AWS CDK 一発で用意してみます。
EKS Auto Mode でクラスターを作成し、kubectl 用のロール、KRO の設定を定義しています。
// EKS クラスターを Auto Mode で用意 const cluster = new eks.Cluster(this, 'EksCluster', { version: eks.KubernetesVersion.V1_32, defaultCapacityType: eks.DefaultCapacityType.AUTOMODE, // Ingress をデプロイするため ALB Controller を追加 albController: { version: eks.AlbControllerVersion.V2_8_2, }, // KRO の設定を L2 経由で行うため設定 kubectlProviderOptions: { kubectlLayer: new KubectlV32Layer(this, 'kubectl'), }, }); // Assume して kubectl を行うためのロールを作成 const eksAdminRole = new iam.Role(this, 'EksAdminRole', { roleName: 'EksAdminRole', assumedBy: new iam.AccountRootPrincipal(), description: 'Role for EKS cluster administration', }); cluster.grantAccess('adminUserAccess', eksAdminRole.roleArn, [ eks.AccessPolicy.fromAccessPolicyName('AmazonEKSClusterAdminPolicy', { accessScopeType: eks.AccessScopeType.CLUSTER, }), ]); // KRO を設定 cluster.addHelmChart('KRO', { chart: 'kro', repository: 'oci://ghcr.io/kro-run/kro/kro', namespace: 'kro', release: 'kro', createNamespace: true, version: process.env.KRO_VERSION, }); // kubectl を実行するために必要なコマンドを Output new cdk.CfnOutput(this, 'KubectlConfigCommand', { value: `aws eks update-kubeconfig --region ${this.region} --name ${cluster.clusterName} --role-arn ${eksAdminRole.roleArn}`, });
Cluster に設定している kubectlLayer は Helm のインストールを行う Lambda 用のレイヤーです。詳細は以下の記事が参考になります。
その上で HelmChart Construct を使うことで、cdk deploy 経由で Helm によるインストールまで一発でできます(実体は裏でカスタムリソースの Lambda が動いている)。なかなか便利😎
デプロイが済んだら Output のコマンドを実行し EKS クラスターに接続します。helm/kubectl で KRO の設定ができていることを確認します。
% helm -n kro list NAME NAMESPACE REVISION UPDATED STATUS CHART APP VERSION kro kro 1 2025-06-22 05:59:50.864449556 +0000 UTC deployed kro-0.3.0 0.3.0 % kubectl get pods -n kro NAME READY STATUS RESTARTS AGE kro-c8fb7b586-2djmf 1/1 Running 0 25s
2. RG をデプロイ
KRO の RG の yaml を作成します。
spec.schema配下に RG の設定を記載します。kindで RG の種類(名称)を定義しますspecで Instance 作成時に可変となるフィールドを定義します。今回だと名称、イメージ、レプリカ数などを可変にしています。- OpenAPI の仕様を踏襲しており、バリデーション、デフォルト値の設定などが可能です。詳細はこちら
statusは Instance の status に注入するフィールドを定義します(後述)
resources配下でグループ化する Kubernetes リソースを定義します。- 今回は namespace, ingress, service, deployment を定義しています(つまり Instance を作成するとこれらがデプロイされる)
spec.schema.specの可変値を参照して埋め込むことができます- リソース間でフィールドを参照することができます。リソース間の依存関係を考慮してくれるのも KRO のメリットの一つです。
apiVersion: kro.run/v1alpha1 kind: ResourceGraphDefinition metadata: name: my-application spec: # ユーザーがRGDをインスタンス化する際に設定可能なものを定義 schema: apiVersion: v1alpha1 kind: Application spec: # ユーザーが提供できるSpecフィールド name: string image: string | required=true replica: integer | default=1 ingress: enabled: boolean | default=false status: # コントローラーがインスタンスのstatusに注入するフィールド namespaceName: ${ns.metadata.name} deploymentConditions: ${deployment.status.conditions} availableReplicas: ${deployment.status.availableReplicas} # このAPIが管理するリソースを定義 resources: - id: ns template: apiVersion: v1 kind: Namespace metadata: name: ${schema.spec.name}-ns labels: app: ${schema.spec.name} managed-by: kro - id: deployment template: apiVersion: apps/v1 kind: Deployment metadata: name: ${schema.spec.name} namespace: ${ns.metadata.name} spec: replicas: ${schema.spec.replica} selector: matchLabels: app: ${schema.spec.name} template: metadata: labels: app: ${schema.spec.name} spec: containers: - name: ${schema.spec.name} image: ${schema.spec.image} ports: - containerPort: 80 - id: service template: apiVersion: v1 kind: Service metadata: name: ${schema.spec.name}-service namespace: ${ns.metadata.name} spec: selector: ${deployment.spec.selector.matchLabels} ports: - protocol: TCP port: 80 targetPort: 80 - id: ingress includeWhen: - ${schema.spec.ingress.enabled} template: apiVersion: networking.k8s.io/v1 kind: Ingress metadata: name: ${schema.spec.name}-ingress namespace: ${ns.metadata.name} annotations: kubernetes.io/ingress.class: alb alb.ingress.kubernetes.io/scheme: internet-facing alb.ingress.kubernetes.io/target-type: ip alb.ingress.kubernetes.io/healthcheck-path: /health alb.ingress.kubernetes.io/listen-ports: '[{"HTTP": 80}]' alb.ingress.kubernetes.io/target-group-attributes: stickiness.enabled=true,stickiness.lb_cookie.duration_seconds=60 spec: rules: - http: paths: - path: "/" pathType: Prefix backend: service: name: ${service.metadata.name} port: number: 80
実際にデプロイ(apply)し、問題なく作成されていることを確認しました。この時点では「テンプレート」を用意しただけなので、リソースの実体はありません。
% kubectl apply -f rg.yaml resourcegraphdefinition.kro.run/my-application configured % kubectl get rgd my-application -owide NAME APIVERSION KIND STATE TOPOLOGICALORDER AGE my-application v1alpha1 Application Active ["ns","deployment","service","ingress"] 26m
3. Instance をデプロイ
リソースの実体(Instance)をデプロイしていきます。今回 Instance の YAML を2つ用意しました。同じ RG を使用していますが
- Dev Team A は
nginxを使い、レプリカも2つにしたい - Dev Team B は
httpdを使うが、レプリカは未指定(RG のデフォルトの1が使用される)
というようにします。このような形で Dev Team は可変値のみ意識し、共通部分である RG は透過的に扱うことができます。
apiVersion: kro.run/v1alpha1 kind: Application metadata: name: team-a-instance spec: name: team-a image: nginx:alpine replica: 2 ingress: enabled: true
apiVersion: kro.run/v1alpha1 kind: Application metadata: name: team-b-instance spec: name: team-b image: httpd:alpine ingress: enabled: true
では Instance を作成してみます。
% kubectl apply -f instance_team_a.yaml application.kro.run/team-a-instance created % kubectl apply -f instance_team_b.yaml application.kro.run/team-b-instance created % kubectl get applications NAME STATE SYNCED AGE team-a-instance ACTIVE True 61s team-b-instance ACTIVE True 23s
なお RG の spec.schema.status 配下で設定した情報(今回だと availableReplicas, deploymentConditions, namespaceName)は Instance に注入されるため、describe すると確認可能です。
% kubectl describe applications team-a-instance Name: team-a-instance # ...中略... Status: Available Replicas: 2 # ...中略... Deployment Conditions: Last Transition Time: 2025-06-22T09:21:01Z Last Update Time: 2025-06-22T09:21:01Z Message: Deployment has minimum availability. Reason: MinimumReplicasAvailable Status: True Type: Available Last Transition Time: 2025-06-22T09:20:59Z Last Update Time: 2025-06-22T09:21:01Z Message: ReplicaSet "team-a-cb9ff597" has successfully progressed. Reason: NewReplicaSetAvailable Status: True Type: Progressing # ...以下略...
実際にリソースが作成されているかを確認します。まずは Dev TeamAのリソース (namespace: team-a-ns) を確認します。想定通り pod が 2つ作成されています。
% kubectl get all -n team-a-ns NAME READY STATUS RESTARTS AGE pod/team-a-6cbb99dc7b-b6gzx 1/1 Running 0 2m3s pod/team-a-6cbb99dc7b-g5ksj 1/1 Running 0 2m3s NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE service/team-a-service ClusterIP 172.20.219.180 <none> 80/TCP 2m NAME READY UP-TO-DATE AVAILABLE AGE deployment.apps/team-a 2/2 2 2 2m4s NAME DESIRED CURRENT READY AGE replicaset.apps/team-a-6cbb99dc7b 2 2 2 2m4s
Dev Team Bのリソース (namespace: team-b-ns) も確認します。こちらはレプリカ数を指定しなかったため、RG のデフォルト値により pod が 1つ作成されています。
% kubectl get all -n team-b-ns NAME READY STATUS RESTARTS AGE pod/team-b-6c78957d5b-k8zd2 1/1 Running 0 2m5s NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE service/team-b-service ClusterIP 172.20.219.75 <none> 80/TCP 2m3s NAME READY UP-TO-DATE AVAILABLE AGE deployment.apps/team-b 1/1 1 1 2m6s NAME DESIRED CURRENT READY AGE replicaset.apps/team-b-6c78957d5b 1 1 1 2m6s
今回は Ingress (ALB) でサービスを公開しているため、エンドポイントを取得してアクセスしてみます。
% kubectl get ingress --all-namespaces -o custom-columns="NAMESPACE:.metadata.namespace,NAME:.metadata.name,ALB_ADDRESS:.status.loadBalancer.ingress[0].hostname,RULES:.spec.rules[*].host" NAMESPACE NAME ALB_ADDRESS RULES team-a-ns team-a-ingress {***mask***}.us-east-1.elb.amazonaws.com <none> team-b-ns team-b-ingress {***mask***}.us-east-1.elb.amazonaws.com <none>
エンドポイントにアクセスしてみると、想定通りDev Team A は nginx, Dev Team B は httpd のデフォルト画面が表示されました。問題ないですね。

後片付け
まずは Instance を削除します。Instance で作成される Ingress (ALB) は AWS CDK の管理下ではないため、先に消しておかないと後続の Stack 削除で ALB に紐づく VPC リソースが削除できずに失敗します。
kubectl delete -f instance_team_a.yaml kubectl delete -f instance_team_b.yaml
Instance を削除したら、cdk destroy で Stack を削除し完了です。
npx cdk destroy --force
終わりに
EKS v2 モジュールで EKS のクラスターの作成がやりやすくなりました。また KRO を活用することで Kubernetes リソースの抽象化、Platform チームと Dev チームの責務の分離を行えます。
執筆時点では双方プレビューですが今後に期待。