Customizing Pulumi Resource Properties
Stelvio provides high-level abstractions for AWS resources, exposing the most commonly used configuration options through component constructors. However, in some cases you might need fine-grained control of certain aspects of the underlying Pulumi resources that Stelvio creates.
The customize parameter allows you to override or extend default Pulumi resource properties without modifying Stelvio's source code.
When to Use Customization
Use the customize parameter when you need to:
- Set Pulumi properties not exposed by Stelvio's API (e.g.,
force_destroyon S3 buckets) - Override default values that Stelvio sets internally
- Add tags, encryption settings, or other resource-specific configurations
- Configure advanced features like VPC settings or custom IAM policies
Customize doesn't change what Stelvio itself reads
customize only changes what Stelvio sends to AWS. Stelvio's own properties, links and
IAM roles read the constructor arguments, not customize. For these settings, always use
the argument: stream= (DynamoTable), generate_secret= (UserPoolClient),
allow_unauthenticated= (IdentityPool), stage_name= (RestApi, HttpApi, WebsocketApi),
endpoint_type= (RestApi), disable_execute_api_endpoint= (WebsocketApi), runtime=
and architecture= (Function). Changing them through customize puts AWS and Stelvio
out of sync, and Stelvio doesn't check for it.
Basic Usage
Pass a customize dictionary to any Stelvio component. The dictionary keys correspond to the underlying resources that the component creates. The value for each key can be either a plain dict of properties or a callable that computes them:
from stelvio.aws.s3 import Bucket
@app.run
def run() -> None:
bucket = Bucket(
"example-bucket",
customize={
"bucket": {"force_destroy": True}
}
)
In this example, "bucket" refers to the S3 bucket resource created by the Bucket component, and force_destroy is a Pulumi property that allows the bucket to be deleted even when it contains objects.
You can also use a callable as the value for a resource key to compute properties dynamically. It receives that resource's computed properties — not necessarily every Pulumi property, see Using Callables for Dynamic Customization for details — and returns the properties to use. Callables behave differently from dicts: they aren't shallow-merged, and a global callable can even override explicit values — see below.
@app.run
def run() -> None:
bucket = Bucket(
"example-bucket",
customize={
"bucket": lambda props: {
**props,
"force_destroy": True,
}
}
)
Understanding Resource Keys
Each Stelvio component creates one or more underlying Pulumi resources. The customize dictionary keys match the resource names defined in the component's resources dataclass.
Each component's page lists its resource keys. The Quick Reference table links to all of them.
Advanced: Subscription Customization
subscribe() on DynamoTable, Queue and Topic, Topic.subscribe_queue() and Bucket.notify_function() take their own customize for the resources the call creates. When the call creates a Lambda function, its function key takes the same keys as Function(customize=...).
Keys per component: DynamoDB, Queues, Topics, S3.
Example with DynamoDB stream subscription:
from stelvio.aws.dynamo_db import DynamoTable
table = DynamoTable(
"orders",
fields={"id": "string"},
partition_key="id",
stream="new-and-old-images",
)
table.subscribe(
"process-orders",
"functions/orders.process",
customize={
"function": {
"function": {"reserved_concurrent_executions": 5}
},
"event_source_mapping": {
"maximum_retry_attempts": 3,
"bisect_batch_on_function_error": True,
},
},
)
How Customization Works
Stelvio picks each property's value in this order (highest to lowest precedence):
- Per-instance customize: the
customizeyou pass to the component - Global callable: a callable in
StelvioAppConfig(customize=...) - Explicit values: properties you set through constructor arguments (not
None) - Global dict: a dict in
StelvioAppConfig(customize=...) - Stelvio defaults: built-in default values
The global entry for a resource key is either a dict or a callable, so only one of 2 and 4 applies. A global dict is a default: your constructor arguments beat it. A global callable sees your arguments, and any value it returns that isn't None beats them. A key it leaves out or returns as None keeps the value it would have had without the callable. See Using Callables for Dynamic Customization for details.
Shallow Merge
A callable on a component isn't merged: what it returns is exactly what's sent. A global callable's non-None values are merged over the other values.
Dicts are merged shallowly at each property level. If you customize a nested object, your entire object replaces the default, rather than being deep-merged.
For example, if defaults have {"encryption": {"enabled": true, "kms_key": "key-1"}} and you provide
{"encryption": {"enabled": false}}, the result is {"encryption": {"enabled": false}}—the kms_key is lost.
Common Pitfalls
Nested Object Replacement
This applies to dict-based customize values (see the Shallow Merge note above — callables aren't merged). When customizing nested objects, the entire nested object is replaced, not merged:
# ❌ This replaces entire encryption config - kms_key is lost!
bucket = Bucket(
"my-bucket",
customize={"bucket": {"encryption": {"enabled": True}}}
)
# Result: encryption = {"enabled": True} (kms_key removed)
# ✅ To keep existing encryption settings, include them:
bucket = Bucket(
"my-bucket",
customize={
"bucket": {
"encryption": {
"enabled": True,
"kms_key": "arn:aws:kms:...", # Preserved
}
}
}
)
Explicit Values Override Global Defaults
Explicit values take precedence over a global dict:
@app.config
def configuration(env: str) -> StelvioAppConfig:
return StelvioAppConfig(
customize={
Function: {"function": {"memory_size": 512}}
}
)
@app.run
def run() -> None:
# Uses global default: memory_size = 512
fn1 = Function("fn1", handler="handlers.handler")
# ✅ Explicit value overrides global default: memory_size = 1024
fn2 = Function(
"fn2",
handler="handlers.handler",
memory=1024, # Explicit value takes precedence
)
Explicit constructor arguments always override a global customize dict, so you don't need customize just to override a global default. Use it when you need to set a property that isn't exposed as a constructor argument.
Constructor arguments don't always match Pulumi property names
Stelvio constructor arguments don't always map 1:1 to the Pulumi property
name you'd use inside customize. For example, Stelvio's memory
constructor argument maps to the underlying Pulumi memory_size property.
Check each component's customization guide (linked in the
Quick Reference table) for the exact property names.
Global Customization
Apply default customizations to all instances of a component type using the customize option in StelvioAppConfig. A global dict acts as a default: explicit values in component constructors override it. A global callable can override them, see Using Callables for Dynamic Customization.
from stelvio.app import StelvioApp
from stelvio.config import StelvioAppConfig
from stelvio.aws.s3 import Bucket
from stelvio.aws.function import Function
app = StelvioApp("my-project")
@app.config
def configuration(env: str) -> StelvioAppConfig:
return StelvioAppConfig(
customize={
Bucket: {
"bucket": {"force_destroy": True}
},
Function: {
"function": {
"memory_size": 512,
"tracing_config": {"mode": "Active"}
}
}
}
)
@app.run
def run() -> None:
# Both buckets inherit force_destroy=True (global default)
bucket1 = Bucket("bucket-one")
bucket2 = Bucket("bucket-two")
# All functions get 512 MB memory and X-Ray tracing (global defaults)
fn1 = Function("my-fn", handler="functions/handler.main")
# Explicit value overrides the global default: 1024 MB instead of 512
fn2 = Function("fast-fn", handler="functions/handler.main", memory=1024)
The global customize dictionary uses component types as keys (e.g., Bucket, Function) and the same resource customization dictionaries as values, except the keys listed in Keys That Configure Another Component.
Stelvio checks global customize when @app.config returns, before any component exists. A key that isn't a component type, a resource key the component doesn't have, or a value that isn't a dict raises an error, also for a type your app never creates.
Global Customize vs. Explicit Values
A global dict is useful for environment-wide defaults, and explicit values take precedence over it:
@app.config
def configuration(env: str) -> StelvioAppConfig:
return StelvioAppConfig(
customize={
Function: {"function": {"timeout": 30}}
}
)
@app.run
def run() -> None:
# Uses global default: timeout = 30
fn1 = Function("quick-task", handler="handler.main")
# Explicit value overrides: timeout = 300
fn2 = Function("slow-task", handler="handler.main", timeout=300)
Combining Global and Per-Instance Customization
Per-instance customize beats both explicit values and global customize. The full order is in How Customization Works.
@app.config
def configuration(env: str) -> StelvioAppConfig:
return StelvioAppConfig(
customize={
Function: {"function": {"memory_size": 512}}
}
)
@app.run
def run() -> None:
# Uses global default: memory_size = 512
fn1 = Function("fn1", handler="handlers.handler")
# Explicit value overrides global default: memory_size = 1024
fn2 = Function(
"fn2",
handler="handlers.handler",
memory=1024, # Explicit value takes precedence over the global default
)
# Per-instance customize overrides everything: memory_size = 2048
fn3 = Function(
"fn3",
handler="handlers.handler",
customize={"function": {"memory_size": 2048}} # Highest precedence
)
Keys That Configure Another Component
Some resource keys configure a component that another component creates, such as the function a Cron runs. These keys work only in per-instance customize, in the call that creates the component. In global customize they raise an error.
from stelvio.aws.cron import Cron
@app.run
def run() -> None:
# Only this Cron's function gets 1024 MB
Cron(
"cleanup",
"rate(1 day)",
"functions/cleanup.handler",
customize={"function": {"function": {"memory_size": 1024}}},
)
| Component | Key | Pass it in |
|---|---|---|
Cron |
function |
Cron(...) |
DynamoSubscription |
function |
table.subscribe(...) |
QueueSubscription |
function |
queue.subscribe(...) |
TopicSubscription |
function |
topic.subscribe(...) |
BucketNotifySubscription |
function |
bucket.notify_function(...) |
AppSyncDataSource |
function |
api.data_source_lambda(...) |
AppSync |
auth_functions, acm_validated_domain |
AppSync(...) |
UserPool |
trigger_functions, acm_validated_domain |
UserPool(...) |
RestApi |
acm_validated_domain |
RestApi(...) |
CloudFrontDistribution |
acm_validated_domain |
CloudFrontDistribution(...) |
Router |
acm_validated_domain |
Router(...) |
S3StaticWebsite |
bucket, cloudfront_distribution |
S3StaticWebsite(...) |
ApiDomain |
certificate |
ApiDomain(...) |
A global {Function: ...} applies to every function in the app, including the ones these components create. A nested dict on a component is that function's own per-instance customize, so it beats the global entry. A Function you create yourself and pass in, for example as a Cron handler, keeps its own customize: the parent's key doesn't change it.
Using Callables for Dynamic Customization
For any resource key you can pass a callable instead of a dictionary. The callable receives the resource's properties as a dictionary and returns the properties to use — handy when a value has to be computed rather than hard-coded.
On a component, a callable fully replaces the properties with whatever it returns, so spread the incoming props to keep the values you don't want to change. A global callable works differently, see What the Callable Receives.
Function(
"my-fn",
handler="functions/handler.main",
customize={
"function": lambda props: {
**props,
"description": f"{props['memory_size']} MB function",
}
},
)
What the Callable Receives
The properties passed to a callable depend on where you use it:
- Per-instance
customize: the callable receives the fully resolved properties, with Stelvio defaults, global customize, and explicit values already applied. Whatever it returns is used as-is. - Global
customize: the callable receives Stelvio's defaults with the computed properties on top, whereNonemarks a value the user did not set explicitly. The non-Nonevalues it returns are merged over Stelvio's defaults and the explicit values. A key it leaves out or returns asNonekeeps the value it would have had without the callable, so you can return only the keys you change or spreadprops. Because the callable sees the explicit values, it decides how to treat them, so it can overwrite, extend, or transform them.
Removing a property
A global callable can't remove a property: a key it returns as None keeps its value. To remove one, use a callable in the component's own customize and leave the key out of what it returns.
Global Callables Act as Defaults
A global callable is the dynamic counterpart of a global dictionary. Check for None to honor values the user set explicitly:
def function_defaults(props):
# Default to 512 MB unless the user set memory explicitly
memory = props["memory_size"] if props.get("memory_size") is not None else 512
return {**props, "memory_size": memory}
@app.config
def configuration(env: str) -> StelvioAppConfig:
return StelvioAppConfig(
customize={Function: {"function": function_defaults}},
)
A global callable can override explicit values
Unlike a global dictionary (where explicit values always win), a global
callable is in full control. Returning {**props, "memory_size": 512}
unconditionally would override even a Function(..., memory=1024). Check
for None whenever you want explicit values to take precedence.
Adding to a Value Stelvio Sets
A global callable can extend a value instead of replacing it. This adds an environment variable to every function and keeps the ones the function already has:
def add_log_level(props):
variables = props["environment"]["variables"]
return {"environment": {"variables": {**variables, "LOG_LEVEL": "info"}}}
@app.config
def configuration(env: str) -> StelvioAppConfig:
return StelvioAppConfig(
customize={Function: {"function": add_log_level}},
)
A global dict can't set environment variables
Stelvio always sets a function's environment: the STLV_* variables from links plus
your environment= argument. It counts as an explicit value, so a global dict like
{Function: {"function": {"environment": ...}}} never applies. Use a callable as above,
or the environment= argument.
Environment-Specific Customization
Combine customization with environment-based configuration for environment-specific settings:
@app.config
def configuration(env: str) -> StelvioAppConfig:
if env == "dev":
return StelvioAppConfig(
customize={
Bucket: {"bucket": {"force_destroy": True}},
}
)
else:
# Production: keep default safe behavior
return StelvioAppConfig()
Finding Available Properties
To discover which properties you can customize for each resource, refer to the Pulumi AWS provider documentation:
- S3 Bucket
- Lambda Function
- DynamoDB Table
- SQS Queue
- SNS Topic
- API Gateway REST API
- API Gateway HTTP API
IDE Support
If you're using an IDE with Python type checking, the customization dictionaries are fully typed. Your IDE can provide autocompletion and validation for available properties.
Quick Reference
| Component | Guide |
|---|---|
Bucket |
S3 |
Function |
Lambda |
Queue |
Queues |
Topic |
Topics |
DynamoTable |
DynamoDB |
Cron |
Cron |
Email |
|
Layer |
Lambda |
RestApi |
REST API |
HttpApi |
HTTP API |
WebsocketApi |
WebSocket API |
ApiDomain |
HTTP API |
AppSync |
AppSync |
UserPool, IdentityPool |
Cognito |
CloudFrontDistribution |
CloudFront |
Router |
CloudFront Router |
S3StaticWebsite |
S3 |
Vpc |
VPC |
Nested Customization
Some Stelvio components create sub-components rather than Pulumi resources directly. For these, the customization structure mirrors what you'd use when instantiating the sub-component on its own. Component pages mark these keys as Nested. They work only in per-instance customize, see Keys That Configure Another Component.