Production Deployment¶
This guide covers production-readiness for zae-limiter deployments.
Production Checklist¶
Before deploying to production:
| Feature | CLI Flag | Default | Recommendation |
|---|---|---|---|
| Point-in-Time Recovery | --pitr-recovery-days N |
Disabled | Enable (7-35 days) |
| CloudWatch Alarms | --enable-alarms |
Enabled | Keep enabled |
| SNS Notifications | --alarm-sns-topic ARN |
None | Configure for alerts |
| Log Retention | --log-retention-days N |
30 | 90+ for compliance |
| Audit Archival | --enable-audit-archival |
Enabled | Keep enabled for compliance |
| Glacier Transition | --audit-archive-glacier-days N |
90 | Adjust based on access patterns |
| Permission Boundary | --permission-boundary ARN |
None | Use in restricted IAM environments |
| X-Ray Tracing | --enable-tracing |
Disabled | Enable for debugging/performance analysis |
| IAM Roles | --create-iam-roles |
Disabled | Create App/Admin/ReadOnly roles |
| Skip IAM | --no-iam |
Disabled | Skip all IAM resources for restricted environments |
| External Role | --aggregator-role-arn ARN |
None | Use existing IAM role for Lambda |
Example Production Deployment¶
zae-limiter deploy \
--name prod-limiter \
--region us-east-1 \
--pitr-recovery-days 7 \
--log-retention-days 90 \
--alarm-sns-topic arn:aws:sns:us-east-1:123456789012:alerts \
--enable-audit-archival \
--audit-archive-glacier-days 90
Security Best Practices¶
Encryption¶
- DynamoDB uses AWS-managed keys (SSE-S3) by default
- For customer-managed keys (CMK), use CloudFormation template customization
IAM¶
Application Access Policies¶
The stack creates three IAM managed policies by default:
| Policy | Suffix | Use Case | When to Use |
|---|---|---|---|
| AcquireOnlyPolicy | -acq |
Applications calling acquire() |
Production workloads, Lambda functions, ECS tasks |
| FullAccessPolicy | -full |
Ops teams managing config | CLI tools, admin scripts, CI/CD pipelines |
| ReadOnlyPolicy | -read |
Monitoring and dashboards | Grafana, CloudWatch dashboards, audit tools |
Best practices:
- Attach AcquireOnlyPolicy for applications - Provides only the permissions needed for rate limiting
- Attach FullAccessPolicy for config management - Separate from application credentials
- Attach ReadOnlyPolicy for observability - Safe access for monitoring systems
- Disable with
--no-iam- When using existing IAM policies or cross-account access - Create roles with
--create-iam-roles- When you want pre-built roles that attach these policies
Lambda Aggregator¶
The Lambda aggregator processes DynamoDB Stream events for usage aggregation, proactive bucket refill, and audit archival. It uses a separate execution role with least-privilege permissions:
dynamodb:GetItem,PutItem,UpdateItem,Querys3:PutObject(when audit archival is enabled)
Permission Boundaries¶
For restricted IAM environments:
- Use
--permission-boundaryto apply organizational policies to all created roles - Use
--role-name-formatfor organizational naming conventions
# Enterprise deployment with permission boundary
zae-limiter deploy \
--name prod-limiter \
--permission-boundary arn:aws:iam::123456789012:policy/ServiceBoundary \
--role-name-format "svc-{}"
IAM Behavior Matrix¶
The following table shows what IAM resources are created based on flag combinations:
| Flag Combination | Policies | App Roles | Aggr Role | Lambda |
|---|---|---|---|---|
| (default) | Created | No | Created | Enabled |
--no-iam |
No | No | No | Disabled |
--no-iam --aggregator-role-arn |
No | No | External | Enabled |
--aggregator-role-arn |
Created | No | External | Enabled |
--create-iam-roles |
Created | Created | Created | Enabled |
--no-iam --create-iam-roles |
Error | - | - | - |
For PowerUserAccess or similar restricted IAM environments:
# Deploy without any IAM resources (aggregator disabled)
zae-limiter deploy --name myapp --no-iam
# Deploy with external Lambda role (aggregator enabled)
zae-limiter deploy --name myapp --no-iam \
--aggregator-role-arn arn:aws:iam::123456789012:role/MyLambdaRole
Multi-Tenant Deployments¶
zae-limiter supports multi-tenant architectures through namespace isolation. All tenants share a single DynamoDB table, but each tenant's data is logically isolated by a namespace ID prefix on all partition keys. This provides:
- Cost efficiency — One table, one Lambda, one set of CloudWatch alarms
- Isolation — Namespace-scoped IAM policies prevent cross-tenant access via TBAC
- Lifecycle — Namespaces can be registered, soft-deleted, recovered, and purged independently
Use namespaces when you need per-tenant isolation without deploying separate stacks.
Namespace-Scoped Access Control¶
For multi-tenant deployments, namespace-scoped policies restrict each tenant to its own namespace's DynamoDB items using tag-based access control (TBAC).
The stack creates three additional namespace-scoped policies:
| Policy | Suffix | Use Case | Tag Required |
|---|---|---|---|
| NamespaceAcquirePolicy | -ns-acq |
Tenant apps calling acquire() |
Yes |
| NamespaceFullAccessPolicy | -ns-full |
Tenant config management | Yes |
| NamespaceReadOnlyPolicy | -ns-read |
Tenant monitoring | Yes |
These policies use dynamodb:LeadingKeys with the zael_namespace_id principal tag to restrict DynamoDB access to items whose partition key starts with the tenant's namespace ID.
Single Namespace (Most Common)¶
Attach a namespace-scoped policy and tag the principal with the namespace ID:
# 1. Deploy the stack (policies are created automatically)
zae-limiter deploy --name shared-table --region us-east-1
# 2. Look up the default namespace ID
zae-limiter namespace show default --name shared-table
# Output:
# Namespace: default
# Namespace ID: default
# Status: active
# Created At: 2025-01-15T10:30:00Z
# 3. Attach the namespace-scoped policy to your app role
aws iam attach-role-policy --role-name my-app-role \
--policy-arn arn:aws:iam::123456789012:policy/shared-table-ns-acq
# 4. Tag the role with the namespace ID
aws iam tag-role --role-name my-app-role \
--tags Key=zael_namespace_id,Value=default
For additional tenants, register a namespace and use its ID:
# Register a new tenant namespace
zae-limiter namespace register tenant-alpha --name shared-table
# Look up the opaque namespace ID
zae-limiter namespace show tenant-alpha --name shared-table
# Namespace ID: a7x3kq2m
# Tag the tenant's role
aws iam tag-role --role-name tenant-alpha-role \
--tags Key=zael_namespace_id,Value=a7x3kq2m
Admin / Cross-Namespace Access¶
For admin roles that need access to all namespaces, attach a table-level policy (no tag needed):
aws iam attach-role-policy --role-name admin-role \
--policy-arn arn:aws:iam::123456789012:policy/shared-table-full
Selective Multi-Namespace Access¶
Option A: STS Session Policy — Assume a base role with a session policy that narrows access to specific namespaces:
import json
import boto3
sts = boto3.client("sts")
ns_ids = ["a7x3kq2m", "b9y4lr3n"] # resolved namespace IDs
response = sts.assume_role(
RoleArn="arn:aws:iam::123456789012:role/shared-table-base-role",
RoleSessionName="multi-tenant-session",
Policy=json.dumps({
"Version": "2012-10-17",
"Statement": [{
"Effect": "Allow",
"Action": [
"dynamodb:GetItem", "dynamodb:BatchGetItem",
"dynamodb:Query", "dynamodb:UpdateItem",
],
"Resource": "arn:aws:dynamodb:us-east-1:123456789012:table/shared-table",
"Condition": {
"ForAllValues:StringLike": {
"dynamodb:LeadingKeys":
[f"{ns}/*" for ns in ns_ids] + ["_/*"]
}
}
}]
})
)
# Use response["Credentials"] for scoped access
The session policy intersects with the base role's table-level policy, narrowing access to exactly those namespaces.
Option B: Custom IAM Policy — Create a managed policy listing specific namespace IDs:
# Resolve namespace IDs
NSID1=$(zae-limiter namespace show tenant-alpha --name shared-table \
| grep "Namespace ID:" | awk '{print $3}')
NSID2=$(zae-limiter namespace show tenant-beta --name shared-table \
| grep "Namespace ID:" | awk '{print $3}')
# Create a custom policy for multiple namespaces
aws iam create-policy --policy-name my-multi-ns-policy \
--policy-document "{
\"Version\": \"2012-10-17\",
\"Statement\": [{
\"Effect\": \"Allow\",
\"Action\": [
\"dynamodb:GetItem\", \"dynamodb:BatchGetItem\",
\"dynamodb:Query\", \"dynamodb:UpdateItem\"
],
\"Resource\": \"arn:aws:dynamodb:us-east-1:123456789012:table/shared-table\",
\"Condition\": {
\"ForAllValues:StringLike\": {
\"dynamodb:LeadingKeys\": [\"${NSID1}/*\", \"${NSID2}/*\", \"_/*\"]
}
}
}]
}"
This policy is operator-managed (not owned by the zae-limiter CloudFormation stack).
Network¶
- No VPC required; uses AWS service endpoints
- For VPC deployment, configure VPC endpoints for DynamoDB and Lambda
Secrets Management¶
- No secrets stored in DynamoDB
- Use AWS Secrets Manager or Parameter Store for API keys
- Rate limit entity IDs should not contain sensitive data
Audit Logging¶
- All entity and limit changes are automatically logged
- Track who made changes with optional
principalparameter - Events auto-expire after 90 days (configurable via
--audit-retention-days) - Expired events are archived to S3 for long-term retention
- Archives transition to Glacier IR after 90 days (configurable via
--audit-archive-glacier-days) - For compliance requirements, see Audit Logging Guide
Multi-Region Considerations¶
zae-limiter is designed for single-region deployment:
| Scenario | Approach |
|---|---|
| Single region | Standard deployment |
| Multi-region (independent) | Deploy separate stacks per region |
| Global rate limiting | Application-level coordination required |
Why Not Global Tables?¶
- Rate limit state is time-sensitive (token buckets refill continuously)
- Cross-region replication lag would cause inconsistent limits
- Each region should enforce its own limits
Cross-Region Pattern¶
# Connect to separate stacks per region
us_repo = await Repository.open(stack="prod", region="us-east-1")
eu_repo = await Repository.open(stack="prod", region="eu-west-1")
us_limiter = RateLimiter(repository=us_repo)
eu_limiter = RateLimiter(repository=eu_repo)
# Application coordinates between regions if needed
Monitoring & Alerting¶
The stack deploys CloudWatch alarms by default:
| Alarm | Trigger | Action |
|---|---|---|
| Lambda Errors | > 1 per 5 min | Check logs, verify DynamoDB access |
| Lambda Duration | > 80% timeout | Increase memory or timeout |
| Iterator Age | > 30 seconds | Check Lambda concurrency |
| DLQ Messages | >= 1 | Investigate failed records |
| DynamoDB Throttles | > 1 per 5 min | Review capacity planning |
For dashboard templates and Logs Insights queries, see Monitoring Guide.
Cost Estimation¶
Costs scale with request volume (us-east-1 pricing, v0.7.0+ O(1) costs):
| Volume | DynamoDB | Lambda | CloudWatch | S3 Archive | Total |
|---|---|---|---|---|---|
| 10K req/day | ~$0.25 | ~$0.20 | ~$0.10 | ~$0.01 | ~$0.56/month |
| 100K req/day | ~$2.50 | ~$2 | ~$1 | ~$0.10 | ~$5.60/month |
| 1M req/day | ~$22 | ~$12 | ~$5 | ~$1 | ~$40/month |
S3 costs include:
- Standard storage: First 90 days (or configured --audit-archive-glacier-days)
- Glacier IR storage: After transition (~80% cheaper than Standard)
- PUT requests: One per Lambda batch (~$0.005 per 1000 requests)
For detailed capacity planning and optimization, see Performance Guide.
Next Steps¶
- Monitoring Guide - Dashboards, alerts, Logs Insights
- Audit Logging - Compliance tracking and incident investigation
- Performance Guide - Capacity planning, optimization
- Operations Guide - Troubleshooting, recovery