[レポート] Master KRMOps: Declarative resource management with Amazon EKS and kro #CNS407 #AWSreInvent

[レポート] Master KRMOps: Declarative resource management with Amazon EKS and kro #CNS407 #AWSreInvent

2025.12.04

セッションの概要

Managing cloud resources and applications in Kubernetes environments often leads to complex, fragmented deployment processes that hinder operational efficiency. Attend this advanced session to discover how to unify cloud resource management using Kubernetes Resource Model Operations (KRMOps) with kro and AWS Controllers for Kubernetes (ACK). Learn how to leverage these tools to package applications and cloud resource dependencies into cohesive deployable units using the KRMOps. You will gain practical experience in implementing declarative AWS resource management, automating dependency resolution, and enabling self-healing infrastructure - essential skills required for running large-scale Amazon EKS deployments for data and AI.

Type: Builders' session
Level: 400 – Expert
Features: Hands-on, Interactive
Topic: Developer Tools, Serverless & Containers
Area of Interest: Innovation & Transformation, Kubernetes
Role: DevOps Engineer, IT Professional / Technical Manager, Solution / Systems Architect
Services: Amazon Elastic Kubernetes Service (Amazon EKS)

Kubernetes Resource Model Operations (KRMOps) について学ぶためのハンズオンに参加してきました。
AWS Controllers for Kubernetes (ACK) を利用して Kubernetes API 経由で AWS リソースを扱えるようにしつつ、Kube Resource Orchestrator(kro) を利用してアプリケーションとクラウドリソースを上手くパッケージ化しながらデプロイする方法について実際に試してみました。
1 時間でやり切るために調整した部分はありそうでしたが、基本的には krmops-on-eks-workshop リポジトリの資材を利用していたと思います。

https://github.com/aws-samples/krmops-on-eks-workshop/tree/main/kro-and-ack/bootstrap

Terraform で AWS インフラを作成すれば同じような内容を実施できるので、興味がある方は是非試してみて下さい。

KRMOps とは

Kubernetes Resource Model (KRM) は Kubernetes の基本的なコンセプトで下記特徴があります。

  • API 中心:ユーザーや各種コントローラが Kubernetes API サーバーを介してやり取りを行う。
  • 宣言的制御: 構成ファイルを用いてリソースの望ましい状態を定義する。Kubernetes コントローラーは実際の状態と望ましい状態を一致させるように動作する。
  • 標準化された構造と動作: metadata(ラベル、アノテーション)、spec(望ましい構成)、status(監視対象の状態)などを含むリソースの標準化された構造を定義する。
  • 拡張性: KRM とそのコントローラーパターンの原則は、ACK や kro などのツールを通じてクラウドサービスなどの外部リソースの管理にも拡張できる。

KRM を Kubernetes リソースだけでなく AWS のサービスやインフラストラクチャにも拡張してより効率的に運用しようとする考え方が KRMOps です。

ACK について

ACK を利用することで Kubernetes API 経由で AWS リソースを管理でき、Kubernetes リソースと AWS リソースの依存関係管理の複雑さを軽減できます。

スクリーンショット 2025-12-04 6.14.17.png

各 ACK サービスコントローラーは個別のコンテナイメージにパッケージ化されてパブリックリポジトリに公開されています。
プロビジョニングする AWS サービスごとに対応するコントローラーのリソースを Amazon EKS クラスターにインストールする必要があり、各 ACK サービスコントローラーは個別のコンテナイメージにパッケージ化されて Amazon ECR Public Gallery に公開されています。

https://gallery.ecr.aws/aws-controllers-k8s/

今回のハンズオンでは、最初から利用するコントローラが EKS にインストールされていました。

$ environment kubectl get deployment -n ack-system
NAME                 READY   UP-TO-DATE   AVAILABLE   AGE
ack-dynamodb         1/1     1            1           21h
ack-ec2              1/1     1            1           21h
ack-eks              1/1     1            1           21h
ack-iam              1/1     1            1           21h
ack-kms              1/1     1            1           21h
ack-rds              1/1     1            1           21h
ack-s3               1/1     1            1           21h
ack-secretsmanager   1/1     1            1           21h

ちなにに、先日発表された EKS Capabilities では AWS 領域にインストールされたサービスコントローラを利用可能です。

https://dev.classmethod.jp/articles/eks-capabilities-argo-cd/

