Configure AWS S3 Bucket Event Notifications

The aws:s3/bucketNotification:BucketNotification resource, part of the Pulumi AWS provider, configures S3 event notifications that trigger when objects are created, deleted, or modified in a bucket. This guide focuses on three capabilities: Lambda function invocation, SQS queue delivery, and EventBridge routing.

S3 buckets support only one notification configuration resource. Declaring multiple BucketNotification resources for the same bucket causes configuration conflicts. Notification configurations reference Lambda functions, SQS queues, or SNS topics that must exist separately, along with appropriate IAM permissions. The examples are intentionally small. Combine them with your own functions, queues, and access policies.

Invoke Lambda functions when objects are created

Many serverless workflows begin when files land in S3. Lambda functions process uploads immediately, transforming data or triggering downstream pipelines.

import * as pulumi from "@pulumi/pulumi";
import * as aws from "@pulumi/aws";

const assumeRole = aws.iam.getPolicyDocument({
    statements: [{
        effect: "Allow",
        principals: [{
            type: "Service",
            identifiers: ["lambda.amazonaws.com"],
        }],
        actions: ["sts:AssumeRole"],
    }],
});
const iamForLambda = new aws.iam.Role("iam_for_lambda", {
    name: "iam_for_lambda",
    assumeRolePolicy: assumeRole.then(assumeRole => assumeRole.json),
});
const func = new aws.lambda.Function("func", {
    code: new pulumi.asset.FileArchive("your-function.zip"),
    name: "example_lambda_name",
    role: iamForLambda.arn,
    handler: "exports.example",
    runtime: aws.lambda.Runtime.NodeJS20dX,
});
const bucket = new aws.s3.Bucket("bucket", {bucket: "your-bucket-name"});
const allowBucket = new aws.lambda.Permission("allow_bucket", {
    statementId: "AllowExecutionFromS3Bucket",
    action: "lambda:InvokeFunction",
    "function": func.arn,
    principal: "s3.amazonaws.com",
    sourceArn: bucket.arn,
});
const bucketNotification = new aws.s3.BucketNotification("bucket_notification", {
    bucket: bucket.id,
    lambdaFunctions: [{
        lambdaFunctionArn: func.arn,
        events: ["s3:ObjectCreated:*"],
        filterPrefix: "AWSLogs/",
        filterSuffix: ".log",
    }],
}, {
    dependsOn: [allowBucket],
});
import pulumi
import pulumi_aws as aws

assume_role = aws.iam.get_policy_document(statements=[{
    "effect": "Allow",
    "principals": [{
        "type": "Service",
        "identifiers": ["lambda.amazonaws.com"],
    }],
    "actions": ["sts:AssumeRole"],
}])
iam_for_lambda = aws.iam.Role("iam_for_lambda",
    name="iam_for_lambda",
    assume_role_policy=assume_role.json)
func = aws.lambda_.Function("func",
    code=pulumi.FileArchive("your-function.zip"),
    name="example_lambda_name",
    role=iam_for_lambda.arn,
    handler="exports.example",
    runtime=aws.lambda_.Runtime.NODE_JS20D_X)
bucket = aws.s3.Bucket("bucket", bucket="your-bucket-name")
allow_bucket = aws.lambda_.Permission("allow_bucket",
    statement_id="AllowExecutionFromS3Bucket",
    action="lambda:InvokeFunction",
    function=func.arn,
    principal="s3.amazonaws.com",
    source_arn=bucket.arn)
bucket_notification = aws.s3.BucketNotification("bucket_notification",
    bucket=bucket.id,
    lambda_functions=[{
        "lambda_function_arn": func.arn,
        "events": ["s3:ObjectCreated:*"],
        "filter_prefix": "AWSLogs/",
        "filter_suffix": ".log",
    }],
    opts = pulumi.ResourceOptions(depends_on=[allow_bucket]))
package main

import (
	"github.com/pulumi/pulumi-aws/sdk/v7/go/aws/iam"
	"github.com/pulumi/pulumi-aws/sdk/v7/go/aws/lambda"
	"github.com/pulumi/pulumi-aws/sdk/v7/go/aws/s3"
	"github.com/pulumi/pulumi/sdk/v3/go/pulumi"
)

func main() {
	pulumi.Run(func(ctx *pulumi.Context) error {
		assumeRole, err := iam.GetPolicyDocument(ctx, &iam.GetPolicyDocumentArgs{
			Statements: []iam.GetPolicyDocumentStatement{
				{
					Effect: pulumi.StringRef("Allow"),
					Principals: []iam.GetPolicyDocumentStatementPrincipal{
						{
							Type: "Service",
							Identifiers: []string{
								"lambda.amazonaws.com",
							},
						},
					},
					Actions: []string{
						"sts:AssumeRole",
					},
				},
			},
		}, nil)
		if err != nil {
			return err
		}
		iamForLambda, err := iam.NewRole(ctx, "iam_for_lambda", &iam.RoleArgs{
			Name:             pulumi.String("iam_for_lambda"),
			AssumeRolePolicy: pulumi.String(assumeRole.Json),
		})
		if err != nil {
			return err
		}
		_func, err := lambda.NewFunction(ctx, "func", &lambda.FunctionArgs{
			Code:    pulumi.NewFileArchive("your-function.zip"),
			Name:    pulumi.String("example_lambda_name"),
			Role:    iamForLambda.Arn,
			Handler: pulumi.String("exports.example"),
			Runtime: pulumi.String(lambda.RuntimeNodeJS20dX),
		})
		if err != nil {
			return err
		}
		bucket, err := s3.NewBucket(ctx, "bucket", &s3.BucketArgs{
			Bucket: pulumi.String("your-bucket-name"),
		})
		if err != nil {
			return err
		}
		allowBucket, err := lambda.NewPermission(ctx, "allow_bucket", &lambda.PermissionArgs{
			StatementId: pulumi.String("AllowExecutionFromS3Bucket"),
			Action:      pulumi.String("lambda:InvokeFunction"),
			Function:    _func.Arn,
			Principal:   pulumi.String("s3.amazonaws.com"),
			SourceArn:   bucket.Arn,
		})
		if err != nil {
			return err
		}
		_, err = s3.NewBucketNotification(ctx, "bucket_notification", &s3.BucketNotificationArgs{
			Bucket: bucket.ID(),
			LambdaFunctions: s3.BucketNotificationLambdaFunctionArray{
				&s3.BucketNotificationLambdaFunctionArgs{
					LambdaFunctionArn: _func.Arn,
					Events: pulumi.StringArray{
						pulumi.String("s3:ObjectCreated:*"),
					},
					FilterPrefix: pulumi.String("AWSLogs/"),
					FilterSuffix: pulumi.String(".log"),
				},
			},
		}, pulumi.DependsOn([]pulumi.Resource{
			allowBucket,
		}))
		if err != nil {
			return err
		}
		return nil
	})
}
using System.Collections.Generic;
using System.Linq;
using Pulumi;
using Aws = Pulumi.Aws;

