Referencing global dynamodb table from the secondary region in CloudFormation template

Viewed 75

We are using CloudFormation for defining our infrastructure. A global dynamodb table is used that is defined and created in one region (the primary region), but has replicas in another region. How do I reference this same table in the template in another region? Specifically I need to get the global table name and stream ARN for the stack in secondary region. Reason? One lambda of this stack from secondary region uses this name and stream ARN to create new records for this table. I thought anyone using global tables will encounter this, but I searched on Internet and couldn't find any easy solution.

Here's how it's defined in the template right now:

 Table1:
    Type: AWS::DynamoDB::GlobalTable
    Condition: CreateGlobalTable
    Properties:
      TableName: !Sub "Table1-${StageName}"
      AttributeDefinitions:
        - AttributeName: store_id
          AttributeType: S
        - AttributeName: client_name
          AttributeType: S
      BillingMode: PAY_PER_REQUEST
      KeySchema:
        - AttributeName: store_id
          KeyType: HASH
        - AttributeName: client_name
          KeyType: RANGE
      StreamSpecification:
        StreamViewType: NEW_IMAGE
      Replicas:
        - Region: us-east-1
        - Region: us-west-2
      TimeToLiveSpecification:
          AttributeName: time_to_live
          Enabled: true
2 Answers

I need to get the global table name and stream ARN for the stack in secondary region.

You need a custom resource for that. So it will be a lambda function that will get what you need, and return it to your template.

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:

  1. I'm not sure if for this particular case, we need to return stream_arn in the create(...) function of the Lambda.
  2. 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.
Related