CloudFormation Template¶
This guide covers the CloudFormation template used by zae-limiter and how to customize it.
Template Overview¶
The template creates:
Export Template¶
# Export to file
zae-limiter cfn-template > template.yaml
# View template
zae-limiter cfn-template | less
Template Parameters¶
The DynamoDB table name is automatically derived from the CloudFormation stack name using the AWS::StackName pseudo-parameter. This ensures consistency between stack and resource names.
| Parameter | Type | Default | Description |
|---|---|---|---|
SnapshotWindows |
String | hourly,daily |
Comma-separated list of snapshot windows |
SnapshotRetentionDays |
Number | 90 |
Days to retain usage snapshots (1-3650) |
LambdaMemorySize |
Number | 256 |
Memory for aggregator Lambda (128-3008 MB) |
LambdaTimeout |
Number | 60 |
Timeout for aggregator Lambda (1-900 seconds) |
LambdaDurationThreshold |
Number | 54000 |
Duration alarm threshold in ms (90% of timeout) |
EnableAggregator |
String | true |
Whether to deploy the aggregator Lambda |
SchemaVersion |
String | 1.0.0 |
Schema version for infrastructure |
PITRRecoveryPeriodDays |
String | (empty) | PITR period (1-35 days, empty for AWS default) |
EnableAlarms |
String | true |
Whether to deploy CloudWatch alarms |
AlarmSNSTopicArn |
String | (empty) | SNS topic ARN for alarm notifications |
LogRetentionDays |
Number | 30 |
CloudWatch log retention (standard periods) |
PermissionBoundary |
String | (empty) | IAM permission boundary (ARN or policy name) |
RoleName |
String | (empty) | Custom IAM role name (use {} as placeholder for base name) |
EnableAuditArchival |
String | true |
Archive expired audit events to S3 |
AuditArchiveGlacierDays |
Number | 90 |
Days before Glacier IR transition (1-3650) |
EnableTracing |
String | false |
Enable AWS X-Ray tracing for Lambda |
EnableIAMRoles |
String | true |
Create App/Admin/ReadOnly IAM roles |
EnableIAM |
String | true |
Create all IAM resources (policies + roles) |
AggregatorRoleArn |
String | (empty) | External IAM role ARN for aggregator Lambda |
AggregatorRoleName |
String | (empty) | Custom name for aggregator Lambda role |
AppRoleName |
String | (empty) | Custom name for application IAM role |
AdminRoleName |
String | (empty) | Custom name for admin IAM role |
ReadOnlyRoleName |
String | (empty) | Custom name for read-only IAM role |
ProvisionerRoleName |
String | (empty) | Custom name for provisioner Lambda role |
AcquireOnlyPolicyName |
String | (empty) | Custom name for acquire-only policy |
FullAccessPolicyName |
String | (empty) | Custom name for full-access policy |
ReadOnlyPolicyName |
String | (empty) | Custom name for read-only policy |
EnableProvisioner |
String | true |
Deploy limits provisioner Lambda |
EnableDeletionProtection |
String | false |
Enable DynamoDB table deletion protection |
NamespaceAcquirePolicyName |
String | (empty) | Custom name for namespace acquire-only policy |
NamespaceFullAccessPolicyName |
String | (empty) | Custom name for namespace full-access policy |
NamespaceReadOnlyPolicyName |
String | (empty) | Custom name for namespace read-only policy |
DynamoDB Table¶
Schema¶
AttributeDefinitions:
- AttributeName: PK
AttributeType: S
- AttributeName: SK
AttributeType: S
- AttributeName: GSI1PK
AttributeType: S
- AttributeName: GSI1SK
AttributeType: S
- AttributeName: GSI2PK
AttributeType: S
- AttributeName: GSI2SK
AttributeType: S
- AttributeName: GSI3PK
AttributeType: S
- AttributeName: GSI3SK
AttributeType: S
- AttributeName: GSI4PK
AttributeType: S
- AttributeName: GSI4SK
AttributeType: S
KeySchema:
- AttributeName: PK
KeyType: HASH
- AttributeName: SK
KeyType: RANGE
Global Secondary Indexes¶
GSI1 - Parent to children lookups:
GlobalSecondaryIndexes:
- IndexName: GSI1
KeySchema:
- AttributeName: GSI1PK # PARENT#{parent_id}
KeyType: HASH
- AttributeName: GSI1SK # CHILD#{child_id}
KeyType: RANGE
GSI2 - Resource aggregation:
- IndexName: GSI2
KeySchema:
- AttributeName: GSI2PK # RESOURCE#{resource}
KeyType: HASH
- AttributeName: GSI2SK # BUCKET#{entity_id}
KeyType: RANGE
GSI3 - Entity config queries (sparse index):
- IndexName: GSI3
KeySchema:
- AttributeName: GSI3PK # ENTITY_CONFIG#{resource}
KeyType: HASH
- AttributeName: GSI3SK # entity_id
KeyType: RANGE
Projection:
ProjectionType: KEYS_ONLY
GSI4 - Namespace item discovery (for purge_namespace()):
- IndexName: GSI4
KeySchema:
- AttributeName: GSI4PK # namespace_id
KeyType: HASH
- AttributeName: GSI4SK # PK (original partition key)
KeyType: RANGE
Projection:
ProjectionType: KEYS_ONLY
Stream Configuration¶
S3 Audit Archive Bucket¶
When EnableAuditArchival is true, the template creates an S3 bucket to store expired audit events.
AuditArchiveBucket:
Type: AWS::S3::Bucket
Condition: DeployAuditArchive
Properties:
# BucketName omitted: CloudFormation auto-generates a globally unique name
BucketEncryption:
ServerSideEncryptionConfiguration:
- ServerSideEncryptionByDefault:
SSEAlgorithm: AES256
PublicAccessBlockConfiguration:
BlockPublicAcls: true
BlockPublicPolicy: true
IgnorePublicAcls: true
RestrictPublicBuckets: true
LifecycleConfiguration:
Rules:
- Id: GlacierTransition
Status: Enabled
Prefix: audit/
Transitions:
- StorageClass: GLACIER_IR
TransitionInDays: !Ref AuditArchiveGlacierDays
Object Structure¶
Archived audit events are stored as gzip-compressed JSONL files:
s3://<auto-generated-bucket-name>/
audit/
year=2024/
month=01/
day=15/
audit-{request_id}-{timestamp}.jsonl.gz
Lifecycle Policy¶
Objects transition to Glacier Instant Retrieval after AuditArchiveGlacierDays (default: 90 days) for cost-effective long-term storage while maintaining millisecond retrieval times.
Lambda Aggregator¶
The aggregator Lambda processes DynamoDB Stream events to maintain usage snapshots and archive expired audit events.
Performance Tuning
For guidance on memory tuning, concurrency management, and error handling configuration, see the Performance Tuning Guide.
Function Configuration¶
AggregatorFunction:
Type: AWS::Lambda::Function
Properties:
Runtime: python3.12
Handler: zae_limiter_aggregator.handler.handler
MemorySize: 256
Timeout: 60
Environment:
Variables:
TABLE_NAME: !Ref AWS::StackName
SNAPSHOT_WINDOWS: !Ref SnapshotWindows
SNAPSHOT_TTL_DAYS: !Ref SnapshotRetentionDays
# When audit archival is enabled:
ENABLE_ARCHIVAL: "true"
ARCHIVE_BUCKET_NAME: !Ref AuditArchiveBucket
Event Source Mapping¶
StreamEventMapping:
Type: AWS::Lambda::EventSourceMapping
Properties:
EventSourceArn: !GetAtt Table.StreamArn
FunctionName: !Ref AggregatorFunction
StartingPosition: LATEST
BatchSize: 100
MaximumBatchingWindowInSeconds: 5
IAM Permissions¶
Lambda Execution Role¶
AggregatorRole:
Type: AWS::IAM::Role
Properties:
AssumeRolePolicyDocument:
Statement:
- Effect: Allow
Principal:
Service: lambda.amazonaws.com
Action: sts:AssumeRole
Policies:
- PolicyName: DynamoDBAccess
PolicyDocument:
Statement:
- Effect: Allow
Action:
- dynamodb:GetItem
- dynamodb:PutItem
- dynamodb:UpdateItem
- dynamodb:Query
Resource: !GetAtt Table.Arn
- Effect: Allow
Action:
- dynamodb:GetRecords
- dynamodb:GetShardIterator
- dynamodb:DescribeStream
- dynamodb:ListStreams
Resource: !Sub "${Table.Arn}/stream/*"
# When audit archival is enabled:
- Effect: Allow
Action:
- s3:PutObject
Resource: !Sub "${AuditArchiveBucket.Arn}/*"
Application IAM Policies¶
The template creates three IAM managed policies by default for different access patterns. These policies can be attached to your own IAM roles, users, or federated identities.
Policy Summary¶
| Policy | Suffix | Use Case | DynamoDB Permissions |
|---|---|---|---|
AcquireOnlyPolicy |
-acq |
Applications calling acquire() |
GetItem, BatchGetItem, Query, TransactWriteItems |
FullAccessPolicy |
-full |
Ops teams managing config | All of the above + PutItem, DeleteItem, UpdateItem, BatchWriteItem, Scan, DescribeTable |
ReadOnlyPolicy |
-read |
Monitoring and dashboards | GetItem, BatchGetItem, Query, Scan, DescribeTable |
Permission Matrix¶
| Action | ReadOnly | AcquireOnly | FullAccess |
|---|---|---|---|
GetItem |
Y | Y | Y |
BatchGetItem |
Y | Y | Y |
Query |
Y | Y | Y |
Scan |
Y | -- | Y |
DescribeTable |
Y | -- | Y |
TransactWriteItems |
-- | Y | Y |
PutItem |
-- | -- | Y |
UpdateItem |
-- | -- | Y |
DeleteItem |
-- | -- | Y |
BatchWriteItem |
-- | -- | Y |
IAM Roles (Opt-In)¶
When EnableIAMRoles is true (or --create-iam-roles is passed), the template also creates IAM roles that attach these managed policies. All roles trust the same AWS account root principal:
AssumeRolePolicyDocument:
Version: '2012-10-17'
Statement:
- Effect: Allow
Principal:
AWS: !Sub arn:${AWS::Partition}:iam::${AWS::AccountId}:root
Action: sts:AssumeRole
Policy and Role Naming¶
- Policies:
${StackName}-{acq,full,read}(e.g.,my-app-acq) - Roles (when enabled):
${StackName}-{acq,full,read}(same naming) - With custom
PolicyNameFormat/RoleName: format pattern applied to all
Policies and roles respect PermissionBoundary if configured.
Usage Example¶
import boto3
# Attach AcquireOnlyPolicy to your own role, or assume a created role
sts = boto3.client('sts')
credentials = sts.assume_role(
RoleArn='arn:aws:iam::123456789012:role/my-app-acq',
RoleSessionName='my-app'
)['Credentials']
# Use assumed credentials
session = boto3.Session(
aws_access_key_id=credentials['AccessKeyId'],
aws_secret_access_key=credentials['SecretAccessKey'],
aws_session_token=credentials['SessionToken']
)
Namespace-Scoped IAM Policies¶
The template also creates three namespace-scoped policies that restrict DynamoDB access to items within a single namespace using tag-based access control (TBAC):
| Policy | Suffix | Use Case | Tag Required |
|---|---|---|---|
NamespaceAcquirePolicy |
-ns-acq |
Tenant apps calling acquire() |
zael_namespace_id |
NamespaceFullAccessPolicy |
-ns-full |
Tenant config management | zael_namespace_id |
NamespaceReadOnlyPolicy |
-ns-read |
Tenant monitoring | zael_namespace_id |
These policies use dynamodb:LeadingKeys with the caller's zael_namespace_id principal tag to scope DynamoDB operations to items prefixed with that namespace ID. Read access to the reserved namespace _/* (namespace registry, shared config) is always granted.
For setup instructions, see Namespace-Scoped Access Control.
Customization¶
Add Dead Letter Queue¶
Parameters:
EnableDLQ:
Type: String
Default: "false"
AllowedValues: ["true", "false"]
Conditions:
CreateDLQ: !Equals [!Ref EnableDLQ, "true"]
Resources:
DeadLetterQueue:
Type: AWS::SQS::Queue
Condition: CreateDLQ
Properties:
QueueName: !Sub "${AWS::StackName}-aggregator-dlq"
MessageRetentionPeriod: 1209600 # 14 days
StreamEventMapping:
Properties:
DestinationConfig:
OnFailure:
Destination: !If
- CreateDLQ
- !GetAtt DeadLetterQueue.Arn
- !Ref AWS::NoValue
Add CloudWatch Alarms¶
ReadThrottleAlarm:
Type: AWS::CloudWatch::Alarm
Properties:
AlarmName: !Sub "${AWS::StackName}-read-throttle"
AlarmDescription: Alert when DynamoDB read requests are throttled
MetricName: ReadThrottleEvents
Namespace: AWS/DynamoDB
Statistic: Sum
Period: 300 # 5 minutes
EvaluationPeriods: 2
Threshold: 1
ComparisonOperator: GreaterThanThreshold
Dimensions:
- Name: TableName
Value: !Ref RateLimitsTable
TreatMissingData: notBreaching
AlarmActions: !If
- HasSNSTopic
- [!Ref AlarmSNSTopicArn]
- !Ref AWS::NoValue
Enable Encryption with CMK¶
Parameters:
KmsKeyArn:
Type: String
Default: ""
Conditions:
UseCustomKey: !Not [!Equals [!Ref KmsKeyArn, ""]]
Resources:
Table:
Properties:
SSESpecification:
SSEEnabled: true
SSEType: !If [UseCustomKey, "KMS", "AWS_OWNED_KEY"]
KMSMasterKeyId: !If [UseCustomKey, !Ref KmsKeyArn, !Ref AWS::NoValue]
Deployment Examples¶
Basic Deployment¶
aws cloudformation deploy \
--template-file template.yaml \
--stack-name zae-limiter \
--capabilities CAPABILITY_NAMED_IAM
With Custom Parameters¶
aws cloudformation deploy \
--template-file template.yaml \
--stack-name prod \
--parameter-overrides \
PITRRecoveryPeriodDays=35 \
SnapshotRetentionDays=365 \
LogRetentionDays=90 \
EnableAlarms=true \
--capabilities CAPABILITY_NAMED_IAM
Note: The DynamoDB table name is automatically set to match the stack name (e.g., prod).
Using SAM¶
# samconfig.toml
[default.deploy.parameters]
stack_name = "limiter"
capabilities = "CAPABILITY_NAMED_IAM"
Outputs¶
The template exports:
| Output | Condition | Description |
|---|---|---|
TableName |
Always | DynamoDB table name |
TableArn |
Always | DynamoDB table ARN |
StreamArn |
Always | DynamoDB stream ARN |
AggregatorFunctionArn |
Aggregator deployed | Aggregator Lambda function ARN |
AggregatorFunctionName |
Aggregator deployed | Aggregator Lambda function name |
AggregatorDLQUrl |
Aggregator enabled | Dead Letter Queue URL |
AggregatorDLQArn |
Aggregator enabled | Dead Letter Queue ARN |
AggregatorDLQAlarmName |
Aggregator alarms | CloudWatch alarm for DLQ monitoring |
LambdaErrorRateAlarmName |
Aggregator alarms | CloudWatch alarm for Lambda error rate |
LambdaDurationAlarmName |
Aggregator alarms | CloudWatch alarm for Lambda duration |
DynamoDBReadThrottleAlarmName |
Alarms enabled | CloudWatch alarm for read throttles |
DynamoDBWriteThrottleAlarmName |
Alarms enabled | CloudWatch alarm for write throttles |
StreamIteratorAgeAlarmName |
Aggregator alarms | CloudWatch alarm for stream iterator age |
AuditArchiveBucketName |
Audit archival | S3 bucket for audit archives |
AuditArchiveBucketArn |
Audit archival | S3 bucket ARN |
AcquireOnlyPolicyArn |
IAM enabled | IAM policy ARN for acquire-only access |
FullAccessPolicyArn |
IAM enabled | IAM policy ARN for full access |
ReadOnlyPolicyArn |
IAM enabled | IAM policy ARN for read-only access |
NamespaceAcquirePolicyArn |
IAM enabled | Namespace-scoped acquire-only policy ARN |
NamespaceFullAccessPolicyArn |
IAM enabled | Namespace-scoped full-access policy ARN |
NamespaceReadOnlyPolicyArn |
IAM enabled | Namespace-scoped read-only policy ARN |
AppRoleArn |
IAM roles enabled | IAM role ARN for application access |
AppRoleName |
IAM roles enabled | IAM role name for application access |
AdminRoleArn |
IAM roles enabled | IAM role ARN for admin access |
AdminRoleName |
IAM roles enabled | IAM role name for admin access |
ReadOnlyRoleArn |
IAM roles enabled | IAM role ARN for read-only access |
ReadOnlyRoleName |
IAM roles enabled | IAM role name for read-only access |
PermissionBoundaryArn |
Always | Permission boundary ARN (empty if none) |
RoleNameFormat |
Always | Role name format template (empty if default) |
CodeBucketName |
Audit archival | S3 bucket for deployment artifacts |
ProvisionerFunctionArn |
Provisioner deployed | Limits provisioner Lambda function ARN |
ProvisionerFunctionName |
Provisioner deployed | Limits provisioner Lambda function name |
Discovering namespace IDs
Namespace IDs are opaque strings stored in DynamoDB, not CloudFormation outputs.
Use zae-limiter namespace show default --name <stack> to retrieve the ID
for the default namespace, or any registered namespace.
Access outputs:
Next Steps¶
- Deployment - Deployment guide
- LocalStack - Local development