return await Deployment.RunAsync(() => 
{
    var assumeRole = Aws.Iam.GetPolicyDocument.Invoke(new()
    {
        Statements = new[]
        {
            new Aws.Iam.Inputs.GetPolicyDocumentStatementInputArgs
            {
                Effect = "Allow",
                Principals = new[]
                {
                    new Aws.Iam.Inputs.GetPolicyDocumentStatementPrincipalInputArgs
                    {
                        Type = "Service",
                        Identifiers = new[]
                        {
                            "lambda.amazonaws.com",
                        },
                    },
                },
                Actions = new[]
                {
                    "sts:AssumeRole",
                },
            },
        },
    });

    var iamForLambda = new Aws.Iam.Role("iam_for_lambda", new()
    {
        Name = "iam_for_lambda",
        AssumeRolePolicy = assumeRole.Apply(getPolicyDocumentResult => getPolicyDocumentResult.Json),
    });

    var func = new Aws.Lambda.Function("func", new()
    {
        Code = new FileArchive("your-function.zip"),
        Name = "example_lambda_name",
        Role = iamForLambda.Arn,
        Handler = "exports.example",
        Runtime = Aws.Lambda.Runtime.NodeJS20dX,
    });

    var bucket = new Aws.S3.Bucket("bucket", new()
    {
        BucketName = "your-bucket-name",
    });

    var allowBucket = new Aws.Lambda.Permission("allow_bucket", new()
    {
        StatementId = "AllowExecutionFromS3Bucket",
        Action = "lambda:InvokeFunction",
        Function = func.Arn,
        Principal = "s3.amazonaws.com",
        SourceArn = bucket.Arn,
    });

    var bucketNotification = new Aws.S3.BucketNotification("bucket_notification", new()
    {
        Bucket = bucket.Id,
        LambdaFunctions = new[]
        {
            new Aws.S3.Inputs.BucketNotificationLambdaFunctionArgs
            {
                LambdaFunctionArn = func.Arn,
                Events = new[]
                {
                    "s3:ObjectCreated:*",
                },
                FilterPrefix = "AWSLogs/",
                FilterSuffix = ".log",
            },
        },
    }, new CustomResourceOptions
    {
        DependsOn =
        {
            allowBucket,
        },
    });

});
package generated_program;

import com.pulumi.Context;
import com.pulumi.Pulumi;
import com.pulumi.core.Output;
import com.pulumi.aws.iam.IamFunctions;
import com.pulumi.aws.iam.inputs.GetPolicyDocumentArgs;
import com.pulumi.aws.iam.Role;
import com.pulumi.aws.iam.RoleArgs;
import com.pulumi.aws.lambda.Function;
import com.pulumi.aws.lambda.FunctionArgs;
import com.pulumi.aws.s3.Bucket;
import com.pulumi.aws.s3.BucketArgs;
import com.pulumi.aws.lambda.Permission;
import com.pulumi.aws.lambda.PermissionArgs;
import com.pulumi.aws.s3.BucketNotification;
import com.pulumi.aws.s3.BucketNotificationArgs;
import com.pulumi.aws.s3.inputs.BucketNotificationLambdaFunctionArgs;
import com.pulumi.asset.FileArchive;
import com.pulumi.resources.CustomResourceOptions;
import java.util.List;
import java.util.ArrayList;
import java.util.Map;
import java.io.File;
import java.nio.file.Files;
import java.nio.file.Paths;

public class App {
    public static void main(String[] args) {
        Pulumi.run(App::stack);
    }

    public static void stack(Context ctx) {
        final var assumeRole = IamFunctions.getPolicyDocument(GetPolicyDocumentArgs.builder()
            .statements(GetPolicyDocumentStatementArgs.builder()
                .effect("Allow")
                .principals(GetPolicyDocumentStatementPrincipalArgs.builder()
                    .type("Service")
                    .identifiers("lambda.amazonaws.com")
                    .build())
                .actions("sts:AssumeRole")
                .build())
            .build());

        var iamForLambda = new Role("iamForLambda", RoleArgs.builder()
            .name("iam_for_lambda")
            .assumeRolePolicy(assumeRole.json())
            .build());

        var func = new Function("func", FunctionArgs.builder()
            .code(new FileArchive("your-function.zip"))
            .name("example_lambda_name")
            .role(iamForLambda.arn())
            .handler("exports.example")
            .runtime("nodejs20.x")
            .build());

        var bucket = new Bucket("bucket", BucketArgs.builder()
            .bucket("your-bucket-name")
            .build());

        var allowBucket = new Permission("allowBucket", PermissionArgs.builder()
            .statementId("AllowExecutionFromS3Bucket")
            .action("lambda:InvokeFunction")
            .function(func.arn())
            .principal("s3.amazonaws.com")
            .sourceArn(bucket.arn())
            .build());

        var bucketNotification = new BucketNotification("bucketNotification", BucketNotificationArgs.builder()
            .bucket(bucket.id())
            .lambdaFunctions(BucketNotificationLambdaFunctionArgs.builder()
                .lambdaFunctionArn(func.arn())
                .events("s3:ObjectCreated:*")
                .filterPrefix("AWSLogs/")
                .filterSuffix(".log")
                .build())
            .build(), CustomResourceOptions.builder()
                .dependsOn(allowBucket)
                .build());

    }
}
resources:
  iamForLambda:
    type: aws:iam:Role
    name: iam_for_lambda
    properties:
      name: iam_for_lambda
      assumeRolePolicy: ${assumeRole.json}
  allowBucket:
    type: aws:lambda:Permission
    name: allow_bucket
    properties:
      statementId: AllowExecutionFromS3Bucket
      action: lambda:InvokeFunction
      function: ${func.arn}
      principal: s3.amazonaws.com
      sourceArn: ${bucket.arn}
  func:
    type: aws:lambda:Function
    properties:
      code:
        fn::FileArchive: your-function.zip
      name: example_lambda_name
      role: ${iamForLambda.arn}
      handler: exports.example
      runtime: nodejs20.x
  bucket:
    type: aws:s3:Bucket
    properties:
      bucket: your-bucket-name
  bucketNotification:
    type: aws:s3:BucketNotification
    name: bucket_notification
    properties:
      bucket: ${bucket.id}
      lambdaFunctions:
        - lambdaFunctionArn: ${func.arn}
          events:
            - s3:ObjectCreated:*
          filterPrefix: AWSLogs/
          filterSuffix: .log
    options:
      dependsOn:
        - ${allowBucket}
