AWS Lambda で遊ぼう(第3話: AWS SAMとDynamoDB)

SAM で DynamoDB のテーブルを作る

前回はだいぶ駆け足で AWS SAM を使いました。

ただし、DynamoDB とか S3 とか、他のリソースと組み合わせるところまでは到達できませんでした。

なので、今日は DynamoDB のテーブルを作って、Lambda から書き込んでみようと思う。

前回のお話
tercel-tech.hatenablog.com


今回のポイントは、SAM を使って DynamoDB のテーブルを自動作成する点と、それを Lambda と連携させる点である。

リソースの自動作成に関しては CloudFormation と呼ばれるサービスが有名だが、SAM を使うとより簡易にリソース定義を記述できることを示す。

tercel-tech.hatenablog.com

そんなわけで、本日のお品書きがコチラ。 3つの演習を通じて、インフラ構成を含めてコード化する技術 (IaC (Infrastructure as Code)) に触れる。

【練習①】2行で作れる DynamoDB

前回の続きからスタートする。 前回の記事をいちいち読むのがだるい場合は、sam initPython 3.8 の AWS Quick Start Templates から、Hello World Example のボイラープレートを選んでプロジェクトを新規作成した状態をスタート地点にしても全く問題ない。

ここでは最も簡単な例として、 String型の id というプライマリキーを持つ DynamoDB をひとつ作ってみよう。

SAM のプロジェクトでは、Lambda や API Gateway を含め、あらゆるリソースを template.yaml というファイルに定義している。 そのため DynamoDB を足したくなったらこのファイルを弄る必要があるのだ。

template.yaml の編集(2行追加)

template.yaml を開くと、まず夥しい既存コードに泡を吹くかも知れないが、とりあえず落ち着いてほしい。

DynamoDB を作るだけであれば、たった2行のリソース定義を追加するだけでよいのだ(ちなみに、HelloWorldDB の代わりに好きな名前をつけることができる)。

template.yaml (diff)

  AWSTemplateFormatVersion: '2010-09-09'
  Transform: AWS::Serverless-2016-10-31
  Description: >
    sam-app
  
    Sample SAM Template for sam-app
  
  Globals:
    Function:
      Timeout: 3
  
  Resources:
+   HelloWorldDB:
+     Type: AWS::Serverless::SimpleTable
    HelloWorldFunction:
      Type: AWS::Serverless::Function
      Properties:
        CodeUri: hello_world/
        Handler: app.lambda_handler
        Runtime: python3.8
        Events:
          HelloWorld:
            Type: Api
            Properties:
              Path: /hello
              Method: get
  
  Outputs:
    HelloWorldApi:
      Description: "API Gateway endpoint URL for Prod stage for Hello World function"
      Value: !Sub "https://${ServerlessRestApi}.execute-api.${AWS::Region}.amazonaws.com/Prod/hello/"
    HelloWorldFunction:
      Description: "Hello World Lambda Function ARN"
      Value: !GetAtt HelloWorldFunction.Arn
    HelloWorldFunctionIamRole:
      Description: "Implicit IAM Role created for Hello World function"
      Value: !GetAtt HelloWorldFunctionRole.Arn

たったこれだけで、DynamoDB が出来上がる。

DynamoDB のデプロイ

物は試しにデプロイしてみよう。

$ sam deploy --guided

f:id:tercel_s:20210501204048p:plain

少し待つと、ターミナルに DynamoDB が作られる様子が出力される。

f:id:tercel_s:20210501204722p:plain

デプロイが正常終了すると、Successfully が表示される。

f:id:tercel_s:20210501204839p:plain

結果の確認

AWS マネジメントコンソールにログインして、DynamoDB → 「テーブル」を選択しよう。 新しいテーブルが追加されていたら成功だ。

f:id:tercel_s:20210501205435p:plain

template.yaml にたった2行書き足してデプロイするだけで DynamoDB のリソースが作られてしまった。