下記コマンドを実行して ACK 経由で S3 バケットを作成します。
若干複雑になっているのは S3 バケット名の重複を避けるためで、やっていることは s3.services.k8s.aws/v1alpha1 を指定してリソースを作成しているのみです。

export AWS_ACCOUNT_ID=$(aws sts get-caller-identity --query "Account" --output text)
export BUCKET_NAME=krmops-ack-s3-bucket-$AWS_ACCOUNT_ID

read -r -d '' BUCKET_MANIFEST <<EOF
apiVersion: s3.services.k8s.aws/v1alpha1
kind: Bucket
metadata:
  name: $BUCKET_NAME
spec:
  name: $BUCKET_NAME
EOF

echo "${BUCKET_MANIFEST}" > bucket.yaml

kubectl create -f bucket.yaml

ステータスを確認すると、ACK.ResourceSynced となっていることを確認できました。

$ environment kubectl describe bucket/$BUCKET_NAME
Name:         krmops-ack-s3-bucket-xxxxxxxxxxxx
Namespace:    default
Labels:       <none>
Annotations:  <none>
API Version:  s3.services.k8s.aws/v1alpha1
Kind:         Bucket
Metadata:
  Creation Timestamp:  2025-12-03T21:11:26Z
  Finalizers:
    finalizers.s3.services.k8s.aws/Bucket
  Generation:  1
  Managed Fields:
    API Version:  s3.services.k8s.aws/v1alpha1
    Fields Type:  FieldsV1
    fieldsV1:
      f:metadata:
        f:finalizers:
          .:
          v:"finalizers.s3.services.k8s.aws/Bucket":
    Manager:      controller
    Operation:    Update
    Time:         2025-12-03T21:11:26Z
    API Version:  s3.services.k8s.aws/v1alpha1
    Fields Type:  FieldsV1
    fieldsV1:
      f:spec:
        .:
        f:name:
    Manager:      kubectl-create
    Operation:    Update
    Time:         2025-12-03T21:11:26Z
    API Version:  s3.services.k8s.aws/v1alpha1
    Fields Type:  FieldsV1
    fieldsV1:
      f:status:
        .:
        f:ackResourceMetadata:
          .:
          f:arn:
          f:ownerAccountID:
          f:region:
        f:conditions:
        f:location:
    Manager:         controller
    Operation:       Update
    Subresource:     status
    Time:            2025-12-03T21:11:27Z
  Resource Version:  422722
  UID:               7bda5bfe-fe61-478a-9c62-645c3505adee
Spec:
  Name:  krmops-ack-s3-bucket-xxxxxxxxxxxx
Status:
  Ack Resource Metadata:
    Arn:               arn:aws:s3:::krmops-ack-s3-bucket-xxxxxxxxxxxx
    Owner Account ID:  xxxxxxxxxxxx
    Region:            us-west-2
  Conditions:
    Last Transition Time:  2025-12-03T21:11:27Z
    Message:               Resource synced successfully
    Reason:
    Status:                True
    Type:                  ACK.ResourceSynced
  Location:                http://krmops-ack-s3-bucket-xxxxxxxxxxxx.s3.amazonaws.com/
Events:                    <none>

無事 S3 バケットも作成されていました。

$ aws s3 ls
2025-12-03 21:11:28 krmops-ack-s3-bucket-xxxxxxxxxxxx
2025-12-02 23:40:50 krmops-s3adopt-xxxxxxxxxxxx

※ krmops-s3adopt-xxxxxxxxxxxx は今後の作業のために、元からあったリソースです。

kro について

kro で Kubernetes リソースのセットを作成する

kro をインストールすると ResourceGraphDefinition (RGD) と呼ばれるカスタムリソース定義 (CRD) を利用できるようになります。
例えば「Application Stack」という名前の RGD を作成してクラスターに適用すれば、ApplicationStack という新しい API を作成可能です。
作成したカスタム API を利用することで必要なリソースをパッケージ化してデプロイでき、ネイティブ Kubernetes リソースとクラスターにインストールされたカスタムリソース定義(CRD)の両方を含めたリソースセットにすることが可能です。

スクリーンショット 2025-12-04 6.14.58.png

また、特定の要件に合わせてカスタマイズしながらリソースグループを利用することも可能です。

スクリーンショット 2025-12-04 6.15.28.png

