AWSサーバーレス実践!APIを拡張してS3でWebページを公開してみた

記事タイトルとURLをコピーする

はじめに

注意点:この記事は、前回の記事で作成したアーキテクチャを拡張する形で実践編としています。詳細が気になる方はぜひ前回の記事を閲覧してください!

この記事で紹介すること

  • 既存のアーキテクチャ(図参照)に対してAPI GatewayにCRUD(Create,Read,Update,Delete)機能を追加

    既存のアーキテクチャ

  • Amazon S3を用いてCRUD操作が可能なWebフロントエンドを公開

この記事で学べる事

  • GETのみのAPIでなく完全な機能(CRUD)を持ったAPIの構築

  • API Gatewayとフロントエンドの連携方法

  • Amazon S3での静的ホスティング公開方法

対象読者

  • 前回の記事を読んだ方

  • サーバレスAPIを実践形式で学びたい方

今回実装するもの

実装イメージ

実装イメージ

各サービスの機能と連携

  • Amazon S3:
    静的Webサイトホスティングによるフロントエンド(Webページ)の公開を行う。S3バケットにWebページの構成要素であるHTML,CSS,Javasceiptのファイルを格納する。

  • API Gateway:
    前回の記事と同様の構成(REST API)で、GETメソッドのみでなくPOST(データ追加),PUT(データ更新),DELETE(データ削除)を実装する。

  • Lambda:
    API Gatewayからのリクエストを受けてDynamoDBのデータを操作する処理を行う。今回はAPI Gatewayで作成するHTTPメソッドに応じたCRUD処理の実装を行う。

  • DynamoDB:
    前回同様操作の対象となるDB。今回はCRUD操作の対象となる。

実装手順

Lambda関数のCRUD拡張と権限設定

今回作成するLambda関数はCRUD機能を実装します。
機能の実装に伴い、前回作成した「GetUsersFunction」という関数名は変更する必要があります。
しかし、Lambda関数では関数名の変更はできないので新規に関数を作成し、権限を付与します。

※今回の記事から作業を始めた方について、Lambda関数の作成方法(コードの反映方法含む)やこの際に必要な権限の与え方については前回の記事に掲載しています!

以下、作成するLambdaの情報です。

  • 関数名:UserCrudApiFunction

  • 付与する権限:AmazonDynamoDBFullAccess
     ※今回はサンプルでの実装なので一時的にFullAccessにしていますが必要最小限での権限の構成が望ましいです。

実装コードは以下の通りです。

実装したLambda関数 (クリックして表示)

