Serverless FrameworkとAWS SAMからDynamoDBのTTL機能を有効化する

はじめに

こんにちは、中山です。

少し前の話ですがDynamoDBにTTL機能が追加されました。属性の作成日時をベースに特定期間経過した場合、自動的にアイテムを削除してくれる機能です。DynamoDBを一時的なデータの保管場所として利用しているユースケースでは、嬉しいアップデートだったのではないでしょうか。以前までは、バッチ処理などでアイテムの削除を実施するアプリを自分で作り込む必要がありました。が、この機能を利用することによりAWSにまるっとその部分をおまかせできるという訳です。より詳しい内容は以下のリンクを参照ください。

DynamoDBはサーバレスアーキテクチャのデータストアとしてよく利用されます。私はServerless FrameworkやAWS SAM(AWS Serverless Application Model)をよく利用しているので、それらのツールから使いたかったのですが、執筆時点(2017/04/29)ではCloudFormationのAWS::DynamoDB::TableおよびAWS::Serverless::SimpleTableリソースがTTL用プロパティをサポートしていません。そのため、少し工夫が必要になります。

という訳で、本エントリではこれらのツールからDynamoDBのTTL機能を利用する方法をご紹介します。

Serverless Frameworkの場合

以下のプラグインを利用することで対応可能です。

使い方は簡単です。プラグインのインストール後、 serverlelss.yml を修正するだけです。

  • プラグインのインストール
$ npm install --save serverless-dynamodb-ttl
  • serverless.yml の修正
# プラグインの読み込み
plugins:
  - serverless-dynamodb-ttl

# TTLの設定
custom:
  dynamodb:
    ttl:
      - table: your-dynamodb-table-name
        field: your-ttl-property-name

例えば、以下のように serverless.yml を定義したとします。

service: dynamodb-ttl
custom:
  config: ${file(config.yml)}
  dynamodb:
    ttl:
      - table: ${self:custom.config.tableName}
        field: ttl

frameworkVersion: ">=1.12.1 <2.0.0"

provider:
  name: aws
  runtime: nodejs6.10
  stage: ${self:custom.config.stage}
  region: ${self:custom.config.region}
  cfLogs: true
  iamRoleStatements:
    - Sid: DynamoDBAccess
      Effect: Allow
      Resource:
        - Fn::Join: [ "", [ "arn:aws:dynamodb:", Ref: "AWS::Region", ":", Ref: "AWS::AccountId", ":", "table/", Ref: TestTable ] ]
      Action:
        - dynamodb:*

plugins:
  - serverless-dynamodb-ttl

