API GatewayとLambdaで作られたWebアプリのセッション情報をDynamoDBに保存する
AWS DVAの出題範囲の1つでもある、AWSのサービスによる開発に関する内容です。
API GatewayおよびLambda関数で作成するWebアプリにおいて、セッション情報をどのように扱うかについて考えます。
今回はセッション情報をDynamoDBで保存する方法をご紹介します。
なおセッション管理には、Cookieを使用します。
構築する環境

DynamoDBテーブルを作成します。
このテーブルには、セッション情報を保存します。
具体的には、セッションごとに2つのデータを保存します。
- セッションID
 - カウンタ
 
カウンタはセッション継続中に、アプリにアクセスした回数を記録するものです。
Lambda関数を作成します。
この関数の働きは、DynamoDBにアクセスし、セッション情報を取得/保存することと、セッションIDをCookieに保存することです。
関数のランタイム環境はPython3.8とします。
API Gatewayを作成し、バックエンドにLambda関数を配置します。
ユーザからHTTPリクエストを受けた場合、API Gatewayがエンドポイントとなって、代理で関数を呼び出し、同関数の実行結果をユーザに返すというシンプルな構成です。
API GatewayはHTTP APIタイプとします。
CloudFormationテンプレートファイル
上記の構成をCloudFormationで構築します。
以下のURLにCloudFormationテンプレートを配置しています。
https://github.com/awstut-an-r/awstut-dva/tree/main/03/006
テンプレートファイルのポイント解説
本ページでは、DynamoDBでセッション情報を管理する方法を中心にご紹介します。
HTTP APIタイプのAPI Gatewayに関する基本的な事項については、以下のページをご確認ください。

DynamoDB
Resources:
  Table:
    Type: AWS::DynamoDB::Table
    Properties:
      AttributeDefinitions:
        - AttributeName: session-id
          AttributeType: S
      BillingMode: PAY_PER_REQUEST
      KeySchema:
        - AttributeName: session-id
          KeyType: HASH
      TableName: !Sub "${Prefix}-table"
Code language: YAML (yaml)
DynamoDBテーブルを作成します。
DynamoDBテーブルに関する基本的な事項については、以下のページをご確認ください。

テーブルに「session-id」属性を設定し、これをパーティションキーとします。
Lambda
関数
Resources:
  Function:
    Type: AWS::Lambda::Function
    Properties:
      Architectures:
        - !Ref Architecture
      Code:
        ZipFile: |
          import boto3
          import json
          import os
          import uuid
          from http import cookies
          ATTR_COUNTER = 'counter'
          ATTR_SESSION_ID = 'session-id'
          COOKIE_KEY = 'cookie-test'
          DYNAMODB_ITEM_KEY = 'Item'
          TABLE_NAME = os.environ['TABLE_NAME']
          dynamodb_client = boto3.client('dynamodb')
          def lambda_handler(event, context):
            session_id = ''
            if not 'cookies' in event:
              session_id = str(uuid.uuid4())
            else:
              C = cookies.SimpleCookie()
              C.load('; '.join([cookie for cookie in event['cookies']]))
              cookie_dict = {k: v.value for k, v in C.items()}
              if not COOKIE_KEY in cookie_dict:
                session_id = str(uuid.uuid4())
              else:
                session_id = cookie_dict[COOKIE_KEY]
            dynamodb_get_item_response = dynamodb_client.get_item(
              TableName=TABLE_NAME,
              Key={
                ATTR_SESSION_ID: {'S': session_id}
              }
            )
            #print(dynamodb_get_item_response)
            counter = 0
            if not DYNAMODB_ITEM_KEY in dynamodb_get_item_response:
              counter = 1
            else:
              counter = int(
                dynamodb_get_item_response[DYNAMODB_ITEM_KEY][ATTR_COUNTER]['N']) + 1
            dynamodb_put_item_response = dynamodb_client.put_item(
              TableName=TABLE_NAME,
              Item={
                ATTR_SESSION_ID: {'S': session_id},
                ATTR_COUNTER: {'N': str(counter)}
              }
            )
            #print(dynamodb_put_item_response)
            body = 'session-id: {session_id}, counter: {counter}'.format(
              session_id=session_id,
              counter=counter)
            set_cookie = '{cookie_key}={session_id}'.format(
              cookie_key=COOKIE_KEY,
              session_id=session_id)
            return {
              'statusCode': 200,
              'body': body,
              'headers': {
                'Set-Cookie': set_cookie
              }
            }
      Environment:
        Variables:
          TABLE_NAME: !Ref Table
      FunctionName: !Sub "${Prefix}-function"
      Handler: !Ref Handler
      Runtime: !Ref Runtime
      Role: !GetAtt FunctionRole.Arn