import json
import boto3
from botocore.exceptions import ClientError
#DynamoDBクライアントを初期化
dynamodb = boto3.resource('dynamodb')
table_name = 'UsersTable'
table = dynamodb.Table(table_name)
def lambda_handler(event, context):
    http_method = event.get('httpMethod')
    path_parameters = event.get('pathParameters',{}) # パスパラメータを取得
    headers = {
        'Content-Type': 'application/json',
        'Access-Control-Allow-Origin': '*', # CORS対応:どのオリジンからのアクセスも許可
        'Access-Control-Allow-Methods': 'GET,POST,PUT,DELETE,OPTIONS', # CORS対応:許可するメソッド
        'Access-Control-Allow-Headers': 'Content-Type,X-Amz-Date,Authorization,X-Api-Key,X-Amz-Security-Token'
    }
    #GETメソッド
    if http_method == 'GET':
        try:
            #DynamoDBから全ての項目を取得
            response = table.scan()
            items = response.get('Items', [])
            # DynamoDBから取得した項目をJSONで返すために整形
            for item in items:
                    for key, value in item.items():
                        if isinstance(value, type(boto3.dynamodb.types.Decimal('0'))):
                            item[key] = float(value) # または int(value)
            return {
                    'statusCode': 200,
                    'headers': headers,
                    'body': json.dumps(items, ensure_ascii=False) # 日本語対応
            }
        except ClientError as e:
            # DynamoDBクライアント関連のエラーハンドリング
            print(f"DynamoDB Client Error: {e.response['Error']['Message']}")
            return {
                'statusCode': 500,
                'headers': {
                    'Content-Type': 'application/json',
                    'Access-Control-Allow-Origin': '*'
                },
                'body': json.dumps({'error': e.response['Error']['Message']})
            }
        except Exception as e:
            # その他の一般的なエラーハンドリング
            print(f"General Error: {e}")
            return {
                'statusCode': 500,
                'headers': {
                    'Content-Type': 'application/json',
                    'Access-Control-Allow-Origin': '*'
                },
                'body': json.dumps({'error': str(e)})
            }
    #POSTメソッド
    elif http_method == 'POST':
        try:
            # リクエストボディからJSONデータを取得し、Python辞書に変換
            body = json.loads(event.get('body', '{}'))
            # USerIDは必須項目なので存在チェック
            if not body.get('UserID'):
                return {
                    'statusCode': 400, #Bad Request
                    'headers': headers,
                    'body': json.dumps({'error': 'UserID is required for POST'})
                }
            # DynamoDBに項目を追加
            table.put_item(Item=body)
            return {
                'statusCode': 201, # Created
                'headers': headers,
                'body': json.dumps({'message': 'Item added successfully', 'item':body}, ensure_ascii=False)
            }
        except ClientError as e:
            # DynamoDBクライアント関連のエラーハンドリング
            print(f"DynamoDB Client Error: {e.response['Error']['Message']}")
            return {
                'statusCode': 500,
                'headers': headers,
                'body': json.dumps({'error': e.response['Error']['Message']})
            }
        except json.JSONDecodeError:
            # リクエストボディのJSON形式が不正な場合のエラー
            return {
                'statusCode': 400,
                'headers': headers,
                'body': json.dumps({'error': 'Invalid JSON in request body'})
            }
        except Exception as e:
            # その他の一般的なエラーハンドリング
            print(f"General Error: {e}")
            return {
                'statusCode': 500,
                'headers': headers,
                'body': json.dumps({'error': str(e)})
            }
    # PUTメソッド(項目更新)の処理
    # このメソッドは /users/{userId} のようにパスパラメータを使用することを想定
    elif http_method == 'PUT':
        # パスパラメータから UserID を取得
        user_id = path_parameters.get('userId') 
        if not user_id:
            return {
                'statusCode': 400,
                'headers': headers,
                'body': json.dumps({'error': 'UserID is required in path for PUT'})
            }
        try:
            body = json.loads(event.get('body', '{}'))
            # 更新する属性(UserID以外の全てのキー)を動的に組み立てる
            update_expression_parts = [] # 例: "Name = :name", "Birthplace = :birthplace"
            expression_attribute_values = {} # 例: {':name': '新しい名前', ':birthplace': '新しい出身地'}
            expression_attribute_names = {} # 例: {'#N': 'Name'} (予約語対策)
            for key, value in body.items():
                if key != 'UserID': # UserIDはプライマリキーなので更新対象にしない
                    # 更新式の組み立て
                    # #keyName は予約語回避のためのプレースホルダー
                    # :valueName は値のプレースホルダー
                    update_expression_parts.append(f"#{key} = :{key}") 
                    expression_attribute_values[f':{key}'] = value
                    expression_attribute_names[f'#{key}'] = key # 実際の属性名をマッピング
            if not update_expression_parts:
                return {
                    'statusCode': 400,
                    'headers': headers,
                    'body': json.dumps({'error': 'No attributes to update'})
                }
            # 更新式全体を組み立てる
            update_expression = "SET " + ", ".join(update_expression_parts)
            # DynamoDBのupdate_itemを呼び出し
            response = table.update_item(
                Key={'UserID': user_id}, # 更新対象の項目を一意に特定するキー
                UpdateExpression=update_expression, # 実際の更新内容を指定する式
                ExpressionAttributeValues=expression_attribute_values, # 更新式のプレースホルダーに対応する値
                ExpressionAttributeNames=expression_attribute_names, # 予約語対策のプレースホルダーに対応する実際の属性名
                ReturnValues="UPDATED_NEW" # 更新後の項目(更新された属性のみ)を返す
            )
            # 正常に更新された場合
            if 'Attributes' in response:
                return {
                    'statusCode': 200,
                    'headers': headers,
                    'body': json.dumps({'message': 'Item updated successfully', 'updated_attributes': response['Attributes']}, ensure_ascii=False)
                }
            # 項目が見つからなかった場合(UpdateExpressionに属性が存在しない、など)
            else:
                return {
                    'statusCode': 404,
                    'headers': headers,
                    'body': json.dumps({'error': 'Item not found or no update occurred'})
                }
        except ClientError as e:
            print(f"DynamoDB Client Error (PUT): {e.response['Error']['Message']}")
            return {
                'statusCode': 500,
                'headers': headers,
                'body': json.dumps({'error': e.response['Error']['Message']})
            }
        except json.JSONDecodeError:
            return {
                'statusCode': 400,
                'headers': headers,
                'body': json.dumps({'error': 'Invalid JSON in request body'})
            }
        except Exception as e:
            print(f"General Error (PUT): {e}")
            return {
                'statusCode': 500,
                'headers': headers,
                'body': json.dumps({'error': str(e)})
            }  
    # DELETEメソッド(項目削除)の処理
    # このメソッドも /users/{userId} のようにパスパラメータを使用することを想定
    elif http_method == 'DELETE':
        # パスパラメータから UserID を取得
        user_id = path_parameters.get('userId') 
        if not user_id:
            return {
                'statusCode': 400,
                'headers': headers,
                'body': json.dumps({'error': 'UserID is required in path for DELETE'})
            }
        try:
            # DynamoDBから項目を削除
            response = table.delete_item(
                Key={'UserID': user_id}, # 削除対象の項目を一意に特定するキー
                ReturnValues="ALL_OLD" # 削除された項目(削除前)を返す
            )
            # 削除された項目が存在した場合
            if 'Attributes' in response:
                return {
                    'statusCode': 200,
                    'headers': headers,
                    'body': json.dumps({'message': 'Item deleted successfully', 'deleted_item': response['Attributes']}, ensure_ascii=False)
                }
            # 削除された項目が存在しなかった場合
            else:
                return {
                    'statusCode': 404, # Not Found (項目が見つからない)
                    'headers': headers,
                    'body': json.dumps({'error': 'Item not found'})
                }
        except ClientError as e:
            print(f"DynamoDB Client Error (DELETE): {e.response['Error']['Message']}")
            return {
                'statusCode': 500,
                'headers': headers,
                'body': json.dumps({'error': e.response['Error']['Message']})
            }
        except Exception as e:
            print(f"General Error (DELETE): {e}")
            return {
                'statusCode': 500,
                'headers': headers,
                'body': json.dumps({'error': str(e)})
            }
    # 未対応のHTTPメソッドがリクエストされた場合
    else:
        return {
            'statusCode': 405, # Method Not Allowed (許可されていないHTTPメソッド)
            'headers': headers,
            'body': json.dumps({'error': 'Method Not Allowed'})
        }