variables:
  assumeRole:
    fn::invoke:
      function: aws:iam:getPolicyDocument
      arguments:
        statements:
          - effect: Allow
            principals:
              - type: Service
                identifiers:
                  - lambda.amazonaws.com
            actions:
              - sts:AssumeRole

When an object matching the filter lands in the bucket, S3 invokes the Lambda function. The lambdaFunctions property defines the function ARN, event types to watch, and optional prefix/suffix filters. The lambda.Permission resource grants S3 permission to invoke your function; without it, invocations fail with access denied errors.

Route different prefixes to separate Lambda functions

Applications often need different processing logic for different file types or paths. A single notification configuration routes uploads to multiple Lambda functions based on prefix patterns.

import * as pulumi from "@pulumi/pulumi";
import * as aws from "@pulumi/aws";

const assumeRole = aws.iam.getPolicyDocument({
    statements: [{
        effect: "Allow",
        principals: [{
            type: "Service",
            identifiers: ["lambda.amazonaws.com"],
        }],
        actions: ["sts:AssumeRole"],
    }],
});
const iamForLambda = new aws.iam.Role("iam_for_lambda", {
    name: "iam_for_lambda",
    assumeRolePolicy: assumeRole.then(assumeRole => assumeRole.json),
});
const func1 = new aws.lambda.Function("func1", {
    code: new pulumi.asset.FileArchive("your-function1.zip"),
    name: "example_lambda_name1",
    role: iamForLambda.arn,
    handler: "exports.example",
    runtime: aws.lambda.Runtime.NodeJS20dX,
});
const bucket = new aws.s3.Bucket("bucket", {bucket: "your-bucket-name"});
const allowBucket1 = new aws.lambda.Permission("allow_bucket1", {
    statementId: "AllowExecutionFromS3Bucket1",
    action: "lambda:InvokeFunction",
    "function": func1.arn,
    principal: "s3.amazonaws.com",
    sourceArn: bucket.arn,
});
const func2 = new aws.lambda.Function("func2", {
    code: new pulumi.asset.FileArchive("your-function2.zip"),
    name: "example_lambda_name2",
    role: iamForLambda.arn,
    handler: "exports.example",
});
const allowBucket2 = new aws.lambda.Permission("allow_bucket2", {
    statementId: "AllowExecutionFromS3Bucket2",
    action: "lambda:InvokeFunction",
    "function": func2.arn,
    principal: "s3.amazonaws.com",
    sourceArn: bucket.arn,
});
const bucketNotification = new aws.s3.BucketNotification("bucket_notification", {
    bucket: bucket.id,
    lambdaFunctions: [
        {
            lambdaFunctionArn: func1.arn,
            events: ["s3:ObjectCreated:*"],
            filterPrefix: "AWSLogs/",
            filterSuffix: ".log",
        },
        {
            lambdaFunctionArn: func2.arn,
            events: ["s3:ObjectCreated:*"],
            filterPrefix: "OtherLogs/",
            filterSuffix: ".log",
        },
    ],
}, {
    dependsOn: [
        allowBucket1,
        allowBucket2,
    ],
});
import pulumi
import pulumi_aws as aws

assume_role = aws.iam.get_policy_document(statements=[{
    "effect": "Allow",
    "principals": [{
        "type": "Service",
        "identifiers": ["lambda.amazonaws.com"],
    }],
    "actions": ["sts:AssumeRole"],
}])
iam_for_lambda = aws.iam.Role("iam_for_lambda",
    name="iam_for_lambda",
    assume_role_policy=assume_role.json)
func1 = aws.lambda_.Function("func1",
    code=pulumi.FileArchive("your-function1.zip"),
    name="example_lambda_name1",
    role=iam_for_lambda.arn,
    handler="exports.example",
    runtime=aws.lambda_.Runtime.NODE_JS20D_X)
bucket = aws.s3.Bucket("bucket", bucket="your-bucket-name")
allow_bucket1 = aws.lambda_.Permission("allow_bucket1",
    statement_id="AllowExecutionFromS3Bucket1",
    action="lambda:InvokeFunction",
    function=func1.arn,
    principal="s3.amazonaws.com",
    source_arn=bucket.arn)
func2 = aws.lambda_.Function("func2",
    code=pulumi.FileArchive("your-function2.zip"),
    name="example_lambda_name2",
    role=iam_for_lambda.arn,
    handler="exports.example")
allow_bucket2 = aws.lambda_.Permission("allow_bucket2",
    statement_id="AllowExecutionFromS3Bucket2",
    action="lambda:InvokeFunction",
    function=func2.arn,
    principal="s3.amazonaws.com",
    source_arn=bucket.arn)
bucket_notification = aws.s3.BucketNotification("bucket_notification",
    bucket=bucket.id,
    lambda_functions=[
        {
            "lambda_function_arn": func1.arn,
            "events": ["s3:ObjectCreated:*"],
            "filter_prefix": "AWSLogs/",
            "filter_suffix": ".log",
        },
        {
            "lambda_function_arn": func2.arn,
            "events": ["s3:ObjectCreated:*"],
            "filter_prefix": "OtherLogs/",
            "filter_suffix": ".log",
        },
    ],
    opts = pulumi.ResourceOptions(depends_on=[
            allow_bucket1,
            allow_bucket2,
        ]))
