mazyu36の日記

クラウドのことを中心に色々と。All posts are my own.

KRO (Kube Resource Orchestrator) を AWS CDK EKS v2 L2 Constructs で試す

KRO (Kube Resource Orchestrator) を検証する機会があったので、AWS CDK EKS v2モジュールと組み合わせて試してみました。

kro.run

@aws-cdk/aws-eks-v2-alpha module · AWS CDK

※ブログ執筆時点では KRO は alpha ステージ、AWS CDK EKS v2 Module は alpha モジュール(developer preview)です

目次

KRO とは

AWS が開発する OSSKubernetes のリソースを抽象化して扱うことができます。

なお発音は crow(クロー)とのことです。

aws.amazon.com

概念としては大きく以下の2つです。

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 に記載されています。

github.com

実際に試してみる

今回は以下のようなイメージで試してみます。

  • 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 用のレイヤーです。詳細は以下の記事が参考になります。

qiita.com

その上で 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 チームの責務の分離を行えます。

執筆時点では双方プレビューですが今後に期待。