API Gateway の再設定

API Gatewayに存在する既存リソースとメソッドの削除

もしも前回の記事で作成したAPI Gatewayを流用する場合、「2.今回実装するもの」で削除したはずのLambda関数が紐づいたメソッドが残っていると思います。
そこで、/usersリソースごと削除した後、新たなLambda関数を紐付けたメソッドで再構成します。

1.API Gatewayの管理コンソールから既存のリソースとメソッドを削除(流用する場合はここから)

メソッドとリソースの削除ボタン。ルートリソース「/」のみが残る状態に。

2.API Gatewayのリソースの作成

  • リソース名:users
  • リソースパス:users
  • 「CORSを有効にする」にチェック

3./usersリソースにメソッド追加(GET,POST)
※GET,POSTメソッド追加の詳細な手順については前回の記事に記載

4. /users リソース配下にuserIdというパスパラメータ ({userId}) を持つ子リソースを作成(CORSを有効にする にチェック)。

パスパラメータを入力しCORSを有効化する。

5./users/{userId}リソース配下に PUT および DELETE メソッドを追加。
※GET,POSTのメソッドとPUT,DELETEのメソッドを別々にしたのはパスパラメータによってuserIdの値をフロントエンドから受け取らなければ処理ができないという問題があるため

6.変更を反映させるために、必ずAPIをデプロイ(既存の prod ステージを更新)。

ステージはprodでデプロイ

フロントエンドの準備とS3での静的ホスティング

フロントエンドの準備

今回のフロントエンド構成はシンプルです。 構成としてはindex.html, style.css, script.js の3ファイル構成になります。

以下、コード構成です。

index.html (クリックして表示)

<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>サーバーレスユーザー管理アプリ</title>
    <link rel="stylesheet" href="style.css">
