Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions examples/aws-bucket-lifecycle-rules/sst-env.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
/* tslint:disable */
/* eslint-disable */
import "sst"
declare module "sst" {
export interface Resource {
MyBucket: {
name: string
type: "sst.aws.Bucket"
}
}
}
export {}
38 changes: 38 additions & 0 deletions examples/aws-bucket-lifecycle-rules/sst.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
/// <reference path="./.sst/platform/config.d.ts" />

/**
* ## Bucket lifecycle policies
*
* Configure S3 bucket lifecycle policies to expire objects automatically.
*/
export default $config({
app(input) {
return {
name: "aws-bucket-lifecycle-rules",
home: "aws",
removal: input?.stage === "production" ? "retain" : "remove",
};
},
async run() {
const bucket = new sst.aws.Bucket("MyBucket", {
lifecycle: [
{
expiresIn: "60 days",
},
{
id: "expire-tmp-files",
prefix: "tmp/",
expiresIn: "30 days",
},
{
prefix: "data/",
expiresAt: "2028-12-31",
},
],
});

return {
bucket: bucket.name,
};
},
});
68 changes: 67 additions & 1 deletion platform/src/components/aws/bucket.ts
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,25 @@ interface BucketCorsArgs {
}

interface BucketLifecycleArgs {
/**
* The unique identifier for the lifecycle rule.
*
* This ID must be unique across all lifecycle rules in the bucket and cannot exceed 255 characters.
* Whitespace-only values are not allowed.
*
* If not provided, SST will generate a unique ID based on the bucket component name and rule index.
*
* @example
* Use stable IDs to ensure rule identity is preserved when reordering rules.
* ```js
* {
* id: "expire-tmp-files",
* prefix: "/tmp",
* expiresIn: "7 days"
* }
* ```
*/
id?: Input<string>;
/**
* An S3 object key prefix that the lifecycle rule applies to.
* @example
Expand Down Expand Up @@ -443,6 +462,24 @@ export interface BucketArgs {
* ]
* }
* ```
*
* Use stable IDs to preserve rule identity when reordering.
* ```js
* {
* lifecycle: [
* {
* id: "expire-tmp-files",
* prefix: "/tmp",
* expiresIn: "7 days"
* },
* {
* id: "archive-old-logs",
* prefix: "/logs",
* expiresIn: "90 days"
* }
* ]
* }
* ```
*/
lifecycle?: Input<Input<Prettify<BucketLifecycleArgs>>[]>;
/**
Expand Down Expand Up @@ -924,14 +961,43 @@ export class Bucket extends Component implements Link.Linkable {
return output(args.lifecycle).apply((lifecycleRules) => {
if (!lifecycleRules || lifecycleRules.length === 0) return;

const seenIds = new Map<string, number>();

const resolvedIds = lifecycleRules.map((rule, index) => {
const rawId = rule.id ?? `${name}LifecycleRule${index}`;
const resolvedId = rawId.trim();

if (resolvedId.length === 0) {
throw new VisibleError(
`Lifecycle rule at index ${index} has an empty or whitespace-only "id". Please provide a valid id or omit it to use the auto-generated id.`,
);
}

if (resolvedId.length > 255) {
throw new VisibleError(
`Lifecycle rule at index ${index} has an "id" that is ${resolvedId.length} characters long. AWS S3 lifecycle rule IDs cannot exceed 255 characters.`,
);
}

const existingIndex = seenIds.get(resolvedId);
if (existingIndex !== undefined) {
throw new VisibleError(
`Lifecycle rule "id" values must be unique. The id "${resolvedId}" is used by rules at indexes ${existingIndex} and ${index}.`,
);
}
seenIds.set(resolvedId, index);

return resolvedId;
});

return new s3.BucketLifecycleConfigurationV2(
...transform(
args.transform?.lifecycle,
`${name}Lifecycle`,
{
bucket: bucket.bucket,
rules: lifecycleRules.map((rule, index) => ({
id: `${name}LifecycleRule${index}`,
id: resolvedIds[index],
status: rule.enabled !== false ? "Enabled" : "Disabled",
expiration:
rule.expiresIn || rule.expiresAt
Expand Down