3
6

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

AWS SAM:S3 ホスティング + Lambda + API Gateway + DynamoDB 構成を試してみた

Posted at

はじめに

AWS SAM(Serverless Application Model)は、API Gateway + Lambda + DynamoDB + S3 などを簡単に構築できる便利なフレームワークです。

この記事では、S3 ホスティング × Lambda × API Gateway × DynamoDB を連携させた構成を、AWS SAM を使って一括デプロイし、API 経由で DynamoDB のデータを取得・表示するまでの実践手順をまとめています。

※内容に不備などがございましたら、お手数ですが優しくご指摘いただけますと幸いです。

書こうと思ったきっかけ

個人的に AWS SAM をキャッチアップしたくて勉強を進めていたのですが、やってみたかった検証が一通りまとまったので、整理してみました。

1本目の記事では、シンプルな Hello World チュートリアルを実施した内容をまとめています。

興味のある方はぜひご覧ください!

1本目はこんな感じの検証内容です

Screenshot 2025-06-07 at 7.41.42.png

引用元:https://docs.aws.amazon.com/ja_jp/serverless-application-model/latest/developerguide/serverless-getting-started-hello-world.html

2本目の記事では、API 経由で DynamoDB に保存されたテストデータを取得し、Lambda 関数からレスポンスとして返す構成を検証しています。

こちらもぜひご参考ください!

2本目はこんな感じの検証内容です

Screenshot 2025-06-07 at 16.08.20.png

引用元:https://docs.aws.amazon.com/ja_jp/apigateway/latest/developerguide/http-api-dynamo-db.html

今回の検証内容

以下のようなシンプルかつ実用的なサーバーレス構成になります:

+-------------------+
|    Web Browser    |
+-------------------+
         │
         ▼
 静的Web(HTML/JS) ← S3でホスティング
         │
         ▼
 JavaScript → API呼び出し(fetchなど)
         │
         ▼
 API Gateway → Lambda → DynamoDB

構成図はこんな感じになります

Screenshot 2025-06-08 at 13.13.22.png

技術構成の詳細

番号 構成要素 説明
S3 静的Webサイト(HTML / CSS / JS)をホスティング
JavaScript API Gateway のエンドポイントを呼び出すコード
API Gateway + Lambda + DynamoDB バックエンド処理(データ取得)

SAM で構築する範囲

  • Lambda 関数
  • DynamoDB テーブル
  • API Gateway エンドポイント
  • S3 バケット(静的ホスティング)

ファイル構成と内容

ディレクトリ構成

sam-s3-api-example/
├── template.yaml         # SAM テンプレートインフラ定義
├── samconfig.toml        # デプロイ設定ファイル--guided の保存内容
├── hello_world/
   ├── app.py            # Lambda 関数DynamoDB スキャン
   └── __init__.py       # 空で OKPython モジュール認識
└── frontend/
    └── index.html        # 静的ホスティングされる HTML

template.yaml(インフラ全体定義)

AWSTemplateFormatVersion: '2010-09-09'
Transform: AWS::Serverless-2016-10-31
Description: API Gateway + Lambda + DynamoDB + S3 Static Web Hosting

Globals:
  Function:
    Timeout: 10

Resources:

  WebHostingBucket:
    Type: AWS::S3::Bucket
    Properties:
      WebsiteConfiguration:
        IndexDocument: index.html
      OwnershipControls:
        Rules:
          - ObjectOwnership: ObjectWriter
      PublicAccessBlockConfiguration:
        BlockPublicAcls: false
        BlockPublicPolicy: false
        IgnorePublicAcls: false
        RestrictPublicBuckets: false

  BucketPolicy:
    Type: AWS::S3::BucketPolicy
    Properties:
      Bucket: !Ref WebHostingBucket
      PolicyDocument:
        Statement:
          - Action: s3:GetObject
            Effect: Allow
            Resource: !Sub "${WebHostingBucket.Arn}/*"
            Principal: "*"

  ItemsTable:
    Type: AWS::DynamoDB::Table
    Properties:
      TableName: ItemsTable
      AttributeDefinitions:
        - AttributeName: id
          AttributeType: S
      KeySchema:
        - AttributeName: id
          KeyType: HASH
      BillingMode: PAY_PER_REQUEST

  MyApi:
    Type: AWS::Serverless::Api
    Properties:
      Name: MyApi
      StageName: Prod
      Cors:
        AllowMethods: "'GET,OPTIONS'"
        AllowHeaders: "'Content-Type,X-Amz-Date,Authorization,X-Api-Key'"
        AllowOrigin: "'*'"

  GetItemsFunction:
    Type: AWS::Serverless::Function
    Properties:
      CodeUri: hello_world/
      Handler: app.lambda_handler
      Runtime: python3.12
      Environment:
        Variables:
          TABLE_NAME: !Ref ItemsTable
      Policies:
        - DynamoDBReadPolicy:
            TableName: !Ref ItemsTable
      Events:
        GetItemsApi:
          Type: Api
          Properties:
            RestApiId: !Ref MyApi
            Path: /items
            Method: get