</head>
<body>
    <div class="container">
        <h1>ユーザー管理ダッシュボード</h1>
        <div class="section">
            <h2>ユーザー一覧 (GET)</h2>
            <button id="getUsersButton">ユーザー一覧を取得</button>
            <div id="message"></div>
            <table id="userTable">
                <thead>
                    <tr>
                        <th>UserID</th>
                        <th>Name</th>
                        <th>Birthplace</th>
                        <th>操作</th>
                    </tr>
                </thead>
                <tbody>
                    </tbody>
            </table>
        </div>
        <div class="section">
            <h2>新規ユーザー登録 (POST)</h2>
            <label for="postUserId">UserID:</label>
            <input type="text" id="postUserId">
            <label for="postName">Name:</label>
            <input type="text" id="postName">
            <label for="postBirthplace">Birthplace:</label>
            <input type="text" id="postBirthplace">
            <button id="postUserButton">ユーザー登録</button>
        </div>
        <div class="section">
            <h2>ユーザー情報更新 (PUT)</h2>
            <label for="putUserId">更新対象UserID:</label>
            <input type="text" id="putUserId">
            <label for="putName">Name (Optional):</label>
            <input type="text" id="putName">
            <label for="putBirthplace">Birthplace (Optional):</label>
            <input type="text" id="putBirthplace">
            <button id="putUserButton">ユーザー更新</button>
        </div>
        <div class="section">
            <h2>ユーザー削除 (DELETE)</h2>
            <label for="deleteUserId">削除対象UserID:</label>
            <input type="text" id="deleteUserId">
            <button id="deleteUserButton" class="delete">ユーザー削除</button>
        </div>
    </div>
    <script src="script.js"></script>
</body>
</html>

style.css (クリックして表示)

body {
    font-family: Arial, sans-serif;
    margin: 20px;
    background-color: #f4f4f4;
    color: #333;
}
.container {
    max-width: 900px;
    margin: 0 auto;
    background-color: #fff;
    padding: 20px;
    border-radius: 8px;
    box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
h1, h2 {
    color: #0056b3;
    border-bottom: 2px solid #eee;
    padding-bottom: 10px;
    margin-bottom: 20px;
}
.section {
    margin-bottom: 30px;
    padding: 15px;
    border: 1px solid #ddd;
    border-radius: 5px;
    background-color: #f9f9f9;
}
label {
    display: block;
    margin-bottom: 5px;
    font-weight: bold;
}
input[type="text"], input[type="button"], button {
    width: calc(100% - 12px);
    padding: 10px;
    margin-bottom: 10px;
    border: 1px solid #ccc;
    border-radius: 4px;
    box-sizing: border-box; /* paddingを含めて幅を計算 */
}
input[type="button"], button {
    background-color: #007bff;
    color: white;
    border: none;
    cursor: pointer;
    font-weight: bold;
    transition: background-color 0.3s ease;
}
input[type="button"]:hover, button:hover {
    background-color: #0056b3;
}
button.delete {
    background-color: #dc3545;
}
button.delete:hover {
    background-color: #c82333;
}
table {
    width: 100%;
    border-collapse: collapse;
    margin-top: 15px;
}
th, td {
    border: 1px solid #ddd;
    padding: 8px;
    text-align: left;
}
th {
    background-color: #f2f2f2;
}
 #message {
    margin-top: 15px;
    padding: 10px;
    border-radius: 4px;
    background-color: #e2e3e5;
    color: #4a4a4a;
    font-weight: bold;
}
.error {
    background-color: #f8d7da;
    color: #721c24;
    border-color: #f5c6cb;
}

script.js

script.js (クリックして表示)

// --- 設定情報 ---
// ここをあなたのAPI GatewayのベースURLに置き換えてください
// 例: "https://abcdef1234.execute-api.ap-northeast-1.amazonaws.com/prod"
const API_BASE_URL = "https://abcdef1234.execute-api.ap-northeast-1.amazonaws.com/prod"; 
//ここをあなたのAPI GatewayのAPIキーに置き換えてください
//キーを設定していない場合は必要なし
const API_KEY = "APIkey"; 
// --- DOM要素の宣言。これらの変数は、HTMLの要素が完全に読み込まれてから値が設定されます。
let userTableBody;
let messageDiv;
let getUsersButton;
let postUserButton;
let putUserButton;
let deleteUserButton;

