Lab: Creating and Testing Lambda Functions
Lab Objective
Create, configure, and test Lambda functions with different triggers. This lab uses LocalStack to simulate the AWS environment.
Part 1: Hello World Function
Exercise 1.1: Create the Function
Code to create: hello_world.py
import json
from datetime import datetime
def lambda_handler(event, context):
"""
Hello World function with logging
"""
# Log the received event
print(f"Event received: {json.dumps(event)}")
print(f"Context: {context.function_name}, {context.aws_request_id}")
# Extract the name
name = event.get('name', 'World')
timestamp = datetime.now().isoformat()
# Build the response
response = {
'statusCode': 200,
'headers': {
'Content-Type': 'application/json'
},
'body': json.dumps({
'message': f'Hello, {name}!',
'timestamp': timestamp,
'function': context.function_name
})
}
print(f"Response: {json.dumps(response)}")
return response
Exercise 1.2: LocalStack Deployment
# 1. Start LocalStack (if not already done)
docker-compose up -d
# 2. Create the IAM role
aws --endpoint-url=http://localhost:4566 iam create-role \
--role-name lambda-role \
--assume-role-policy-document '{
"Version": "2012-10-17",
"Statement": [{
"Effect": "Allow",
"Principal": {"Service": "lambda.amazonaws.com"},
"Action": "sts:AssumeRole"
}]
}'
# 3. Zip the code
zip hello_world.zip hello_world.py
# 4. Create the function
aws --endpoint-url=http://localhost:4566 lambda create-function \
--function-name hello-world \
--runtime python3.9 \
--role arn:aws:iam::000000000000:role/lambda-role \
--handler hello_world.lambda_handler \
--zip-file fileb://hello_world.zip
# 5. Test
aws --endpoint-url=http://localhost:4566 lambda invoke \
--function-name hello-world \
--payload '{"name": "LocalStack"}' \
response.json
cat response.json
Questions
- What is the content of
response.json? - Modify the code to add an invocation counter. Where would you store this counter?
- What happens if you pass an invalid payload (not JSON)?
💡 Solutions
- Content of response.json:
{
"statusCode": 200,
"headers": {"Content-Type": "application/json"},
"body": "{\"message\": \"Hello, LocalStack!\", \"timestamp\": \"...\", \"function\": \"hello-world\"}"
}
-
Invocation counter:
- DynamoDB for persistence
- Global variable (reset on each cold start)
- CloudWatch Metrics
-
Invalid payload:
- JSON parsing error
- The function receives the raw string
- Handle with try/except
Part 2: Function with Environment Variables
Exercise 2.1: Configuration
# config_demo.py
import json
import os
def lambda_handler(event, context):
"""
Demonstration of environment variables
"""
# Read environment variables
config = {
'environment': os.environ.get('ENVIRONMENT', 'development'),
'log_level': os.environ.get('LOG_LEVEL', 'INFO'),
'api_endpoint': os.environ.get('API_ENDPOINT', 'http://localhost'),
'max_retries': int(os.environ.get('MAX_RETRIES', '3'))
}
print(f"Configuration loaded: {json.dumps(config)}")
# Simulate different behavior based on the environment
if config['environment'] == 'production':
message = "Running in PRODUCTION mode"
else:
message = f"Running in {config['environment']} mode"
return {
'statusCode': 200,
'body': json.dumps({
'message': message,
'config': config
})
}
Exercise 2.2: Deployment with Variables
# Create with environment variables
aws --endpoint-url=http://localhost:4566 lambda create-function \
--function-name config-demo \
--runtime python3.9 \
--role arn:aws:iam::000000000000:role/lambda-role \
--handler config_demo.lambda_handler \
--zip-file fileb://config_demo.zip \
--environment 'Variables={
ENVIRONMENT=staging,
LOG_LEVEL=DEBUG,
API_ENDPOINT=https://api.example.com,
MAX_RETRIES=5
}'
# Test
aws --endpoint-url=http://localhost:4566 lambda invoke \
--function-name config-demo \
response.json
# Modify a variable
aws --endpoint-url=http://localhost:4566 lambda update-function-configuration \
--function-name config-demo \
--environment 'Variables={
ENVIRONMENT=production,
LOG_LEVEL=WARN,
API_ENDPOINT=https://api.example.com,
MAX_RETRIES=3
}'
Questions
Complete the table:
| Variable | Dev Value | Staging Value | Prod Value |
|---|---|---|---|
| ENVIRONMENT | ? | ? | ? |
| LOG_LEVEL | ? | ? | ? |
| MAX_RETRIES | ? | ? | ? |
💡 Solution
| Variable | Dev Value | Staging Value | Prod Value |
|---|---|---|---|
| ENVIRONMENT | development | staging | production |
| LOG_LEVEL | DEBUG | DEBUG | WARN |
| MAX_RETRIES | 3 | 5 | 3 |
Part 3: S3 Trigger (Simulated)
Exercise 3.1: File Processor
# s3_processor.py
import json
def lambda_handler(event, context):
"""
Process S3 events (file uploads)
"""
results = []
for record in event.get('Records', []):
# Extract S3 information
s3_info = record.get('s3', {})
bucket = s3_info.get('bucket', {}).get('name', 'unknown')
key = s3_info.get('object', {}).get('key', 'unknown')
size = s3_info.get('object', {}).get('size', 0)
event_type = record.get('eventName', 'unknown')
print(f"Processing: s3://{bucket}/{key}")
print(f"Event: {event_type}, Size: {size} bytes")
# Simulate processing
result = {
'bucket': bucket,
'key': key,
'size': size,
'event': event_type,
'status': 'processed'
}
# Conditional logic based on file type
if key.endswith('.jpg') or key.endswith('.png'):
result['action'] = 'image_processing'
elif key.endswith('.json'):
result['action'] = 'data_import'
elif key.endswith('.csv'):
result['action'] = 'csv_processing'
else:
result['action'] = 'archive'
results.append(result)
print(f"Result: {json.dumps(result)}")
return {
'statusCode': 200,
'body': json.dumps({
'processed': len(results),
'results': results
})
}
Exercise 3.2: Simulate an S3 Event
# Create a simulated S3 event file
cat > s3_event.json << 'EOF'
{
"Records": [
{
"eventName": "ObjectCreated:Put",
"s3": {
"bucket": {"name": "my-uploads"},
"object": {
"key": "images/photo.jpg",
"size": 1024000
}
}
},
{
"eventName": "ObjectCreated:Put",
"s3": {
"bucket": {"name": "my-uploads"},
"object": {
"key": "data/report.csv",
"size": 5000
}
}
}
]
}
EOF
# Deploy and test
zip s3_processor.zip s3_processor.py
aws --endpoint-url=http://localhost:4566 lambda create-function \
--function-name s3-processor \
--runtime python3.9 \
--role arn:aws:iam::000000000000:role/lambda-role \
--handler s3_processor.lambda_handler \
--zip-file fileb://s3_processor.zip
aws --endpoint-url=http://localhost:4566 lambda invoke \
--function-name s3-processor \
--payload file://s3_event.json \
response.json
cat response.json | python -m json.tool
Questions
- What action is assigned to
photo.jpg? - What action is assigned to
report.csv? - How would you modify the code to handle files > 1 MB differently?
💡 Solutions
photo.jpg→ action:image_processingreport.csv→ action:csv_processing- Add a condition:
if size > 1_000_000: # > 1 MB
result['processing_mode'] = 'async' # Asynchronous processing
else:
result['processing_mode'] = 'sync'
Part 4: Error Handling
Exercise 4.1: Function with Errors
# error_handling.py
import json
class ValidationError(Exception):
"""Custom validation error"""
pass
def validate_input(event):
"""Validate input data"""
if 'name' not in event:
raise ValidationError("The 'name' field is required")
if len(event['name']) < 2:
raise ValidationError("The name must be at least 2 characters long")
if 'age' in event and not isinstance(event['age'], int):
raise ValidationError("Age must be an integer")
return True
def lambda_handler(event, context):
"""
Function with comprehensive error handling
"""
print(f"Input: {json.dumps(event)}")
try:
# Validate input
validate_input(event)
# Processing
name = event['name']
age = event.get('age', 'not specified')
return {
'statusCode': 200,
'body': json.dumps({
'message': f'Hello {name}!',
'age': age,
'status': 'success'
})
}
except ValidationError as e:
print(f"Validation error: {str(e)}")
return {
'statusCode': 400,
'body': json.dumps({
'error': 'Validation Error',
'message': str(e),
'status': 'failed'
})
}
except Exception as e:
print(f"Unexpected error: {str(e)}")
return {
'statusCode': 500,
'body': json.dumps({
'error': 'Internal Server Error',
'message': 'An unexpected error occurred',
'status': 'failed'
})
}
Exercise 4.2: Scenario Tests
Test with different payloads and complete the table:
| Payload | Status Code | Error Type |
|---|---|---|
{"name": "Alice"} | ? | ? |
{} | ? | ? |
{"name": "A"} | ? | ? |
{"name": "Bob", "age": "twenty"} | ? | ? |
{"name": "Charlie", "age": 25} | ? | ? |
💡 Solutions
| Payload | Status Code | Error Type |
|---|---|---|
{"name": "Alice"} | 200 | None |
{} | 400 | ValidationError (name required) |
{"name": "A"} | 400 | ValidationError (min 2 chars) |
{"name": "Bob", "age": "twenty"} | 400 | ValidationError (age int) |
{"name": "Charlie", "age": 25} | 200 | None |
Part 5: Performance Analysis
Exercise 5.1: Measure Execution Times
# performance_test.py
import json
import time
def lambda_handler(event, context):
"""
Function to measure performance
"""
start_time = time.time()
# Simulate different workloads
workload = event.get('workload', 'light')
if workload == 'light':
# Light work
result = sum(range(1000))
elif workload == 'medium':
# Medium work
result = sum(range(100000))
elif workload == 'heavy':
# Heavy work
result = sum(range(10000000))
else:
result = 0
execution_time = (time.time() - start_time) * 1000 # in ms
remaining_time = context.get_remaining_time_in_millis()
return {
'statusCode': 200,
'body': json.dumps({
'workload': workload,
'result': result,
'execution_time_ms': round(execution_time, 2),
'remaining_time_ms': remaining_time,
'memory_limit_mb': context.memory_limit_in_mb
})
}
Analysis Questions
- Run with
workload: light,medium, andheavy. Note the times. - What happens if the execution time exceeds the timeout?
- How would you optimize a slow function?
Final Deliverable
Document to Produce
- Code of the 4 functions (hello, config, s3, errors)
- Test results (payloads and responses)
- Error analysis table
- Optimization recommendations
Evaluation Criteria
| Criteria | Points |
|---|---|
| Functions created and tested | /30 |
| Error handling | /20 |
| Environment variables | /15 |
| S3 event analysis | /15 |
| Code quality | /10 |
| Documentation | /10 |
| Total | /100 |
Next week: MODULE 12 - Lambda Troubleshooting