package main

import (
	"github.com/pulumi/pulumi-aws/sdk/v7/go/aws/iam"
	"github.com/pulumi/pulumi-aws/sdk/v7/go/aws/lambda"
	"github.com/pulumi/pulumi-aws/sdk/v7/go/aws/s3"
	"github.com/pulumi/pulumi/sdk/v3/go/pulumi"
)

func main() {
	pulumi.Run(func(ctx *pulumi.Context) error {
		assumeRole, err := iam.GetPolicyDocument(ctx, &iam.GetPolicyDocumentArgs{
			Statements: []iam.GetPolicyDocumentStatement{
				{
					Effect: pulumi.StringRef("Allow"),
					Principals: []iam.GetPolicyDocumentStatementPrincipal{
						{
							Type: "Service",
							Identifiers: []string{
								"lambda.amazonaws.com",
							},
						},
					},
					Actions: []string{
						"sts:AssumeRole",
					},
				},
			},
		}, nil)
		if err != nil {
			return err
		}
		iamForLambda, err := iam.NewRole(ctx, "iam_for_lambda", &iam.RoleArgs{
			Name:             pulumi.String("iam_for_lambda"),
			AssumeRolePolicy: pulumi.String(assumeRole.Json),
		})
		if err != nil {
			return err
		}
		func1, err := lambda.NewFunction(ctx, "func1", &lambda.FunctionArgs{
			Code:    pulumi.NewFileArchive("your-function1.zip"),
			Name:    pulumi.String("example_lambda_name1"),
			Role:    iamForLambda.Arn,
			Handler: pulumi.String("exports.example"),
			Runtime: pulumi.String(lambda.RuntimeNodeJS20dX),
		})
		if err != nil {
			return err
		}
		bucket, err := s3.NewBucket(ctx, "bucket", &s3.BucketArgs{
			Bucket: pulumi.String("your-bucket-name"),
		})
		if err != nil {
			return err
		}
		allowBucket1, err := lambda.NewPermission(ctx, "allow_bucket1", &lambda.PermissionArgs{
			StatementId: pulumi.String("AllowExecutionFromS3Bucket1"),
			Action:      pulumi.String("lambda:InvokeFunction"),
			Function:    func1.Arn,
			Principal:   pulumi.String("s3.amazonaws.com"),
			SourceArn:   bucket.Arn,
		})
		if err != nil {
			return err
		}
		func2, err := lambda.NewFunction(ctx, "func2", &lambda.FunctionArgs{
			Code:    pulumi.NewFileArchive("your-function2.zip"),
			Name:    pulumi.String("example_lambda_name2"),
			Role:    iamForLambda.Arn,
			Handler: pulumi.String("exports.example"),
		})
		if err != nil {
			return err
		}
		allowBucket2, err := lambda.NewPermission(ctx, "allow_bucket2", &lambda.PermissionArgs{
			StatementId: pulumi.String("AllowExecutionFromS3Bucket2"),
			Action:      pulumi.String("lambda:InvokeFunction"),
			Function:    func2.Arn,
			Principal:   pulumi.String("s3.amazonaws.com"),
			SourceArn:   bucket.Arn,
		})
		if err != nil {
			return err
		}
		_, err = s3.NewBucketNotification(ctx, "bucket_notification", &s3.BucketNotificationArgs{
			Bucket: bucket.ID(),
			LambdaFunctions: s3.BucketNotificationLambdaFunctionArray{
				&s3.BucketNotificationLambdaFunctionArgs{
					LambdaFunctionArn: func1.Arn,
					Events: pulumi.StringArray{
						pulumi.String("s3:ObjectCreated:*"),
					},
					FilterPrefix: pulumi.String("AWSLogs/"),
					FilterSuffix: pulumi.String(".log"),
				},
				&s3.BucketNotificationLambdaFunctionArgs{
					LambdaFunctionArn: func2.Arn,
					Events: pulumi.StringArray{
						pulumi.String("s3:ObjectCreated:*"),
					},
					FilterPrefix: pulumi.String("OtherLogs/"),
					FilterSuffix: pulumi.String(".log"),
				},
			},
		}, pulumi.DependsOn([]pulumi.Resource{
			allowBucket1,
			allowBucket2,
		}))
		if err != nil {
			return err
		}
		return nil
	})
}
using System.Collections.Generic;
using System.Linq;
using Pulumi;
using Aws = Pulumi.Aws;

return await Deployment.RunAsync(() => 
{
    var assumeRole = Aws.Iam.GetPolicyDocument.Invoke(new()
    {
        Statements = new[]
        {
            new Aws.Iam.Inputs.GetPolicyDocumentStatementInputArgs
            {
                Effect = "Allow",
                Principals = new[]
                {
                    new Aws.Iam.Inputs.GetPolicyDocumentStatementPrincipalInputArgs
                    {
                        Type = "Service",
                        Identifiers = new[]
                        {
                            "lambda.amazonaws.com",
                        },
                    },
                },
                Actions = new[]
                {
                    "sts:AssumeRole",
                },
            },
        },
    });

    var iamForLambda = new Aws.Iam.Role("iam_for_lambda", new()
    {
        Name = "iam_for_lambda",
        AssumeRolePolicy = assumeRole.Apply(getPolicyDocumentResult => getPolicyDocumentResult.Json),
    });

    var func1 = new Aws.Lambda.Function("func1", new()
    {
        Code = new FileArchive("your-function1.zip"),
        Name = "example_lambda_name1",
        Role = iamForLambda.Arn,
        Handler = "exports.example",
        Runtime = Aws.Lambda.Runtime.NodeJS20dX,
    });

    var bucket = new Aws.S3.Bucket("bucket", new()
    {
        BucketName = "your-bucket-name",
    });

    var allowBucket1 = new Aws.Lambda.Permission("allow_bucket1", new()
    {
        StatementId = "AllowExecutionFromS3Bucket1",
        Action = "lambda:InvokeFunction",
        Function = func1.Arn,
        Principal = "s3.amazonaws.com",
        SourceArn = bucket.Arn,
    });

    var func2 = new Aws.Lambda.Function("func2", new()
    {
        Code = new FileArchive("your-function2.zip"),
        Name = "example_lambda_name2",
        Role = iamForLambda.Arn,
        Handler = "exports.example",
    });

    var allowBucket2 = new Aws.Lambda.Permission("allow_bucket2", new()
    {
        StatementId = "AllowExecutionFromS3Bucket2",
        Action = "lambda:InvokeFunction",
        Function = func2.Arn,
        Principal = "s3.amazonaws.com",
        SourceArn = bucket.Arn,
    });

    var bucketNotification = new Aws.S3.BucketNotification("bucket_notification", new()
    {
        Bucket = bucket.Id,
        LambdaFunctions = new[]
        {
            new Aws.S3.Inputs.BucketNotificationLambdaFunctionArgs
            {
                LambdaFunctionArn = func1.Arn,
                Events = new[]
                {
                    "s3:ObjectCreated:*",
                },
                FilterPrefix = "AWSLogs/",
                FilterSuffix = ".log",
            },
            new Aws.S3.Inputs.BucketNotificationLambdaFunctionArgs
            {
                LambdaFunctionArn = func2.Arn,
                Events = new[]
                {
                    "s3:ObjectCreated:*",
                },
                FilterPrefix = "OtherLogs/",
                FilterSuffix = ".log",
            },
        },
    }, new CustomResourceOptions
    {
        DependsOn =
        {
            allowBucket1,
            allowBucket2,
        },
    });

});
package generated_program;