// 汎用メッセージ表示関数
// 画面上部にメッセージを表示したり、エラー時に赤く表示したりします。
function showMessage(msg, isError = false) {
    // messageDivがまだ取得できていない場合のガード (DOMContentLoaded前など)
    if (!messageDiv) {
        console.warn('MessageDiv is not yet available, message:', msg);
        return;
    }
    messageDiv.textContent = msg; // メッセージを設定
    messageDiv.className = isError ? 'error' : ''; // エラー時はCSSクラス 'error' を適用して赤背景に
    if (!isError) {
        // 成功メッセージは短時間(3秒)で消える
        setTimeout(() => {
            messageDiv.textContent = '';
            messageDiv.className = '';
        }, 3000);
    }
}
// --- 汎用API呼び出し関数 ---
// fetch APIを使って、API GatewayのエンドポイントへHTTPリクエストを送信します。
async function callApi(method, path, body = null) {
    // 完全なURLを構築 (例: https://your-api-id.execute-api.region.amazonaws.com/prod/users/0001)
    const url = '${API_BASE_URL}${path}'; 
// リクエストのオプションを設定。メソッド、ヘッダー、ボディなど。
    const options = {
        method: method, // HTTPメソッド (GET, POST, PUT, DELETE)
        headers: {
            'x-api-key': API_KEY, // APIキーをHTTPヘッダー 'x-api-key' に含める
            'Content-Type': 'application/json' // リクエストボディがJSON形式であることを示す
        }
    };
   // POSTやPUTの場合、リクエストボディをJSON文字列に変換して追加
    if (body) {
        options.body = JSON.stringify(body);
    }
    try {
        // fetch APIでAPI呼び出しを実行します。
        const response = await fetch(url, options); 
        // レスポンスのContent-Typeをチェックし、JSONでない場合はテキストとして扱う
        const contentType = response.headers.get("content-type");
        let data;
        if (contentType && contentType.indexOf("application/json") !== -1) {
            data = await response.json(); // レスポンスボディをJSONとしてパース
        } else {
            // JSON形式ではない場合、テキストとして取得しコンソールに警告
            data = await response.text();
            console.warn("API response was not JSON:", data);
        }
        // HTTPステータスコードが200番台以外の場合
        if (!response.ok) { 
            // APIからのエラーメッセージがあればそれを表示、なければ一般的なステータス表示
            const errorMessage = data && (data.error || data.message) ? data.error || data.message : response.statusText;
            showMessage('Error: ${errorMessage}', true);
            return null; // 呼び出し失敗としてnull
        }
        showMessage('API呼び出し成功!');
        return data; // 呼び出し成功としてレスポンスデータ
    } catch (error) {
        // ネットワークエラーなど、API呼び出し自体が失敗した場合
        console.error('API呼び出しエラー:', error);
        showMessage('API呼び出しエラー: ${error.message}', true);
        return null;
    }
}
// --- GET: ユーザー一覧取得 ---
// DynamoDBに保存されているユーザーデータを取得し、テーブルに表示します。
async function getUsers() {
    // userTableBodyがまだ取得できていない場合のガード
    if (!userTableBody) {
        console.warn('UserTableBody is not yet available.');
        return;
    }
    userTableBody.innerHTML = ''; // テーブルの中身を一度クリアします
    // APIを呼び出し (GETメソッド, パスは '/users' なので空文字列)
    const result = await callApi('GET', '/users'); 
    if (result) {
        let users;
        // API Gatewayの設定によっては、直接JSONオブジェクトが返されることもあるため、型をチェック。
        if (typeof result.body === 'string') {
            try {
                users = JSON.parse(result.body);
            } catch (e) {
                console.error('Failed to parse JSON from result.body:', result.body, e);
                showMessage('ユーザーデータの解析に失敗しました。APIからの応答形式を確認してください。', true);
                return;
            }
        } else if (Array.isArray(result) || (typeof result === 'object' && result !== null)) {
            // result自体がJSONオブジェクトや配列である可能性 (API Gatewayの非プロキシ統合など)
            users = result;
        } else {
            console.error('Unexpected API response format:', result);
            showMessage('予期しないAPI応答形式です。', true);
            return;
        }
        // usersが配列であることを確認。
        if (!Array.isArray(users)) {
            showMessage('APIから返されたユーザーデータが配列ではありません。', true);
            console.error('Expected an array of users, but got:', users);
            return;
        }       
        // 取得したユーザーデータをテーブルに挿入。
        users.forEach(user => {
            const row = userTableBody.insertRow(); // 新しい行をテーブルボディに追加
            row.insertCell().textContent = user.UserID; // UserIDセル
            row.insertCell().textContent = user.Name; // Nameセル
            row.insertCell().textContent = user.Birthplace; // Birthplaceセル
            const opsCell = row.insertCell(); // 操作ボタン用のセル            
            // 編集ボタンの作成
            const editButton = document.createElement('button');
            editButton.textContent = '編集';
            editButton.onclick = () => fillUpdateForm(user); // クリックで更新フォームにデータ入力
            opsCell.appendChild(editButton);
            // 削除ボタンの作成
            const deleteButton = document.createElement('button');
            deleteButton.textContent = '削除';
            deleteButton.className = 'delete'; // CSSで赤色にするためのクラス
            deleteButton.onclick = async () => {
                // 削除前に確認ダイアログを表示します。
                if (confirm('UserID: ${user.UserID} を削除しますか?')) {
                    await deleteUser(user.UserID); // 削除処理を呼び出し
                    getUsers(); // 削除後にユーザー一覧を再取得して画面を更新
                }
            };
            opsCell.appendChild(deleteButton);
        });
        showMessage('ユーザー一覧を更新しました。');
    }
}
// --- POST: 新規ユーザー登録 ---
// 入力フォームからデータを取得し、新しいユーザーをDynamoDBに登録します。
async function postUser() {
    // 入力フォームから値を取得
    const userId = document.getElementById('postUserId').value;
    const name = document.getElementById('postName').value;
    const birthplace = document.getElementById('postBirthplace').value;
    // 必須項目(UserID, Name, Birthplace)のチェック
    if (!userId || !name || !birthplace) {
        showMessage('UserID, Name, Birthplaceは必須です。', true);
        return;
    }
    // 新規ユーザーデータオブジェクトを構築
    const newUser = { UserID: userId, Name: name, Birthplace: birthplace }; 
    // APIを呼び出し (POSTメソッド, パスは '/users')
    const result = await callApi('POST', '/users', newUser); 
    if (result) {
        showMessage('ユーザー ${userId} を登録しました。');
        // フォーム入力欄をクリアします
        document.getElementById('postUserId').value = '';
        document.getElementById('postName').value = '';
        document.getElementById('postBirthplace').value = '';
        getUsers(); // 登録後に一覧を再取得して画面を更新
    }
}
// --- PUT: ユーザー情報更新 ---
// 編集ボタンが押されたときに、更新フォームに既存データを自動入力するヘルパー関数
function fillUpdateForm(user) {
    document.getElementById('putUserId').value = user.UserID;
    document.getElementById('putName').value = user.Name;
    document.getElementById('putBirthplace').value = user.Birthplace;
    showMessage('UserID: ${user.UserID} の情報を更新フォームに読み込みました。');
}
async function putUser() {
    // 更新フォームから値を取得
    const userId = document.getElementById('putUserId').value;
    const name = document.getElementById('putName').value;
    const birthplace = document.getElementById('putBirthplace').value;
    // 更新対象のUserIDは必須です。
    if (!userId) {
        showMessage('更新対象のUserIDは必須です。', true);
        return;
    }
    // 更新する属性のみを含むオブジェクトを構築します (入力がなければ含めません)。
    const updateData = {};
    if (name) updateData.Name = name; 
    if (birthplace) updateData.Birthplace = birthplace; 
    // 更新する属性が何も入力されていない場合のエラーチェック
    if (Object.keys(updateData).length === 0) {
        showMessage('更新する属性を少なくとも一つ入力してください。', true);
        return;
    }
    // APIを呼び出し (PUTメソッド, パスは '/users/{userId}')
    const result = await callApi('PUT', '/users/${userId}', updateData); 
    if (result) {
        showMessage(  ' ユーザー  ${userId}  の情報を更新しました。 ');
        // フォーム入力欄をクリアします
        document.getElementById('putUserId').value = '';
        document.getElementById('putName').value = '';
        document.getElementById('putBirthplace').value = '';
        getUsers(); // 更新後に一覧を再取得して画面を更新
    }
}
// --- DELETE: ユーザー削除 ---
// 特定のユーザーをDynamoDBから削除します。
// HTMLのボタンから呼ばれる際は引数なし、テーブル内の削除ボタンから呼ばれる際は引数あり
async function deleteUser(userIdFromButton = null) { 
    // 引数があればそれを使い、なければフォームの値を使います。
    const userId = userIdFromButton || document.getElementById('deleteUserId').value;
    // 削除対象のUserIDは必須です。
    if (!userId) {
        showMessage('削除対象のUserIDは必須です。', true);
        return;
    }
    // 削除前に確認ダイアログを表示し、ユーザーに最終確認を促す。
    if (!confirm( '本当にユーザー ${userId} を削除しますか?' )) {
        return; // 確認がキャンセルされたら処理を中断
    }
    // APIを呼び出し (DELETEメソッド, パスは '/users/{userId}')
    const result = await callApi('DELETE', '/users/${userId}');
    if (result) {
        showMessage('ユーザー ${userId} を削除しました' );
        document.getElementById('deleteUserId').value = ''; // フォーム入力欄をクリア
        getUsers(); // 削除後に一覧を再取得して画面を更新
    }
}
// --- DOMContentLoadedイベントリスナー ---
// この部分は、HTMLコンテンツが全て読み込まれ、DOMツリーが構築された後に実行。
// これにより、「Cannot read properties of null」のようなエラーを防ぐ。
document.addEventListener('DOMContentLoaded', () => {
    // DOM要素の取得をここで行います。
    userTableBody = document.querySelector('#userTable tbody');
    messageDiv = document.getElementById('message');
    getUsersButton = document.getElementById('getUsersButton');
    postUserButton = document.getElementById('postUserButton');
    putUserButton = document.getElementById('putUserButton');
    deleteUserButton = document.getElementById('deleteUserButton');
    // 各ボタン要素がHTMLに存在するかチェックしてからイベントリスナーを設定。
    if (getUsersButton) getUsersButton.addEventListener('click', getUsers);
    if (postUserButton) postUserButton.addEventListener('click', postUser);
    if (putUserButton) putUserButton.addEventListener('click', putUser);
    if (deleteUserButton) deleteUserButton.addEventListener('click', deleteUser);
    // ウェブページが最初にロードされた時に、自動的にユーザー一覧を取得して表示。
    getUsers();
});