package:
  include:
    - src/**
  exclude:
    - .git/**
    - config.yml
    - package.json
    - serverless.yml
    - node_modules/**

functions:
  scan:
    handler: src/handlers/dynamodb/index.scan
    environment:
      TableName:
        Ref: TestTable
  put:
    handler: src/handlers/dynamodb/index.put
    environment:
      TableName:
        Ref: TestTable

resources:
  Resources:
    TestTable:
      Type: AWS::DynamoDB::Table
      Properties:
        TableName: ${self:custom.config.tableName}
        AttributeDefinitions:
          - AttributeName: ttl
            AttributeType: N
          - AttributeName: time
            AttributeType: S
        KeySchema:
          - AttributeName: ttl
            KeyType: HASH
          - AttributeName: time
            KeyType: RANGE
        ProvisionedThroughput:
          ReadCapacityUnits: 1
          WriteCapacityUnits: 1

デプロイ後にHookがバインドされているので sls deploy の最後に、以下のような出力が表示されTTLが有効化されます。

$ sls deploy -v -r ap-northeast-1
<snip>
Serverless: Enabling TTL setting(s) for DynamoDB

DynamoDBのテーブルを確認すると、意図した動作がされていることを確認できます。

$ aws dynamodb describe-time-to-live \
  --table-name testTable
{
    "TimeToLiveDescription": {
        "TimeToLiveStatus": "ENABLED",
        "AttributeName": "ttl"
    }
}

注意点があります。このプラグインでは、現状(v0.0.6)DynamoDBオブジェクトを作成する際のリージョン指定がオプションから渡されるため、 -r <region> でCLIから明示的にリージョンを指定する必要があります。この指定をしないと、以下のようなエラーが表示されます。

Serverless: Enabling TTL setting(s) for DynamoDB

  Config Error -------------------------------------------

     Missing region in config

AWS SAMの場合

AWS SAMはプラグイン機構がないので、自分で作り込む必要があります。とはいえ、大した話ではないです。実装方法はいくつか考えられますが、一番シンプルな方法はLambda-backed Custom ResourceでUpdateTimeToLive APIを叩く方法だと思います。実装方法はLambdaがまだDead Letter Queue用プロパティをサポートしていない際にご紹介した方法とほぼ同じです。

メインとなるテンプレートを以下のように定義しておきます。

---
AWSTemplateFormatVersion: 2010-09-09
Transform: AWS::Serverless-2016-10-31
Description: DynamoDB TTL Main Stack

Resources:
  DynamoDB:
    Type: AWS::CloudFormation::Stack
    Properties:
      TemplateURL: src/templates/dynamodb.yml
  Scan:
    Type: AWS::Serverless::Function
    Properties:
      CodeUri: src/handlers/dynamodb
      Handler: index.scan
      Runtime: nodejs6.10
      Policies:
        - Version: 2012-10-17
          Statement:
            - Sid: DynamoDBAccess
              Effect: Allow
              Action:
                - dynamodb:*
              Resource: !Sub arn:aws:dynamodb:${AWS::Region}:${AWS::AccountId}:table/${DynamoDB.Outputs.TableName}
      Environment:
        Variables:
          TableName: !GetAtt DynamoDB.Outputs.TableName
  Put:
    Type: AWS::Serverless::Function
    Properties:
      CodeUri: src/handlers/dynamodb
      Handler: index.put
      Runtime: nodejs6.10
      Environment:
        Variables:
          TableName: !GetAtt DynamoDB.Outputs.TableName
      Policies:
        - Version: 2012-10-17
          Statement:
            - Sid: DynamoDBAccess
              Effect: Allow
              Action:
                - dynamodb:*
              Resource: !Sub arn:aws:dynamodb:${AWS::Region}:${AWS::AccountId}:table/${DynamoDB.Outputs.TableName}
  Custom:
    Type: AWS::CloudFormation::Stack
    Properties:
      TemplateURL: src/templates/custom.yml
      Parameters:
        TableName: !GetAtt DynamoDB.Outputs.TableName
        AttributeName: ttl
Outputs:
  TableName:
    Value: !GetAtt DynamoDB.Outputs.TableName
  Scan:
    Value: !Ref Scan
  Put:
    Value: !Ref Put

Lambda-backed Custom Resourceを利用するテンプレートを以下のように定義します。ちゃんとエラー処理を実装していない点は生暖かく見守ってください。

---
AWSTemplateFormatVersion: 2010-09-09
Description: DynamoDB TTL Custom Stack

Parameters:
  TableName:
    Type: String
  AttributeName:
    Type: String

Resources:
  LambdaRole:
    Type: AWS::IAM::Role
    Properties:
      AssumeRolePolicyDocument:
        Version: 2012-10-17
        Statement:
          - Sid: AssumeRolePolicy
            Effect: Allow
            Principal:
              Service: lambda.amazonaws.com
            Action: sts:AssumeRole
      Path: /
      ManagedPolicyArns:
        - arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole
      Policies:
        - PolicyName: !Sub ${AWS::StackName}-Policy
          PolicyDocument:
            Version: 2012-10-17
            Statement:
              - Sid: DynamoDBAccess
                Effect: Allow
                Action:
                  - dynamodb:UpdateTimeToLive
                Resource: !Sub arn:aws:dynamodb:${AWS::Region}:${AWS::AccountId}:table/${TableName}
  LambdaUpdateTimeToLive:
    Type: AWS::Lambda::Function
    Properties:
      Handler: index.handler
      Role: !GetAtt LambdaRole.Arn
      Runtime: nodejs6.10
      Code:
        ZipFile: |
            const response = require('cfn-response');
            const AWS = require('aws-sdk');
            exports.handler = (event, context, callback) => {
              let responseData = {};
              if (event.RequestType === 'Delete') {
                response.send(event, context, response.SUCCESS, responseData);
              }
              const dynamodb = new AWS.DynamoDB();
              let params = {
                TableName: event.ResourceProperties.TableName,
                TimeToLiveSpecification: {
                  AttributeName: event.ResourceProperties.AttributeName,
                  Enabled: true
                }
              };
              dynamodb.updateTimeToLive(params, (err, data) => {
                if (err) {
                  console.log(err, err.stack);
                  response.send(event, context, response.FAILED, responseData);
                } else {
                  response.send(event, context, response.SUCCESS, responseData);
                }
              });
            };
  CustomUpdateTimeToLive:
    Type: Custom::UpdateTimeToLive
    Version: 1.0
    Properties:
      ServiceToken: !GetAtt LambdaUpdateTimeToLive.Arn
      TableName: !Ref TableName
      AttributeName: !Ref AttributeName

いつも通り aws cloudformation packageaws cloudformation deploy 実施後、DynamoDBのテーブルを確認すると、以下のように意図した結果になっていることが確認できます。

$ aws dynamodb describe-time-to-live \
  --table-name dynamodb-ttl-DynamoDB-13DRL8A11KJYA-TestTable-1LP3MV3HIR1U8{
    "TimeToLiveDescription": {
        "TimeToLiveStatus": "ENABLED",
        "AttributeName": "ttl"
    }
}

まとめ

いかがだったでしょうか。

Serverless FrameworkとAWS SAMからDynamoDBのTTL機能を利用する方法をご紹介しました。2つのツールはかなり似ていますが、できること/できないことが微妙に異なります。今後も、これらのツールについてご紹介していきたいと思います。

本エントリがみなさんの参考になれば幸いに思います。