import com.pulumi.Context;
import com.pulumi.Pulumi;
import com.pulumi.core.Output;
import com.pulumi.aws.iam.IamFunctions;
import com.pulumi.aws.iam.inputs.GetPolicyDocumentArgs;
import com.pulumi.aws.iam.Role;
import com.pulumi.aws.iam.RoleArgs;
import com.pulumi.aws.lambda.Function;
import com.pulumi.aws.lambda.FunctionArgs;
import com.pulumi.aws.s3.Bucket;
import com.pulumi.aws.s3.BucketArgs;
import com.pulumi.aws.lambda.Permission;
import com.pulumi.aws.lambda.PermissionArgs;
import com.pulumi.aws.s3.BucketNotification;
import com.pulumi.aws.s3.BucketNotificationArgs;
import com.pulumi.aws.s3.inputs.BucketNotificationLambdaFunctionArgs;
import com.pulumi.asset.FileArchive;
import com.pulumi.resources.CustomResourceOptions;
import java.util.List;
import java.util.ArrayList;
import java.util.Map;
import java.io.File;
import java.nio.file.Files;
import java.nio.file.Paths;

public class App {
    public static void main(String[] args) {
        Pulumi.run(App::stack);
    }

    public static void stack(Context ctx) {
        final var assumeRole = IamFunctions.getPolicyDocument(GetPolicyDocumentArgs.builder()
            .statements(GetPolicyDocumentStatementArgs.builder()
                .effect("Allow")
                .principals(GetPolicyDocumentStatementPrincipalArgs.builder()
                    .type("Service")
                    .identifiers("lambda.amazonaws.com")
                    .build())
                .actions("sts:AssumeRole")
                .build())
            .build());

        var iamForLambda = new Role("iamForLambda", RoleArgs.builder()
            .name("iam_for_lambda")
            .assumeRolePolicy(assumeRole.json())
            .build());

        var func1 = new Function("func1", FunctionArgs.builder()
            .code(new FileArchive("your-function1.zip"))
            .name("example_lambda_name1")
            .role(iamForLambda.arn())
            .handler("exports.example")
            .runtime("nodejs20.x")
            .build());

        var bucket = new Bucket("bucket", BucketArgs.builder()
            .bucket("your-bucket-name")
            .build());

        var allowBucket1 = new Permission("allowBucket1", PermissionArgs.builder()
            .statementId("AllowExecutionFromS3Bucket1")
            .action("lambda:InvokeFunction")
            .function(func1.arn())
            .principal("s3.amazonaws.com")
            .sourceArn(bucket.arn())
            .build());

        var func2 = new Function("func2", FunctionArgs.builder()
            .code(new FileArchive("your-function2.zip"))
            .name("example_lambda_name2")
            .role(iamForLambda.arn())
            .handler("exports.example")
            .build());

        var allowBucket2 = new Permission("allowBucket2", PermissionArgs.builder()
            .statementId("AllowExecutionFromS3Bucket2")
            .action("lambda:InvokeFunction")
            .function(func2.arn())
            .principal("s3.amazonaws.com")
            .sourceArn(bucket.arn())
            .build());

        var bucketNotification = new BucketNotification("bucketNotification", BucketNotificationArgs.builder()
            .bucket(bucket.id())
            .lambdaFunctions(            
                BucketNotificationLambdaFunctionArgs.builder()
                    .lambdaFunctionArn(func1.arn())
                    .events("s3:ObjectCreated:*")
                    .filterPrefix("AWSLogs/")
                    .filterSuffix(".log")
                    .build(),
                BucketNotificationLambdaFunctionArgs.builder()
                    .lambdaFunctionArn(func2.arn())
                    .events("s3:ObjectCreated:*")
                    .filterPrefix("OtherLogs/")
                    .filterSuffix(".log")
                    .build())
            .build(), CustomResourceOptions.builder()
                .dependsOn(                
                    allowBucket1,
                    allowBucket2)
                .build());

    }
}
resources:
  iamForLambda:
    type: aws:iam:Role
    name: iam_for_lambda
    properties:
      name: iam_for_lambda
      assumeRolePolicy: ${assumeRole.json}
  allowBucket1:
    type: aws:lambda:Permission
    name: allow_bucket1
    properties:
      statementId: AllowExecutionFromS3Bucket1
      action: lambda:InvokeFunction
      function: ${func1.arn}
      principal: s3.amazonaws.com
      sourceArn: ${bucket.arn}
  func1:
    type: aws:lambda:Function
    properties:
      code:
        fn::FileArchive: your-function1.zip
      name: example_lambda_name1
      role: ${iamForLambda.arn}
      handler: exports.example
      runtime: nodejs20.x
  allowBucket2:
    type: aws:lambda:Permission
    name: allow_bucket2
    properties:
      statementId: AllowExecutionFromS3Bucket2
      action: lambda:InvokeFunction
      function: ${func2.arn}
      principal: s3.amazonaws.com
      sourceArn: ${bucket.arn}
  func2:
    type: aws:lambda:Function
    properties:
      code:
        fn::FileArchive: your-function2.zip
      name: example_lambda_name2
      role: ${iamForLambda.arn}
      handler: exports.example
  bucket:
    type: aws:s3:Bucket
    properties:
      bucket: your-bucket-name
  bucketNotification:
    type: aws:s3:BucketNotification
    name: bucket_notification
    properties:
      bucket: ${bucket.id}
      lambdaFunctions:
        - lambdaFunctionArn: ${func1.arn}
          events:
            - s3:ObjectCreated:*
          filterPrefix: AWSLogs/
          filterSuffix: .log
        - lambdaFunctionArn: ${func2.arn}
          events:
            - s3:ObjectCreated:*
          filterPrefix: OtherLogs/
          filterSuffix: .log
    options:
      dependsOn:
        - ${allowBucket1}
        - ${allowBucket2}
