AWS SAMのDynamoDBテーブル定義を別のCloudFormationテンプレートに分離してみた

CloudFormationの既存リソースのインポート機能を使って、AWS SAMで定義したDynamoDBテーブルの定義を異なるテンプレートに分離してみました。
2020.07.22

この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。

AWS SAMでサーバーレスなアプリケーションを作っていると、1つのテンプレートファイル内にLambdaやDynamoDBの定義を書くと楽です。 しかし、あとから「DynamoDBの定義を分離したいな……」となることがたまにあります。例えば、CloudFormationのリソース上限200個が見えてきたり、そもそもデータストア層とアプリ層を同じテンプレートに書いて頻繁にデプロイ対象にしたくないなどです。

そんなとき、CloudFormationのインポート機能を使えばテンプレートファイルを分離できるのでは?と思ったので試してみました。

適当なAPIを作成する

アプリを初期化

sam init \
    --runtime python3.7 \
    --name CfnImportSampleApp \
    --app-template hello-world

AWS SAMテンプレート

最初は、AWS SAMテンプレート内にDynamoDBテーブルの定義を書いています。

template.yaml

AWSTemplateFormatVersion: '2010-09-09'
Transform: AWS::Serverless-2016-10-31
Description: CfnImportSampleApp

Resources:
  HelloWorldFunction:
    Type: AWS::Serverless::Function
    Properties:
      CodeUri: hello_world/
      Handler: app.lambda_handler
      Runtime: python3.7
      Timeout: 5
      Environment:
        Variables:
          TABLE_NAME: !Ref ParameterTable
      Policies:
        - arn:aws:iam::aws:policy/AmazonDynamoDBReadOnlyAccess
      Events:
        HelloWorld:
          Type: Api
          Properties:
            Path: /hello
            Method: get

  ParameterTable:
    Type: AWS::DynamoDB::Table
    Properties:
      TableName: CfnImportSampleTable
      AttributeDefinitions:
        - AttributeName: userId
          AttributeType: S
      KeySchema:
        - AttributeName: userId
          KeyType: HASH
      BillingMode: PAY_PER_REQUEST

Outputs:
  HelloWorldApi:
    Value: !Sub "https://${ServerlessRestApi}.execute-api.${AWS::Region}.amazonaws.com/Prod/hello/"

Lambdaコード

app.py

import boto3
import json
import os

dynamodb = boto3.resource('dynamodb')
table_name = os.environ['TABLE_NAME']

def lambda_handler(event, context):
    table = dynamodb.Table(table_name)
    res = table.get_item(Key={
        'userId': '1234'
    })

    return {
        'statusCode': 200,
        'body': json.dumps({
            'message': 'hello world',
            'data': res['Item']
        }),
    }

デプロイ

sam build

sam package \
    --output-template-file packaged.yaml \
    --s3-bucket cm-fujii.genki-deploy

sam deploy \
    --template-file packaged.yaml \
    --stack-name Cfn-Import-Sample-Stack \
    --capabilities CAPABILITY_NAMED_IAM \
    --no-fail-on-empty-changeset

DynamoDBへデータ追加

適当に追加しました。

{
  "todo": "xxx",
  "userId": "1234"
}

DynamoDBテーブルの様子

APIを叩いて確認

実際にAPIを叩いてみると、下記の応答が返ってきます。バッチリですね。

$ curl https://xxx.execute-api.ap-northeast-1.amazonaws.com/Prod/hello/
{"message": "hello world", "data": {"userId": "1234", "todo": "xxx"}}

AWS SAMのDynamoDBを削除する

DynamoDBのDeletionPolicyをRetainにする

このままDynamoDBテーブルの定義を削除した場合、テーブル自体が削除されてしまいします。そのため、DeletionPolicy属性を設定することで、リソース(DynamoDBテーブル)を残しつつCloudFormationスタックからDynamoDBの定義を削除させます。

