Bucket
A private S3 bucket with encryption, TLS-only access and production versioning on by default. Grant access in one line instead of writing IAM policy.
Infrastructure
import { App } from '@anvil-cloud/sdk';
import * as anvil from '@anvil-cloud/sdk';
export default new App({
run(ctx) {
const bucket = new anvil.aws.Bucket('uploads', {
versioning: true,
versionRetention: '90d',
storageTransition: '90d',
});
const fn = new anvil.aws.Lambda('processor', {
runtime: 'nodejs24.x',
handler: 'functions/processor/index.handler',
environment: {
BUCKET_NAME: bucket.bucketName,
},
});
// GetObject/PutObject on keys under uploads/ only, plus ListBucket.
bucket.grantReadWrite(fn, ['uploads/*']);
ctx.export('bucketName', bucket.bucketName);
},
});import anvil_cloud as anvil
def infra(ctx: anvil.Context):
bucket = anvil.aws.Bucket("uploads",
versioning=True,
version_retention="90d",
storage_transition="90d",
)
fn = anvil.aws.Lambda("processor",
runtime="nodejs24.x",
handler="functions/processor/index.handler",
environment={"BUCKET_NAME": bucket.bucket_name},
)
# GetObject/PutObject on keys under uploads/ only, plus ListBucket.
bucket.grant_read_write(fn, paths=["uploads/*"])
ctx.export("bucket_name", bucket.bucket_name)
anvil.App(run=infra)package main
import (
"github.com/anvil-cloud/anvil/sdk/go/anvil"
anvilaws "github.com/anvil-cloud/anvil/sdk/go/anvil/aws"
"github.com/pulumi/pulumi/sdk/v3/go/pulumi"
)
func main() {
anvil.Run(anvil.AppConfig{
Run: func(ctx *anvil.Context) error {
bucket, err := anvilaws.NewBucket(ctx.PulumiCtx(), "uploads", &anvilaws.BucketArgs{
Versioning: pulumi.Bool(true),
VersionRetention: pulumi.String("90d"),
StorageTransition: pulumi.String("90d"),
})
if err != nil {
return err
}
_, err = anvilaws.NewLambda(ctx.PulumiCtx(), "processor", &anvilaws.LambdaArgs{
Runtime: anvilaws.LambdaRuntime_Nodejs24_x,
Handler: pulumi.String("functions/processor/index.handler"),
Environment: pulumi.StringMap{"BUCKET_NAME": bucket.BucketName},
// Go has no grant methods yet: the same actions as grantReadWrite(fn, ['uploads/*']).
Permissions: anvilaws.LambdaPermissionArray{
anvilaws.LambdaPermissionArgs{
Actions: pulumi.ToStringArray([]string{"s3:GetObject", "s3:PutObject"}),
Resource: bucket.Arn,
Path: pulumi.String("uploads"),
},
anvilaws.LambdaPermissionArgs{
Actions: pulumi.ToStringArray([]string{"s3:ListBucket"}),
Resource: bucket.Arn,
},
},
})
if err != nil {
return err
}
ctx.Export("bucketName", bucket.BucketName)
return nil
},
})
}Runtime code
Works from any compute granted access (Lambda, ECS, EC2…), using each language's AWS SDK. Set BUCKET_NAME in its environment.
// storage.ts
import { S3Client, PutObjectCommand, GetObjectCommand } from '@aws-sdk/client-s3';
const s3 = new S3Client({});
const bucket = process.env.BUCKET_NAME!;
export async function save(key: string, body: string): Promise<void> {
await s3.send(new PutObjectCommand({ Bucket: bucket, Key: `uploads/${key}`, Body: body }));
}
export async function load(key: string): Promise<string | undefined> {
const object = await s3.send(new GetObjectCommand({ Bucket: bucket, Key: `uploads/${key}` }));
return object.Body?.transformToString();
}# storage.py
import os
import boto3
s3 = boto3.client("s3")
BUCKET = os.environ["BUCKET_NAME"]
def save(key: str, body: str) -> None:
s3.put_object(Bucket=BUCKET, Key=f"uploads/{key}", Body=body)
def load(key: str) -> str:
obj = s3.get_object(Bucket=BUCKET, Key=f"uploads/{key}")
return obj["Body"].read().decode("utf-8")// storage.go
package storage
import (
"bytes"
"context"
"io"
"os"
"github.com/aws/aws-sdk-go-v2/aws"
"github.com/aws/aws-sdk-go-v2/service/s3"
)
var bucket = os.Getenv("BUCKET_NAME")
// Save writes body to uploads/<key>. Create the client once, with
// s3.NewFromConfig(cfg) after config.LoadDefaultConfig(ctx).
func Save(ctx context.Context, client *s3.Client, key, body string) error {
_, err := client.PutObject(ctx, &s3.PutObjectInput{
Bucket: aws.String(bucket),
Key: aws.String("uploads/" + key),
Body: bytes.NewReader([]byte(body)),
})
return err
}
// Load reads uploads/<key>.
func Load(ctx context.Context, client *s3.Client, key string) (string, error) {
out, err := client.GetObject(ctx, &s3.GetObjectInput{
Bucket: aws.String(bucket),
Key: aws.String("uploads/" + key),
})
if err != nil {
return "", err
}
defer out.Body.Close()
b, err := io.ReadAll(out.Body)
return string(b), err
}