S3での静的ホスティング

では、いよいよフロントエンドの構成ファイルをAmazon S3へアップロードし、静的ウェブサイトホスティングとして公開します。
ここからは、バケットの作成やポリシー設定、公開設定に関して記載しています。

1.S3コンソールに移動し、バケットの作成

 バケット名:my-serverless-crud-app-dragont (適当な名前でいいです)
 リージョン:API Gatewayの場所と同一
 パブリックアクセスのブロック:無効
 その他の設定はデフォルトのままバケットを作製

2.対象バケットのコンソールを開き、ファイルをアップロード

ファイルをドラッグアンドドロップで設定、下スクロール後アップロードボタンを押す

3.対象バケットのコンソールを開き「プロパティ」タブから「静的ウェブサイトホスティング」のメニューを見つけ、編集ボタンを押す。

「プロパティ」タブ最下部に存在する

4.編集画面よりホスティングに必要な設定を行う。(画像参照)

ホスティングを有効、インデックスドキュメントを設定し変更の保存をする

5.再度「プロパティ」タブの「静的ウェブサイトホスティング」セクションに戻ると、「バケットウェブサイトエンドポイント」のURLが表示されているのでこのURLを必ずコピーして控えておく

動作確認

S3での静的ホスティングの手順5でコピーしたURLをwebブラウザにて入力すると以下のような画面が出力されます。

前回の参照のみの実装ではなく更新や削除を行ったユーザがユーザ一覧を取得するたびに反映されていきます。

終わりに

今回はAPI Gateway + Lambda + DynamoDB の組み合わせでCRUD API を構築し、そのAPIをAmazon S3でホスティングしたシンプルなフロントエンドから利用する一連の流れを体験しました。

その中で、実際に手を動かして実装することで私自身サーバーレスアーキテクチャの具体的なイメージが湧いたと感じています。

また、サーバーレスの学習は奥深く常に新しいサービスや機能が登場しています。
しかし、今回得た知識と経験はその広大なサーバレスという技術範囲の学習を援助するための知識となると思います。

今回の一連の学習がこの記事を読んでいただいている読者の方にとってのサーバレス構成の技術獲得への一歩目になれましたら幸いです!

山本 竜也 (記事一覧)

2025年度新入社員です!AWSについてはほぼ未経験なのでたくさんアウトプットできるよう頑張ります✨