Overview#

The Crossplane integration provides an alternative deployment model for AsyncActor resources using Crossplane compositions instead of the native Kubernetes operator. Crossplane manages the entire infrastructure lifecycle declaratively, from SQS queues to Deployments to KEDA autoscaling.

Architecture#

AsyncActor resources are implemented as Crossplane Composite Resources:

AsyncActor Claim (asya.sh/v1alpha1)
         │
         ▼
    XAsyncActor (XRD)
         │
         ▼
    Composition (SQS)
         │
    ┌────┼────────────────────────┐
    ▼    ▼            ▼           ▼
  SQS  Service-  ScaledObject  Deployment
 Queue Account   (KEDA)
 (AWS)  (IRSA)

Key components: - XRD (xasyncactors.asya.sh): Defines the AsyncActor composite resource schema - Composition (asyncactor-sqs): Orchestrates creation of managed resources - Managed Resources: SQS Queue, ServiceAccount, TriggerAuthentication, ScaledObject, Deployment

How AsyncActor Claims Work#

  1. User creates AsyncActor claim in their namespace
  2. Crossplane creates XAsyncActor XR (composite resource) from the claim
  3. Composition pipeline executes:
  4. Renders SQS Queue resource with asya-{namespace}-{actor} naming
  5. Creates ServiceAccount with IRSA annotation (if enabled)
  6. Creates KEDA TriggerAuthentication for queue metrics
  7. Waits for queue URL from SQS status
  8. Creates KEDA ScaledObject with queue URL
  9. Creates Deployment with pod labels for injection
  10. Crossplane providers reconcile each managed resource
  11. Status aggregation: Composition patches XR status with infrastructure state

SQS Composition Pipeline#

The asyncactor-sqs composition uses function-go-templating to render resources dynamically.

Pipeline steps:

  1. resolve-flavors (conditional, when functions.flavorsEnabled): Resolves flavor EnvironmentConfigs and merges them into the XR's desired spec. Actors without spec.flavors pass through unchanged. See Actor Flavors.

  2. render-sqs-queue: Creates SQS Queue

  3. Name: asya-{namespace}-{actor}
  4. Visibility timeout: 30s
  5. Message retention: 4 days
  6. Receive wait time: 20s (long polling)
  7. Tags: asya.sh/namespace, asya.sh/actor

  8. render-serviceaccount: Creates ServiceAccount with IRSA annotation (if irsa.enabled=true)

  9. Name: Configured via irsa.serviceAccountName (shared per namespace)
  10. Annotation: eks.amazonaws.com/role-arn from irsa.roleArn or pattern

  11. render-triggerauthentication: Creates KEDA TriggerAuthentication

  12. Auth provider: podIdentity or secret (configured via keda.authProvider)
  13. Enables KEDA to read SQS queue metrics

  14. render-scaledobject: Creates KEDA ScaledObject (if scaling.enabled=true)

  15. Waits for queue URL from SQS Queue status
  16. References TriggerAuthentication for AWS credentials
  17. Configures autoscaling policies (scale-up aggressive, scale-down gradual)
  18. Min replicas: spec.scaling.minReplicaCount (default: 0)
  19. Max replicas: spec.scaling.maxReplicaCount (default: 10)
  20. Queue length target: spec.scaling.queueLength (default: 5 messages/replica)

  21. render-deployment: Creates Deployment

  22. Pod spec rendered from flat fields: spec.image, spec.handler, spec.env, spec.resources, etc.
  23. Injects labels: asya.sh/inject=true, asya.sh/actor={name}
  24. ServiceAccount: Uses IRSA ServiceAccount if enabled
  25. Replicas: From spec.replicas if scaling disabled, otherwise managed by KEDA

  26. patch-status-and-derive-phase: Aggregates status from managed resources

  27. Reads queue URL, queue ARN from SQS Queue status
  28. Checks Ready conditions on Queue, ScaledObject, Deployment
  29. Calculates phase: Creating, Ready, or Napping

Status Model#

The XAsyncActor status aggregates infrastructure readiness:

Phase values: - Creating: Initial state, infrastructure provisioning in progress - Ready: All infrastructure ready and workload has replicas > 0 - Napping: KEDA scaled to zero (no messages in queue)

Status fields:

status:
  phase: Ready
  queueUrl: https://sqs.us-east-1.amazonaws.com/123456789/asya-prod-text-processor
  queueIdentifier: arn:aws:sqs:us-east-1:123456789:asya-prod-text-processor
  infrastructure:
    queue:
      ready: true
      message: ""
    keda:
      ready: true
      message: ""
    workload:
      ready: true
      replicas: 5
      readyReplicas: 5

Phase calculation logic: - Creating: Default state when any infrastructure not ready - Ready: Queue ready AND KEDA ready AND Workload ready AND replicas > 0 - Napping: All ready conditions met but replicas = 0 (KEDA scaled down)

Queue Management#

All message queues are automatically managed by Crossplane AWS Provider.

Queue naming: asya-{namespace}-{actor_name} - Example: Actor text-analyzer in namespace prod → Queue asya-prod-text-analyzer - Example: Actor image-processor in namespace dev → Queue asya-dev-image-processor - System actors: asya-{namespace}-x-sink, asya-{namespace}-x-sump

Queue lifecycle: - ✅ Created when AsyncActor claim reconciled by Crossplane - ✅ Deleted when AsyncActor claim deleted (cascade via Crossplane) - ✅ Preserved when AsyncActor claim updated (immutable resource)

Queue properties (SQS): - Visibility timeout: 30 seconds - Message retention: 345600 seconds (4 days) - Receive wait time: 20 seconds (long polling) - Deletion policy: Delete (queue removed with AsyncActor)

