Deploy complete serverless applications on AWS with a single kubectl apply using the Hybrid Kro Compositions pattern — combining Kro, ACK, and Crossplane on Kubernetes.
- Overview
- The Sample Application
- Architecture
- What Gets Deployed
- Prerequisites
- Getting Started
- Repository Structure
- Documentation
- Related Resources
This repository implements a three-layer hybrid architecture for deploying serverless applications on AWS using Kubernetes-native orchestration:
| Layer | What | How | Lifecycle |
|---|---|---|---|
| Layer 1 — Shared Resources | CloudFront, Cognito, IAM roles, S3 buckets | Standalone ACK/Crossplane manifests (kubectl apply) |
One-time setup, long-lived |
| Layer 2 — ResourceGraphDefinition | Kro RGD orchestrating 15 resources (ACK + Crossplane + K8s) | kubectl apply the RGD definition |
One-time per cluster |
| Layer 3 — Application Instances | Complete serverless app (API + Lambda + DynamoDB + UI) | kubectl apply a 5-field instance YAML |
Per-application, dynamic |
Kro is the central orchestrator. It takes a simple instance spec (team, developer, app, environment, region) and provisions 15 AWS resources with automatic dependency management — in 5-8 minutes.
💡 Why the separation? Shared resources (Layer 1) are intentionally kept as individual manifests so developers can explore each ACK/Crossplane resource type independently. The Kro RGD (Layer 2) demonstrates the real power: orchestrating complex, multi-resource applications while referencing pre-existing platform infrastructure.
The application deployed by this stack is Smart Inventory Hub — a product inventory management system with a REST API and a web UI. It's intentionally simple so the focus stays on the infrastructure orchestration pattern, not the business logic.
What it does:
- CRUD operations for products (create, read, update, delete) via a REST API
- Dashboard with inventory metrics: total products, inventory value, low stock alerts, categories
- Search and filter products by name, SKU, category
- Interactive charts for stock status and category distribution
Why these AWS services:
| Service | Why it's needed | What it demonstrates |
|---|---|---|
| DynamoDB | Stores product data (NoSQL, serverless, on-demand) | ACK managing a data store |
| Lambda | Runs the Python API handler — no servers to manage | Crossplane managing compute |
| API Gateway | Exposes the Lambda as HTTP endpoints (GET, POST, PUT, DELETE) | ACK managing API routing with multiple routes |
| CloudFront | Serves the static UI via CDN with path-based routing per instance | ACK managing a shared CDN |
| Route 53 | Maps yourdomain.com to CloudFront (single record for all instances) |
Prerequisite DNS configuration |
| Cognito | JWT authentication for the API with centralized callback | Crossplane managing auth resources |
| S3 | Stores Lambda code and UI static files | ACK managing object storage |
| IAM | Least-privilege roles for Lambda and S3 sync jobs | ACK managing security |
The combination of these services creates a realistic serverless stack that exercises cross-controller dependencies — DynamoDB → Lambda → API Gateway — which is exactly what Kro is designed to orchestrate.
Kro orchestrates resources across ACK, Crossplane, and native Kubernetes seamlessly:
%%{init: {'theme':'dark'}}%%
sequenceDiagram
participant Dev as Developer
participant K8s as Kubernetes
participant Kro
participant Controllers as ACK/Crossplane
participant AWS
Dev->>K8s: kubectl apply instance.yaml
K8s->>Kro: Create SmartInventoryHub
Kro->>Kro: Analyze dependencies
Kro->>Controllers: Create resources in order
Controllers->>AWS: Provision AWS resources
AWS-->>Controllers: Resources ready
Kro->>K8s: Create Job for UI sync
K8s->>AWS: Sync UI assets to S3
Kro-->>Dev: Instance ready (5-8 min)
A single RGD instance provisions 15 resources:
%%{init: {'theme':'dark'}}%%
graph TB
subgraph Instance["SmartInventoryHub Instance"]
YAML["instance.yaml<br/>5 fields"]
end
subgraph DataLayer["Data Layer"]
DDB["DynamoDB Table<br/>Encrypted, On-Demand"]
end
subgraph ComputeLayer["Compute Layer"]
Lambda["Lambda Function<br/>Python 3.12"]
Perm["Lambda Permission"]
end
subgraph APILayer["API Layer"]
API["API Gateway HTTP"]
INT["Lambda Integration"]
Routes["5 Routes<br/>GET/POST/PUT/DELETE/OPTIONS"]
Stage["API Stage"]
end
subgraph AutomationLayer["Automation Layer"]
SA["ServiceAccount"]
Jobs["Job<br/>UI Sync"]
CM["ConfigMap"]
end
YAML --> DDB
DDB --> Lambda
Lambda --> INT
API --> INT
INT --> Routes
Routes --> Stage
Stage --> Perm
Lambda --> CM
API --> CM
SA --> Jobs
style Instance fill:#1a1a2e,stroke:#4A90E2,stroke-width:2px
style DataLayer fill:#1a1a2e,stroke:#FF9F43,stroke-width:2px
style ComputeLayer fill:#1a1a2e,stroke:#4A90E2,stroke-width:2px
style APILayer fill:#1a1a2e,stroke:#FF9F43,stroke-width:2px
style AutomationLayer fill:#1a1a2e,stroke:#26DE81,stroke-width:2px
| # | Resource | Controller | Purpose |
|---|---|---|---|
| 1 | DynamoDB Table | ACK | Data storage |
| 2 | Lambda Function | Crossplane | Business logic |
| 3 | API Gateway | ACK | HTTP API |
| 4 | API Integration | ACK | Lambda proxy |
| 5 | JWT Authorizer | ACK | Cognito JWT validation |
| 6-10 | API Routes (5) | ACK | CRUD endpoints |
| 11 | API Stage | ACK | Environment stage |
| 12 | Lambda Permission | Crossplane | API Gateway invoke |
| 13 | ConfigMap | K8s | App info |
| 14 | S3 Uploader SA | K8s | IRSA for S3 |
| 15 | UI Sync Job | K8s | Sync UI assets |
| Feature | Description |
|---|---|
| 🚀 One-Command Deployment | Deploy complete serverless stacks with a single kubectl apply |
| 🔄 GitOps Ready | Declarative YAML manifests for version control and CI/CD integration |
| 🔐 Security First | Pre-configured IAM roles with least-privilege access and Cognito JWT authentication |
| 📊 Multi-Environment | Support for dev, staging, and production environments |
| 🌐 CDN Integration | Shared CloudFront distribution with per-app path routing |
| 🔑 Authentication | Integrated Cognito authentication with JWT-protected API endpoints |
| 💰 Cost Optimized | 90%+ cost reduction through shared platform resources |
Before starting, ensure you have the following infrastructure and tools ready.
| Tool | Version | Install |
|---|---|---|
| 2.x | Install Guide | |
| 1.28+ | Install Guide | |
| 3.x | Install Guide | |
| any | brew install gettext (macOS) |
| Component | Version | Install |
|---|---|---|
| 1.28+ | EKS User Guide | |
| v0.8.4+ | EKS Capability · Helm | |
| Latest | EKS Capability · Helm | |
| 2.x | Helm Install |
| Provider | Version | Install |
|---|---|---|
| v1.16.0 | Upbound Marketplace | |
| v2.4.0 | Upbound Marketplace |
| Resource | Required | Details |
|---|---|---|
| ✅ | Certificate for yourdomain.com in us-east-1 — required for CloudFront · ACM Guide |
|
| ✅ | DNS management · Route 53 Guide |
🌐 About the domain: This stack uses a single domain with path-based routing to give each application instance its own URL (e.g.,
yourdomain.com/john-inventory-dev/). You need a domain registered in Route 53 with an ACM certificate inus-east-1. If you don't have a domain, you can register one through Route 53 or use an existing domain by delegating a subdomain to a Route 53 hosted zone. A single DNS record and a single Cognito callback URL cover all application instances — no per-instance changes needed.
💡 EKS Capabilities vs Helm: EKS Capabilities are AWS-managed — no IRSA needed for controllers, automatic updates, single command install. Helm gives you more control but requires IRSA configuration per controller. This repo includes trust policy files in
prerequisites/for the EKS Capability approach.
⚠️ IRSA Note: ACK/Kro controllers as EKS Capabilities do not need IRSA. However, the shared resource IAM roles (s3-uploader) and Crossplane providers still require IRSA because they are assumed by Kubernetes Pods via ServiceAccount annotations.
IRSA requires an IAM OIDC identity provider associated with your EKS cluster. This is a one-time setup:
# Check if already associated
aws iam list-open-id-connect-providers --no-cli-pager | grep $(aws eks describe-cluster \
--name <cluster-name> --region <region> \
--query "cluster.identity.oidc.issuer" --output text --no-cli-pager | sed 's|https://||' | cut -d'/' -f4)
# If not found, associate it
eksctl utils associate-iam-oidc-provider \
--cluster <cluster-name> --region <region> --approveThe RGD uses Crossplane for Lambda and Cognito resources. Provider manifests are in prerequisites/:
# 1. Apply runtime config (IRSA annotation for provider pods)
kubectl apply -f prerequisites/crossplane-runtime-config.yaml
# 2. Install providers
kubectl apply -f prerequisites/crossplane-providers.yaml
# 3. Wait for HEALTHY
kubectl get providers -w
# 4. Apply ProviderConfig
kubectl apply -f prerequisites/crossplane-provider-config.yaml| Provider | Version | CRDs Used | Layer |
|---|---|---|---|
| v1.16.0 | Function, Permission |
Layer 2 (RGD) | |
| v2.4.0 | UserPool, UserPoolClient, UserPoolDomain |
Layer 1 (Shared) | |
| v2.4.0 | Base provider (auto-installed) | — |
⚠️ Version Compatibility: Thecognitoidpandlambdaproviders must use compatibleprovider-family-awsversions. If you seeincompatible dependencieserrors, ensure both providers target the same family version (currently v2.4.0).
ACK installed via EKS Capability uses newer CRD schemas than Helm-based installations. The manifests in this repo are tested with EKS Capability. If you use Helm-installed ACK controllers, you may need to adjust field names:
| Service | Field | EKS Capability (this repo) | Helm (older versions) |
|---|---|---|---|
| S3 | Public access block | blockPublicACLs, ignorePublicACLs |
blockPublicAcls, ignorePublicAcls |
| IAM | Inline policies | inlinePolicies: { Name: "json" } (map) |
policies: [{ policyName, policyDocument }] (array) |
| CloudFront | Collection fields | quantity not needed (inferred from items) |
quantity: N required alongside items |
| CloudFront | Error response code | responseCode: "200" (string) |
responseCode: 200 (integer) |
| CloudFront | Origin fields | All fields required (originPath, customHeaders, connectionAttempts) |
Partial fields accepted |
| Route 53 | Record name | Relative to hosted zone (subdomain only, e.g., hkc) |
FQDN (e.g., hkc.example.com) |
💡 Tip: To check the exact field names on your cluster, use
kubectl explain:kubectl explain bucket.s3.services.k8s.aws.spec.publicAccessBlock kubectl explain role.iam.services.k8s.aws.spec.inlinePolicies kubectl explain distribution.cloudfront.services.k8s.aws.spec.distributionConfig.origins
The deployment follows the three-layer architecture: first set up shared platform resources (Layer 1), then apply the RGD definition (Layer 2), and finally create application instances (Layer 3).
git clone https://github.com/aws-samples/kro-serverless-app-stack.git
cd kro-serverless-app-stack
# Copy and customize the environment template
cp scripts/env.template scripts/env.sh
vim scripts/env.sh # Edit Sections 1-2 only (see below)
# Source the environment
source scripts/env.shFill in Sections 1-2 of env.sh with values you already know:
| Section | Variables | How to find them |
|---|---|---|
| 1 — Core AWS Config | AWS_ACCOUNT_ID, AWS_REGION, OIDC_PROVIDER |
aws sts get-caller-identity, aws eks describe-cluster |
| 2 — DNS & Certificates | ROUTE53_HOSTED_ZONE_ID, DOMAIN_NAME, DNS_SUBDOMAIN, ACM_GLOBAL_CERT_ARN |
aws route53 list-hosted-zones, aws acm list-certificates --region us-east-1 |
💡 Note: Sections 3-5 (shared resources, IAM roles, Cognito) are auto-populated by the deployment script in Step 2. Leave them as placeholders for now.
Layer 1 — Deploy the shared platform infrastructure that all application instances will reference. These are standalone ACK/Crossplane manifests applied individually.
# Verify prerequisites are met
./scripts/0-verify-prerequisites.sh
# Deploy all shared resources (Cognito, CloudFront, S3, IAM)
./scripts/1-deploy-shared-resources.shThis creates:
- Cognito — User Pool, App Client, User Groups (Crossplane)
- S3 — Platform Assets bucket, Shared UI bucket (ACK)
- IAM — Lambda Execution role, S3 Uploader role (ACK)
- CloudFront — Distribution with OAC and URL rewrite Function (ACK)
Each resource is a standalone Kubernetes manifest you can inspect and customize:
| Resource | Controller | Manifest |
|---|---|---|
| User Pool | Crossplane | shared-resources/cognito/user-pool.yaml |
| User Pool Domain | Crossplane | shared-resources/cognito/user-pool-domain.yaml |
| User Groups | Crossplane | shared-resources/cognito/user-groups.yaml |
| App Client | Crossplane | shared-resources/cognito/app-client.yaml |
| Platform Assets Bucket | ACK S3 | shared-resources/s3/platform-assets-bucket.yaml |
| Shared UI Bucket | ACK S3 | shared-resources/s3/shared-ui-bucket.yaml |
| Lambda Execution Role | ACK IAM | shared-resources/iam/lambda-execution-role.yaml |
| S3 Uploader Role | ACK IAM | shared-resources/iam/s3-uploader-role.yaml |
| Origin Access Control | ACK CloudFront | shared-resources/cloudfront/origin-access-control.yaml |
| URL Rewrite Function | ACK CloudFront | shared-resources/cloudfront/cloudfront-function-resource.yaml |
| CDN Distribution | ACK CloudFront | shared-resources/cloudfront/distribution.yaml |
| DNS Record | ACK Route 53 | shared-resources/route53/dns-record.yaml |
⏱️ Note: CloudFront distribution takes ~15-20 minutes to deploy. Other resources complete in 1-2 minutes.
The script automatically updates env.sh with the created resource values (S3 bucket names, IAM role ARNs, CloudFront distribution ID, Cognito IDs). Re-source it before continuing:
source scripts/env.shVerify all shared resources are deployed:
kubectl get userpool.cognitoidp.aws.upbound.io,userpoolclient.cognitoidp.aws.upbound.io,userpoolDomain.cognitoidp.aws.upbound.io,usergroup.cognitoidp.aws.upbound.io,bucket.s3.services.k8s.aws,role.iam.services.k8s.aws,originaccesscontrol.cloudfront.services.k8s.aws,function.cloudfront.services.k8s.aws,distribution.cloudfront.services.k8s.aws,recordset.route53.services.k8s.aws -l app.kubernetes.io/part-of=shared-platformUpload the Lambda function code and UI static files to the shared S3 assets bucket. These are the application artifacts that every instance will reference — the Lambda handler (lambda-code/index.py) and the frontend UI (ui-src/).
./scripts/2-upload-assets.shLayer 2 — This is the core of the pattern. The RGD (rgd/smart-inventory-hub-v5.yaml) defines how Kro orchestrates 15 resources across ACK, Crossplane, and native Kubernetes from a single instance spec. Applying the RGD registers the SmartInventoryHubV5 custom resource type on your cluster.
Once applied, the RGD acts as a template: each developer or team can create their own isolated Smart Inventory Hub instance by specifying just 5 fields (team, developer, app, environment, region). Kro uses those fields to generate unique resource names, connect dependencies, and provision a complete serverless stack — each with its own DynamoDB table, Lambda function, API Gateway, and UI path in the shared S3 bucket. Multiple instances coexist on the same cluster without conflicts.
./scripts/3-apply-rgd.sh
# Verify the RGD is active
kubectl get rgd smart-inventory-hub-v5
# Inspect the generated CRD schema
kubectl get crd smartinventoryhubv5s.kro.run -o yaml | head -80Layer 3 — This is the payoff. Each actor (developer, team, environment) creates their own instance by providing just 5 fields. Kro provisions a complete, isolated serverless stack — DynamoDB table, Lambda function, API Gateway with 5 routes, and a UI synced to the shared S3 bucket — all wired together with correct dependencies.
The naming convention ensures isolation: each instance generates unique resource names based on the combination of team, developer, app, and environment. Multiple developers can deploy simultaneously on the same cluster without conflicts.
# Developer "john" deploys a dev instance
./scripts/4-create-instance.sh --team team-backend --app inventory --env dev --developer john
# Developer "carol" deploys her own dev instance (same cluster, no conflicts)
./scripts/4-create-instance.sh --team team-backend --app inventory --env dev --developer carol
# Team deploys a staging instance (no developer name — team-level resource)
kubectl apply -f instances/staging-instance.yamlThe instance YAML is minimal — just 5 fields:
apiVersion: kro.run/v1alpha1
kind: SmartInventoryHubV5
metadata:
name: team-backend-john-inventory-dev
spec:
teamName: team-backend
developerName: john
appName: inventory
environment: dev
region: us-east-2Each instance gets its own:
- DynamoDB table:
team-backend-john-inventory-dev-inventory - Lambda function:
team-backend-john-inventory-dev-function - API Gateway:
team-backend-john-inventory-dev-api - UI path:
s3://shared-bucket/john-inventory-dev/ - URL:
https://yourdomain.com/john-inventory-dev/
Kro automatically provisions all 15 resources with correct dependencies in 5-8 minutes.
Confirm all 15 resources were provisioned correctly. The verification script checks each resource type (DynamoDB, Lambda, API Gateway, Jobs) and tests the API endpoint. You can also access the UI via the path prefix on the shared CloudFront domain.
./scripts/5-verify-deployment.sh
# Get application URLs
kubectl get configmap john-inventory-dev-info -o jsonpath='{.data.api-url}'
kubectl get configmap john-inventory-dev-info -o jsonpath='{.data.ui-url}'
# Test the API
API_URL=$(kubectl get configmap john-inventory-dev-info -o jsonpath='{.data.api-endpoint}')
curl -s "$API_URL/products" | jq .To remove resources when you're done:
# Delete a specific instance (removes all 15 instance resources)
./scripts/cleanup.sh --instance team-backend-john-inventory-dev
# Delete all shared resources (Cognito, S3, IAM, CloudFront)
./scripts/cleanup.sh --shared-only
# Delete everything (all instances + shared resources)
./scripts/cleanup.sh --all
⚠️ Warning: Deleting shared resources affects all application instances. Delete instances first, then shared resources. CloudFront distribution deletion takes ~15-20 minutes.
kro-serverless-app-stack/
├── README.md # This file
├── LICENSE # MIT-0 (No Attribution) license
├── CONTRIBUTING.md # Contribution guidelines
├── prerequisites/ # IAM roles for EKS Capabilities
│ ├── kro-capability-trust-policy.json
│ └── ack-capability-trust-policy.json
├── docs/ # Documentation
│ ├── ARCHITECTURE.md # Detailed architecture
│ ├── DEPLOYMENT-GUIDE.md # Step-by-step deployment
│ ├── PREREQUISITES.md # Requirements and installation
│ ├── TROUBLESHOOTING.md # Common issues
│ ├── COST-ESTIMATION.md # Cost breakdown
│ └── SECURITY.md # Security best practices
├── shared-resources/ # Layer 1: Shared resources
│ ├── cloudfront/ # CloudFront distribution, OAC, Function
│ ├── s3/ # S3 buckets
│ ├── iam/ # IAM roles
│ └── cognito/ # Cognito User Pool + Client
├── rgd/ # Layer 2: ResourceGraphDefinition
│ ├── smart-inventory-hub-v5.yaml # The RGD definition
│ └── rgd-env.sh # Environment variables for RGD
├── instances/ # Layer 3: Instance templates
│ ├── dev-instance.yaml # Development template
│ ├── staging-instance.yaml # Staging template
│ └── prod-instance.yaml # Production template
├── lambda-code/ # Lambda function source
│ ├── index.py # Python handler
│ └── package.sh # Packaging script
├── ui-src/ # UI source files
│ ├── index.html # Main HTML
│ ├── app.js # JavaScript
│ ├── styles.css # CSS styles
│ └── auth/callback/index.html # OAuth callback handler
└── scripts/ # Deployment automation
├── env.template # Environment variables template
├── 0-verify-prerequisites.sh
├── 1-deploy-shared-resources.sh
├── 2-upload-assets.sh
├── 3-apply-rgd.sh
├── 4-create-instance.sh
├── 5-verify-deployment.sh
└── cleanup.sh
| Document | Description |
|---|---|
| ARCHITECTURE.md | Detailed architecture explanation |
| DEPLOYMENT-GUIDE.md | Step-by-step deployment instructions |
| PREREQUISITES.md | Requirements and installation |
| TROUBLESHOOTING.md | Common issues and solutions |
| SECURITY.md | Security best practices |
See CONTRIBUTING for security issue reporting.
This project is licensed under the MIT-0 (MIT No Attribution) License - see the LICENSE file for details.
- AWS WWSO LATAM Team


