This document provides a detailed explanation of the Kro Serverless Application Stack architecture.
- Overview
- Core Concept
- Three-Layer Architecture
- Cross-Controller Integration
- Resource Dependencies
- CEL Expressions
- Variable Resolution
- Security Model
- Deployment Flow
The Kro Serverless Application Stack implements a Hybrid Composition Pattern that combines:
- Pre-provisioned shared resources (CloudFront, IAM roles, Cognito, DNS, CloudFront Function) managed outside the RGD
- Dynamically created per-application resources (S3, Lambda, API Gateway, DynamoDB) managed by Kro/ACK/Crossplane
This pattern provides:
- Cost efficiency through resource sharing (90%+ reduction)
- Isolation through per-application resources
- Flexibility through Kubernetes-native orchestration
- Security through integrated Cognito authentication
- Path-based routing through a single CloudFront distribution with CloudFront Function
The fundamental idea behind Hybrid Kro Compositions is simple: one ResourceGraphDefinition orchestrates a complete serverless stack.
What makes this powerful:
- Developers create one simple YAML manifest
- Kro automatically provisions all required resources
- Dependencies are resolved automatically
- The entire stack is ready in 5-8 minutes
The architecture separates concerns into three distinct layers:
%%{init: {'theme':'dark'}}%%
graph TB
subgraph SharedPlatform["Layer 1: Shared Platform Resources"]
S3Assets["S3 Platform Assets Bucket<br/><br/>• functions/ (Lambda code)<br/>• ui/ (UI templates)"]
S3SharedUI["S3 Shared UI Bucket<br/><br/>• /developer1-app-dev/<br/>• /developer2-app-dev/<br/>• /team-app-staging/"]
CloudFront["CloudFront Distribution<br/><br/>domain.com (path-based)<br/>Environments: ALL"]
CFFunction["CloudFront Function<br/><br/>URL Rewriting<br/>index.html append"]
OAC["Origin Access Control<br/><br/>Secure S3 Access<br/>Signing: Always"]
IAMRoles["IAM Roles (Shared)<br/><br/>• lambda-execution-role<br/>• s3-uploader-role"]
Cognito["Cognito Resources<br/><br/>• User Pool<br/>• App Client<br/>• Single Callback URL"]
DNSRecord["Route 53 DNS Record<br/><br/>domain.com → CloudFront"]
S3Assets -.->|Templates| S3SharedUI
S3SharedUI -->|Origin| CloudFront
CloudFront -->|viewer-request| CFFunction
CloudFront -->|Secure Access| OAC
OAC -->|Signs Requests| S3SharedUI
IAMRoles -.->|Permissions| S3SharedUI
DNSRecord -.->|Points to| CloudFront
end
subgraph RGDLayer["Layer 2: Kro ResourceGraphDefinition"]
RGD["SmartInventoryHubV5 RGD<br/><br/>References shared resources:<br/>• Shared CloudFront<br/>• Shared S3 UI Bucket<br/>• Shared IAM roles<br/>+ Creates app-specific:<br/>• DynamoDB table<br/>• Lambda function<br/>• API Gateway<br/>• S3 prefix (via Job)"]
end
subgraph AppInstances["Layer 3: Application Instances"]
App1["domain.com<br/>/developer1-app-dev/<br/><br/>S3: /developer1-app-dev/<br/>CloudFront: Shared"]
App2["domain.com<br/>/developer2-app-dev/<br/><br/>S3: /developer2-app-dev/<br/>CloudFront: Shared"]
App3["domain.com<br/>/team-app-staging/<br/><br/>S3: /team-app-staging/<br/>CloudFront: Shared"]
end
SharedPlatform -->|Referenced by| RGDLayer
RGD -->|Creates| App1
RGD -->|Creates| App2
RGD -->|Creates| App3
style SharedPlatform fill:#1a1a2e,stroke:#4A90E2,stroke-width:3px
style RGDLayer fill:#1a1a2e,stroke:#FFE66D,stroke-width:3px
style AppInstances fill:#1a1a2e,stroke:#26DE81,stroke-width:2px
style S3Assets fill:#FF9F43,color:#000
style S3SharedUI fill:#FF9F43,color:#000
style CloudFront fill:#95E1D3,color:#000
style CFFunction fill:#95E1D3,color:#000
style OAC fill:#FFE66D,color:#000
style IAMRoles fill:#FF6B6B
style Cognito fill:#4A90E2
style DNSRecord fill:#95E1D3,color:#000
style RGD fill:#FFE66D,color:#000
style App1 fill:#26DE81,color:#000
style App2 fill:#26DE81,color:#000
style App3 fill:#26DE81,color:#000
Resources deployed once and reused across all applications:
| Resource | Purpose | Benefit |
|---|---|---|
| CloudFront Distribution | CDN with single domain alias | Eliminates 20-min provisioning per app |
| CloudFront Function | Appends index.html to directory paths | Enables SPA routing for all apps |
| Origin Access Control | Secure S3 access | Centralized security policy |
| IAM Roles (2) | Lambda execution, S3 uploader | Consistent permissions |
| Cognito User Pool | Authentication with single callback URL | Single sign-on across apps via state parameter |
| Platform Assets Bucket | Lambda code and UI templates | Centralized asset management |
| S3 Shared UI Bucket | Hosts UI assets for all instances | Path-based multi-tenancy |
| Route 53 DNS Record | Points domain.com to CloudFront | Single DNS record for all apps |
The RGD defines a complete serverless stack:
apiVersion: kro.run/v1alpha1
kind: ResourceGraphDefinition
metadata:
name: smart-inventory-hub-v5
spec:
schema:
apiVersion: v1alpha1
kind: SmartInventoryHubV5
spec:
teamName: string | required
appName: string | required
environment: string | required
developerName: string | required
region: string | default=us-east-2
resources:
# 15 resources defined here
- id: dynamodbTable
- id: lambdaFunction
- id: apiGateway
# ... etcSimple YAML manifests that create complete stacks:
apiVersion: kro.run/v1alpha1
kind: SmartInventoryHubV5
metadata:
name: john-inventory-dev
namespace: kro-serverless-stack
spec:
teamName: team-backend
developerName: john
appName: inventory
environment: dev
region: us-east-2The power of Hybrid Kro Compositions lies in seamless integration between different controllers:
- ACK DynamoDB creates table → exposes
tableName - Crossplane Lambda references
tableName→ exposesfunctionArn - ACK API Gateway references
functionArn→ exposesapiId - Crossplane Permission references
apiId→ authorizes invocation - Kubernetes Job references outputs → syncs UI assets to S3
Kro analyzes CEL expressions to build the dependency graph:
# Example: Lambda depends on DynamoDB
- id: lambdaFunction
template:
spec:
environment:
variables:
TABLE_NAME: "${dynamodbTable.spec.tableName}" # ← Implicit dependency
# Example: API Integration depends on Lambda
- id: apiIntegration
template:
spec:
integrationURI: "${lambdaFunction.status.atProvider.arn}" # ← Implicit dependencyKey Rules:
- Resources are created in array order
${resource.status...}references create implicit dependencies- Kro waits for referenced resources to be ready
- No explicit
dependenciesfield needed
Common Expression Language (CEL) enables dynamic configuration:
# Dev environments include developer name
name: "${schema.spec.environment == 'dev' ?
schema.spec.developerName + '-' + schema.spec.appName + '-dev' :
schema.spec.teamName + '-' + schema.spec.appName + '-' + schema.spec.environment}"# Reference ACK resource status
integrationURI: "arn:aws:apigateway:${schema.spec.region}:lambda:path/2015-03-31/functions/${lambdaFunction.status.atProvider.arn}/invocations"
# Reference Crossplane resource status
functionName: "${lambdaFunction.status.atProvider.functionName}"
# Reference Kubernetes resource
serviceAccountName: "${s3UploaderServiceAccount.metadata.name}"# Different configurations per environment
billingMode: "${schema.spec.environment == 'prod' ? 'PROVISIONED' : 'PAY_PER_REQUEST'}"Variable resolution in Hybrid Kro Compositions happens at two distinct moments:
Shared resources injected once before RGD deployment:
- IAM role ARNs for Lambda execution
- Cognito User Pool IDs for authentication
- CloudFront distribution domains for CDN
- S3 bucket names for platform assets
- OAC ID and CloudFront Function ARN
# Before envsubst
roleArn: "${PLATFORM_LAMBDA_ROLE_ARN}"
# After envsubst
roleArn: "arn:aws:iam::123456789012:role/platform-lambda-role"Per-instance values resolved dynamically:
- Resource names (DynamoDB table, Lambda function, API Gateway)
- Environment-specific settings (dev, staging, prod)
- Team and developer identifiers for multi-tenancy
- S3 path prefixes for UI assets
# CEL expression in RGD
name: "${schema.spec.teamName}-${schema.spec.appName}-${schema.spec.environment}"
# Developer creates instance with:
spec:
teamName: "team-backend"
appName: "inventory"
environment: "dev"
# Kro resolves to:
name: "team-backend-inventory-dev"%%{init: {'theme':'dark'}}%%
graph TB
subgraph IRSA["IRSA (IAM Roles for Service Accounts)"]
SA["Kubernetes<br/>ServiceAccount"]
Role["IAM Role"]
SA -->|"annotated with"| Role
end
subgraph Resources["AWS Resources"]
Lambda["Lambda<br/>Function"]
S3["S3<br/>Bucket"]
DDB["DynamoDB<br/>Table"]
end
Role -->|"AssumeRole"| Lambda
Role -->|"GetObject/PutObject"| S3
Lambda -->|"GetItem/PutItem"| DDB
style IRSA fill:#1a1a2e,stroke:#4A90E2
style Resources fill:#1a1a2e,stroke:#FF9F43
%%{init: {'theme':'dark'}}%%
sequenceDiagram
participant User
participant CF as CloudFront (domain.com)
participant Cognito
participant Callback as Auth Callback Handler
participant APIGW as API Gateway
participant Lambda
User->>CF: Access domain.com/app-prefix/
CF->>Cognito: Redirect to login (state=app-prefix)
Cognito->>User: Login form
User->>Cognito: Credentials
Cognito->>Callback: Redirect to domain.com/auth/callback
Callback->>Callback: Read state parameter
Callback->>CF: Redirect to domain.com/app-prefix/
CF->>User: Serve UI with JWT token
User->>APIGW: API request + JWT
APIGW->>APIGW: Validate JWT
APIGW->>Lambda: Invoke function
Lambda->>APIGW: Response
APIGW->>User: Response
The shared UI bucket uses SourceAccount condition for CloudFront access:
{
"Condition": {
"StringEquals": {
"aws:SourceAccount": "ACCOUNT_ID"
}
}
}This is simpler than SourceArn (which requires the distribution ID) and works with any CloudFront distribution in the account.
| Practice | Implementation |
|---|---|
| Least Privilege | 2 shared IAM roles with minimal permissions |
| JWT Validation | API Gateway authorizer with Cognito |
| S3 Security | Origin Access Control (OAC) for CloudFront |
| Encryption | DynamoDB encryption at rest |
| HTTPS | ACM certificates for all endpoints |
| SPA Auth | generateSecret: false for Cognito client (SPA-compatible) |
| Multi-tenant Auth | Single callback URL with state parameter routing |
The complete orchestration process happens in two phases:
%%{init: {'theme':'dark'}}%%
sequenceDiagram
participant Dev as Developer
participant K8s as Kubernetes
participant Kro
participant ACK
participant XP as Crossplane
participant AWS
Dev->>K8s: kubectl apply instance.yaml
K8s->>Kro: Create SmartInventoryHubV5
rect rgb(30, 40, 60)
Note over Kro: Phase 1: Analysis
Kro->>Kro: Parse CEL expressions
Kro->>Kro: Build dependency graph
Kro->>Kro: Validate configuration
end
rect rgb(40, 50, 70)
Note over Kro,AWS: Phase 2: Provisioning
Kro->>ACK: Create DynamoDB Table
ACK->>AWS: CreateTable API
AWS-->>ACK: Table created
Kro->>XP: Create Lambda Function
XP->>AWS: CreateFunction API
AWS-->>XP: Function created
Kro->>ACK: Create API Gateway
ACK->>AWS: CreateApi API
AWS-->>ACK: API created
end
rect rgb(50, 60, 80)
Note over Kro,K8s: Phase 3: Automation
Kro->>K8s: Create UI Sync Job
K8s->>AWS: Sync UI assets to S3
end
Kro-->>Dev: Instance ready (5-8 min)
The RGD orchestrates 15 resources across three different controllers:
%%{init: {'theme':'dark'}}%%
pie showData
title Resources by Controller
"ACK (10)" : 10
"Crossplane (2)" : 2
"Kubernetes (3)" : 3
| Controller | Resources | Rationale |
|---|---|---|
| ACK | DynamoDB, API Gateway (+ Authorizer, Integration, Routes, Stage) | Direct AWS API mapping, fast provisioning, excellent status exposure |
| Crossplane | Lambda, Lambda Permission | Complex IAM integration, S3-based code deployment (50MB limit vs 4KB inline) |
| Kubernetes | ServiceAccount, Job, ConfigMap | Native orchestration, IRSA integration, post-provisioning automation |
The Hybrid Kro Compositions architecture provides:
- Unified Orchestration: Single RGD for complete serverless stacks (15 resources)
- Cross-Controller Integration: ACK, Crossplane, and Kubernetes working together
- Automatic Dependencies: CEL expressions enable implicit dependency resolution
- Cost Optimization: Shared resources reduce costs by 90%+
- Security Integration: Cognito authentication with single callback URL and
state-based routing - Path-Based Routing: Single CloudFront distribution with CloudFront Function for index.html append
- GitOps Ready: Declarative manifests for version control