template.yaml

AWSTemplateFormatVersion: '2010-09-09'
Transform: AWS::Serverless-2016-10-31
Description: CfnImportSampleApp

Resources:
  HelloWorldFunction:
    Type: AWS::Serverless::Function
    Properties:
      CodeUri: hello_world/
      Handler: app.lambda_handler
      Runtime: python3.7
      Timeout: 5
      Environment:
        Variables:
          TABLE_NAME: !Ref ParameterTable
      Policies:
        - arn:aws:iam::aws:policy/AmazonDynamoDBReadOnlyAccess
      Events:
        HelloWorld:
          Type: Api
          Properties:
            Path: /hello
            Method: get

  ParameterTable:
    Type: AWS::DynamoDB::Table
    DeletionPolicy: Retain
    Properties:
      TableName: CfnImportSampleTable
      AttributeDefinitions:
        - AttributeName: userId
          AttributeType: S
      KeySchema:
        - AttributeName: userId
          KeyType: HASH
      BillingMode: PAY_PER_REQUEST

Outputs:
  HelloWorldApi:
    Value: !Sub "https://${ServerlessRestApi}.execute-api.${AWS::Region}.amazonaws.com/Prod/hello/"

DeletionPolicy: Retainを付与したあと、再度デプロイを行います。これでAWS SAMテンプレートから削除する準備が整いました。

DynamoDBテーブルを削除する

AWS SAMテンプレートからDynamoDBテーブルの定義をまるっと削除します。Lambdaの環境変数部分も一度削除しておきます。 この作業中でもDynamoDBテーブルを参照したい場合は、環境変数部分にDynamoDBテーブル名を直接記載すると良いですね。

template.yaml

AWSTemplateFormatVersion: '2010-09-09'
Transform: AWS::Serverless-2016-10-31
Description: CfnImportSampleApp

Resources:
  HelloWorldFunction:
    Type: AWS::Serverless::Function
    Properties:
      CodeUri: hello_world/
      Handler: app.lambda_handler
      Runtime: python3.7
      Timeout: 5
      Policies:
        - arn:aws:iam::aws:policy/AmazonDynamoDBReadOnlyAccess
      Events:
        HelloWorld:
          Type: Api
          Properties:
            Path: /hello
            Method: get

Outputs:
  HelloWorldApi:
    Value: !Sub "https://${ServerlessRestApi}.execute-api.${AWS::Region}.amazonaws.com/Prod/hello/"

この状態でデプロイすると、CloudFormation管理リソースからDynamoDBが削除されました。

CloudFormation管理からDynamoDBテーブルが外れた様子

しかし、実際のDynamoDBテーブルは残り続けています。

DynamoDBテーブルが残っている様子

CloudFormationで既存のDynamoDBテーブルをインポートする

CloudFormatioin用のテンプレートを作成する

datastore.yamlというファイルを新規作成し、先ほどのDynamoDBテーブルをインポートさせます。なお、CloudFormationで既存リソースをインポートする場合はDeletionPolicy: Retainが必須なので付与させています。

datastore.yaml

AWSTemplateFormatVersion: '2010-09-09'

Resources:
  ParameterTable:
    Type: AWS::DynamoDB::Table
    DeletionPolicy: Retain
    Properties:
      TableName: CfnImportSampleTable
      AttributeDefinitions:
        - AttributeName: userId
          AttributeType: S
      KeySchema:
        - AttributeName: userId
          KeyType: HASH
      BillingMode: PAY_PER_REQUEST

CloudFormationで既存リソースをインポートする

CloudFormation画面で「既存のリソースを使用」を選択します。

CloudFormationで既存リソースをインポートする

先ほど作成したテンプレートファイル(datastore.yaml)をアップロードして次に進みます。

CloudFormationのテンプレートファイルをアップロードする

識別子にインポートしたいDynamoDBテーブル名を入力して次に進みます。

  • 識別子の値: CfnImportSampleTable

