This document analyzes LocalStack's implementation of AWS services (S3, DynamoDB, Lambda) to inform the design of RustStack - a high-fidelity AWS local emulator focused on integration testing for Flask/Lambda applications.
A Flask application running as AWS Lambda, fronted by API Gateway, using:
- S3 for file storage
- DynamoDB for data persistence
- Lambda for compute
RustStack prioritizes depth over breadth - bulletproof core operations rather than broad but shallow coverage.
| Operation | Priority | Notes |
|---|---|---|
| GetObject | P0 | Range requests, conditional gets |
| PutObject | P0 | Streaming upload, Content-MD5 |
| DeleteObject | P0 | Simple delete (no versioning) |
| HeadObject | P0 | Metadata retrieval |
| ListObjectsV2 | P0 | Pagination, prefix filtering |
| CreateBucket | P1 | Basic creation |
| DeleteBucket | P1 | Empty bucket only |
| HeadBucket | P1 | Existence check |
Error codes that must match AWS exactly:
NoSuchKey(404) - Object doesn't existNoSuchBucket(404) - Bucket doesn't existBucketAlreadyExists(409) - Bucket name takenBucketNotEmpty(409) - Delete non-empty bucketInvalidArgument(400) - Bad request parametersAccessDenied(403) - Permission denied
| Operation | Priority | Notes |
|---|---|---|
| GetItem | P0 | Consistent read support |
| PutItem | P0 | Condition expressions |
| DeleteItem | P0 | Condition expressions |
| UpdateItem | P0 | Update expressions, conditions |
| Query | P0 | Key conditions, filter expressions, GSI |
| Scan | P0 | Filter expressions, pagination |
| CreateTable | P1 | GSI support required |
| DeleteTable | P1 | |
| DescribeTable | P1 | Table status |
Critical behaviors:
- Condition expression failures →
ConditionalCheckFailedException - Item not found on GetItem → empty response (not error)
- GSI queries with correct behavior
- Proper
LastEvaluatedKeypagination
| Operation | Priority | Notes |
|---|---|---|
| Invoke | P0 | Sync invocation, proper event format |
| CreateFunction | P1 | Zip upload, env vars |
| DeleteFunction | P1 | Cleanup |
| GetFunction | P2 | Inspection |
Critical behaviors:
- API Gateway event format (v1) compatibility
- Flask/WSGI handler execution
- Environment variable injection
- Proper error response format
LocalStack's S3 implements operations through handler methods:
@handler("GetObject")
def get_object(self, context: RequestContext, request: GetObjectRequest) -> GetObjectOutput:
# 1. Validate bucket exists
# 2. Resolve version (we skip this - no versioning)
# 3. Check object exists
# 4. Handle range requests
# 5. Stream response bodyKey behaviors to replicate:
-
ETag Calculation:
etag = hashlib.md5(data).hexdigest() # Returns: "d41d8cd98f00b204e9800998ecf8427e" # With quotes in HTTP header
-
Range Requests:
Range: bytes=0-99→ first 100 bytesRange: bytes=-100→ last 100 bytes- Returns
206 Partial ContentwithContent-Rangeheader
-
Conditional Operations:
If-Match/If-None-Matchwith ETagIf-Modified-Since/If-Unmodified-Since- Returns
304 Not Modifiedor412 Precondition Failed
-
ListObjectsV2 Pagination:
# MaxKeys default: 1000 # ContinuationToken: opaque string for next page # IsTruncated: true if more results
LocalStack uses EphemeralS3ObjectStore:
SpooledTemporaryFilefor objects (memory up to 512KB, then disk)- Thread-safe with reader/writer locks
- MD5 calculated during write
We can simplify: Use pure in-memory with DashMap since integration tests are ephemeral.
LocalStack does NOT implement DynamoDB's query engine. It:
- Starts DynamoDB Local (Java) as a subprocess
- Proxies all requests to it
- Adds features on top (streams, global tables, ARN fixing)
def forward_request(self, context: RequestContext) -> ServiceResponse:
self.prepare_request_headers(headers, account_id, region_name)
return self.server.proxy(context, service_request)-
ARN Transformation:
- DynamoDB Local returns
arn:aws:dynamodb:ddblocal:... - LocalStack fixes to
arn:aws:dynamodb:us-east-1:123456789012:...
- DynamoDB Local returns
-
Streams (out of scope for us):
- Generates stream records on mutations
- Forwards to DynamoDB Streams API
-
Error Enhancement:
- Better error messages
- Consistent error codes
DynamoDB Local is highly compatible with AWS DynamoDB for:
- All item operations (Get, Put, Update, Delete)
- Query and Scan with expressions
- GSI and LSI
- Condition expressions
- Batch operations
Our approach: Use DynamoDB Local as backend, similar to LocalStack.
From the tests, critical expression behaviors:
# Condition expression failure
response = client.put_item(
TableName='test',
Item={'pk': {'S': 'key1'}},
ConditionExpression='attribute_not_exists(pk)'
)
# Raises: ConditionalCheckFailedException
# Update expression
response = client.update_item(
TableName='test',
Key={'pk': {'S': 'key1'}},
UpdateExpression='SET #attr = :val',
ExpressionAttributeNames={'#attr': 'data'},
ExpressionAttributeValues={':val': {'S': 'new-value'}}
)Lambda uses Docker containers with a custom Runtime API:
┌─────────────────────────────────────────┐
│ Lambda Container │
│ ├── AWS Lambda Runtime (RIC) │
│ ├── Function Code (/var/task) │
│ └── Runtime API Client │
└─────────────────────────────────────────┘
│
▼ HTTP (localhost:9001)
┌─────────────────────────────────────────┐
│ Runtime API Server (in LocalStack) │
│ ├── GET /invocation/next │
│ ├── POST /invocation/{id}/response │
│ └── POST /invocation/{id}/error │
└─────────────────────────────────────────┘
For Flask apps, the handler typically uses a wrapper:
# Using aws-wsgi or mangum
from mangum import Mangum
from flask import Flask
app = Flask(__name__)
handler = Mangum(app)The handler receives an API Gateway v1 event:
{
"httpMethod": "GET",
"path": "/api/users",
"headers": {"Content-Type": "application/json"},
"queryStringParameters": {"page": "1"},
"body": null,
"isBase64Encoded": false,
"requestContext": {
"requestId": "abc123",
"stage": "prod",
"httpMethod": "GET",
"path": "/api/users"
}
}And returns:
{
"statusCode": 200,
"headers": {"Content-Type": "application/json"},
"body": "{\"users\": []}",
"isBase64Encoded": false
}Critical for Flask apps:
AWS_REGION,AWS_DEFAULT_REGIONAWS_ACCESS_KEY_ID,AWS_SECRET_ACCESS_KEY- Custom app config (database URLs, etc.)
Lambda injects these into the container environment.
The s3s project provides:
- Complete S3 API trait (generated from Smithy)
- SigV4 authentication
- File system backend (s3s-fs)
- Active maintenance
Pros for us:
- GetObject, PutObject, etc. already structured
- Error types match AWS
- Can implement custom backend
We should: Use s3s as foundation, implement EphemeralStorage backend.
No good Rust DynamoDB server exists. Options:
- Proxy to DynamoDB Local (recommended, like LocalStack)
- Native implementation (massive effort)
<?xml version="1.0" encoding="UTF-8"?>
<Error>
<Code>NoSuchKey</Code>
<Message>The specified key does not exist.</Message>
<Key>my-object-key</Key>
<RequestId>tx00000000000000000001</RequestId>
<HostId>...</HostId>
</Error>HTTP Status: 404
Headers: x-amz-request-id, x-amz-id-2
{
"__type": "com.amazonaws.dynamodb.v20120810#ConditionalCheckFailedException",
"message": "The conditional request failed"
}HTTP Status: 400
Header: x-amzn-RequestId
For function errors:
{
"errorMessage": "division by zero",
"errorType": "ZeroDivisionError",
"stackTrace": ["..."]
}Headers: X-Amz-Function-Error: Unhandled
#[tokio::test]
async fn test_get_nonexistent_key() {
let client = create_s3_client().await;
let result = client.get_object()
.bucket("test-bucket")
.key("nonexistent")
.send()
.await;
let err = result.unwrap_err();
// Must be NoSuchKey, not generic error
assert!(err.to_string().contains("NoSuchKey"));
}
#[tokio::test]
async fn test_list_objects_pagination() {
// Create 1500 objects
// List with MaxKeys=1000
// Verify IsTruncated=true
// Use ContinuationToken
// Verify all objects retrieved
}#[tokio::test]
async fn test_condition_expression_failure() {
let client = create_dynamodb_client().await;
// Put item
client.put_item()
.table_name("test")
.item("pk", AttributeValue::S("key1".into()))
.send()
.await
.unwrap();
// Put with condition that should fail
let result = client.put_item()
.table_name("test")
.item("pk", AttributeValue::S("key1".into()))
.condition_expression("attribute_not_exists(pk)")
.send()
.await;
assert!(result.is_err());
// Must be ConditionalCheckFailedException
}#[tokio::test]
async fn test_flask_lambda_invocation() {
// Create function with Flask app
// Invoke with API Gateway event
// Verify response matches Flask route
}- ❌ Versioning
- ❌ Lifecycle rules
- ❌ Replication
- ❌ Object lock
- ❌ Notifications
- ❌ Multipart upload (can add later if needed)
- ❌ Streams
- ❌ Transactions
- ❌ DAX
- ❌ Global tables
- ❌ Backup/restore
- ❌ Kinesis streaming
- ❌ Layers
- ❌ Provisioned concurrency
- ❌ Destinations
- ❌ Event source mappings
- ❌ Aliases/versions
- Use s3s framework
- Implement in-memory storage
- Focus on: GetObject, PutObject, DeleteObject, HeadObject, ListObjectsV2
- Perfect error codes
- Integrate DynamoDB Local as subprocess
- Implement proxy with ARN fixing
- Verify expression handling
- Test with real Flask app patterns
- Docker container execution
- API Gateway v1 event format
- Flask/Mangum compatibility testing
- Environment variable injection
- End-to-end Flask app testing
- Error scenario coverage
- Performance baseline
- Documentation