今回のハンズオンでは下記マニフェストファイルを利用して、複数の Kubernetes リソースをリソースグループにまとめて利用できるようにしました。
Deployment、Service、Ingress を一つのリソースグループにまとめています。

apiVersion: kro.run/v1alpha1
kind: ResourceGraphDefinition
metadata:
  name: webapp.kro.run
spec:
  schema:
    apiVersion: v1alpha1
    kind: WebApp
    spec:
      name: string
      namespace: string | default=default
      image: string | default=nginx
      port: integer | default=8080
      replicas: integer | default=1
      service:
        enabled: boolean | default=true
      ingress:
        enabled: boolean | default=false
      serviceAccount: string | default=default
    status:
      deploymentConditions: ${deployment.status.conditions}
      availableReplicas: ${deployment.status.availableReplicas}
      url: ${ingress.status.loadBalancer.ingress[0].hostname}

  resources:
  - id: deployment
    readyWhen:
      - ${deployment.spec.replicas == deployment.status.availableReplicas}
    template:
      apiVersion: apps/v1
      kind: Deployment
      metadata:
        name: ${schema.spec.name}
        namespace: ${schema.spec.namespace}
        labels:
          app.kubernetes.io/name: ${schema.spec.name}
      spec:
        replicas: ${schema.spec.replicas}
        selector:
          matchLabels:
            app.kubernetes.io/name: ${schema.spec.name}
            app: ${schema.spec.name}
        template:
          metadata:
            labels:
              app.kubernetes.io/name: ${schema.spec.name}
              app: ${schema.spec.name}
          spec:
            serviceAccountName: ${schema.spec.serviceAccount}
            containers:
            - name: webapp-demo
              image: ${schema.spec.image}
              imagePullPolicy: Always
              ports:
              - containerPort: ${schema.spec.port}
              resources:
                requests:
                  memory: "64Mi"
                  cpu: "250m"
                limits:
                  memory: "1Gi"
                  cpu: "1"
            restartPolicy: Always

  - id: service
    includeWhen:
    - ${schema.spec.service.enabled}
    template:
      apiVersion: v1
      kind: Service
      metadata:
        name: ${deployment.metadata.name}
        namespace: ${deployment.metadata.namespace}
      spec:
        selector:
          app: ${schema.spec.name}
        ports:
        - name: http
          protocol: TCP
          port: 80
          targetPort: ${schema.spec.port}

  - id: ingress
    includeWhen:
    - ${schema.spec.ingress.enabled}
    template:
      apiVersion: networking.k8s.io/v1
      kind: Ingress
      metadata:
        name: ${deployment.metadata.name}
        namespace: ${deployment.metadata.namespace}
        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

作成したリソースグループを利用して、インスタンスを作成します。

apiVersion: kro.run/v1alpha1
kind: WebApp
metadata:
  name: test-app
spec:
  name: test-app
  port: 80 # nginx default port is 80
  ingress:
    enabled: true
  service: {} # this is needed

各種リソースがまとめて作成されました。

$ webapp kubectl get deployment,service,ingress -l kro.run/instance-name=test-app
NAME                       READY   UP-TO-DATE   AVAILABLE   AGE
deployment.apps/test-app   1/1     1            1           28s

NAME               TYPE        CLUSTER-IP      EXTERNAL-IP   PORT(S)   AGE
service/test-app   ClusterIP   172.20.220.12   <none>        80/TCP    22s

NAME                                 CLASS   HOSTS   ADDRESS                                                                PORTS   AGE
ingress.networking.k8s.io/test-app   alb     *       k8s-default-testapp-xxxxx-xxxxxxx.us-west-2.elb.amazonaws.com   80      19s

WebApp リソースを作成だけで、アプリケーションが展開され、リクエストを正常に受け付けられるようになりました。

$ curl -s $(kubectl get ingress test-app -o jsonpath='{.status.loadBalancer.ingress[0].hostname}') | sed -n '/<body>/,/<\/body>/p' | sed -e 's/<[^>]*>//g'


Welcome to nginx!
If you see this page, the nginx web server is successfully installed and
working. Further configuration is required.

For online documentation and support please refer to
nginx.org.
Commercial support is available at
nginx.com.

Thank you for using nginx.

kro と ACK を組み合わせて S3 バケットを作成する