CloudFormationでインポートするDynamoDBテーブル名を入力する

スタック名を入力して次に進みます。

  • スタック名: Cfn-Import-Datastore-Sample-Stack

CloudFormationで作成するスタック名を入力する

最終チェックを行い、既存リソースをインポートしましょう!

最終チェックをする

無事に完了しました!!

CloudFormationで既存リソースのインポートが成功した様子

ここまでの作業で、DynamoDBテーブルの定義を分離できました。

AWS SAMで異なるテンプレートで定義したDynamoDBを参照する

異なるテンプレート(CloudFormationスタック)で定義したDynamoDBテーブルを参照する方法はいくつかあります。

  • AWS SAMテンプレート内でDynamoDBテーブル名を固定文字列として持つ
  • AWS SAMテンプレート内でクロススタック参照をする

今回はクロススタック参照を利用してみます。

Outputsを定義する

クロススタック参照を行うためには、まずOutputでExportする必要があります。 そのため、datastore.yamlOutputsを追加します。 (最初から追加しようとしましたが、OutputsがあるテンプレートでCloudFormationの既存リソースはインポートできませんでした)

datastore.yaml

AWSTemplateFormatVersion: '2010-09-09'

Resources:
  ParameterTable:
    Type: AWS::DynamoDB::Table
    DeletionPolicy: Retain
    Properties:
      TableName: CfnImportSampleTable
      AttributeDefinitions:
        - AttributeName: userId
          AttributeType: S
      KeySchema:
        - AttributeName: userId
          KeyType: HASH
      BillingMode: PAY_PER_REQUEST

Outputs:
  ParameterTableName:
    Value: !Ref ParameterTable
    Export:
      Name: !Sub ${AWS::StackName}-ParameterTableName

デプロイします。

aws cloudformation deploy \
    --template-file datastore.yaml \
    --stack-name Cfn-Import-Datastore-Sample-Stack \
    --capabilities CAPABILITY_NAMED_IAM \
    --no-fail-on-empty-changeset

無事にOutputsできました。

CloudFormationでOutputsが成功した様子

AWS SAMで異なるスタックのDynamoDBテーブルを参照する(クロススタック参照)

Lambdaの環境変数でクロススタック参照を行うようにtemplate.yamlを修正します。

template.yaml

AWSTemplateFormatVersion: '2010-09-09'
Transform: AWS::Serverless-2016-10-31
Description: CfnImportSampleApp

Resources:
  HelloWorldFunction:
    Type: AWS::Serverless::Function
    Properties:
      CodeUri: hello_world/
      Handler: app.lambda_handler
      Runtime: python3.7
      Timeout: 5
      Environment:
        Variables:
          TABLE_NAME:
            Fn::ImportValue: Cfn-Import-Datastore-Sample-Stack-ParameterTableName
      Policies:
        - arn:aws:iam::aws:policy/AmazonDynamoDBReadOnlyAccess
      Events:
        HelloWorld:
          Type: Api
          Properties:
            Path: /hello
            Method: get

Outputs:
  HelloWorldApi:
    Value: !Sub "https://${ServerlessRestApi}.execute-api.${AWS::Region}.amazonaws.com/Prod/hello/"

再デプロイを行いましょう。

動作確認

せっかくなのでDynamoDBの内容を書き換えました。

{
  "todo": "finish!!!",
  "userId": "1234"
}

DynamoDBテーブルの様子

この状態でAPIを叩くと、バッチリと返ってきました!!!

$ curl https://xxx.execute-api.ap-northeast-1.amazonaws.com/Prod/hello/
{"message": "hello world", "data": {"userId": "1234", "todo": "finish!!!"}}

さいごに

CloudFormationの既存リソースインポート機能を使って、DynamoDBテーブルの定義を別テンプレート(スタック)に分離してみました。 何らかの参考になれば幸いです。

参考