Credential Management#

Crossplane separates credentials by scope for security and namespace isolation.

Crossplane AWS Provider credentials (in crossplane-system namespace): - Used by: Crossplane AWS Provider for queue management (create/delete/configure) - Secret: Configured in awsProviderConfig.secretRef (e.g., aws-creds) - Permissions: Admin-level queue operations - Alternative: IRSA via credentialsSource: InjectedIdentity

Actor credentials (in actor's namespace): - Used by: Sidecar containers for message operations (send/receive/delete) - Method: IRSA (IAM Roles for Service Accounts) via ServiceAccount annotation - ServiceAccount: Created by composition with eks.amazonaws.com/role-arn annotation - Permissions: Message-level operations only (SQS SendMessage, ReceiveMessage, DeleteMessage)

IRSA flow: 1. Composition creates ServiceAccount with IAM role ARN annotation 2. Deployment references ServiceAccount 3. EKS injects AWS credentials into pod via webhook 4. Sidecar uses ambient credentials for SQS operations

Resource Ownership#

Crossplane manages all resources via composite resource ownership:

Managed Resources (owned by XAsyncActor XR): - ✅ SQS Queue: AWS queue via Crossplane AWS Provider - ✅ ServiceAccount: IRSA-annotated ServiceAccount (if enabled) - ✅ TriggerAuthentication: KEDA auth for queue metrics - ✅ ScaledObject: KEDA autoscaling configuration (if scaling enabled) - ✅ Deployment: Actor workload

Deletion behavior: - Deleting AsyncActor claim triggers cascade deletion of XR - Crossplane deletes all managed resources automatically - SQS queue deleted via AWS Provider (respects deletionPolicy: Delete)

Deployment#

Prerequisites: - Crossplane installed with AWS Provider (SQS) and Kubernetes Provider - Function Go-Templating installed - KEDA installed

Installation:

# Install Crossplane compositions and XRD
helm install asya-crossplane deploy/helm-charts/asya-crossplane/ \
  --namespace crossplane-system \
  --set awsProviderConfig.credentialsSource=Secret \
  --set awsAccountId=123456789012 \
  --set actorNamespace=asya-e2e

Configuration (via values.yaml): - providers.aws.sqsVersion: AWS SQS provider version - providers.kubernetes.version: Kubernetes provider version - awsProviderConfig.credentialsSource: Secret, InjectedIdentity, or Upbound - irsa.enabled: Enable ServiceAccount creation with IRSA - keda.authProvider: podIdentity or secret

Example AsyncActor Claim#

apiVersion: asya.sh/v1alpha1
kind: AsyncActor
metadata:
  name: text-processor
  namespace: asya
spec:
  image: my-processor:v1
  handler: processor.TextProcessor.process

  scaling:
    enabled: true
    minReplicaCount: 0
    maxReplicaCount: 10
    queueLength: 5
    pollingInterval: 30
    cooldownPeriod: 300

  resources:
    requests:
      cpu: 100m
      memory: 256Mi

Important: The command field for the runtime container is managed by the Crossplane composition — do not set it via the AsyncActor spec. Use pythonExecutable to override the Python binary path if needed.

XRD Schema Validation#

The XRD enforces constraints at admission time via OpenAPI v3 schema:

  • Required fields: image, handler
  • actor: must match ^[a-z0-9]([a-z0-9-]*[a-z0-9])?$ (DNS label format), max 63 chars
  • imagePullPolicy: enum — Always, IfNotPresent, Never
  • handler, image, pythonExecutable: minLength: 1
  • replicas: minimum: 1
  • ⚠️ volumes, tolerations, volumeMounts, nodeSelector, resources: use x-kubernetes-preserve-unknown-fields: true — any object shape is accepted at XRD schema level; validation of these fields happens when Kubernetes validates the rendered Deployment

Comparison with asya-operator#

Feature asya-operator asya-crossplane
Queue Management Operator creates queues via SDK Crossplane AWS Provider
Deployment Operator creates Deployment Crossplane Kubernetes Provider
Sidecar Injection Operator injects directly Crossplane composition renders inline
CEL Validation In operator code In XRD schema
Credential Management Operator copies secrets IRSA via ServiceAccount annotations
Status Model Operator calculates status Composition aggregates status
GitOps CRD-based Crossplane claims

Observability#

kubectl commands:

# List AsyncActor claims
kubectl get asyncactors -n asya-e2e

# Get AsyncActor status
kubectl get asyncactor text-processor -n asya-e2e -o yaml

# List XAsyncActor composite resources
kubectl get xasyncactors

# Check managed resources
kubectl get managed -l crossplane.io/claim-name=text-processor

Status columns: - Actor: Actor name from asya.sh/actor label - Status: Current phase (Creating, Ready, Napping) - Ready: Number of ready replicas - Replicas: Total desired replicas - Queue: Queue URL (priority 1, hidden by default) - Age: Time since creation

Configuration#

Crossplane configuration via Helm values:

# AWS ProviderConfig
awsProviderConfig:
  name: default
  credentialsSource: Secret  # or InjectedIdentity
  secretRef:
    namespace: crossplane-system
    name: aws-creds
    key: credentials

# IRSA configuration
irsa:
  enabled: true
  serviceAccountName: asya-actors
  roleArnPattern: "arn:aws:iam::ACCOUNT_ID:role/asya-actors-{namespace}"

# KEDA authentication
keda:
  authProvider: podIdentity  # or secret

Transport is selected cluster-wide via the asya-crossplane chart's transport Helm value, which sets defaultCompositionRef on the AsyncActor XRD. Individual actors do not declare a transport field.