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.

Storage · anvil:aws:Bucket

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
}

← Back to Bucket docs