Outputs:
  WebURL:
    Value: !GetAtt WebHostingBucket.WebsiteURL

  ApiURL:
    Value: !Sub "https://${MyApi}.execute-api.${AWS::Region}.amazonaws.com/Prod/items"

コードの中でやっていること

  • S3 バケットを作成

    • 静的 Web サイトホスティングを有効化(index.html がトップページ)
    • パブリックアクセスを許可(全員がオブジェクトを閲覧可能
    • バケットポリシーを設定し、バケット内のファイル(HTML など)を 誰でも取得できるように設定
  • DynamoDB テーブルを作成

    • テーブル名:ItemsTable
    • 主キー:id(文字列型)
    • 課金モード:PAY_PER_REQUEST(従量課金)
  • API Gateway を作成

    • 名前:MyApi
    • ステージ:Prod
    • CORS 対応:全オリジンからの GET / OPTIONS を許可
  • Lambda 関数を定義

    • コード配置ディレクトリ:hello_world/
    • ハンドラー関数:app.lambda_handler
    • DynamoDB からデータを読み取る機能 を実装
    • API Gateway の /items GET リクエストに応答
  • 出力値(Outputs)を設定

    • WebURL:S3 静的サイトの URL を出力
    • ApiURL:API Gateway のエンドポイント URL(例:/items)を出力

hello_world/app.py(Lambda関数)

import os
import boto3
import json
from decimal import Decimal

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

# Decimal を float/int に変換できるようにするヘルパー関数
class DecimalEncoder(json.JSONEncoder):
    def default(self, o):
        if isinstance(o, Decimal):
            # 整数化できる場合は int、それ以外は float
            return int(o) if o % 1 == 0 else float(o)
        return super(DecimalEncoder, self).default(o)

def lambda_handler(event, context):
    print("=== Lambda triggered ===")
    try:
        response = table.scan()
        print("Scan result:", response)
        items = response.get('Items', [])

        return {
            'statusCode': 200,
            'headers': {
                "Access-Control-Allow-Origin": "*",
                "Access-Control-Allow-Headers": "Content-Type",
                "Access-Control-Allow-Methods": "GET,OPTIONS",
                "Content-Type": "application/json"
            },
            'body': json.dumps(items, cls=DecimalEncoder)
        }
    except Exception as e:
        print("Error occurred:", str(e))
        return {
            'statusCode': 500,
            'headers': {
                "Access-Control-Allow-Origin": "*"
            },
            'body': json.dumps({'error': str(e)})
        }

コードの中でやっていること

  • DynamoDB のデータを全件取得し、JSON 形式でレスポンスとして返す

    • Lambda 関数内で scan() を使用して DynamoDB の全データを取得
    • クライアントには JSON 形式で整形されたレスポンス を返す
  • API Gateway 経由で呼び出され、CORS に対応

    • フロントエンド(S3)からのリクエストを許可するため、CORS ヘッダーを付与
    • 例:Access-Control-Allow-Origin: * をレスポンスヘッダーに追加
  • Decimal 型(DynamoDB 特有)を Python の int や float に変換してから返す

    • DynamoDB の scan() 結果には Decimal 型が含まれるため、
      Python の int / float に変換する処理を追加(例:カスタム JSON エンコーダーの実装)

frontend/index.html(静的Web)

<!-- API Gateway エンドポイントからデータ取得して表示 -->
<!DOCTYPE html>
<html lang="ja">
<head>
  <meta charset="UTF-8">
  <title>Item Viewer</title>
</head>
<body>
  <h1>Items from DynamoDB</h1>
  <ul id="items"></ul>

  <script>
    fetch('https://YOUR_API_ID.ap-northeast-1.amazonaws.com/Prod/items')
      .then(res => res.json())
      .then(jsonString => {
        const data = typeof jsonString === "string" ? JSON.parse(jsonString) : jsonString;

        const ul = document.getElementById('items');
        data.forEach(item => {
          const li = document.createElement('li');
          li.textContent = `${item.name}${item.id}): ¥${item.price}`;
          ul.appendChild(li);
        });
      })
      .catch(err => {
        console.error("Fetch error:", err);
      });
  </script>
</body>
</html>

YOUR_API_ID は実際のデプロイ後に表示される API URL に置き換えてください。

コードの中でやっていること

  • API Gateway のエンドポイントにアクセス

    • fetch('https://YOUR_API_ID.ap-northeast-1.amazonaws.com/Prod/items') で、Lambda 経由で DynamoDB のデータを取得
  • 取得した JSON データを処理

    • レスポンスが 文字列形式かオブジェクト形式かをチェック し、安全にパース
    • 各アイテムをループで処理
  • HTML の <ul> 要素にリスト表示

    • 取得したデータを元に <li> 要素を作成
    • <ul id="items"> に追加
    • 表示形式の例ホンダ(car1): ¥150
  • エラー時の処理

    • fetch() に失敗した場合は、コンソールにエラーメッセージを出力

実際に AWS 環境上にデプロイしてみた

SAM テンプレートをビルド

sam build

問題なくビルドされていることが確認できました!

Screenshot 2025-06-08 at 13.18.06.png

対話形式でデプロイ

sam deploy --guided

例)対話での入力内容:

