Optional stacks (rayito stack)¶
Since 0.6.0 there is a single convention to deploy, query and delete the
optional infrastructure of the cost-bearing features (ADR-016):
OptionalStacks in the SDK and rayito stack in the CLI. No feature
deploys itself — it is always an explicit call of yours — and the SDK
itself hosts no server: everything lives in your account, as a
CloudFormation stack.
Cost and activation
- Default:
rayito stack listmakes no AWS call at all (it is catalog metadata);deploy/status/destroyonly act when you invoke them. - Turns on:
OptionalStacks().deploy("<component>")orrayito stack deploy <component>. - Resources and AWS calls: CloudFormation
CreateStack/UpdateStack/DescribeStacks/DeleteStackon the component's template (AWS_API_NOTES.md§21); each component also documents its own. - Approximate cost: that of the resources each component creates
(
rayito stack listprints it); CloudFormation itself does not charge. - IAM:
cloudformation:CreateStack/UpdateStack/DescribeStacks/ DeleteStackon the stack, plus whatever the template creates (CAPABILITY_IAMwhen it creates policies). - How to turn it off:
rayito stack destroy <component>(orOptionalStacks().destroy(...)); each component says what it keeps.
Today's components¶
| Component | What it creates | Idle cost | What it is for |
|---|---|---|---|
metadata-index |
DynamoDB table + 2 IAM policies (reader/writer) | $0 (on-demand, empty table) | Metadata index |
secrets-access |
2 IAM policies over a Secrets Manager prefix | $0 | Secrets |
s3-mounts |
1 IAM policy over a bucket (and its prefixes) | $0 | S3 mounts |
sizes-guard |
1 IAM policy that denies RunMicrovm outside the listed images and publishing images |
$0 | Sizes |
events-webhooks |
HMAC secret, DynamoDB table with streams, 3 Lambdas with their log groups, subscription filter, scheduler, failure SQS queues, roles and 4 IAM policies | ≈ $0.40/month (the secret) | Events and webhooks |
otlp-export |
1 IAM policy (cloudwatch:PutMetricData) |
$0 | OTLP export |
templates |
1 IAM policy for whoever builds templates | $0 | Templates |
custom-domain (experimental) |
CloudFront distribution, routing CloudFront Function and KeyValueStore | $0 with no traffic | Custom domain |
dashboard |
Lambda with Function URL (the page and the dashboard API), Cognito user pool with its client and domain, 2 DynamoDB tables (audit and cache), read role (and, with AllowActions, actions role), log group; no CloudFront or buckets |
≈ $0 (everything on demand) | Dashboard in your account |
dashboard-cloudwatch |
1 CloudWatch dashboard (/rayito/* logs, OTLP metrics and optional quota usage) |
$0 with fewer than 4 dashboards | Dashboard: CloudWatch, accounts and alerts |
dashboard-read-role |
1 read-only IAM role, in the target account of the multi-account view | $0 | Dashboard: CloudWatch, accounts and alerts |
quota-alerts |
Scheduled Lambda with its EventBridge rule, SNS topic, DynamoDB state table and role | ≈ $0.30/month (one metric) | Dashboard: CloudWatch, accounts and alerts |
efs-volumes (experimental) |
In an existing VPC: encrypted EFS file system, one mount target per subnet, 2 new security groups, a VPC egress connector, its role and the RayitoEfsVolumeClient policy |
$0 with the file system empty | EFS volumes in your VPC |
custom-domain is experimental: the template has not yet been fully
deployed on real AWS (see Custom domain). Besides
rayito stack, it has its own CLI, rayito domain deploy|status|destroy.
Parameters of each component¶
They are passed with parameters={...} (TypeScript: { parameters: {...} })
or --param K=V. A parameter the component does not declare is
InvalidArgumentException before calling AWS.
| Component | Parameter | Default on create |
|---|---|---|
metadata-index |
TableName, PointInTimeRecovery, DeletionProtection |
rayito-sandboxes, false, false |
secrets-access |
SecretPrefix, KmsKeyArn |
rayito/, empty (no kms:Decrypt) |
s3-mounts |
BucketName (required), Prefixes, ReadOnly |
—, * (the whole bucket), true |
sizes-guard |
ImageArns (required) |
— |
events-webhooks |
LogGroupName (required), ReconcilerIntervalMinutes; ArtifactBucket is filled in by artifact_bucket=/--artifact-bucket |
—, 5 |
otlp-export |
none | — |
templates |
ArtifactBucketArn, BuildRoleArn (required), ImageLogGroupPrefix, BaseImageBucketArn, ProtectedImageNamePrefix |
—, —, /rayito, empty (the artifact bucket), rayito-base |
custom-domain |
PublicDomain, CertificateArn (required, ACM certificate in us-east-1), AlternateDomainNames |
—, —, empty (wildcard alias *.<PublicDomain>) |
dashboard |
ArtifactBucket (filled in by artifact_bucket=/--artifact-bucket), AllowActions, EnableCostExplorer, ImageNamePrefix, InitialUserEmail, DomainPrefix, ThemeJson, ThemeParameterName, LogoParameterName, ReservedConcurrency, LogRetentionDays |
—, false, false, rayito-, empty, empty, empty, empty, empty, empty, 30 |
dashboard-cloudwatch |
DashboardName, LogGroupPrefix, IncludeOtlp, IncludeQuota |
empty (the stack name), /rayito/, false, false |
dashboard-read-role |
TrustedPrincipalArn (required), ExternalId, MaxSessionSeconds |
—, empty, 3600 |
quota-alerts |
ArtifactBucket (filled in by artifact_bucket=/--artifact-bucket), ScheduleExpression, WarningPercent, CriticalPercent, NotificationEmail, TopicArn, LogRetentionDays |
—, rate(1 hour), 80, 95, empty, empty, 30 |
efs-volumes |
VpcId, SubnetIds (required, 1 to 3), ConnectorName, AllowWrite, AccessPointArns, ReadOnlyAccessPointArns |
—, —, rayito-efs, true, empty (any access point of the file system), empty |
stacks.components() returns this same catalog (with each parameter's
description and the cost) without calling AWS.
Redeploying does not undo the configuration¶
deploy on a stack that already exists updates it. Parameters you do not
pass again keep the value the stack is deployed with (CloudFormation
UsePreviousValue); the catalog defaults only apply on creation, or to a new
parameter the stack does not have yet. So redeploying s3-mounts without
repeating --param Prefixes=... does not reopen the whole bucket, nor does
redeploying metadata-index without TableName replace the table.
rayito stack deploy prints which parameters change before asking for
confirmation, and OptionalStacks().parameter_changes(...) (TypeScript:
parameterChanges(...)) returns the same without deploying anything (just a
DescribeStacks). Since 0.6.1; in 0.6.0, redeploying without
repeating a parameter reset it to its default value.
from rayito import OptionalStacks
stacks = OptionalStacks(region="us-east-1")
current = stacks.status("s3-mounts")
if current is not None:
print(current.state, current.parameters) # {"BucketName": "...", "Prefixes": "runs/*", ...}
for change in stacks.parameter_changes("s3-mounts", parameters={"ReadOnly": "false"}):
print(f"{change.name}: {change.before} -> {change.after}")
stacks.deploy("s3-mounts", parameters={"ReadOnly": "false"}) # Prefixes is kept
import { OptionalStacks } from "rayito";
const stacks = new OptionalStacks({ region: "us-east-1" });
const current = await stacks.status("s3-mounts");
console.log(current?.state, current?.parameters);
const changes = await stacks.parameterChanges("s3-mounts", {
parameters: { ReadOnly: "false" },
});
for (const change of changes) console.log(`${change.name}: ${change.before} -> ${change.after}`);
await stacks.deploy("s3-mounts", { parameters: { ReadOnly: "false" } });
CLI¶
rayito stack list
rayito stack deploy metadata-index --param TableName=rayito-sandboxes
rayito stack status metadata-index
rayito stack destroy metadata-index
deploy always prints the component's "Cost and activation" block and the
parameters that change, and asks for confirmation unless --yes; destroy
says what is kept.
Python and TypeScript¶
from rayito import OptionalStacks
stacks = OptionalStacks(region="us-east-1")
for component in stacks.components(): # pure metadata, no AWS call
print(component.name, component.supported)
status = stacks.deploy("metadata-index", parameters={"TableName": "rayito-sandboxes"})
print(status.state, status.outputs)
stacks.destroy("metadata-index")
AsyncOptionalStacks has the same surface, in async/await.
import { OptionalStacks } from "rayito";
const stacks = new OptionalStacks({ region: "us-east-1" });
for (const component of stacks.components()) {
console.log(component.name, component.supported);
}
const status = await stacks.deploy("metadata-index", {
parameters: { TableName: "rayito-sandboxes" },
});
console.log(status.state, status.outputs);
await stacks.destroy("metadata-index");
How it behaves¶
- Stack name:
rayito-<component>(rayito-s3-mounts), unlessstack_name=/stackName/--stack-name. With different names you can deploy the same component several times, for example ones3-mountsstack per bucket. - Waiting:
deploy()anddestroy()wait for the stack to settle (at most 600 s,wait_timeout=/waitTimeoutMs); withwait=Falsethey return as soon as CloudFormation accepts the request. - Tags: each stack carries
rayito:component,rayito:managed-byandrayito:sdk-version, plus the ones you pass intags=/--tag K=V(for organizations that require tags when creating resources). The three fixed ones always win. - A failed stack (
ROLLBACK_COMPLETE) cannot be updated:deploy()raisesStackException(code="blocked")(TypeScript:StackError) without callingUpdateStack. Delete it withdestroy()and deploy it again. - Lambda code: a component with code (
events-webhooks) uploads it torayito/stacks/<component>/<sha256>.zipofartifact_bucket=/--artifact-bucket, which must be in your account (each call carriesExpectedBucketOwner). If the key already exists, the SDK compares the content: equal, it does not upload it again; different (a tampered object), it overwrites it and warns. It needss3:GetObjectands3:PutObjecton the bucket'srayito/*(those ofCallerPolicyorImagePublisherPolicy). - TypeScript:
OptionalStacksloads the optional peer@aws-sdk/client-cloudformationon the first AWS call (pnpm add @aws-sdk/client-cloudformation).