これを知っているだけでだいぶ友達に差がつけられるのではと思う。 たった2行で DynamoDB を作り終えている傍らで、ライバルはマネジメントコンソールでぽちぽちやっているかも知れない。

おまけ: CloudFormation だったら……?

SAM は、従来の CloudFormation よりも簡略化された表記でリソースを定義できる。

もしも CloudFormation で似たような DynamoDB を作ろうとすると、けっこう大変なのだ。

SAM版

Resources:
  HelloWorldDB:
    Type: AWS::Serverless::SimpleTable

CloudFormation版

Resources:
  HelloWorldDB:
    Type: AWS::DynamoDB::Table
    Properties:
      AttributeDefinitions:
        - AttributeName: id
          AttributeType: S
      KeySchema:
        - AttributeName: id
          KeyType: HASH
      ProvisionedThroughput:
        ReadCapacityUnits: 5
        WriteCapacityUnits: 5
      StreamSpecification:
        StreamViewType: NEW_IMAGE

とはいえすべてのリソースにこのような簡便記法が用意されているわけではないし、やはり細かくチューニングしたくなった場合はそれだけパラメータを細かく設定する必要がある。

なお、リソースの一覧は以下のページに詳しい。

docs.aws.amazon.com

【練習②】テーブル名を Lambda から参照する

作成した DynamoDB のテーブル名には、sam-app-HelloWorldDB-██████ のような適当な乱数が与えられている。

コイツをどうにかして Lambda 関数から参照できるようにしてみよう。

template.yaml の編集(3行追加)

template.yaml (diff)

  (前略)

  Resources:
    HelloWorldDB:
      Type: AWS::Serverless::SimpleTable
    HelloWorldFunction:
      Type: AWS::Serverless::Function
      Properties:
        CodeUri: hello_world/
        Handler: app.lambda_handler
        Runtime: python3.8
        Events:
          HelloWorld:
            Type: Api
            Properties:
              Path: /hello
              Method: get
+       Environment:
+         Variables:
+          TableName: !Ref HelloWorldDB
 
  (後略)

yaml を編集する際にはインデントに十分注意しよう。EnvironmentEvents は同じ位置にすること。

また、!Ref の後ろに続く HelloWorldDB は、前節で作った DynamoDB のリソース名に合わせる必要がある(下図の緑枠)。

f:id:tercel_s:20210501214106p:plain:w400

これで、TableName という環境変数に、テーブル名が自動的に格納されるようになる。

環境変数の参照

Python環境変数 TableName を参照するには、os.environ['TableName'] と書けばよい*1

app.py

import os
import json

def lambda_handler(event, context):
    table_name = os.environ['TableName']
    
    return {
        "statusCode": 200,
        "body": json.dumps({
            "result": table_name
        }),
    }

template.yamlapp.py の対応関係は下図のとおりである。

うっかりしていると見落としがちだが、template.yamlapp.py はあくまで TableName という環境変数を媒介してテーブル名を参照している点に注目してみよう。

f:id:tercel_s:20210501235012p:plain

では、その TableName の値はどこから来ているかというと、template.yaml 内部で HelloWorldDB というリソース名を !Ref している。

f:id:tercel_s:20210501235438p:plain

参照元のリソースが、色々乗り移って最終的に Lambda に繋がっているわけだが、しょけんだと些かややこしく認知負荷が高いので注意したい。

ちなみに、一見もっともらしいTableName という環境変数名は僕が勝手に付けたものなので、たとえば UNKO とか命名を変えても参照関係がしっかり繋がってさえいれば動くので好きにしてほしい。

では、実際にデプロイして curl コマンドを叩いてみよう、ターミナルにテーブル名が出力されるはずだ。

Lambda 側から、きちんと環境変数経由でテーブル名を参照できている証左である。

f:id:tercel_s:20210501220824p:plain

【練習③】 DynamoDB にデータを書き込んでみよう

権限の追加(お手軽編)

第1話でやったときと同じく、Lambda に対してガバガバ権限の AmazonDynamoDBFullAccess を与えるだけなら、template.yaml に1行追加するだけでよい。