variables:
  assumeRole:
    fn::invoke:
      function: aws:iam:getPolicyDocument
      arguments:
        statements:
          - effect: Allow
            principals:
              - type: Service
                identifiers:
                  - lambda.amazonaws.com
            actions:
              - sts:AssumeRole

Each entry in the lambdaFunctions array defines a separate routing rule. Here, files under “AWSLogs/” trigger func1, while “OtherLogs/” files trigger func2. Each Lambda function needs its own Permission resource. The dependsOn ensures permissions exist before S3 attempts invocations.

Send event notifications to SQS queues

Teams building decoupled architectures route S3 events through SQS queues, allowing multiple consumers to process uploads at their own pace.

import * as pulumi from "@pulumi/pulumi";
import * as aws from "@pulumi/aws";

const bucket = new aws.s3.Bucket("bucket", {bucket: "your-bucket-name"});
const queue = aws.iam.getPolicyDocumentOutput({
    statements: [{
        effect: "Allow",
        principals: [{
            type: "*",
            identifiers: ["*"],
        }],
        actions: ["sqs:SendMessage"],
        resources: ["arn:aws:sqs:*:*:s3-event-notification-queue"],
        conditions: [{
            test: "ArnEquals",
            variable: "aws:SourceArn",
            values: [bucket.arn],
        }],
    }],
});
const queueQueue = new aws.sqs.Queue("queue", {
    name: "s3-event-notification-queue",
    policy: queue.apply(queue => queue.json),
});
const bucketNotification = new aws.s3.BucketNotification("bucket_notification", {
    bucket: bucket.id,
    queues: [{
        queueArn: queueQueue.arn,
        events: ["s3:ObjectCreated:*"],
        filterSuffix: ".log",
    }],
});
import pulumi
import pulumi_aws as aws

bucket = aws.s3.Bucket("bucket", bucket="your-bucket-name")
queue = aws.iam.get_policy_document_output(statements=[{
    "effect": "Allow",
    "principals": [{
        "type": "*",
        "identifiers": ["*"],
    }],
    "actions": ["sqs:SendMessage"],
    "resources": ["arn:aws:sqs:*:*:s3-event-notification-queue"],
    "conditions": [{
        "test": "ArnEquals",
        "variable": "aws:SourceArn",
        "values": [bucket.arn],
    }],
}])
queue_queue = aws.sqs.Queue("queue",
    name="s3-event-notification-queue",
    policy=queue.json)
bucket_notification = aws.s3.BucketNotification("bucket_notification",
    bucket=bucket.id,
    queues=[{
        "queue_arn": queue_queue.arn,
        "events": ["s3:ObjectCreated:*"],
        "filter_suffix": ".log",
    }])
package main

import (
	"github.com/pulumi/pulumi-aws/sdk/v7/go/aws/iam"
	"github.com/pulumi/pulumi-aws/sdk/v7/go/aws/s3"
	"github.com/pulumi/pulumi-aws/sdk/v7/go/aws/sqs"
	"github.com/pulumi/pulumi/sdk/v3/go/pulumi"
)

func main() {
	pulumi.Run(func(ctx *pulumi.Context) error {
		bucket, err := s3.NewBucket(ctx, "bucket", &s3.BucketArgs{
			Bucket: pulumi.String("your-bucket-name"),
		})
		if err != nil {
			return err
		}
		queue := iam.GetPolicyDocumentOutput(ctx, iam.GetPolicyDocumentOutputArgs{
			Statements: iam.GetPolicyDocumentStatementArray{
				&iam.GetPolicyDocumentStatementArgs{
					Effect: pulumi.String("Allow"),
					Principals: iam.GetPolicyDocumentStatementPrincipalArray{
						&iam.GetPolicyDocumentStatementPrincipalArgs{
							Type: pulumi.String("*"),
							Identifiers: pulumi.StringArray{
								pulumi.String("*"),
							},
						},
					},
					Actions: pulumi.StringArray{
						pulumi.String("sqs:SendMessage"),
					},
					Resources: pulumi.StringArray{
						pulumi.String("arn:aws:sqs:*:*:s3-event-notification-queue"),
					},
					Conditions: iam.GetPolicyDocumentStatementConditionArray{
						&iam.GetPolicyDocumentStatementConditionArgs{
							Test:     pulumi.String("ArnEquals"),
							Variable: pulumi.String("aws:SourceArn"),
							Values: pulumi.StringArray{
								bucket.Arn,
							},
						},
					},
				},
			},
		}, nil)
		queueQueue, err := sqs.NewQueue(ctx, "queue", &sqs.QueueArgs{
			Name: pulumi.String("s3-event-notification-queue"),
			Policy: pulumi.String(queue.ApplyT(func(queue iam.GetPolicyDocumentResult) (*string, error) {
				return &queue.Json, nil
			}).(pulumi.StringPtrOutput)),
		})
		if err != nil {
			return err
		}
		_, err = s3.NewBucketNotification(ctx, "bucket_notification", &s3.BucketNotificationArgs{
			Bucket: bucket.ID(),
			Queues: s3.BucketNotificationQueueArray{
				&s3.BucketNotificationQueueArgs{
					QueueArn: queueQueue.Arn,
					Events: pulumi.StringArray{
						pulumi.String("s3:ObjectCreated:*"),
					},
					FilterSuffix: pulumi.String(".log"),
				},
			},
		})
		if err != nil {
			return err
		}
		return nil
	})
}
using System.Collections.Generic;
using System.Linq;
using Pulumi;
using Aws = Pulumi.Aws;

