I ran into the exact same problem. With the help of this blog entry, I came up with the following solution using a custom resource.
In short, I split my stack into 2: In one I deploy the DynamoDB::GlobalTable, along with a Serverless::Function which will be in charge of getting the stream ARN for us. In the other, I have the rest of components.
The template for the first stack looks as follows:
AWSTemplateFormatVersion: '2010-09-09'
Transform: AWS::Serverless-2016-10-31
Parameters:
DynamoTableName:
Type: "String"
Default: TableNameExample
MainRegion:
Type: "String"
Default: "eu-west-1"
Conditions:
IsMainRegionCondition: !Equals [ !Ref AWS::Region, !Ref MainRegion ]
Resources:
DynamoTable:
Condition: IsMainRegionCondition # Note: Deployed only in one region, replicated in others
Type: AWS::DynamoDB::GlobalTable
Properties:
TableName: !Ref DynamoTableName
BillingMode: PAY_PER_REQUEST
KeySchema:
- AttributeName: entity_id
KeyType: HASH
- AttributeName: language
KeyType: RANGE
AttributeDefinitions:
- AttributeName: entity_id
AttributeType: S
- AttributeName: language
AttributeType: S
StreamSpecification:
StreamViewType: NEW_AND_OLD_IMAGES
Replicas:
- Region: eu-west-1
PointInTimeRecoverySpecification:
PointInTimeRecoveryEnabled: true
- Region: us-east-1
PointInTimeRecoverySpecification:
PointInTimeRecoveryEnabled: true
GetGlobalTableStreamFunction:
Type: AWS::Serverless::Function
Properties:
Environment:
Variables:
TABLE_NAME: !Ref DynamoTableName
MemorySize: 256
CodeUri: functions/dynamo_db_stream/
Handler: app.lambda_handler
Runtime: python3.7
Policies:
- Version: '2012-10-17'
Statement:
- Effect: Allow
Action:
- dynamodb:DescribeTable
Resource: '*'
Outputs:
GetGlobalTableStreamFunctionArn:
Value: !GetAtt GetGlobalTableStreamFunction.Arn
Export:
Name: "get-global-table-stream-function-arn"
The code for the Lambda is the following (note you need to pack the crhelper dependency along the Lambda).
import logging
import os
import boto3
from crhelper import CfnResource
TABLE_NAME = os.environ["TABLE_NAME"]
logger = logging.getLogger(__name__)
helper = CfnResource(
json_logging=False,
log_level='DEBUG',
boto_level='CRITICAL'
)
dynamo_db_client = boto3.client("dynamodb")
def lambda_handler(event, context):
helper(event, context)
def get_table_stream_arn():
logger.info(f"Getting stream ARN for table '{TABLE_NAME}'...")
response = dynamo_db_client.describe_table(
TableName=TABLE_NAME
)
logger.debug(f"Describe table response: {response}")
stream_arn = response["Table"]["LatestStreamArn"]
logger.info(f"ARN for table {TABLE_NAME}: {stream_arn}")
return stream_arn
@helper.create
def create(event, context):
logger.info("Received a 'Create' event")
stream_arn = get_table_stream_arn()
# This will make the stream ARN accessible via Cfn
# `!GetAtt DynamoTableStreamArnGetter.StreamArn`
helper.Data.update({"StreamArn": stream_arn})
return stream_arn
@helper.update
def update(event, context):
logger.info("Received an 'Update' event, doing nothing")
@helper.delete
def delete(event, context):
logger.info("Received a 'Delete' event, doing nothing")
Then, in the second stack, we need to create the custom resource.
Resources:
[...]
DynamoTableStreamArnGetter:
Type: 'Custom::DynamoTableStreamArnFunction'
Version: '1.0'
Properties:
ServiceToken: !ImportValue "get-global-table-stream-function-arn"
Finally, you can get/reference the stream ARN in the replica regions (in the second stack template) through:
!GetAtt DynamoTableStreamArnGetter.StreamArn
Some notes about this solution:
- I'm not sure if for this particular case, we need to
return stream_arn in the create(...) function of the Lambda.
- In the first template, I don't like the giving of permissions to describe all tables (
Resource: '*'). However, there you cannot reference the DynamoTable resource since it will not exist in the replica regions. If anyone knows a better way to just restrict it to that table, please let me know.