API GatewayとLambdaで作られたWebアプリのセッション情報をDynamoDBに保存する

API GatewayとLambdaで作られたWebアプリのセッション情報をDynamoDBに保存する

API GatewayとLambdaで作られたWebアプリのセッション情報をDynamoDBに保存する

AWS DVAの出題範囲の1つでもある、AWSのサービスによる開発に関する内容です。

API GatewayおよびLambda関数で作成するWebアプリにおいて、セッション情報をどのように扱うかについて考えます。

今回はセッション情報をDynamoDBで保存する方法をご紹介します。
なおセッション管理には、Cookieを使用します。

構築する環境

Diagram of storing session data from web apps created with API Gateway and Lambda in DynamoDB.

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に関する基本的な事項については、以下のページをご確認ください。

https://awstut.com/2021/12/03/serverless-api-gateway-lambda

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テーブルに関する基本的な事項については、以下のページをご確認ください。

https://awstut.com/2022/02/07/introduction-to-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関数で実行するコードをインライン形式で記載します。
詳細につきましては、以下のページをご確認ください。

https://awstut.com/2022/02/02/3-patterns-for-creating-lambda-with-cloudformation

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スタックを作成します。
スタックの作成および各スタックの確認方法については、以下のページをご確認ください。

https://awstut.com/2021/12/02/cloudformation-nested-stacks

各スタックのリソースを確認した結果、今回作成された主要リソースの情報は以下の通りです。

  • DynamoDBテーブル:dva-03-006-table
  • Lambda関数:dva-03-006-function
  • APIゲートウェイのエンドポイント:dva-03-006-HttpApi

AWS Management Consoleから各リソースを確認します。

まずDynamoDBテーブルを確認します。

Detail of DyanmoDB 1.

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

Lambda関数を確認します。

Detail of Lambda 1.

正常に関数が作成されていることがわかります。

API Gatewayを確認します。

Detail of API Gateway 1.
Detail of API Gateway 2.

正常にHTTPタイプのAPI Gatewayが作成されていることがわかります。
API GatewayとLambda関数が統合されていることもわかります。

動作確認

準備が整いましたので、API Gatewayのエンドポイントにアクセスします。

Detail of API Gateway 3.

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

Developer ToolでCookieを確認します。

Detail of API Gateway 4.

確かに「cookie-test」という名前でセッションIDが設定されています。

もう一度、同じページにアクセスします。

Detail of API Gateway 5.

カウンタ値が更新されました。

DynamoDBテーブルを確認します。

Detail of DyanmoDB 2.

テーブルには、セッションIDおよびカウンタ値を保存されていることがわかります。

このように、Cookieに保存されているCookieの値を参照することによって、セッションが継続されて、カウンタ値が更新されたということです。

最後に、別のブラウザ(シークレットウィンドウ)で同URLにアクセスします。

Detail of API Gateway 6.
Detail of DyanmoDB 3.

新たなセッションIDが生成されて、カウンタ値が「1」で登録されてます。
このようにブラウザごとにセッション管理がされているということが確認できます。

まとめ

セッション情報をDynamoDBで保存する方法をご紹介しました。