まず、S3 バケットのプロビジョニング用の RGD を作成します。
kro を利用することで、バケットとバケットポリシーなどの関連するリソースをリソースグループとしてまとめることが可能です。

apiVersion: kro.run/v1alpha1
kind: ResourceGraphDefinition
metadata:
  name: s3bucket.kro.run
spec:
  schema:
    apiVersion: v1alpha1
    kind: S3Bucket
    spec:
      name: string
      access: string | default="write"
    status:
      s3ARN: ${s3bucket.status.ackResourceMetadata.arn}
      s3PolicyARN: ${s3PolicyWrite.status.ackResourceMetadata.arn}

  resources:
    - id: s3bucket
      template:
        apiVersion: s3.services.k8s.aws/v1alpha1
        kind: Bucket
        metadata:
          name: ${schema.spec.name}
        spec:
          name: ${schema.spec.name}
    - id: s3PolicyWrite
      includeWhen:
        - ${schema.spec.access == "write"}
      template:
        apiVersion: iam.services.k8s.aws/v1alpha1
        kind: Policy
        metadata:
          name: ${schema.spec.name}-s3-write-policy
        spec:
          name: ${schema.spec.name}-s3-write-policy
          policyDocument: |
            {
              "Version": "2012-10-17",
              "Statement": [
                {
                  "Effect": "Allow",
                  "Action": [
                    "s3:GetObject",
                    "s3:PutObject",
                    "s3:PutObjectAcl",
                    "s3:DeleteObject"
                  ],
                  "Resource": [
                    "${s3bucket.status.ackResourceMetadata.arn}/*"
                  ]
                },
                {
                  "Effect": "Allow",
                  "Action": [
                    "s3:ListBucket",
                    "s3:GetBucketLocation"
                  ],
                  "Resource": [
                    "${s3bucket.status.ackResourceMetadata.arn}"
                  ]
                }
              ]
            }

上記マニフェストファイルをデプロイすることで RGD として追加されます。

$ kubectl get resourcegraphdefinition
NAME               APIVERSION   KIND       STATE    AGE
s3bucket.kro.run   v1alpha1     S3Bucket   Active   18s
webapp.kro.run     v1alpha1     WebApp     Active   14m

作成された API を利用してインスタンスを作成します。

s3bucket export AWS_ACCOUNT_ID=$(aws sts get-caller-identity --query "Account" --output text)
export BUCKET_NAME=krmops-kro-s3-bucket-$AWS_ACCOUNT_ID

read -r -d '' INSTANCE_MANIFEST <<EOF
apiVersion: kro.run/v1alpha1
kind: S3Bucket
metadata:
  name: s3demo
  namespace: default
spec:
  name: $BUCKET_NAME
EOF

echo "${INSTANCE_MANIFEST}" > instance.yaml

無事インスタンスが ACTIVE になりました。

$ s3bucket kubectl get s3bucket -n default
NAME     STATE    SYNCED   AGE
s3demo   ACTIVE   True     20s

S3 が作成されていることも確認できました。

$ s3bucket aws s3 ls
2025-12-03 21:11:28 krmops-ack-s3-bucket-xxxxxxxxxxxx
2025-12-03 21:32:40 krmops-kro-s3-bucket-xxxxxxxxxxxx
2025-12-02 23:40:50 krmops-s3adopt-xxxxxxxxxxxx

kro と ACK で既存リソースをインポートする

ACK では既に存在するリソースをインポートすることも可能です。

https://aws-controllers-k8s.github.io/docs/guides/adoption/

この際、下記 annotations を付与します。

ラベル 備考
services.k8s.aws/read-only true ソースのステータスを同期するが、基盤となる AWS リソースの作成、更新、削除は行わない
services.k8s.aws/adoption-policy adopt インポート作業のみ、adopt-or-create を指定すればリソースがない場合に作成する
services.k8s.aws/adoption-field S3 バケット名 リソースタイプごとに固定値が決まっている

今回は下記マニフェストファイルを利用しました。

apiVersion: kro.run/v1alpha1
kind: ResourceGraphDefinition
metadata:
  name: s3bucketadopt.kro.run
spec:
  schema:
    apiVersion: v1alpha1
    kind: S3BucketAdopt
    spec:
      name: string

  resources:
    - id: s3bucket
      template:
        apiVersion: s3.services.k8s.aws/v1alpha1
        kind: Bucket
        metadata:
          name: ${schema.spec.name}
          annotations:
            services.k8s.aws/read-only: "true"
            services.k8s.aws/adoption-policy: "adopt"
            services.k8s.aws/adoption-fields: |
              {
                "name": "${schema.spec.name}"
              }

