Actor Flavors#
This guide covers actor flavors from the platform engineer's perspective — how to create, configure, and manage reusable infrastructure presets for actors.
Overview#
Flavors are named, reusable configuration bundles backed by Crossplane EnvironmentConfig resources. Platform engineers define what "GPU workload", "high-throughput scaler", or "S3-persisted actor" means once, in a controlled place. Actor authors reference them by name and get the right infrastructure without touching cloud-provider details.
This guide assumes you are a platform engineer responsible for defining flavors. For how actor authors use flavors, see usage/guide-actor-flavors.md.
How Flavors Work#
A flavor is a cluster-scoped Crossplane EnvironmentConfig resource with two requirements:
- Resource name matching the flavor name (the function fetches by name)
- A
datafield shaped like a partial AsyncActor spec
By convention, flavors also carry the label asya.sh/flavor: <name> for discoverability (kubectl get environmentconfigs -l asya.sh/flavor).
When Crossplane reconciles an AsyncActor that lists flavors, the function-asya-flavors composition function:
- Reads
spec.flavorsfrom the actor - Requests each
EnvironmentConfigby name from Crossplane - Merges all flavor data using type-aware rules
- Applies the actor's inline spec as the final override
- Writes the resolved spec back onto the XR's desired state
Downstream composition steps (render-deployment, render-scaledobject) read from the resolved spec.
Creating a Flavor#
Minimal Example#
apiVersion: apiextensions.crossplane.io/v1beta1
kind: EnvironmentConfig
metadata:
name: cpu-standard
labels:
asya.sh/flavor: cpu-standard
data:
resources:
requests:
cpu: 500m
memory: 512Mi
limits:
cpu: "1"
memory: 1Gi
Apply:
kubectl apply -f cpu-standard.yaml
Result: Any AsyncActor with flavors: [cpu-standard] inherits the resource limits.
Anatomy of a Flavor#
| Field | Description |
|---|---|
metadata.name |
Flavor name — actors reference this name in spec.flavors (the function fetches by resource name) |
metadata.labels["asya.sh/flavor"] |
Convention label for discoverability (kubectl get environmentconfigs -l asya.sh/flavor) |
data |
Partial AsyncActor spec — only the fields this flavor provides |
Naming convention: The metadata.name is the canonical flavor identifier. Keep the asya.sh/flavor label value in sync with the resource name for discoverability.
Flavor Composability#
Flavors are designed to compose. An actor can use multiple flavors simultaneously (e.g., gpu-a100 + checkpoint-state + spot-tolerant).
Merge Semantics#
| Go type | Behavior | Example fields |
|---|---|---|
| Lists | Append across flavors | env, tolerations, volumes, volumeMounts, stateProxy, secretRefs |
| Maps/structs | Merge keys recursively; same leaf key = error | nodeSelector, scaling, resources, sidecar, resiliency |
| Scalars | Error if two flavors both set the field | image, handler, replicas, imagePullPolicy, pythonExecutable |
Key principle: Distinct leaf keys at any nesting depth are merged. Only same leaf key overlap triggers an error.
Example: Composing Tolerations#
Two flavors that both provide tolerations entries get all entries combined:
# gpu-tolerations flavor
data:
tolerations:
- key: nvidia.com/gpu
operator: Exists
effect: NoSchedule
# spot-tolerations flavor
data:
tolerations:
- key: cloud.google.com/gke-spot
operator: Equal
value: "true"
effect: NoSchedule
Actor:
spec:
flavors: [gpu-tolerations, spot-tolerations]
Result: Pod has both tolerations.
Example: Composing Resources#
Two flavors with distinct resource keys merge:
# cpu-flavor
data:
resources:
limits:
cpu: "2"
# memory-flavor
data:
resources:
limits:
memory: 4Gi
Actor:
spec:
flavors: [cpu-flavor, memory-flavor]
Result: Pod has both CPU and memory limits.
Example: Conflict#
Two flavors with the same leaf key error:
# flavor-a
data:
scaling:
minReplicaCount: 1
# flavor-b
data:
scaling:
minReplicaCount: 5
Actor:
spec:
flavors: [flavor-a, flavor-b]
Result: Error at reconciliation time:
flavor merge conflict: flavors "flavor-a" and "flavor-b" conflict on scaling.minReplicaCount
Fix: Consolidate the conflicting field into a single flavor, or have the actor set it inline (actor always wins).
Common Flavor Patterns#
GPU Compute Profile#
apiVersion: apiextensions.crossplane.io/v1beta1
kind: EnvironmentConfig
metadata:
name: gpu-a100
labels:
asya.sh/flavor: gpu-a100
data:
scaling:
minReplicaCount: 1
maxReplicaCount: 4
queueLength: 1 # Process one message at a time on GPU
resources:
requests:
cpu: 4
memory: 16Gi
nvidia.com/gpu: "1"
limits:
nvidia.com/gpu: "1"
tolerations:
- key: nvidia.com/gpu
operator: Exists
effect: NoSchedule
nodeSelector:
accelerator: nvidia-a100
Use case: Actors that require A100 GPUs for inference.
High-Throughput Scaling#
apiVersion: apiextensions.crossplane.io/v1beta1
kind: EnvironmentConfig
metadata:
name: high-throughput
labels:
asya.sh/flavor: high-throughput
data:
scaling:
minReplicaCount: 5
maxReplicaCount: 50
queueLength: 10
pollingInterval: 10 # Scale up faster
cooldownPeriod: 60 # Scale down faster
Use case: Actors with high message volume and fast processing time.
S3 Checkpoints#
apiVersion: apiextensions.crossplane.io/v1beta1
kind: EnvironmentConfig
metadata:
name: s3-checkpoints
labels:
asya.sh/flavor: s3-checkpoints
data:
stateProxy:
- name: checkpoints
mount:
path: /state/checkpoints
connector:
image: ghcr.io/deliveryhero/asya-state-proxy-s3-buffered-lww:v1.0.0
env:
- name: STATE_BUCKET
value: ml-checkpoints
- name: AWS_REGION
value: us-west-2
resources:
requests:
cpu: 50m
memory: 128Mi
Use case: Actors that persist checkpoints to S3.
Spot Instance Tolerations#
apiVersion: apiextensions.crossplane.io/v1beta1
kind: EnvironmentConfig
metadata:
name: spot-tolerant
labels:
asya.sh/flavor: spot-tolerant
data:
tolerations:
- key: cloud.google.com/gke-spot
operator: Equal
value: "true"
effect: NoSchedule
- key: kubernetes.azure.com/scalesetpriority
operator: Equal
value: spot
effect: NoSchedule
Use case: Actors that can run on spot/preemptible nodes.
Secrets Injection#
apiVersion: apiextensions.crossplane.io/v1beta1
kind: EnvironmentConfig
metadata:
name: openai-secrets
labels:
asya.sh/flavor: openai-secrets
data:
secretRefs:
- secretName: openai-creds
keys:
- key: api-key
envVar: OPENAI_API_KEY
Use case: Actors that need OpenAI API credentials.
Retry Policies#
apiVersion: apiextensions.crossplane.io/v1beta1
kind: EnvironmentConfig
metadata:
name: aggressive-retries
labels:
asya.sh/flavor: aggressive-retries
data:
resiliency:
actorTimeout: 10m
policies:
transient:
maxAttempts: 5
backoff: exponential
initialDelay: 1s
maxInterval: 30s
rules:
- errors: ["TimeoutError", "ConnectionError"]
policy: transient
Use case: Actors that call flaky external APIs.
Flavor Lifecycle#
Creating a Flavor#
kubectl apply -f flavor.yaml
Availability: Flavor is immediately available to all namespaces (cluster-scoped resource).
Updating a Flavor#
Edit the EnvironmentConfig and re-apply:
kubectl edit environmentconfig gpu-a100
# or
kubectl apply -f gpu-a100-v2.yaml
Effect: Crossplane reconciles all AsyncActor resources referencing this flavor. The new configuration is applied on the next reconciliation cycle.
Rollout: Changes propagate gradually as Crossplane re-reconciles actors. Force immediate reconciliation:
kubectl annotate asyncactor my-actor crossplane.io/paused=false --overwrite
Deprecating a Flavor#
- Mark the flavor as deprecated (convention: add a label or annotation):
metadata:
labels:
asya.sh/flavor: gpu-t4
asya.sh/deprecated: "true"
annotations:
asya.sh/deprecation-message: "Use gpu-a100 instead"
-
Communicate the deprecation to users.
-
Monitor usage:
kubectl get asyncactors --all-namespaces -o yaml | grep "gpu-t4"
- Once no actors reference the flavor, delete it:
kubectl delete environmentconfig gpu-t4
Migrating to a New Flavor#
Scenario: Replace gpu-t4 with gpu-a100.
Strategy:
- Create
gpu-a100flavor with the new configuration. - Update actors incrementally:
spec:
flavors: [gpu-a100] # Was [gpu-t4]
- Once all actors migrated, delete
gpu-t4.
Tip: Use a temporary combined flavor during migration:
apiVersion: apiextensions.crossplane.io/v1beta1
kind: EnvironmentConfig
metadata:
name: gpu-migration
labels:
asya.sh/flavor: gpu-migration
data:
# Combine fields from both gpu-t4 and gpu-a100
# Allows actors to switch from [gpu-t4] to [gpu-migration] to [gpu-a100]
Supported Flavor Fields#
Flavors can set any non-infrastructure spec field:
| Field | Type | Description |
|---|---|---|
image |
scalar | Container image (conflicts if two flavors set it) |
handler |
scalar | Handler path (conflicts if two flavors set it) |
imagePullPolicy |
scalar | Always, IfNotPresent, Never |
pythonExecutable |
scalar | Python binary path |
replicas |
scalar | Fixed replica count (when scaling disabled) |
resources |
map | CPU, memory, GPU requests/limits |
env |
list | Environment variables |
tolerations |
list | Pod tolerations |
nodeSelector |
map | Node selector labels |
volumes |
list | Pod volumes |
volumeMounts |
list | Runtime container volume mounts |
scaling |
map | KEDA autoscaling configuration |
resiliency |
map | Retry policies and actor timeout |
sidecar |
map | Sidecar overrides |
stateProxy |
list | State proxy mounts |
secretRefs |
list | Secret references |
Infrastructure fields (actor, transport, flavors) are excluded from the merge — they cannot be set by flavors.
Constraints#
- Maximum 8 flavors per actor: Enforced by XRD
maxItems: 8. - Minimum flavor name length: 3 characters (enforced by XRD).
- Cluster-scoped only:
EnvironmentConfigis a cluster-scoped Crossplane resource, meaning flavors are shared across all namespaces. Only platform engineers with cluster-level RBAC should create or modify them. Namespace-scoped flavors (backed by ConfigMaps) are planned — this will allow data scientists to manage team-specific configuration like API keys and secrets without cluster access. - Missing flavor: If a referenced flavor does not exist, the actor remains in
Waitingstate until the flavor is created.
Debugging Flavors#
Check if a Flavor Exists#
kubectl get environmentconfigs -l asya.sh/flavor=gpu-a100
Expected output:
NAME AGE
gpu-a100 5d
List All Flavors#
kubectl get environmentconfigs -l asya.sh/flavor
Inspect Flavor Data#
kubectl get environmentconfig gpu-a100 -o yaml
Check Actor Status#
If an actor references a missing or conflicting flavor, check the status:
kubectl describe asyncactor my-actor -n prod
Look for conditions from the resolve-flavors step:
- Missing flavor:
Waiting for N flavor EnvironmentConfigs - Conflict:
Synced: False, message naming the conflicting flavors and key path
Force Re-Reconciliation#
After updating a flavor, force Crossplane to re-reconcile an actor:
kubectl annotate asyncactor my-actor crossplane.io/paused=false --overwrite -n prod
View Composition Logs#
Check the function-asya-flavors logs to see the resolved spec:
kubectl logs -n crossplane-system \
-l pkg.crossplane.io/revision \
--all-containers=true | grep "Flavors applied"
Best Practices#
-
Use composable flavors — define small, focused flavors (e.g.,
gpu-a100,spot-tolerant,s3-checkpoints) that actors combine, rather than monolithic flavors that duplicate configuration. -
Avoid scalar conflicts — do not set scalar fields (like
imageorhandler) in flavors unless the flavor is meant to be exclusive. Use flavors for infrastructure, not application code. -
Document flavors — add annotations with usage instructions:
metadata:
annotations:
asya.sh/description: "A100 GPU compute profile with 16Gi memory"
asya.sh/example: "flavors: [gpu-a100]"
-
Version flavors explicitly — if making breaking changes, create a new flavor (
gpu-a100-v2) rather than updating the existing one in-place. -
Monitor flavor usage — track which actors reference which flavors to understand impact before deprecating:
kubectl get asyncactors --all-namespaces -o json | \
jq -r '.items[] | select(.spec.flavors != null) | "\(.metadata.namespace)/\(.metadata.name): \(.spec.flavors)"'
-
Use namespace-aware prefixes for state — when defining
stateProxyflavors, useSTATE_PREFIX=$(NAMESPACE)/to isolate state across namespaces. -
Test flavor changes in staging — validate flavor updates on a test actor before rolling out to production.
Example: Building a Flavor Library#
Step 1: Create base compute profiles:
kubectl apply -f - <<EOF
apiVersion: apiextensions.crossplane.io/v1beta1
kind: EnvironmentConfig
metadata:
name: cpu-small
labels:
asya.sh/flavor: cpu-small
data:
resources:
requests:
cpu: 250m
memory: 256Mi
limits:
cpu: 500m
memory: 512Mi
---
apiVersion: apiextensions.crossplane.io/v1beta1
kind: EnvironmentConfig
metadata:
name: cpu-large
labels:
asya.sh/flavor: cpu-large
data:
resources:
requests:
cpu: "2"
memory: 4Gi
limits:
cpu: "4"
memory: 8Gi
---
apiVersion: apiextensions.crossplane.io/v1beta1
kind: EnvironmentConfig
metadata:
name: gpu-a100
labels:
asya.sh/flavor: gpu-a100
data:
resources:
requests:
cpu: 4
memory: 16Gi
nvidia.com/gpu: "1"
limits:
nvidia.com/gpu: "1"
tolerations:
- key: nvidia.com/gpu
operator: Exists
effect: NoSchedule
nodeSelector:
accelerator: nvidia-a100
EOF
Step 2: Create scaling profiles:
kubectl apply -f - <<EOF
apiVersion: apiextensions.crossplane.io/v1beta1
kind: EnvironmentConfig
metadata:
name: scale-minimal
labels:
asya.sh/flavor: scale-minimal
data:
scaling:
minReplicaCount: 0
maxReplicaCount: 3
queueLength: 5
---
apiVersion: apiextensions.crossplane.io/v1beta1
kind: EnvironmentConfig
metadata:
name: scale-aggressive
labels:
asya.sh/flavor: scale-aggressive
data:
scaling:
minReplicaCount: 10
maxReplicaCount: 100
queueLength: 10
pollingInterval: 10
cooldownPeriod: 60
EOF
Step 3: Create persistence flavors:
kubectl apply -f - <<EOF
apiVersion: apiextensions.crossplane.io/v1beta1
kind: EnvironmentConfig
metadata:
name: s3-checkpoints
labels:
asya.sh/flavor: s3-checkpoints
data:
stateProxy:
- name: checkpoints
mount:
path: /state/checkpoints
connector:
image: ghcr.io/deliveryhero/asya-state-proxy-s3-buffered-lww:v1.0.0
env:
- name: STATE_BUCKET
value: ml-checkpoints
- name: AWS_REGION
value: us-west-2
---
apiVersion: apiextensions.crossplane.io/v1beta1
kind: EnvironmentConfig
metadata:
name: redis-cache
labels:
asya.sh/flavor: redis-cache
data:
stateProxy:
- name: cache
mount:
path: /state/cache
connector:
image: ghcr.io/deliveryhero/asya-state-proxy-redis-buffered-cas:v1.0.0
env:
- name: REDIS_URL
value: redis://redis.default.svc.cluster.local:6379/0
EOF
Step 4: Actors compose flavors:
# Small CPU actor with minimal scaling
spec:
flavors: [cpu-small, scale-minimal]
# GPU actor with aggressive scaling and S3 checkpoints
spec:
flavors: [gpu-a100, scale-aggressive, s3-checkpoints]
# Large CPU actor with Redis cache
spec:
flavors: [cpu-large, scale-minimal, redis-cache]
Security Considerations#
-
Cluster-scoped resources:
EnvironmentConfigis cluster-scoped — any user who can create AsyncActors can reference any flavor by name. Use RBAC to restrict who can create or modifyEnvironmentConfigresources. Once namespace-scoped flavors (via ConfigMaps) are available, teams will be able to self-manage their own configuration (e.g., OpenAI API keys, model endpoints) without cluster-level access. -
Secret injection: Flavors that reference secrets (via
secretRefs) give actors access to those secrets. Ensure flavors only reference secrets that actors in any namespace should access. -
IAM roles: Flavors that configure
stateProxywith S3 backends rely on IRSA. Ensure the IAM role grants appropriate permissions for all actors that might use the flavor. -
Node selectors and tolerations: Flavors that set
nodeSelectorortolerationscontrol where pods are scheduled. Ensure the target nodes are available and appropriately secured.
Using flavors: To choose and use flavors in your actor specs, see usage/guide-actor-flavors.md.