Stack Name [sam-app]: sam-s3-api-example
AWS Region [ap-northeast-1]: ap-northeast-1
#Shows you resources changes to be deployed and require a 'Y' to initiate deploy
Confirm changes before deploy [y/N]: n
#SAM needs permission to be able to create roles to connect to the resources in your template
Allow SAM CLI IAM role creation [Y/n]: y
#Preserves the state of previously provisioned resources when an operation fails
Disable rollback [y/N]: n
GetItemsFunction has no authentication. Is this okay? [y/N]: y
Save arguments to configuration file [Y/n]: y
SAM configuration file [samconfig.toml]: 
SAM configuration environment [default]: 

表示された出力(Outputs)にある WebURL と ApiURL を控えておく

ここが大事!!(マネコン上でも確認できますが...!)

Screenshot 2025-06-08 at 13.23.38.png

frontend/index.html の API URL を修正してビルドディレクトリへコピー

HTML ファイルを S3 バケットへアップロード

aws s3 cp frontend/index.html s3://your-bucket-name/

こんな感じS3バケットに正常にアップロードされていることが確認できました!

Screenshot 2025-06-08 at 13.24.36.png

DynamoDBの動作確認用のテストデータとして、以下のコマンドを実行してください!

aws dynamodb batch-write-item --request-items '{
  "ItemsTable": [
    {
      "PutRequest": {
        "Item": {
          "id": { "S": "car1" },
          "name": { "S": "ホンダ" },
          "price": { "N": "150" }
        }
      }
    },
    {
      "PutRequest": {
        "Item": {
          "id": { "S": "car2" },
          "name": { "S": "トヨタ" },
          "price": { "N": "180" }
        }
      }
    },
    {
      "PutRequest": {
        "Item": {
          "id": { "S": "car3" },
          "name": { "S": "スズキ" },
          "price": { "N": "130" }
        }
      }
    }
  ]
}'

問題なくテストデータが投入されたことが、マネジメントコンソール上でも確認できました!

Screenshot 2025-06-08 at 13.29.26.png

ブラウザで S3のオブジェクト URL にアクセスし、DynamoDB のデータが表示されることを確認

Screenshot 2025-06-08 at 13.31.33.png

まとめ

ここまでお読みいただき、ありがとうございました!

API Gateway + Lambda + DynamoDB + S3 の組み合わせは SAM で簡単に構築でき感動しました...!

今回は GET 処理 に絞った構成でしたが、POST リクエスト や Cognito 認証 との連携など、さらに応用的な構成にも発展させることが可能なので挑戦してみたいです!

不要になったリソースは以下のコマンドで削除できますので、適宜削除しておくことをおすすめします。

aws cloudformation delete-stack --stack-name sam-app

今後も応用的な構成に対応できるよう、引き続きキャッチアップを進めていきたいと思います...!

参考文献

3
6
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
6

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?