マニフェストファイルを生成して、インポート作業を行います。

export AWS_ACCOUNT_ID=$(aws sts get-caller-identity --query "Account" --output text)
export BUCKET_NAME=krmops-s3adopt-$AWS_ACCOUNT_ID

read -r -d '' INSTANCE_MANIFEST <<EOF
apiVersion: kro.run/v1alpha1
kind: S3BucketAdopt
metadata:
  name: s3import
  namespace: default
spec:
  name: $BUCKET_NAME
EOF

echo "${INSTANCE_MANIFEST}" > instance-adopt.yaml
kubectl apply -f instance-adopt.yaml

無事 InstanceSynced になり、正常にインポートすることができました。

$ kubectl describe s3bucketadopt/s3import -n default
Name:         s3import
Namespace:    default
Labels:       kro.run/kro-version=v0.3.0
              kro.run/owned=true
              kro.run/resource-graph-definition-id=717f0263-5af7-4c3c-97e0-b506ee93da12
              kro.run/resource-graph-definition-name=s3bucketadopt.kro.run
Annotations:  <none>
API Version:  kro.run/v1alpha1
Kind:         S3BucketAdopt
Metadata:
  Creation Timestamp:  2025-12-03T21:37:44Z
  Finalizers:
    kro.run/finalizer
  Generation:  1
  Managed Fields:
    API Version:  kro.run/v1alpha1
    Fields Type:  FieldsV1
    fieldsV1:
      f:metadata:
        f:finalizers:
          .:
          v:"kro.run/finalizer":
        f:labels:
          .:
          f:kro.run/kro-version:
          f:kro.run/owned:
          f:kro.run/resource-graph-definition-id:
          f:kro.run/resource-graph-definition-name:
    Manager:      kro
    Operation:    Update
    Time:         2025-12-03T21:37:44Z
    API Version:  kro.run/v1alpha1
    Fields Type:  FieldsV1
    fieldsV1:
      f:metadata:
        f:annotations:
          .:
          f:kubectl.kubernetes.io/last-applied-configuration:
      f:spec:
        .:
        f:name:
    Manager:      kubectl-client-side-apply
    Operation:    Update
    Time:         2025-12-03T21:37:44Z
    API Version:  kro.run/v1alpha1
    Fields Type:  FieldsV1
    fieldsV1:
      f:status:
        .:
        f:conditions:
        f:state:
    Manager:         kro
    Operation:       Update
    Subresource:     status
    Time:            2025-12-03T21:37:47Z
  Resource Version:  431331
  UID:               6a5c4e98-164b-4df3-a304-10c6f2021970
Spec:
  Name:  krmops-s3adopt-783764577371
Status:
  Conditions:
    Last Transition Time:  2025-12-03T21:37:47Z
    Message:               Instance reconciled successfully
    Observed Generation:   1
    Reason:                ReconciliationSucceeded
    Status:                True
    Type:                  InstanceSynced
  State:                   ACTIVE
Events:                    <none>

ネストを利用した RGB を定義して使ってみる

RGD ではフィールドをネストさせて定義することで、複数のインフラストラクチャコンポーネントを含めることが可能です。
3 つのコンポーネントをネストした、DB を扱う Web アプリケーションを作成していきます。

WebAppRds RGD

Web アプリケーションを管理する Kubernetes Deployment および、サービス公開用の Service や Ingress リソースを含んだ RGD。
DB 認証情報は Secrets Store CSI Driver を利用して扱います。

apiVersion: kro.run/v1alpha1
kind: ResourceGraphDefinition
metadata:
  name: webapp.rds.kro.run