return await Deployment.RunAsync(() => 
{
    var bucket = new Aws.S3.Bucket("bucket", new()
    {
        BucketName = "your-bucket-name",
    });

    var queue = Aws.Iam.GetPolicyDocument.Invoke(new()
    {
        Statements = new[]
        {
            new Aws.Iam.Inputs.GetPolicyDocumentStatementInputArgs
            {
                Effect = "Allow",
                Principals = new[]
                {
                    new Aws.Iam.Inputs.GetPolicyDocumentStatementPrincipalInputArgs
                    {
                        Type = "*",
                        Identifiers = new[]
                        {
                            "*",
                        },
                    },
                },
                Actions = new[]
                {
                    "sqs:SendMessage",
                },
                Resources = new[]
                {
                    "arn:aws:sqs:*:*:s3-event-notification-queue",
                },
                Conditions = new[]
                {
                    new Aws.Iam.Inputs.GetPolicyDocumentStatementConditionInputArgs
                    {
                        Test = "ArnEquals",
                        Variable = "aws:SourceArn",
                        Values = new[]
                        {
                            bucket.Arn,
                        },
                    },
                },
            },
        },
    });

    var queueQueue = new Aws.Sqs.Queue("queue", new()
    {
        Name = "s3-event-notification-queue",
        Policy = queue.Apply(getPolicyDocumentResult => getPolicyDocumentResult.Json),
    });

    var bucketNotification = new Aws.S3.BucketNotification("bucket_notification", new()
    {
        Bucket = bucket.Id,
        Queues = new[]
        {
            new Aws.S3.Inputs.BucketNotificationQueueArgs
            {
                QueueArn = queueQueue.Arn,
                Events = new[]
                {
                    "s3:ObjectCreated:*",
                },
                FilterSuffix = ".log",
            },
        },
    });

});
package generated_program;

import com.pulumi.Context;
import com.pulumi.Pulumi;
import com.pulumi.core.Output;
import com.pulumi.aws.s3.Bucket;
import com.pulumi.aws.s3.BucketArgs;
import com.pulumi.aws.iam.IamFunctions;
import com.pulumi.aws.iam.inputs.GetPolicyDocumentArgs;
import com.pulumi.aws.sqs.Queue;
import com.pulumi.aws.sqs.QueueArgs;
import com.pulumi.aws.s3.BucketNotification;
import com.pulumi.aws.s3.BucketNotificationArgs;
import com.pulumi.aws.s3.inputs.BucketNotificationQueueArgs;
import java.util.List;
import java.util.ArrayList;
import java.util.Map;
import java.io.File;
import java.nio.file.Files;
import java.nio.file.Paths;

public class App {
    public static void main(String[] args) {
        Pulumi.run(App::stack);
    }

    public static void stack(Context ctx) {
        var bucket = new Bucket("bucket", BucketArgs.builder()
            .bucket("your-bucket-name")
            .build());

        final var queue = IamFunctions.getPolicyDocument(GetPolicyDocumentArgs.builder()
            .statements(GetPolicyDocumentStatementArgs.builder()
                .effect("Allow")
                .principals(GetPolicyDocumentStatementPrincipalArgs.builder()
                    .type("*")
                    .identifiers("*")
                    .build())
                .actions("sqs:SendMessage")
                .resources("arn:aws:sqs:*:*:s3-event-notification-queue")
                .conditions(GetPolicyDocumentStatementConditionArgs.builder()
                    .test("ArnEquals")
                    .variable("aws:SourceArn")
                    .values(bucket.arn())
                    .build())
                .build())
            .build());

        var queueQueue = new Queue("queueQueue", QueueArgs.builder()
            .name("s3-event-notification-queue")
            .policy(queue.applyValue(_queue -> _queue.json()))
            .build());

        var bucketNotification = new BucketNotification("bucketNotification", BucketNotificationArgs.builder()
            .bucket(bucket.id())
            .queues(BucketNotificationQueueArgs.builder()
                .queueArn(queueQueue.arn())
                .events("s3:ObjectCreated:*")
                .filterSuffix(".log")
                .build())
            .build());

    }
}
resources:
  queueQueue:
    type: aws:sqs:Queue
    name: queue
    properties:
      name: s3-event-notification-queue
      policy: ${queue.json}
  bucket:
    type: aws:s3:Bucket
    properties:
      bucket: your-bucket-name
  bucketNotification:
    type: aws:s3:BucketNotification
    name: bucket_notification
    properties:
      bucket: ${bucket.id}
      queues:
        - queueArn: ${queueQueue.arn}
          events:
            - s3:ObjectCreated:*
          filterSuffix: .log
variables:
  queue:
    fn::invoke:
      function: aws:iam:getPolicyDocument
      arguments:
        statements:
          - effect: Allow
            principals:
              - type: '*'
                identifiers:
                  - '*'
            actions:
              - sqs:SendMessage
            resources:
              - arn:aws:sqs:*:*:s3-event-notification-queue
            conditions:
              - test: ArnEquals
                variable: aws:SourceArn
                values:
                  - ${bucket.arn}

The queues property sends event notifications to an SQS queue instead of invoking Lambda directly. The queue’s policy must grant S3 permission to send messages (shown in the queue policy document). This pattern decouples producers from consumers, enabling multiple workers to process events independently.

Route events through EventBridge for flexible routing

EventBridge provides centralized event routing that can fan out to multiple targets without configuring each destination in the bucket notification.

import * as pulumi from "@pulumi/pulumi";
import * as aws from "@pulumi/aws";

const bucket = new aws.s3.Bucket("bucket", {bucket: "your-bucket-name"});
const bucketNotification = new aws.s3.BucketNotification("bucket_notification", {
    bucket: bucket.id,
    eventbridge: true,
});
import pulumi
import pulumi_aws as aws

