Valentin Jacquemin

Terraform recipe: S3 Bucket with notification to Lambda Function

It might come handy to quickly have setup a bucket that notifies a lambda function on any operation, like for example putting a new object.

It’s a mix of aws_s3, aws_lambda and aws_iam definitions that altogether deploys this in a matter of seconds. The only precondition is to have completed the AWS CLI setup.

With this minimal folder:

.
├── lambda
│   ├── index.js
│   ├── lambda.zip
│   └── package.json
├── s3.tf

And in less than 100 lines, you’ll have all this ready:

terraform {
  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "~> 6.0"
    }
  }
}

provider "aws" {
  region = "eu-central-1"
}


data "aws_caller_identity" "current" {}

resource "aws_s3_bucket" "bucket" {
  bucket        = "your-bucket"
}

data "aws_iam_policy_document" "assume_role" {
  statement {
    effect = "Allow"

    principals {
      type        = "Service"
      identifiers = ["lambda.amazonaws.com"]
    }

    actions = [
      "sts:AssumeRole"
    ]
  }
}

data "aws_iam_policy_document" "access_cloudwatch" {
  statement {
    effect = "Allow"

    actions = [
      "logs:CreateLogGroup",
      "logs:CreateLogStream",
      "logs:PutLogEvents"
    ]

    resources = ["arn:aws:logs:*:${data.aws_caller_identity.current.account_id}:log-group:*"]
  }
}

resource "aws_iam_role" "iam_for_lambda" {
  name               = "iam_for_lambda"
  assume_role_policy = data.aws_iam_policy_document.assume_role.json
}

resource "aws_iam_role_policy" "iam_role_policy_for_lambda" {
  name    = "cloudwatch-access"
  role    = aws_iam_role.iam_for_lambda.id
  policy  = data.aws_iam_policy_document.access_cloudwatch.json
}

data "archive_file" "lambda" {
  type        = "zip"
  source_file = "${path.module}/lambda/index.js"
  output_path = "${path.module}/lambda/lambda.zip"
}

resource "aws_lambda_permission" "allow_bucket" {
  statement_id  = "AllowExecutionFromS3Bucket"
  action        = "lambda:InvokeFunction"
  function_name = aws_lambda_function.func.arn
  principal     = "s3.amazonaws.com"
  source_arn    = aws_s3_bucket.bucket.arn
}

resource "aws_lambda_function" "func" {
  filename      = data.archive_file.lambda.output_path
  function_name = "func-name"
  role          = aws_iam_role.iam_for_lambda.arn
  handler       = "index.handler"
  runtime       = "nodejs24.x"
  code_sha256   = data.archive_file.lambda.output_base64sha256
}

resource "aws_s3_bucket_notification" "bucket_notification" {
  bucket = aws_s3_bucket.bucket.id

  lambda_function {
    lambda_function_arn = aws_lambda_function.func.arn
    events              = ["s3:ObjectCreated:*"]
    filter_prefix       = "path-to-prefix-if-needed/"
    filter_suffix       = ".suffix-if-needed"
  }

  depends_on = [aws_lambda_permission.allow_bucket]
}

lambda/lambda.zip being simply a bundle of both index.js and package.json. You can generate or update anytime with zip -r lambda.zip . while sitting in the lambda directory.

You can start with a minimal index.js like:

exports.handler = async (event, context) => {
  console.log('Event: ', JSON.stringify(event));
};

As a result, a log is pushed to CloudWatch anytime an object is pushed into the S3 bucket. It’s handy for me to have it here, I’ll reuse that pretty soon. More later!