spec:
  schema:
    apiVersion: v1alpha1
    kind: WebAppRds
    spec:
      name: string
      namespace: string | default=default
      image: string | default=nginx
      port: integer | default=8080
      replicas: integer | default=1
      service:
        enabled: boolean | default=true
      serviceAccount: string | default=default
      rds:
        enabled: boolean | default=false
        image: string
        replicas: integer | default=1
        dbHost: string
        dbName: string | default="testdb"
    status:
      availableReplicas: ${rdsdeployment.status.availableReplicas}

  resources:
    # --- RDS Deployment (only included when rds.enabled is true) ---
    - id: rdsdeployment
      includeWhen:
        - ${schema.spec.rds.enabled}
      readyWhen:
        - ${rdsdeployment.spec.replicas == rdsdeployment.status.availableReplicas}
      template:
        apiVersion: apps/v1
        kind: Deployment
        metadata:
          name: ${schema.spec.name}-rdsdeployment
          namespace: ${schema.spec.namespace}
          labels:
            app: ${schema.spec.name}-rds
        spec:
          replicas: ${schema.spec.rds.replicas}
          selector:
            matchLabels:
              app: ${schema.spec.name}-rds
          template:
            metadata:
              labels:
                app: ${schema.spec.name}-rds
            spec:
              serviceAccountName: ${schema.spec.serviceAccount}
              containers:
                - name: rds-demo
                  image: ${schema.spec.rds.image}
                  imagePullPolicy: Always
                  ports:
                    - containerPort: ${schema.spec.port}
                  env:
                    - name: DB_HOST
                      value: ${schema.spec.rds.dbHost}
                    - name: DB_NAME
                      value: ${schema.spec.rds.dbName}
                  volumeMounts:
                    - mountPath: /mnt/secrets-store
                      name: dbsecret
                      readOnly: true
              restartPolicy: Always
              volumes:
                - name: dbsecret
                  csi:
                    driver: secrets-store.csi.k8s.io
                    readOnly: true
                    volumeAttributes:
                      secretProviderClass: ${schema.spec.name}-aws-secrets

    # --- Service for RDS App ---
    - id: rdsservice
      includeWhen:
        - ${schema.spec.service.enabled}
        - ${schema.spec.rds.enabled}
      readyWhen:
        - ${has(rdsservice.spec.clusterIP)}
      template:
        apiVersion: v1
        kind: Service
        metadata:
          name: ${schema.spec.name}-rdsservice
          namespace: ${schema.spec.namespace}
          labels:
            app: ${schema.spec.name}-rds
        spec:
          selector:
            app: ${schema.spec.name}-rds
          ports:
            - port: 80
              targetPort: ${schema.spec.port}
              protocol: TCP
          type: ClusterIP

    # --- Ingress for ALB ---
    - id: rdsingress
      includeWhen:
        - ${schema.spec.service.enabled}
        - ${schema.spec.rds.enabled}
      readyWhen:
        - ${has(rdsingress.status.loadBalancer)}
      template:
        apiVersion: networking.k8s.io/v1
        kind: Ingress
        metadata:
          name: ${schema.spec.name}-rdsingress
          namespace: ${schema.spec.namespace}
          annotations:
            kubernetes.io/ingress.class: alb
            alb.ingress.kubernetes.io/scheme: internet-facing
            alb.ingress.kubernetes.io/target-type: ip
            alb.ingress.kubernetes.io/listen-ports: '[{"HTTP":80}]'
          labels:
            app: ${schema.spec.name}-rds
        spec:
          ingressClassName: alb
          rules:
            - http:
                paths:
                  - path: /
                    pathType: Prefix
                    backend:
                      service:
                        name: ${schema.spec.name}-rdsservice
                        port:
                          number: 80

RdsInstance RGD

RDS インスタンス関連リソースを含む RGD。

apiVersion: kro.run/v1alpha1
kind: ResourceGraphDefinition
metadata:
  name: rds.kro.run