template.yaml (diff)

  (前略)

  Resources:
    HelloWorldDB:
      Type: AWS::Serverless::SimpleTable
    HelloWorldFunction:
      Type: AWS::Serverless::Function
      Properties:
        CodeUri: hello_world/
        Handler: app.lambda_handler
        Runtime: python3.8
+       Policies: AmazonDynamoDBFullAccess
        Events:
          HelloWorld:
            Type: Api
            Properties:
              Path: /hello
              Method: get
        Environment:
          Variables:
           TableName: !Ref HelloWorldDB

  (後略)

ただ、AmazonDynamoDBFullAccess は、DynamoDB のあらゆるテーブルに対してあらゆる操作を許す権限なので、もうちょっと厳しくしたい。

権限の追加(改良編)

HelloWorldDB に対する読み書きができれば十分なので、AmazonDynamoDBFullAccess の代わりにDynamoDBCrudPolicy を使おう。

docs.aws.amazon.com

template.yaml (diff)

  (前略)

  Resources:
    HelloWorldDB:
      Type: AWS::Serverless::SimpleTable
    HelloWorldFunction:
      Type: AWS::Serverless::Function
      Properties:
        CodeUri: hello_world/
        Handler: app.lambda_handler
        Runtime: python3.8
+       Policies: 
+         - DynamoDBCrudPolicy:
+             TableName: !Ref HelloWorldDB
        Events:
          HelloWorld:
            Type: Api
            Properties:
              Path: /hello
              Method: get
        Environment:
          Variables:
           TableName: !Ref HelloWorldDB

  (後略)
DynamoDB に UUID を登録する Lambda を作る

1話で作ったモノを再利用して以下の Lambda を書いてデプロイしよう。

app.py

import os
import json
import uuid
import boto3

dynamodb = boto3.resource('dynamodb')       # DynamoDB オブジェクト

def lambda_handler(event, context):
    table_name = os.environ['TableName']    # 環境変数からテーブル名を取得
    table = dynamodb.Table(table_name)
    
    item = { 'id': str(uuid.uuid4()) }      # UUID を発行
    table.put_item(Item = item)             # DynamoDB に書き込む
    
    return {
        'statusCode': 200,
        'body': json.dumps(item)
    }

curl を叩くと、Lambda の中で発行した UUID が返却される。

f:id:tercel_s:20210501223935p:plain

DynamoDB 登録確認

再び マネジメントコンソールから DynamoDB を開き、テーブルの中身を確認する。

f:id:tercel_s:20210501225401p:plain

Lambda が払い出した UUID が、DynamoDB に登録されていることが確認できた。

全く同じ UUID が払い出される確率はゼロなので*2、偶然たまたま無関係な値が登録されたわけではなく、ちゃんと Lambda から Dynamo にデータが連携されていると断言できる。

まとめとか

今日は SAM の template.yaml を修正して、DynamoDB のテーブルを作り、環境変数を経由して Lambda にテーブル名を引き渡す方法を学んだ。

また、Lambda から DynamoDB にアクセスするための権限設定を AWS SAM で行う方法を学んだ。

初日の Lambda 関数を利用して、DynamoDB にデータを登録することを確認した。

そういえば、宅配ボックスにこんな本が入っていたのだけど……?

あー、これ注文しておいた本だ!

もう届いたのか。

IoT も勉強しないといけないし、学ぶことが急に増えたねー。

とはいえ、いろいろなところで学んだ知識は最終的にひとつに繋がると思う。

結局、センサから吸い上げたデータを溜めるには Dynamo か S3 と連携する Lambda が書けないと話にならないしね。

ふむ。

なんか、まぁ GW が充実してそうで何より。

*1:os.getenv() でもよい。

*2:UUID は有限長の乱数なので、衝突する可能性は厳密にはゼロではないが、明日何らかの理由で人類が滅亡する確率の方が遥かに高いため、実質的にゼロと扱ってよいことになっている。

Copyright (c) 2012 @tercel_s, @iTercel, @pi_cro_s.