Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
54 changes: 39 additions & 15 deletions sqs-lambda-tenant-isolation-sam-py/sqs-processor/index.py
Original file line number Diff line number Diff line change
@@ -1,29 +1,53 @@
import json
import re
import sys
import boto3
import os
from botocore.exceptions import ClientError

lambda_client = boto3.client('lambda')
TENANT_ISOLATED_FUNCTION = os.environ['TENANT_ISOLATED_FUNCTION_NAME']

TENANT_ID_PATTERN = re.compile(r'^[a-zA-Z0-9-]{1,64}$')
MAX_BODY_SIZE_BYTES = 262144 # 256 KB

def handler(event, context):
for record in event['Records']:
body = json.loads(record['body'])

# Get message group ID from SQS attributes
try:
body = json.loads(record['body'])
except json.JSONDecodeError as e:
print(f"Failed to parse message body: {e}")
continue

body_size = sys.getsizeof(record['body'])
if body_size > MAX_BODY_SIZE_BYTES:
print(f"Message body exceeds max size ({body_size} > {MAX_BODY_SIZE_BYTES}), skipping")
continue

if not isinstance(body, dict):
print(f"Message body is not a JSON object, skipping")
continue

attributes = record.get('attributes') or {}
message_group_id = attributes.get('MessageGroupId')

if not message_group_id:
print(f"Missing MessageGroupId in SQS record: {record}")
message_group_id = "default"

lambda_client.invoke(
FunctionName=TENANT_ISOLATED_FUNCTION,
InvocationType='Event',
Payload=json.dumps(body),
TenantId=message_group_id
)

raise ValueError("MessageGroupId is required for tenant isolation")

if not TENANT_ID_PATTERN.match(message_group_id):
raise ValueError(f"Invalid tenant ID format: {message_group_id}")

try:
lambda_client.invoke(
FunctionName=TENANT_ISOLATED_FUNCTION,
InvocationType='Event',
Payload=json.dumps(body),
TenantId=message_group_id
)
except ClientError as e:
print(f"Lambda invocation failed for tenant {message_group_id}: {e}")
raise # Let SQS retry

print(f"Invoked tenant-isolated function for messagegroup: {message_group_id}")

return {'statusCode': 200}
77 changes: 71 additions & 6 deletions sqs-lambda-tenant-isolation-sam-py/template.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,52 @@ Transform: AWS::Serverless-2016-10-31
Description: Lambda Tenant Isolation Demo

Resources:
DeadLetterQueue:
Type: AWS::SQS::Queue
Properties:
QueueName: tenant-isolation-dlq
SqsManagedSseEnabled: true
MessageRetentionPeriod: 1209600 # 14 days

ProcessingQueue:
Type: AWS::SQS::Queue
Properties:
QueueName: tenant-isolation-queue
VisibilityTimeout: 300
SqsManagedSseEnabled: true
RedrivePolicy:
deadLetterTargetArn: !GetAtt DeadLetterQueue.Arn
maxReceiveCount: 3

QueuePolicy:
Type: AWS::SQS::QueuePolicy
Properties:
Queues:
- !Ref ProcessingQueue
PolicyDocument:
Statement:
- Sid: DenyInsecureTransport
Effect: Deny
Principal: '*'
Action: 'sqs:*'
Resource: !GetAtt ProcessingQueue.Arn
Condition:
Bool:
'aws:SecureTransport': 'false'
- Sid: AllowSameAccountSendMessage
Effect: Allow
Principal:
AWS: !Sub 'arn:aws:iam::${AWS::AccountId}:root'
Action: 'sqs:SendMessage'
Resource: !GetAtt ProcessingQueue.Arn
- Sid: DenyExternalSendMessage
Effect: Deny
Principal: '*'
Action: 'sqs:SendMessage'
Resource: !GetAtt ProcessingQueue.Arn
Condition:
StringNotEquals:
'aws:PrincipalAccount': !Ref AWS::AccountId

TenantIsolatedFunctionRole:
Type: AWS::IAM::Role
Expand All @@ -34,6 +75,34 @@ Resources:
TenancyConfig:
TenantIsolationMode: PER_TENANT

SQSProcessorFunctionRole:
Type: AWS::IAM::Role
Properties:
AssumeRolePolicyDocument:
Version: '2012-10-17'
Statement:
- Effect: Allow
Principal:
Service: lambda.amazonaws.com
Action: sts:AssumeRole
ManagedPolicyArns:
- arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole
Policies:
- PolicyName: SQSProcessorPolicy
PolicyDocument:
Version: '2012-10-17'
Statement:
- Effect: Allow
Action:
- sqs:ReceiveMessage
- sqs:DeleteMessage
- sqs:GetQueueAttributes
Resource: !GetAtt ProcessingQueue.Arn
- Effect: Allow
Action:
- lambda:InvokeFunction
Resource: !GetAtt TenantIsolatedFunction.Arn

SQSProcessorFunction:
Type: AWS::Serverless::Function
Properties:
Expand All @@ -42,21 +111,17 @@ Resources:
Handler: index.handler
Runtime: python3.12
Timeout: 60
Role: !GetAtt SQSProcessorFunctionRole.Arn
Environment:
Variables:
TENANT_ISOLATED_FUNCTION_NAME: !Ref TenantIsolatedFunction
Policies:
- Statement:
- Effect: Allow
Action:
- lambda:InvokeFunction
Resource: !GetAtt TenantIsolatedFunction.Arn
Events:
SQSEvent:
Type: SQS
Properties:
Queue: !GetAtt ProcessingQueue.Arn
BatchSize: 10


Outputs:
QueueUrl:
Expand Down
Original file line number Diff line number Diff line change
@@ -1,16 +1,28 @@
import json
import re

TENANT_ID_PATTERN = re.compile(r'^[a-zA-Z0-9-]{1,64}$')

def handler(event, context):
tenant_id = context.tenant_id


if not tenant_id:
raise ValueError("tenant_id is missing from context")

if not TENANT_ID_PATTERN.match(tenant_id):
raise ValueError(f"Invalid tenant ID format: {tenant_id}")

if not isinstance(event, dict):
raise ValueError(f"Expected event to be a dict, got {type(event).__name__}")

print(f"Processing request for tenant: {tenant_id}")
print(f"Event data: {json.dumps(event)}")

# Process tenant-specific logic here
result = {
'tenant_id': tenant_id,
'message': 'Request processed successfully',
'data': event
}

return result