Code language: YAML (yaml)
Lambda関数で実行するコードをインライン形式で記載します。
詳細につきましては、以下のページをご確認ください。

Environmentプロパティで環境変数を定義します。
先述のDynamoDBテーブルのテーブル名を設定します。
この関数で実行する処理の内容は以下の通りです。
- eventオブジェクトからCookie情報にアクセスし、セッションIDを取得する。
CookieにセッションIDが登録されていない場合は、UUIDベースのユニークなIDを生成し、これをセッションIDとする。 - DynamoDBテーブルに対して、セッションIDを使って項目の取得を試み、該当する項目があれば、カウンタ値を取得する。
該当する項目がない場合は、カウンタ値を1とする。 - DynamoDBテーブルに対して、セッションIDをパーティションキーとして、カウンタ値を保存する。
 - HTTPレスポンスのヘッダーにおいて、CookieにセッションIDを設定する。
HTTPレスポンスのボディに、セッションIDとカウンタ値を埋め込んだ文字列を設定する。 
IAMロール
Resources:
  FunctionRole:
    Type: AWS::IAM::Role
    DeletionPolicy: Delete
    Properties:
      AssumeRolePolicyDocument:
        Version: 2012-10-17
        Statement:
          - Effect: Allow
            Action: sts:AssumeRole
            Principal:
              Service:
                - lambda.amazonaws.com
      ManagedPolicyArns:
        - arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole
      Policies:
        - PolicyName: SessionPolicy
          PolicyDocument:
            Version: 2012-10-17
            Statement:
              - Effect: Allow
                Action:
                  - dynamodb:GetItem
                  - dynamodb:PutItem
                Resource:
                  - !Sub "arn:aws:dynamodb:${AWS::Region}:${AWS::AccountId}:table/${Table}"
Code language: YAML (yaml)
インラインポリシーで、DynamoDBテーブルへの読み書きを許可します。
環境構築
CloudFormationを使用して、本環境を構築し、実際の挙動を確認します。
CloudFormationスタックを作成し、スタック内のリソースを確認する
CloudFormationスタックを作成します。
スタックの作成および各スタックの確認方法については、以下のページをご確認ください。

各スタックのリソースを確認した結果、今回作成された主要リソースの情報は以下の通りです。
- DynamoDBテーブル:dva-03-006-table
 - Lambda関数:dva-03-006-function
 - APIゲートウェイのエンドポイント:dva-03-006-HttpApi
 
AWS Management Consoleから各リソースを確認します。
まずDynamoDBテーブルを確認します。

パーティションキーが「session-id」として、テーブルが正常に作成されていることがわかります。
Lambda関数を確認します。

正常に関数が作成されていることがわかります。
API Gatewayを確認します。


正常にHTTPタイプのAPI Gatewayが作成されていることがわかります。
API GatewayとLambda関数が統合されていることもわかります。
動作確認
準備が整いましたので、API Gatewayのエンドポイントにアクセスします。

セッションIDおよびカウンタが表示されました。
カウンタ値は「1」です。
Developer ToolでCookieを確認します。

確かに「cookie-test」という名前でセッションIDが設定されています。
もう一度、同じページにアクセスします。

カウンタ値が更新されました。
DynamoDBテーブルを確認します。

テーブルには、セッションIDおよびカウンタ値を保存されていることがわかります。
このように、Cookieに保存されているCookieの値を参照することによって、セッションが継続されて、カウンタ値が更新されたということです。
最後に、別のブラウザ(シークレットウィンドウ)で同URLにアクセスします。


新たなセッションIDが生成されて、カウンタ値が「1」で登録されてます。
このようにブラウザごとにセッション管理がされているということが確認できます。
まとめ
セッション情報をDynamoDBで保存する方法をご紹介しました。