spec:
  schema:
    apiVersion: v1alpha1
    kind: RdsInstance
    spec:
      awsAccountID: integer
      awsRegion: string
      dbEngine: string
      environment: string
      namespace: string
      applicationName: string
      vpcID: string
      cidrIP: string
      subnetIDs: "[]string"

    status:
      endpointAddress: ${awsDatabase.status.endpoint.address}
      secretARN: ${awsDatabase.status.masterUserSecret.secretARN}
      secretPolicyARN: ${secretManagerPolicy.status.ackResourceMetadata.arn}
  resources:
    - id: subnetGroup
      template:
        apiVersion: rds.services.k8s.aws/v1alpha1
        kind: DBSubnetGroup
        metadata:
          name: db-subnet-group-${schema.spec.applicationName}
        spec:
          description: db test
          name: db-sb-${schema.spec.applicationName}
          subnetIDs: ${schema.spec.subnetIDs}
    - id: securityGroup
      template:
        apiVersion: ec2.services.k8s.aws/v1alpha1
        kind: SecurityGroup
        metadata:
          name: db-security-group-${schema.spec.applicationName}
        spec:
          vpcID: ${schema.spec.vpcID}
          name: db-sg-${schema.spec.applicationName}
          description: inress mysql rule
          ingressRules:
            - fromPort: 3306
              toPort: 3306
              ipProtocol: TCP
              ipRanges:
                - cidrIP: ${schema.spec.cidrIP}
                  description: db test
    - id: awsDatabase
      readyWhen:
        - ${awsDatabase.status.conditions.exists(x, x.type == 'ACK.ResourceSynced' && x.status == "True")}
      template:
        apiVersion: rds.services.k8s.aws/v1alpha1
        kind: DBInstance
        metadata:
          name: ${schema.spec.applicationName}-db-${schema.spec.environment}
        spec:
          manageMasterUserPassword: true
          masterUsername: testUser
          allocatedStorage: 10
          dbInstanceClass: db.t4g.micro
          dbInstanceIdentifier: ${schema.spec.applicationName}-db-${schema.spec.environment}
          engine: ${schema.spec.dbEngine}
          engineVersion: "8.0"
          dbSubnetGroupName: db-sb-${schema.spec.applicationName}
          vpcSecurityGroupIDs:
            - ${securityGroup.status.id}
    - id: secretManagerPolicy
      template:
        apiVersion: iam.services.k8s.aws/v1alpha1
        kind: Policy
        metadata:
          name: ${schema.spec.applicationName}-secretsmanager-policy
        spec:
          name: ${schema.spec.applicationName}-secretsmanager-policy
          policyDocument: |
            {
              "Version": "2012-10-17",
              "Statement": [
                {
                  "Effect": "Allow",
                  "Action": [
                    "secretsmanager:GetSecretValue",
                    "secretsmanager:DescribeSecret"
                  ],
                  "Resource": [
                    "${awsDatabase.status.masterUserSecret.secretARN}"
                  ]
                },
                {
                  "Effect": "Allow",
                  "Action": [
                    "secretsmanager:ListSecrets"
                  ],
                  "Resource": ["*"]
                }
              ]
            }
    - id: secretProviderClass
      template:
        apiVersion: secrets-store.csi.x-k8s.io/v1
        kind: SecretProviderClass
        metadata:
          name: ${schema.spec.applicationName}-aws-secrets
        spec:
          provider: aws
          parameters:
            objects: |
              - objectName: "${awsDatabase.status.masterUserSecret.secretARN}"
                objectType: "secretsmanager"
                objectAlias: "dbsecret"
            usePodIdentity: "true"

PodIdentity RGD

Pod Identity で利用できる IAM ロールを含む RGD。

apiVersion: kro.run/v1alpha1
kind: ResourceGraphDefinition
metadata:
  name: podidentity.kro.run
spec:
  schema:
    apiVersion: v1alpha1
    kind: PodIdentity
    spec:
      name: string
      clusterName: string | default="kro"
      policyARN: string | default=""
    status:
      serviceAccount: ${serviceaccount.metadata.name}

  resources:
    - id: role
      template:
        apiVersion: iam.services.k8s.aws/v1alpha1
        kind: Role
        metadata:
          name: ${schema.spec.name}-role
        spec:
          name: ${schema.spec.name}-role
          policies:
            - ${schema.spec.policyARN}
          assumeRolePolicyDocument: |
            {
              "Version": "2012-10-17",
              "Statement": [
                {
                  "Effect": "Allow",
                  "Principal": {
                    "Service": "pods.eks.amazonaws.com"
                  },
                  "Action": [
                    "sts:AssumeRole",
                    "sts:TagSession"
                  ]
                }
              ]
            }

DbWebStack RGD

下記 3 つのコンポーネントをネストした RGD。

  • WebAppRds RGD
  • RdsInstance RGD
  • PodIdentity RGD
apiVersion: kro.run/v1alpha1
kind: ResourceGraphDefinition
metadata:
  name: db-webstack.kro.run