bucket = aws.s3.Bucket("bucket", bucket="your-bucket-name")
bucket_notification = aws.s3.BucketNotification("bucket_notification",
    bucket=bucket.id,
    eventbridge=True)
package main

import (
	"github.com/pulumi/pulumi-aws/sdk/v7/go/aws/s3"
	"github.com/pulumi/pulumi/sdk/v3/go/pulumi"
)

func main() {
	pulumi.Run(func(ctx *pulumi.Context) error {
		bucket, err := s3.NewBucket(ctx, "bucket", &s3.BucketArgs{
			Bucket: pulumi.String("your-bucket-name"),
		})
		if err != nil {
			return err
		}
		_, err = s3.NewBucketNotification(ctx, "bucket_notification", &s3.BucketNotificationArgs{
			Bucket:      bucket.ID(),
			Eventbridge: pulumi.Bool(true),
		})
		if err != nil {
			return err
		}
		return nil
	})
}
using System.Collections.Generic;
using System.Linq;
using Pulumi;
using Aws = Pulumi.Aws;

return await Deployment.RunAsync(() => 
{
    var bucket = new Aws.S3.Bucket("bucket", new()
    {
        BucketName = "your-bucket-name",
    });

    var bucketNotification = new Aws.S3.BucketNotification("bucket_notification", new()
    {
        Bucket = bucket.Id,
        Eventbridge = true,
    });

});
package generated_program;

import com.pulumi.Context;
import com.pulumi.Pulumi;
import com.pulumi.core.Output;
import com.pulumi.aws.s3.Bucket;
import com.pulumi.aws.s3.BucketArgs;
import com.pulumi.aws.s3.BucketNotification;
import com.pulumi.aws.s3.BucketNotificationArgs;
import java.util.List;
import java.util.ArrayList;
import java.util.Map;
import java.io.File;
import java.nio.file.Files;
import java.nio.file.Paths;

public class App {
    public static void main(String[] args) {
        Pulumi.run(App::stack);
    }

    public static void stack(Context ctx) {
        var bucket = new Bucket("bucket", BucketArgs.builder()
            .bucket("your-bucket-name")
            .build());

        var bucketNotification = new BucketNotification("bucketNotification", BucketNotificationArgs.builder()
            .bucket(bucket.id())
            .eventbridge(true)
            .build());

    }
}
resources:
  bucket:
    type: aws:s3:Bucket
    properties:
      bucket: your-bucket-name
  bucketNotification:
    type: aws:s3:BucketNotification
    name: bucket_notification
    properties:
      bucket: ${bucket.id}
      eventbridge: true

Setting eventbridge to true sends all bucket events to EventBridge. From there, EventBridge rules route events to Lambda, SQS, SNS, or other targets based on event patterns. This approach centralizes routing logic outside the bucket configuration.

Beyond these examples

These snippets focus on specific notification configuration features: Lambda function invocation with prefix/suffix filtering, SQS queue delivery, and EventBridge integration. They’re intentionally minimal rather than full event-driven architectures.

The examples reference pre-existing infrastructure such as Lambda functions with execution roles, SQS queues with appropriate policies, and Lambda permissions granting S3 invoke access. They focus on configuring the notification rather than provisioning everything around it.

To keep things focused, common notification patterns are omitted, including:

  • SNS topic notifications (topics property)
  • Multiple event types beyond s3:ObjectCreated:*
  • Complex filter patterns (filterPrefix and filterSuffix combinations)
  • Event notification IDs for tracking (id property)

These omissions are intentional: the goal is to illustrate how each notification feature is wired, not provide drop-in event processing modules. See the S3 BucketNotification resource reference for all available configuration options.

Let's configure AWS S3 Bucket Event Notifications

Get started with Pulumi Cloud, then follow our quick setup guide to deploy this infrastructure.

Try Pulumi Cloud for FREE

Frequently Asked Questions

Common Pitfalls & Limitations
Why am I seeing perpetual differences when I have multiple BucketNotification resources?
S3 buckets only support a single notification configuration resource. Creating multiple BucketNotification resources for the same bucket causes perpetual configuration drift. Instead, use a single BucketNotification resource with arrays for lambdaFunctions, queues, or topics to configure multiple triggers.
Will this resource overwrite my existing S3 event notifications?
Yes, this resource overwrites any existing event notifications configured for the S3 bucket. Ensure all desired notifications are defined within the single BucketNotification resource.
Can I use BucketNotification with S3 directory buckets?
No, this resource cannot be used with S3 directory buckets. Use standard S3 buckets only.
IAM Permissions & Setup
What permissions does my Lambda function need to receive S3 notifications?
Create an aws.lambda.Permission resource with action set to lambda:InvokeFunction, principal set to s3.amazonaws.com, and sourceArn set to the bucket ARN. Add dependsOn to the BucketNotification to ensure the permission is created first.
What IAM policy does my SNS topic need for S3 notifications?
The SNS topic policy must allow the SNS:Publish action from principal s3.amazonaws.com with a condition that checks aws:SourceArn matches the bucket ARN.
What IAM policy does my SQS queue need for S3 notifications?
The SQS queue policy must allow the sqs:SendMessage action with a condition that checks aws:SourceArn equals the bucket ARN.
Configuration & Filtering
How do I trigger multiple Lambda functions from the same S3 bucket?
Use a single BucketNotification resource with an array of entries in lambdaFunctions. Each entry can have different filterPrefix or filterSuffix values to route events to different functions. Remember to create separate aws.lambda.Permission resources for each function and include them in dependsOn.
How do I filter which S3 events trigger notifications?
Use filterPrefix to filter by object key path (e.g., images/ or AWSLogs/) and filterSuffix to filter by file extension (e.g., .log or .jpg).
Can I send S3 events to EventBridge instead of SNS, SQS, or Lambda?
Yes, set eventbridge to true in the BucketNotification resource to enable Amazon EventBridge notifications.
How do I configure multiple notifications to the same SQS queue with different filters?
Use an array for queues with multiple entries, each specifying the same queueArn but different filterPrefix values and unique id values to distinguish them.

Using a different cloud?

Explore storage guides for other cloud providers: