diff --git a/manifests/modules/automation/controlplanes/kro/.workshop/cleanup.sh b/manifests/modules/automation/controlplanes/kro/.workshop/cleanup.sh
new file mode 100755
index 0000000000..a848ab7d62
--- /dev/null
+++ b/manifests/modules/automation/controlplanes/kro/.workshop/cleanup.sh
@@ -0,0 +1,36 @@
+#!/bin/bash
+
+logmessage "Deleting resources created by kro..."
+
+kubectl delete webapplicationdynamodbs.kro.run/carts -n carts --ignore-not-found
+
+kubectl delete rgd/web-application-ddb --ignore-not-found
+
+kubectl delete rgd/web-application --ignore-not-found
+
+kubectl delete crd/webapplicationdynamodbs.kro.run --ignore-not-found
+
+kubectl delete crd/webapplications.kro.run --ignore-not-found
+
+uninstall-helm-chart kro kro-system
+
+set -e
+
+POD_ASSOCIATION_ID=$(aws eks list-pod-identity-associations --region $AWS_REGION --cluster-name $EKS_CLUSTER_NAME --service-account carts --namespace carts --output text --query 'associations[0].associationId')
+
+if [ "$POD_ASSOCIATION_ID" != "None" ]; then
+ logmessage "Deleting EKS Pod Identity Association..."
+
+ aws eks delete-pod-identity-association --region $AWS_REGION --association-id $POD_ASSOCIATION_ID --cluster-name $EKS_CLUSTER_NAME
+
+fi
+
+check=$(aws eks list-addons --cluster-name $EKS_CLUSTER_NAME --region $AWS_REGION --query "addons[? @ == 'eks-pod-identity-agent']" --output text)
+
+if [ ! -z "$check" ]; then
+ logmessage "Deleting EKS Pod Identity Agent addon..."
+
+ aws eks delete-addon --cluster-name $EKS_CLUSTER_NAME --addon-name eks-pod-identity-agent --region $AWS_REGION
+
+ aws eks wait addon-deleted --cluster-name $EKS_CLUSTER_NAME --addon-name eks-pod-identity-agent --region $AWS_REGION
+fi
diff --git a/manifests/modules/automation/controlplanes/kro/.workshop/manifests/kustomization.yaml b/manifests/modules/automation/controlplanes/kro/.workshop/manifests/kustomization.yaml
new file mode 100644
index 0000000000..8815f17894
--- /dev/null
+++ b/manifests/modules/automation/controlplanes/kro/.workshop/manifests/kustomization.yaml
@@ -0,0 +1,4 @@
+apiVersion: kustomize.config.k8s.io/v1beta1
+kind: Kustomization
+resources:
+ - nlb.yaml
diff --git a/manifests/modules/automation/controlplanes/kro/.workshop/manifests/nlb.yaml b/manifests/modules/automation/controlplanes/kro/.workshop/manifests/nlb.yaml
new file mode 100644
index 0000000000..5e457ff079
--- /dev/null
+++ b/manifests/modules/automation/controlplanes/kro/.workshop/manifests/nlb.yaml
@@ -0,0 +1,19 @@
+apiVersion: v1
+kind: Service
+metadata:
+ name: ui-nlb
+ annotations:
+ service.beta.kubernetes.io/aws-load-balancer-type: external
+ service.beta.kubernetes.io/aws-load-balancer-scheme: internet-facing
+ service.beta.kubernetes.io/aws-load-balancer-nlb-target-type: instance
+ namespace: ui
+spec:
+ type: LoadBalancer
+ ports:
+ - port: 80
+ targetPort: 8080
+ name: http
+ selector:
+ app.kubernetes.io/name: ui
+ app.kubernetes.io/instance: ui
+ app.kubernetes.io/component: service
diff --git a/manifests/modules/automation/controlplanes/kro/.workshop/terraform/main.tf b/manifests/modules/automation/controlplanes/kro/.workshop/terraform/main.tf
new file mode 100644
index 0000000000..b1b2aa6cad
--- /dev/null
+++ b/manifests/modules/automation/controlplanes/kro/.workshop/terraform/main.tf
@@ -0,0 +1,103 @@
+terraform {
+ required_providers {
+ kubectl = {
+ source = "gavinbunney/kubectl"
+ version = ">= 1.14"
+ }
+ }
+}
+
+provider "aws" {
+ region = "us-east-1"
+ alias = "virginia"
+}
+
+data "aws_caller_identity" "current" {}
+data "aws_region" "current" {}
+
+data "aws_ecrpublic_authorization_token" "token" {
+ provider = aws.virginia
+}
+
+
+module "eks_blueprints_addons" {
+ source = "aws-ia/eks-blueprints-addons/aws"
+ version = "1.22.0"
+
+ enable_aws_load_balancer_controller = true
+ aws_load_balancer_controller = {
+ wait = true
+ role_name = "${var.addon_context.eks_cluster_id}-alb-controller"
+ policy_name = "${var.addon_context.eks_cluster_id}-alb-controller"
+ }
+
+ cluster_name = var.addon_context.eks_cluster_id
+ cluster_endpoint = var.addon_context.aws_eks_cluster_endpoint
+ cluster_version = var.eks_cluster_version
+ oidc_provider_arn = var.addon_context.eks_oidc_provider_arn
+
+ eks_addons = {
+ eks-pod-identity-agent = {
+ addon_version = "v1.1.0-eksbuild.1"
+ }
+ }
+
+ observability_tag = null
+}
+
+resource "time_sleep" "blueprints_addons_sleep" {
+ depends_on = [
+ module.eks_blueprints_addons
+ ]
+
+ create_duration = "15s"
+ destroy_duration = "15s"
+}
+
+resource "kubectl_manifest" "nlb" {
+ yaml_body = templatefile("${path.module}/templates/nlb.yaml", {
+
+ })
+
+ wait = true
+
+ depends_on = [time_sleep.blueprints_addons_sleep]
+}
+
+module "eks_ack_addons" {
+ source = "aws-ia/eks-ack-addons/aws"
+
+ # Cluster Info
+ cluster_name = var.addon_context.eks_cluster_id
+ cluster_endpoint = var.addon_context.aws_eks_cluster_endpoint
+ oidc_provider_arn = var.addon_context.eks_oidc_provider_arn
+
+ # ECR Credentials
+ ecrpublic_username = data.aws_ecrpublic_authorization_token.token.user_name
+ ecrpublic_token = data.aws_ecrpublic_authorization_token.token.password
+
+ # Controllers to enable
+ enable_dynamodb = true
+ enable_iam = true
+ enable_eks = true
+ dynamodb = {
+ wait = true
+ role_name = "${var.addon_context.eks_cluster_id}-ack-dynamo"
+ policy_name = "${var.addon_context.eks_cluster_id}-ack-dynamo"
+ }
+
+ iam = {
+ wait = true
+ role_name = "${var.addon_context.eks_cluster_id}-ack-iam"
+ policy_name = "${var.addon_context.eks_cluster_id}-ack-iam"
+ }
+
+ eks = {
+ wait = true
+ role_name = "${var.addon_context.eks_cluster_id}-ack-eks"
+ policy_name = "${var.addon_context.eks_cluster_id}-ack-eks"
+ }
+
+ tags = var.tags
+
+}
\ No newline at end of file
diff --git a/manifests/modules/automation/controlplanes/kro/.workshop/terraform/outputs.tf b/manifests/modules/automation/controlplanes/kro/.workshop/terraform/outputs.tf
new file mode 100644
index 0000000000..9467be6846
--- /dev/null
+++ b/manifests/modules/automation/controlplanes/kro/.workshop/terraform/outputs.tf
@@ -0,0 +1,8 @@
+output "environment_variables" {
+ description = "Environment variables to be added to the IDE shell"
+ value = {
+ DYNAMO_ACK_VERSION = var.dynamo_ack_version,
+ KRO_VERSION = var.kro_version,
+ ACCOUNT_ID = data.aws_caller_identity.current.account_id
+ }
+}
\ No newline at end of file
diff --git a/manifests/modules/automation/controlplanes/kro/.workshop/terraform/templates/nlb.yaml b/manifests/modules/automation/controlplanes/kro/.workshop/terraform/templates/nlb.yaml
new file mode 100644
index 0000000000..5e457ff079
--- /dev/null
+++ b/manifests/modules/automation/controlplanes/kro/.workshop/terraform/templates/nlb.yaml
@@ -0,0 +1,19 @@
+apiVersion: v1
+kind: Service
+metadata:
+ name: ui-nlb
+ annotations:
+ service.beta.kubernetes.io/aws-load-balancer-type: external
+ service.beta.kubernetes.io/aws-load-balancer-scheme: internet-facing
+ service.beta.kubernetes.io/aws-load-balancer-nlb-target-type: instance
+ namespace: ui
+spec:
+ type: LoadBalancer
+ ports:
+ - port: 80
+ targetPort: 8080
+ name: http
+ selector:
+ app.kubernetes.io/name: ui
+ app.kubernetes.io/instance: ui
+ app.kubernetes.io/component: service
diff --git a/manifests/modules/automation/controlplanes/kro/.workshop/terraform/vars.tf b/manifests/modules/automation/controlplanes/kro/.workshop/terraform/vars.tf
new file mode 100644
index 0000000000..b0eda877c4
--- /dev/null
+++ b/manifests/modules/automation/controlplanes/kro/.workshop/terraform/vars.tf
@@ -0,0 +1,49 @@
+# tflint-ignore: terraform_unused_declarations
+variable "eks_cluster_id" {
+ description = "EKS cluster name"
+ type = string
+}
+
+# tflint-ignore: terraform_unused_declarations
+variable "eks_cluster_version" {
+ description = "EKS cluster version"
+ type = string
+}
+
+# tflint-ignore: terraform_unused_declarations
+variable "cluster_security_group_id" {
+ description = "EKS cluster security group ID"
+ type = any
+}
+
+# tflint-ignore: terraform_unused_declarations
+variable "addon_context" {
+ description = "Addon context that can be passed directly to blueprints addon modules"
+ type = any
+}
+
+# tflint-ignore: terraform_unused_declarations
+variable "tags" {
+ description = "Tags to apply to AWS resources"
+ type = any
+}
+
+# tflint-ignore: terraform_unused_declarations
+variable "resources_precreated" {
+ description = "Have expensive resources been created already"
+ type = bool
+}
+
+variable "dynamo_ack_version" {
+ description = "The version of Dynamo ACK to use"
+ type = string
+ # renovate: datasource=github-releases depName=aws-controllers-k8s/dynamodb-controller
+ default = "1.5.2"
+}
+
+variable "kro_version" {
+ description = "The version of Kro to use"
+ type = string
+ # renovate: datasource=github-releases depName=kro-run/kro
+ default = "0.4.1"
+}
\ No newline at end of file
diff --git a/manifests/modules/automation/controlplanes/kro/app/carts-ddb.yaml b/manifests/modules/automation/controlplanes/kro/app/carts-ddb.yaml
new file mode 100644
index 0000000000..9014810dba
--- /dev/null
+++ b/manifests/modules/automation/controlplanes/kro/app/carts-ddb.yaml
@@ -0,0 +1,22 @@
+apiVersion: kro.run/v1alpha1
+kind: WebApplicationDynamoDB
+metadata:
+ name: carts
+ namespace: carts
+spec:
+ # Basic types
+ appName: carts
+ replicas: 1
+ image: "public.ecr.aws/aws-containers/retail-store-sample-cart:1.2.1"
+ port: 8080
+
+ dynamodb:
+ tableName: "eks-workshop-carts-kro"
+
+ env:
+ RETAIL_CART_PERSISTENCE_PROVIDER: "dynamodb"
+ RETAIL_CART_PERSISTENCE_DYNAMODB_TABLE_NAME: "eks-workshop-carts-kro"
+
+ aws:
+ accountID: ${AWS_ACCOUNT_ID}
+ region: ${AWS_REGION}
diff --git a/manifests/modules/automation/controlplanes/kro/app/carts.yaml b/manifests/modules/automation/controlplanes/kro/app/carts.yaml
new file mode 100644
index 0000000000..1d84d86495
--- /dev/null
+++ b/manifests/modules/automation/controlplanes/kro/app/carts.yaml
@@ -0,0 +1,18 @@
+apiVersion: kro.run/v1alpha1
+kind: WebApplication
+metadata:
+ name: carts
+ namespace: carts
+spec:
+ # Basic types
+ appName: carts
+ replicas: 1
+ image: "public.ecr.aws/aws-containers/retail-store-sample-cart:1.2.1"
+ port: 8080
+ env:
+ RETAIL_CART_PERSISTENCE_PROVIDER: "in-memory"
+ RETAIL_CART_PERSISTENCE_DYNAMODB_TABLE_NAME: "Items"
+ RETAIL_CART_PERSISTENCE_DYNAMODB_CREATE_TABLE: "false"
+
+ service:
+ enabled: true
diff --git a/manifests/modules/automation/controlplanes/kro/app/kustomization.yaml b/manifests/modules/automation/controlplanes/kro/app/kustomization.yaml
new file mode 100644
index 0000000000..8713e1a2ed
--- /dev/null
+++ b/manifests/modules/automation/controlplanes/kro/app/kustomization.yaml
@@ -0,0 +1,4 @@
+apiVersion: kustomize.config.k8s.io/v1beta1
+kind: Kustomization
+resources:
+- carts-ddb.yaml
diff --git a/manifests/modules/automation/controlplanes/kro/rgds/webapp-dynamodb-rgd.yaml b/manifests/modules/automation/controlplanes/kro/rgds/webapp-dynamodb-rgd.yaml
new file mode 100644
index 0000000000..e91fa78cca
--- /dev/null
+++ b/manifests/modules/automation/controlplanes/kro/rgds/webapp-dynamodb-rgd.yaml
@@ -0,0 +1,178 @@
+apiVersion: kro.run/v1alpha1
+kind: ResourceGraphDefinition
+metadata:
+ name: web-application-ddb
+spec:
+ schema:
+ apiVersion: v1alpha1
+ kind: WebApplicationDynamoDB
+ spec:
+ appName: string | required=true description="Web Application Name"
+ replicas: integer | default=1 minimum=1 maximum=100
+ image: string | default=nginx
+ port: integer | default=8080
+
+ dynamodb:
+ tableName: string | required=true description="DynamoDB Table Name"
+
+ healthcheck:
+ readinessPath: string | default="/actuator/health/readiness"
+ readinessPort: integer | default=8080
+ livenessPath: string | default="/actuator/health/liveness"
+ livenessPort: integer | default=8080
+
+ service:
+ enabled: boolean | default=true
+
+ aws:
+ accountID: integer | required=true
+ region: string | default="us-west-2"
+
+ env: map[string]string | default={}
+
+ ingress:
+ enabled: boolean | default=false
+ path: string | default="/"
+ healthcheckPath: string | default="/health"
+ groupname: string | default="eks-workshop"
+
+ resources:
+ - id: podIdentityAssociation
+ template:
+ apiVersion: eks.services.k8s.aws/v1alpha1
+ kind: PodIdentityAssociation
+ metadata:
+ name: ${schema.spec.appName}
+ namespace: ${schema.spec.appName}
+ spec:
+ clusterName: "eks-workshop"
+ namespace: ${schema.spec.appName}
+ serviceAccount: ${schema.spec.appName}
+ roleARN: ${itemsTableIAMRole.status.ackResourceMetadata.arn}
+
+ - id: webApplication
+ template:
+ apiVersion: kro.run/v1alpha1
+ kind: WebApplication
+ metadata:
+ name: ${schema.spec.appName}
+ namespace: ${schema.spec.appName}
+ spec:
+ appName: ${schema.spec.appName}
+ replicas: 1
+ image: ${schema.spec.image}
+ port: 8080
+ healthcheck:
+ readinessPath: ${schema.spec.healthcheck.readinessPath}
+ readinessPort: ${schema.spec.healthcheck.readinessPort}
+ livenessPath: ${schema.spec.healthcheck.livenessPath}
+ livenessPort: ${schema.spec.healthcheck.livenessPort}
+
+ service:
+ enabled: ${schema.spec.service.enabled}
+ iamRole: ${podIdentityAssociation.status.ackResourceMetadata.arn}
+
+ env: ${schema.spec.env}
+
+ ingress:
+ enabled: ${schema.spec.ingress.enabled}
+ path: ${schema.spec.ingress.path}
+ healthcheckPath: ${schema.spec.ingress.healthcheckPath}
+ groupname: ${schema.spec.ingress.groupname}
+
+ - id: serviceDDB
+ template:
+ apiVersion: v1
+ kind: Service
+ metadata:
+ name: carts-dynamodb
+ labels:
+ app.kubernetes.io/created-by: eks-workshop
+ spec:
+ type: ClusterIP
+ ports:
+ - port: 8000
+ targetPort: dynamodb
+ protocol: TCP
+ name: dynamodb
+ selector:
+ app.kubernetes.io/name: ${schema.spec.appName}
+ app.kubernetes.io/instance: ${schema.spec.appName}
+ app.kubernetes.io/component: dynamodb
+
+ - id: itemsTable
+ template:
+ apiVersion: dynamodb.services.k8s.aws/v1alpha1
+ kind: Table
+ metadata:
+ name: items
+ namespace: ${schema.spec.appName}
+ spec:
+ keySchema:
+ - attributeName: id
+ keyType: HASH
+ attributeDefinitions:
+ - attributeName: id
+ attributeType: "S"
+ - attributeName: customerId
+ attributeType: "S"
+ billingMode: PAY_PER_REQUEST
+ tableName: ${schema.spec.dynamodb.tableName}
+ globalSecondaryIndexes:
+ - indexName: idx_global_customerId
+ keySchema:
+ - attributeName: customerId
+ keyType: HASH
+ - attributeName: id
+ keyType: RANGE
+ projection:
+ projectionType: "ALL"
+ - id: itemsTableIamPolicy
+ template:
+ apiVersion: iam.services.k8s.aws/v1alpha1
+ kind: Policy
+ metadata:
+ name: ${itemsTable.spec.tableName}-iam-policy
+ spec:
+ name: ${itemsTable.spec.tableName}-iam-policy
+ description: "EKS Workshop Carts DynamoDB Policy"
+ policyDocument: >
+ {
+ "Version": "2012-10-17",
+ "Statement": [
+ {
+ "Sid": "AllAPIActionsOnCart",
+ "Effect": "Allow",
+ "Action": "dynamodb:*",
+ "Resource": [
+ "arn:aws:dynamodb:${schema.spec.aws.region}:${schema.spec.aws.accountID}:table/${itemsTable.spec.tableName}",
+ "arn:aws:dynamodb:${schema.spec.aws.region}:${schema.spec.aws.accountID}:table/${itemsTable.spec.tableName}/index/*"
+ ]
+ }
+ ]
+ }
+ - id: itemsTableIAMRole
+ template:
+ apiVersion: iam.services.k8s.aws/v1alpha1
+ kind: Role
+ metadata:
+ name: ${itemsTable.spec.tableName}-iam-role
+ namespace: ${schema.spec.appName}
+ spec:
+ name: ${itemsTable.spec.tableName}-iam-role
+ description: "EKS Workshop Carts DynamoDB Role"
+ maxSessionDuration: 3600
+ policies:
+ - ${itemsTableIamPolicy.status.ackResourceMetadata.arn}
+ assumeRolePolicyDocument: >
+ {
+ "Version":"2012-10-17",
+ "Statement": [{
+ "Effect":"Allow",
+ "Principal": {"Service": "pods.eks.amazonaws.com"},
+ "Action": [
+ "sts:TagSession",
+ "sts:AssumeRole"
+ ]
+ }]
+ }
diff --git a/manifests/modules/automation/controlplanes/kro/rgds/webapp-rgd.yaml b/manifests/modules/automation/controlplanes/kro/rgds/webapp-rgd.yaml
new file mode 100644
index 0000000000..0c9e7972e3
--- /dev/null
+++ b/manifests/modules/automation/controlplanes/kro/rgds/webapp-rgd.yaml
@@ -0,0 +1,180 @@
+apiVersion: kro.run/v1alpha1
+kind: ResourceGraphDefinition
+metadata:
+ name: web-application
+spec:
+ schema:
+ apiVersion: v1alpha1
+ kind: WebApplication
+ spec:
+ appName: string | required=true description="Web Application Name"
+ replicas: integer | default=1 minimum=1 maximum=100
+ image: string | default=nginx
+ port: integer | default=8080
+ healthcheck:
+ readinessPath: string | default="/actuator/health/readiness"
+ readinessPort: integer | default=8080
+ livenessPath: string | default="/actuator/health/liveness"
+ livenessPort: integer | default=8080
+
+ service:
+ enabled: boolean | default=true
+ iamRole: string | default=""
+
+ env: map[string]string | default={}
+
+ ingress:
+ enabled: boolean | default=false
+ path: string | default="/"
+ healthcheckPath: string | default="/health"
+ groupname: string | default="eks-workshop"
+
+ resources:
+ - id: serviceAccount
+ template:
+ apiVersion: v1
+ kind: ServiceAccount
+ metadata:
+ name: ${schema.spec.appName}
+ namespace: ${schema.spec.appName}
+
+ - id: configMap
+ template:
+ apiVersion: v1
+ kind: ConfigMap
+ metadata:
+ name: ${schema.spec.appName}
+ namespace: ${schema.spec.appName}
+ data: ${schema.spec.env}
+
+ - id: deployment
+ template:
+ apiVersion: apps/v1
+ kind: Deployment
+ metadata:
+ name: ${schema.spec.appName}
+ namespace: ${schema.spec.appName}
+ labels:
+ app.kubernetes.io/created-by: "eks-workshop"
+ app.kubernetes.io/type: app
+ spec:
+ replicas: ${schema.spec.replicas}
+ revisionHistoryLimit: 3
+ selector:
+ matchLabels:
+ app.kubernetes.io/name: ${schema.spec.appName}
+ app.kubernetes.io/instance: ${schema.spec.appName}
+ app.kubernetes.io/component: service
+ template:
+ metadata:
+ annotations:
+ prometheus.io/path: /actuator/prometheus
+ prometheus.io/port: "8080"
+ prometheus.io/scrape: "true"
+ labels:
+ app.kubernetes.io/name: ${schema.spec.appName}
+ app.kubernetes.io/instance: ${schema.spec.appName}
+ app.kubernetes.io/component: service
+ app.kubernetes.io/created-by: eks-workshop
+ spec:
+ serviceAccountName: ${schema.spec.appName}
+ securityContext:
+ fsGroup: 1000
+ containers:
+ - name: ${schema.spec.appName}
+ env:
+ - name: JAVA_OPTS
+ value: -XX:MaxRAMPercentage=75.0 -Djava.security.egd=file:/dev/urandom
+ envFrom:
+ - configMapRef:
+ name: ${schema.spec.appName}
+ securityContext:
+ capabilities:
+ drop:
+ - ALL
+ readOnlyRootFilesystem: true
+ runAsNonRoot: true
+ runAsUser: 1000
+ image: ${schema.spec.image}
+ imagePullPolicy: IfNotPresent
+ ports:
+ - name: http
+ containerPort: ${schema.spec.port}
+ protocol: TCP
+ readinessProbe:
+ httpGet:
+ path: ${schema.spec.healthcheck.readinessPath}
+ port: ${schema.spec.healthcheck.readinessPort}
+ initialDelaySeconds: 15
+ periodSeconds: 3
+ livenessProbe:
+ httpGet:
+ path: ${schema.spec.healthcheck.livenessPath}
+ port: ${schema.spec.healthcheck.livenessPort}
+ initialDelaySeconds: 45
+ periodSeconds: 3
+ resources:
+ limits:
+ memory: 1Gi
+ requests:
+ cpu: 250m
+ memory: 1Gi
+ volumeMounts:
+ - mountPath: /tmp
+ name: tmp-volume
+ volumes:
+ - name: tmp-volume
+ emptyDir:
+ medium: Memory
+
+ - id: service
+ template:
+ apiVersion: v1
+ kind: Service
+ metadata:
+ name: ${schema.spec.appName}
+ namespace: ${schema.spec.appName}
+ labels:
+ app.kubernetes.io/created-by: eks-workshop
+ spec:
+ type: ClusterIP
+ ports:
+ - port: 80
+ targetPort: http
+ protocol: TCP
+ name: http
+ selector:
+ app.kubernetes.io/name: ${schema.spec.appName}
+ app.kubernetes.io/instance: ${schema.spec.appName}
+ app.kubernetes.io/component: service
+ includeWhen:
+ - ${schema.spec.service.enabled}
+
+ - id: ingress
+ template:
+ apiVersion: networking.k8s.io/v1
+ kind: Ingress
+ metadata:
+ name: ${schema.spec.appName}
+ namespace: ${schema.spec.appName}
+ labels:
+ app.kubernetes.io/created-by: eks-workshop
+ annotations:
+ alb.ingress.kubernetes.io/scheme: internet-facing
+ alb.ingress.kubernetes.io/target-type: ip
+ alb.ingress.kubernetes.io/healthcheck-path: ${schema.spec.ingress.healthcheckPath}
+ alb.ingress.kubernetes.io/group.name: ${schema.spec.ingress.groupname}
+ spec:
+ ingressClassName: alb
+ rules:
+ - http:
+ paths:
+ - path: ${schema.spec.ingress.path}
+ pathType: Prefix
+ backend:
+ service:
+ name: ${schema.spec.appName}
+ port:
+ number: 80
+ includeWhen:
+ - ${schema.spec.ingress.enabled}
diff --git a/website/docs/automation/controlplanes/kro/cloud-dynamodb.md b/website/docs/automation/controlplanes/kro/cloud-dynamodb.md
new file mode 100644
index 0000000000..de84add61b
--- /dev/null
+++ b/website/docs/automation/controlplanes/kro/cloud-dynamodb.md
@@ -0,0 +1,139 @@
+---
+title: "Updating the application"
+sidebar_position: 6
+---
+
+In this section, we will replace the in-memory database being used by carts with DynamoDB. We will do this by composing a WebApplicationDynamoDB ResourceGraphDefinition that builds on the base WebApplication template.
+
+Let's examine the ResourceGraphDefinition template that defines the reusable WebApplicationDynamoDB API:
+
+
+ Expand for full RGD manifest
+
+::yaml{file="manifests/modules/automation/controlplanes/kro/rgds/webapp-dynamodb-rgd.yaml"}
+
+
+
+This ResourceGraphDefinition:
+1. Creates a custom `WebApplicationDynamoDB` API that composes the WebApplication RGD
+2. Provisions a DynamoDB table with ACK
+3. Creates IAM roles and policies for DynamoDB access
+4. Configures EKS Pod Identity for secure access from application pods
+
+To learn more about EKS Pod Identity, refer to the [official documentation](https://docs.aws.amazon.com/eks/latest/userguide/pod-identities.html).
+
+:::info
+Notice how this RGD includes the WebApplication RGD in its resources section. By referencing `webApplication`, this template reuses all the Kubernetes resources defined in the base WebApplication RGD while adding DynamoDB, IAM, and Pod Identity resources.
+:::
+
+Let's apply the ResourceGraphDefinition to register the WebApplicationDynamoDB API:
+
+```bash wait=10
+$ kubectl apply -f ~/environment/eks-workshop/modules/automation/controlplanes/kro/rgds/webapp-dynamodb-rgd.yaml
+resourcegraphdefinition.kro.run/web-application-ddb created
+```
+
+This registers the WebApplicationDynamoDB API. Verify the Custom Resource Definition (CRD):
+
+```bash
+$ kubectl get crd webapplicationdynamodbs.kro.run
+NAME CREATED AT
+webapplicationdynamodbs.kro.run 2024-01-15T10:35:00Z
+```
+
+Now let's examine the carts-ddb.yaml file that will use the WebApplicationDynamoDB API to create an instance of the **Carts** component:
+
+::yaml{file="manifests/modules/automation/controlplanes/kro/app/carts-ddb.yaml" paths="kind,metadata,spec.appName,spec.replicas,spec.image,spec.port,spec.dynamodb,spec.env,spec.aws"}
+
+1. Uses the custom WebApplicationDynamoDB API created by our RGD
+2. Creates a resource named `carts` in the `carts` namespace
+3. Specifies the application name for resource naming
+4. Sets single replica
+5. Uses the retail store cart service container image
+6. Exposes the application on port 8080
+7. Specifies the DynamoDB table name
+8. Sets environment variables to enable DynamoDB persistence mode
+9. Provides AWS account ID and region for IAM and Pod Identity configuration
+
+First, let's delete the existing **Carts** component:
+
+```bash
+$ kubectl delete webapplication.kro.run/carts -n carts
+webapplication.kro.run "carts" deleted
+```
+
+Next, let's deploy the updated component leveraging the carts-ddb.yaml file:
+
+```bash wait=10
+$ kubectl kustomize ~/environment/eks-workshop/modules/automation/controlplanes/kro/app \
+ | envsubst | kubectl apply -f-
+webapplicationdynamodb.kro.run/carts created
+```
+
+kro will process this custom resource and create all the underlying resources including the DynamoDB table. Let's verify the custom resource was created:
+
+```bash
+$ kubectl get webapplicationdynamodb -n carts
+NAME AGE
+carts 30s
+```
+
+To verify that the DynamoDB table has been created, we can check the generated ACK resource:
+
+```bash timeout=300
+$ kubectl wait table.dynamodb.services.k8s.aws items -n carts --for=condition=ACK.ResourceSynced --timeout=15m
+table.dynamodb.services.k8s.aws/items condition met
+$ kubectl get table.dynamodb.services.k8s.aws items -n carts -ojson | yq '.status."tableStatus"'
+ACTIVE
+```
+
+Let's confirm that the table has been created using the AWS CLI:
+
+```bash
+$ aws dynamodb list-tables
+
+{
+ "TableNames": [
+ "eks-workshop-carts-kro"
+ ]
+}
+```
+
+Perfect! Our DynamoDB table and component have been successfully created using kro's composable approach.
+
+To verify that the component is working with the new DynamoDB table, we can interact with it through a browser. An NLB has been created to expose the sample application for testing:
+
+```bash
+$ LB_HOSTNAME=$(kubectl -n ui get service ui-nlb -o jsonpath='{.status.loadBalancer.ingress[*].hostname}{"\n"}')
+$ echo "http://$LB_HOSTNAME"
+http://k8s-ui-uinlb-fe4dc7c11e-a362df3b7254c797.elb.us-west-2.amazonaws.com
+```
+
+:::info
+Please note that the actual endpoint will be different when you run this command as a new Network Load Balancer endpoint will be provisioned.
+:::
+
+To wait until the load balancer has finished provisioning, you can run this command:
+
+```bash timeout=610
+$ wait-for-lb $(kubectl get service -n ui ui-nlb -o jsonpath="{.status.loadBalancer.ingress[*].hostname}{'\n'}")
+```
+
+Once the load balancer is provisioned, you can access it by pasting the URL in your web browser. You'll see the UI from the web store displayed and will be able to navigate around the site as a user.
+
+
+
+
+
+To verify that the **Carts** module is indeed using the DynamoDB table we just provisioned, try adding a few items to the cart.
+
+
+
+To confirm that these items are also in the DynamoDB table, run:
+
+```bash
+$ aws dynamodb scan --table-name "eks-workshop-carts-kro"
+```
+
+Congratulations! We have successfully demonstrated kro's composability by building on the base WebApplication template to add DynamoDB storage.
+
diff --git a/website/docs/automation/controlplanes/kro/index.md b/website/docs/automation/controlplanes/kro/index.md
new file mode 100644
index 0000000000..e913053211
--- /dev/null
+++ b/website/docs/automation/controlplanes/kro/index.md
@@ -0,0 +1,29 @@
+---
+title: "kro - Kube Resource Orchestrator"
+sidebar_position: 1
+sidebar_custom_props: { "module": true }
+description: "Compose and manage complex Kubernetes resource graphs with kro on Amazon Elastic Kubernetes Service."
+---
+
+::required-time
+
+:::tip Before you start
+Prepare your environment for this section:
+
+```bash timeout=300 wait=30
+$ prepare-environment automation/controlplanes/kro
+```
+
+This will make the following changes to your lab environment:
+
+- Install the AWS Controller for DynamoDB in the Amazon EKS cluster
+
+You can view the Terraform that applies these changes [here](https://github.com/VAR::MANIFESTS_OWNER/VAR::MANIFESTS_REPOSITORY/tree/VAR::MANIFESTS_REF/manifests/modules/automation/controlplanes/kro/.workshop/terraform).
+
+:::
+
+[kro (Kube Resource Orchestrator)](https://kro.run/) is an open-source Kubernetes operator that enables you to define custom APIs for creating groups of related Kubernetes resources. With kro, you create ResourceGraphDefinitions (RGDs) that use CEL (Common Expression Language) expressions to define relationships between resources and automatically determine their creation order.
+
+kro allows you to compose multiple Kubernetes resources into higher-level abstractions with intelligent dependency handling - it automatically determines the correct order to deploy resources by analyzing how they reference each other. You can pass values between resources using CEL expressions, include conditional logic, and define default values to simplify the user experience.
+
+In this lab, we'll explore kro's capabilities by first deploying the complete **Carts** component with an in-memory database using a WebApplication ResourceGraphDefinition. We'll then enhance this by composing a WebApplicationDynamoDB ResourceGraphDefinition that builds on the base WebApplication template to add Amazon DynamoDB storage.
\ No newline at end of file
diff --git a/website/docs/automation/controlplanes/kro/introduction.md b/website/docs/automation/controlplanes/kro/introduction.md
new file mode 100644
index 0000000000..7c29791b4f
--- /dev/null
+++ b/website/docs/automation/controlplanes/kro/introduction.md
@@ -0,0 +1,49 @@
+---
+title: "Introduction"
+sidebar_position: 3
+---
+
+kro operates within a cluster using two primary components:
+
+1. The kro controller manager, which provides the core orchestration functionality
+2. ResourceGraphDefinitions (RGDs), which define templates for creating groups of related resources
+
+The kro controller manager watches for ResourceGraphDefinition custom resources and orchestrates the creation and management of the underlying Kubernetes resources defined in the template.
+
+kro simplifies complex resource management by allowing platform teams to define ResourceGraphDefinitions that encapsulate multiple related resources. Developers interact with simple custom APIs defined by the RGD schema, while kro handles the complexity of creating and managing the underlying resources. This architecture provides a clear separation between the platform team, who defines the ResourceGraphDefinitions, and application developers, who consume the simplified custom APIs to create complex resource groups.
+
+Let us first install kro to the Kubernetes cluster by using a Helm chart:
+
+```bash wait=60
+$ helm install kro oci://ghcr.io/kro-run/kro/kro \
+ --version=${KRO_VERSION} \
+ --namespace kro-system --create-namespace \
+ --wait
+```
+
+Verify the kro controller is running:
+
+```bash
+$ kubectl get deployment -n kro-system
+NAME READY UP-TO-DATE AVAILABLE AGE
+kro 1/1 1 1 13s
+```
+
+We can also verify that the kro custom resource definitions have been installed:
+
+```bash
+$ kubectl get crd | grep kro
+resourcegraphdefinitions.kro.run 2025-10-15T22:34:13Z
+```
+
+
+## ResourceGraphDefinition Workflow
+
+When you create a ResourceGraphDefinition, kro:
+
+1. **Registers a new Custom API** - Based on the schema defined in the RGD, kro automatically creates a new Kubernetes CRD that developers can use
+2. **Processes resource instances** - When developers create instances of the custom API, kro processes the request using the defined template
+3. **Evaluates CEL expressions** - kro uses Common Expression Language (CEL) to evaluate conditions, pass values between resources, and determine the creation order
+4. **Handles dependencies intelligently** - kro automatically analyzes how resources reference each other and determines the optimal deployment order without manual configuration
+5. **Creates managed resources** - Based on the template and dependency analysis, kro creates the specified Kubernetes resources in the correct order
+6. **Maintains relationships** - kro tracks dependencies between resources and ensures proper lifecycle management
diff --git a/website/docs/automation/controlplanes/kro/provision-resources.md b/website/docs/automation/controlplanes/kro/provision-resources.md
new file mode 100644
index 0000000000..f75ecd4309
--- /dev/null
+++ b/website/docs/automation/controlplanes/kro/provision-resources.md
@@ -0,0 +1,104 @@
+---
+title: "Provisioning resources with kro"
+sidebar_position: 5
+---
+
+Now that kro has been installed, we will deploy the **Carts** component using a kro WebApplication ResourceGraphDefinitions. First, let's examine the ResourceGraphDefinition template that defines the reusable WebApplication API:
+
+
+ Expand for full RGD manifest
+
+::yaml{file="manifests/modules/automation/controlplanes/kro/rgds/webapp-rgd.yaml"}
+
+
+
+This ResourceGraphDefinition creates a custom `WebApplication` API that abstracts the complexity of deploying:
+- ServiceAccount
+- ConfigMap
+- Deployment
+- Service
+- Ingress (optional)
+
+The schema provides sensible defaults while allowing customization of key parameters like the application image, replica count, environment variables, and health check configuration as shown:
+
+::yaml{file="manifests/modules/automation/controlplanes/kro/rgds/webapp-rgd.yaml" zoomPath="spec.schema.spec" zoomBefore="0"}
+
+:::info
+Notice how the schema uses default values and type definitions to create a developer-friendly API that hides the underlying Kubernetes complexity.
+:::
+
+We will use this WebApplication ResourceGraphDefinition to create an instance of the **Carts** component which uses an in-memory database. To do this, let's first clean up the existing carts deployment:
+
+```bash
+$ kubectl delete all --all -n carts
+pod "carts-68d496fff8-9lcpc" deleted
+pod "carts-dynamodb-995f7768c-wtsbr" deleted
+service "carts" deleted
+service "carts-dynamodb" deleted
+deployment.apps "carts" deleted
+deployment.apps "carts-dynamodb" deleted
+```
+
+Next, apply the ResourceGraphDefinition to register the WebApplication API:
+
+```bash wait=10
+$ kubectl apply -f ~/environment/eks-workshop/modules/automation/controlplanes/kro/rgds/webapp-rgd.yaml
+resourcegraphdefinition.kro.run/web-application created
+```
+
+This registers the WebApplication API. kro automatically creates the Custom Resource Definition (CRD) based on the RGD schema. Verify the CRD:
+
+```bash
+$ kubectl get crd webapplications.kro.run
+NAME CREATED AT
+webapplications.kro.run 2024-01-15T10:30:00Z
+```
+
+Now let's examine the `carts.yaml` file that will use the WebApplication API to create an instance of the **Carts** component:
+
+::yaml{file="manifests/modules/automation/controlplanes/kro/app/carts.yaml" paths="kind,metadata,spec.appName,spec.replicas,spec.image,spec.port,spec.env,spec.service"}
+
+1. Uses the custom WebApplication API created by our RGD
+2. Creates a resource named `carts` in the `carts` namespace
+3. Specifies the application name for resource naming
+4. Sets single replica
+5. Uses the retail store cart service container image
+6. Exposes the application on port 8080
+7. Configures environment variables for in-memory persistence mode
+8. Enables the Kubernetes Service resource
+
+Let's deploy the application:
+
+```bash wait=30
+$ kubectl apply -f ~/environment/eks-workshop/modules/automation/controlplanes/kro/app/carts.yaml
+webapplication.kro.run/carts created
+```
+
+kro will process this custom resource and create all the underlying Kubernetes resources. Let's verify the custom resource was created:
+
+```bash
+$ kubectl get webapplication -n carts
+NAME AGE
+carts 30s
+```
+
+Next, verify the deployment:
+
+```bash
+$ kubectl get all -n carts
+NAME READY STATUS RESTARTS AGE
+pod/carts-7d58cfb7c9-xyz12 1/1 Running 0 30s
+
+NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
+service/carts ClusterIP 172.20.123.45 80/TCP 30s
+
+NAME READY UP-TO-DATE AVAILABLE AGE
+deployment.apps/carts 1/1 1 1 30s
+
+NAME DESIRED CURRENT READY AGE
+replicaset.apps/carts-7d58cfb7c9 1 1 1 30s
+```
+
+kro has successfully orchestrated the deployment of all Kubernetes resources required by the **Carts** component as a single unit. By using kro, we've transformed what would typically require applying multiple YAML files into a single, declarative API call. This demonstrates kro's power in simplifying complex resource orchestration.
+
+In the next section, we'll replace the in-memory database that is currently being used by carts with an Amazon DynamoDB table.