The Problem
You have wired up an Amazon S3 bucket to trigger a Lambda function every time a user drops a new file into a folder. On paper, everything looks correct. But in reality, the function never fires. If you try to test the trigger manually via the CLI, you hit this wall:
An error occurred (AccessDeniedException) when calling the Invoke operation: User: s3.amazonaws.com is not authorized to perform: lambda:InvokeFunction on resource: arn:aws:lambda:us-east-1:123456789012:function:my-processor-func
The Root Cause: The Guest List Problem
This failure stems from a missing resource-based policy. Most developers focus on IAM execution roles, which tell the Lambda what it is allowed to touch. However, S3 uses a "push" model. It needs explicit permission to reach out and wake up your function.
Think of an IAM Role as the Lambda's ID card—it shows what the function can do. A resource-based policy is the guest list at the door. If the S3 service principal (s3.amazonaws.com) isn't on that guest list, the request is rejected immediately.
How to Fix It
Approach 1: The Quick Fix (AWS CLI)
You can solve this in seconds by running the add-permission command. This appends a statement to the Lambda’s internal policy without requiring you to manually edit JSON files.
aws lambda add-permission \
--function-name my-processor-func \
--statement-id AllowS3Invocation \
--action lambda:InvokeFunction \
--principal s3.amazonaws.com \
--source-arn arn:aws:s3:::my-app-uploads-123 \
--source-account 123456789012
- **--statement-id**: A unique name for this rule (e.g., `ProductionS3Trigger`).
- **--source-arn**: This is critical for security. It ensures *only* your specific bucket can trigger the function, preventing "confused deputy" attacks from other accounts.
Approach 2: The Visual Way (AWS Console)
If you prefer the web interface, use the Lambda dashboard rather than the S3 dashboard. The Lambda UI handles the background "permission handshake" much more reliably than the S3 notification pane.
- Open your function in the **Lambda Console**.
- Select the **Configuration** tab, then click **Permissions**.
- Look at the **Resource-based policy statements** section.
- If it's empty, click **Add trigger** at the top of the page.
- Select **S3**, choose your bucket, and click **Add**. AWS will now automatically inject the required policy statement.
Approach 3: The Permanent Fix (Terraform)
Manually clicking buttons leads to "configuration drift" where your code no longer matches your actual infrastructure. Use the aws_lambda_permission resource to make this change permanent and repeatable.
resource "aws_lambda_permission" "allow_s3" {
statement_id = "AllowS3Invoke"
action = "lambda:InvokeFunction"
function_name = aws_lambda_function.processor.function_name
principal = "s3.amazonaws.com"
source_arn = "arn:aws:s3:::${var.bucket_name}"
}
Verification
Double-check the policy is actually active by peeking at the raw JSON:
aws lambda get-policy --function-name my-processor-func
Search for "Service": "s3.amazonaws.com" in the output. Once you see it, upload a 1KB test file to your bucket. Check your CloudWatch Logs immediately—you should see a fresh log stream appearing within 5 to 10 seconds.
Pro-Tips for Prevention
- **Lock Down ARNs**: Never leave `--source-arn` empty. If you do, any S3 bucket in the world that knows your function ARN could potentially trigger it.
- **Mind the Regions**: Ensure your bucket and Lambda live in the same region (e.g., `us-east-1`). While cross-region triggers exist, they often fail silently if permissions aren't perfect.
- **Check for Circular Dependencies**: If you use CloudFormation, creating the bucket notification and the Lambda permission at the same time can sometimes cause deployment loops. Always create the permission first.