spec:
  schema:
    apiVersion: v1alpha1
    kind: DbWebStack
    spec:
      name: string
      applicationName: string
      clusterName: string | default="kro"
      image: string
      port: integer | default=8080
      replicas: integer | default=1
      service:
        enabled: boolean | default=true
      rds:
        enabled: boolean | default=false
        dbEngine: string | default="mysql"
        environment: string | default="dev"
        awsAccountID: integer
        awsRegion: string
        vpcID: string
        cidrIP: string
        subnetIDs: "[]string"
    status:
      deploymentConditions: ${webapp.status.conditions}
      availableReplicas: ${webapp.status.availableReplicas}

  resources:
    - id: awsDatabase
      includeWhen:
        - ${schema.spec.rds.enabled}
      readyWhen:
        - "${has(awsDatabase.status.endpointAddress) && has(awsDatabase.status.secretPolicyARN)}"
      template:
        apiVersion: kro.run/v1alpha1
        kind: RdsInstance
        metadata:
          name: ${schema.spec.name}
        spec:
          awsRegion: ${schema.spec.rds.awsRegion}
          dbEngine: ${schema.spec.rds.dbEngine}
          environment: ${schema.spec.rds.environment}
          namespace: default
          applicationName: ${schema.spec.name}
          vpcID: ${schema.spec.rds.vpcID}
          cidrIP: ${schema.spec.rds.cidrIP}
          subnetIDs: ${schema.spec.rds.subnetIDs}

    - id: podidentity
      readyWhen:
        - "${has(podidentity.status.serviceAccount) && size(podidentity.status.serviceAccount) > 0}"
      template:
        apiVersion: kro.run/v1alpha1
        kind: PodIdentity
        metadata:
          name: ${schema.spec.name}
        spec:
          name: ${schema.spec.name}
          policyARN: ${awsDatabase.status.secretPolicyARN}

    - id: webapp
      readyWhen:
        - "${has(webapp.status.availableReplicas) && webapp.status.availableReplicas > 0}"
      template:
        apiVersion: kro.run/v1alpha1
        kind: WebAppRds
        metadata:
          name: ${schema.spec.name}
        spec:
          name: ${schema.spec.name}
          image: ${schema.spec.image}
          port: ${schema.spec.port}
          replicas: ${schema.spec.replicas}
          serviceAccount: ${podidentity.status.serviceAccount}
          service:
            enabled: ${schema.spec.service.enabled}
          rds:
            enabled: ${schema.spec.rds.enabled}
            image: ${schema.spec.image}
            replicas: 1
            dbHost: ${awsDatabase.status.endpointAddress}
            dbName: "testdb"

このように定義することで、依存関係は上手く kro が扱ってくれます。
アプリケーションと合わせて RDS インスタンスや Secret Manager、Pod Identity による権限付与などを一つのパッケージとして扱うことができています。

インスタンス作成

作成されたカスタム API を利用してインスタンスを作成します。

apiVersion: kro.run/v1alpha1
kind: DbWebStack
metadata:
  name: webapp-instance
spec:
  applicationName: webapp-rds
  name: webapp-rds
  image: xxxxxxxx.dkr.ecr.us-west-2.amazonaws.com/krmops-ecr-repo:rds-latest
  port: 8080
  replicas: 2
  service:
    enabled: true
  rds:
    enabled: true
    awsRegion: us-west-2
    vpcID: vpc-040d4d84ffa71830f
    cidrIP: 10.0.0.0/16
    subnetIDs:
      - subnet-024f6b3530ded492c
      - subnet-03cfe9ea01f066dbe

RDS 作成が含まれるので少し時間がかかりますが、9 分程度で STATE が ACTIVE になりました。

$ kubectl get rdsinstance
NAME         STATE    SYNCED   AGE
webapp-rds   ACTIVE   True     8m49s

アプリケーションログを見る限り正常に動作していそうです。
Ingress 経由で払い出された URL にアクセスすると、アプリケーションが作成されて DB 接続も正常に動作していることを確認できました。

スクリーンショット 2025-12-04 6.58.56.png

最後に

kro は難しそうで触れていなかったため、一通り試せてとても満足度が高かったです。
取っつき辛さもあまり無く、かなりシンプルに利用できる印象です。
EKS Capabilities でインストール作業や管理も簡単になったので、複数の Kubernetes リソースをまとめてパッケージ化したい場合は今後選択肢に入ってくるのかなと思います。
まだまだ成長中のツールだと思っているので、今後の機能強化等もキャッチアップしていきたいなと思いました。

この記事をシェアする

FacebookHatena blogX

関連記事