完全無料!?AWSサーバーレスAPIで作る静的サイト内ランキング

こんにちは!
今日はAWSのAlways Free枠を利用して(月の上限を超えない限り)完全無料でゲームのランキングボードを作成していきたいと思います。
作成するのは、シンプルな「ナンバーソートゲーム」と、そのスコアを記録・表示する「ランキングボード」です。
サーバー管理の手間をかけずに、動的なWebアプリケーションを構築してみましょう。
この記事は2025年8月に作成しています。
閲覧された時期によっては記載内容が変更されている可能性があります。
ご注意ください。
概要
今回作成するのは:
数字を順番に並べ替える「ナンバーソートゲーム」
プレイヤーのスコアを登録する「ランキングボード」
自分の順位を検索できる「順位検索ページ」
です。
構成図は以下の通りです。
API Gateway が「入り口」、Lambda が「処理」、DynamoDB が「データ保存」を担当します。

以下はナンバーソートゲームのシーケンス図です。

以下は、ランキング表示ページのシーケンス図です。

ステップ1. 静的Webサイト側の準備
- ゲームページ
このサイトに、ゲーム用のHTML、CSS、JavaScriptを組み込むためのカスタムテンプレート (`page-game.hbs`) を作成し、固定ページとしてゲーム画面を用意しました。 - 順位検索ページ
新たに、ランキング機能のための専用ページを作成します。このページには、上位20位までのランキングが常に表示され、その下で自分の名前を検索して個別の順位を確認できます。
このあたりは本題(Alway Free枠でランキングボードを作成する)からは逸れるので詳細は割愛します。
ステップ2.Lambda と API Gateway の準備
今回利用するサービスについて、簡単に説明を入れつつ作成していきます。
Lambdaについて
Lambda は一言でいうと、「サーバーを立てずにプログラムを動かす仕組み」
自分でPC等のハードウェアを用意し、環境構築することなく、作成したプログラムを動かすことができます。
必要なハードウェアは裏でAWSが用意してくれて、管理も必要ないのでとても楽です。
https://aws.amazon.com/jp/lambda/
API Gatewayについて
API Gateway は一言でいうと。「外部からLambdaを呼び出す窓口」
クライアント(今回でいえばブラウザのゲームページや順位検索ページ)からのリクエストを受け取り、それを Lambda に渡して処理させます。
Lambdaはインターネットから直接アクセスすることができないため、ゲームを実施した後のランキング登録処理や、ランキングデータの呼び出しのためにAPI Gatewayを構築します。
https://aws.amazon.com/jp/api-gateway/
まずは DynamoDB を作らずに、Lambda と API が正しく動くか確認します。
ステップ2.1.Lambdaの作成
AWSのマネジメントコンソールにアクセスし、左上の検索テキストボックスから、Lambdaと入力してLambdaの画面に移動します。

右上の「関数を作成」から作成していきましょう。
基本的な情報
項目 | 推奨設定 | 備考 |
---|---|---|
関数名 | submit-score / get-ranking / search-rank | 半角英数字とハイフン/アンダースコアのみ。わかりやすい名前にする |
ランタイム | Python 3.11 | 今回のサンプルコードが Python なのでこれを選択 |
アーキテクチャ | arm64 | コストの低いarm64を選択 |
アクセス権限
項目 | 推奨設定 | コメント |
---|---|---|
実行ロール | 「基本的な Lambda アクセス権限で新しいロールを作成」 | Lambda が CloudWatch Logs に書き込み可能になる |
DynamoDB 連携が必要になったら | 後で「AmazonDynamoDBFullAccess」ポリシーを追加 | 最初は基本権限だけで OK。後から安全に権限追加可能 |
その他の構成
項目 | 推奨設定 | コメント |
---|---|---|
関数 URL | 無効 | 今回は API Gateway を経由するので不要 |
VPC を有効化 | 無効 | DynamoDB はパブリックサービスなので VPC に入れる必要なし |
コード署名 | 無効 | 無視でOK |
KMS 暗号化 | 無効 | デフォルトの AWS 管理キーで十分 |
タグ | 必要に応じて | 管理用にタグを付けてもOK |
以下の画像のようなイメージで、3つの関数を作成しましょう。

- スコア登録 Lambda (submit-score)
def lambda_handler(event, context): data = event.get('body') # DynamoDBなしで受け取ったデータを返すだけ return { 'statusCode': 200, 'body': f'Received data: {data}' }
event.get('body') はブラウザから送られたデータを受け取る部分
まずは「受け取れているか」を確認するだけです。
- ランキング取得 Lambda (get-ranking)
def lambda_handler(event, context): dummy_scores = [{'PlayerName':'Alice','Score':12.3}, {'PlayerName':'Bob','Score':15.1}] return { 'statusCode': 200, 'body': str(dummy_scores) }
ダミーデータで上位20件を返す練習
DynamoDBはまだ接続していません。
- 順位検索 Lambda (search-rank)
def lambda_handler(event, context): player_name = event.get('queryStringParameters', {}).get('playerName','') return { 'statusCode': 200, 'body': f'{player_name} is rank 1' }
3つとも作成すると、以下の画像のような表示になると思います。

ステップ2.2.API Gateway の設定
AWS Lambda は サーバーを持たずに動くプログラムですが、ブラウザから直接アクセスすることはできません。
そこで登場するのが API Gateway です。API Gateway は、Lambda への「入り口」のような役割を持ちます。
REST API の作成
AWS マネジメントコンソールで API Gateway を開き、「APIの作成」から作成を開始します。

「REST API」を選択して新しい API を作成します。
項目 | 説明 | 選択例 / 注意点 |
---|---|---|
新しい API | 新しい REST API をゼロから作成します | 今回は「新しい API」を選択 |
API 名 | API の名前を入力します | わかりやすい名前を記載 |
説明 | 任意で API の説明を入力できます | 例:ナンバーソートゲーム用のランキングAPI |
API エンドポイントタイプ | API がどのようにアクセス可能かを選択します | - リージョン:このリージョン内で利用(今回利用) - エッジ最適化:CloudFront 経由で高速配信 - プライベート:VPC 内のみアクセス可能(今回のケースでは不要) |
IP アドレスのタイプ | エンドポイントが使用する IP の種類を選択 | - IPv4:通常の IPv4 アドレス- デュアルスタック:IPv4 + IPv6 両方サポート(今回はデュアルスタックを選択) |
REST API は「GET」や「POST」などの HTTP メソッドを使って Lambda と通信できます。
次に、リソースとメソッドを作成していきます。
URL パス | メソッド | Lambda 関数 |
---|---|---|
/scores | POST | submit-score |
/ranking | GET | get-ranking |
/ranking/search | GET | search-rank |
- URL パスの追加
「リソースの作成」で各 URL パスを追加

- メソッド作成
リソースを選択 → 「メソッドの作成」 → GET/POST を選択
統合タイプ:Lambda 関数
Lambdaプロキシ統合はオンにしてください。
対象の Lambda を指定
保存



- CORS の有効化
ブラウザから直接 API を呼び出す場合、CORS を有効化する必要があります。
先ほど作成したURLパスを選択 → 「CORS の有効化」
デフォルト設定で追加 → デプロイ
- ステージの作成とデプロイ
デプロイ先ステージを作成(例:prod)
デプロイすると、URL が発行されます
例:https://xxxx.execute-api.ap-northeast-1.amazonaws.com/prod/scores

- 動作確認
ブラウザや Postman を使って POST/GET リクエストを送信し、Lambda が正しく呼び出され、レスポンスが返ることを確認します。
まずは、デプロイしたステージの画面を確認し、URLを呼び出すの箇所にあるURLをコピペします。
それぞれのLambdaが呼ばれているかの確認をしていきます。

- scoresの確認方法
Powershellで以下のコードを入力します。$body = '{"player": "umalion", "score": 1200}' Invoke-RestMethod -Uri "https://<マネージメントコンソールで確認したURL>/prod/scores" ` -Method POST ` -ContentType "application/json" ` -Body $body

レスポンスが返ってきました!
また、Lambdaの画面で対象の関数を開き、モニタリングタブを開くと、動いた形跡が確認できます。

- rankingの確認画面
https://<マネジメントコンソールで確認したURL>/prod/ranking
をブラウザで入力して確認してみましょう。

こちらも無事にレスポンスが返ってきました!
- ranking/searchの確認方法
https://<マネジメントコンソールで確認したURL>/prod/ranking/search?playerName=<任意の名前>
をブラウザで入力して確認してみましょう。

問題なくレスポンスが返ってきました!
「ステージ」は、ブラウザやアプリから呼び出すときの URL の一部になります。
例えば https://xxxx.execute-api.ap-northeast-1.amazonaws.com/prod の /prod がステージ名です。
ブラウザの入力値を Lambda が受け取り、結果を返すテストが完了しました。
ステップ3.データベースの設計 (Amazon DynamoDB)
Lambda と API Gateway が確認できたら、いよいよデータ保存です。
リソースの作成前に、DynamoDBについてまとめていきましょう。
DynamoDBについて
DynamoDB は一言でいうと、「クラウド上で利用できる高速でスケーラブルなデータベース」です。
自分でサーバーを立てたり、データベースソフトをインストールしたりする必要はありません。
AWS が裏で管理してくれるので、容量の増減やサーバー管理を気にせず利用できます。(たくさん利用すればお金がかかるので、ある程度は気にする必要があります...)
https://aws.amazon.com/jp/dynamodb/
3.1.DynamoDBの作成
ゲームのスコアを保存するためのデータベースを設計します。
以下の内容で、AWSのマネジメントコンソールから作成していきましょう。
項目 | 内容 | 備考 |
テーブル名 | Numbersort-ranking-table | テーブルのタイトル |
パーティションキー | GameId(文字列) | パーティションキー=テーブルを分けるためのID |
ソートキー | Score(数値型) | ソートキー=同じゲーム内でスコアを時間順に並べるための値 |
その他の属性 | PlayerName(文字列) | プレイヤー名 |
CreatedAt(文字列) | レコードがいつ作成されたかをタイムスタンプで生成 |
まずは、マネジメントコンソールからDynamoDBのページを開きます。
「テーブルの作成」から作成を開始しましょう。

表の情報以外はデフォルトで作成しました。

テーブル情報を開き、アクションから、項目を作成を選択し、その他の属性の項目を作成しましょう。
GameId- パーティションキー | NumberSortGame | 文字列 |
Score- ソートキー | 10.00 | 数値型 |
PlayerName | ブランクのまま | 文字列 |
CreatedAt | ブランクのまま | 文字列 |


その他の属性を追加して、「項目を作成」ボタンを押しましょう。
3.2.上位20位までのランキング取得用のGSI作成
今回構築したDynamoDBの構成では、プライマリキーだけでの検索だと、GameIdとScoreTimestampの組み合わせでしか検索ができないです。
GSIで順位の取得をしやすくします。
インデックスタブから、インデックスの作成ボタンを押します。

項目 | 設定内容 |
---|---|
パーティションキー | GameId (文字列) |
ソートキー | Score (数値型) |
属性の射影 | All(全ての属性を射影) |
3.3.名前検索用のGSI作成
次に名前検索用のグローバルセカンダリインデックス(GSI)を作成します。
インデックスタブから、インデックスの作成ボタンを押します。
下表の内容で、GSIを作成しましょう、他はデフォルトで構いません。
項目 | 設定例 |
---|---|
パーティションキー | PlayerName (文字列) |
ソートキー | Score (数値型) |
属性の射影 | All(全ての属性を射影) |
ステップ4. Lambda の修正と DynamoDB 接続
4.1.Lambda に DynamoDB への接続を追加
AWS マネジメントコンソールで対象の Lambda 関数を開きます。
画面上部の「コード」タブで、関数コードを編集します。
Boto3(AWS SDK for Python)を使って DynamoDB に接続します。
【submit-score】
import boto3
import json
from datetime import datetime
from decimal import Decimal
# DynamoDB のテーブル名
TABLE_NAME = "Numbersort-ranking-table"
# DynamoDB に接続
dynamodb = boto3.resource('dynamodb')
table = dynamodb.Table(TABLE_NAME)
def lambda_handler(event, context):
# POST リクエストの body を取得
data = json.loads(event.get('body', '{}'))
player_name = data.get('player', 'Anonymous')
score = data.get('score', 0)
# Decimal 型に変換して小数点対応
score_decimal = Decimal(str(score))
# 現在時刻をタイムスタンプ形式で生成
timestamp = datetime.utcnow().isoformat()
# DynamoDB に保存
table.put_item(
Item={
'GameId': 'NumberSortGame', # 固定値でゲームを識別
'ScoreTimestamp': f"{score_decimal}#{timestamp}", # ユニークキー
'Score': score_decimal,
'PlayerName': player_name,
'CreatedAt': timestamp
}
)
return {
'statusCode': 200,
'headers': {
'Access-Control-Allow-Origin': 'https://infra-engineering.com',
'Access-Control-Allow-Headers': 'Content-Type',
'Access-Control-Allow-Methods': 'POST, OPTIONS'
},
'body': json.dumps(f"Score saved: {player_name} - {score_decimal}")
}
GameId は固定値 "NumberSortGame" で同じゲーム内のスコアをまとめます。ScoreTimestamp はスコア+タイムスタンプでユニークキーに。
put_item で DynamoDB にスコアを保存します。
【get-ranking】
import boto3
import json
from decimal import Decimal
TABLE_NAME = "Numbersort-ranking-table"
INDEX_NAME = "GameId-Score-index" # 作成済みGSI
dynamodb = boto3.resource('dynamodb')
table = dynamodb.Table(TABLE_NAME)
def lambda_handler(event, context):
# 上位20件をスコア昇順で取得
response = table.query(
IndexName=INDEX_NAME,
KeyConditionExpression=boto3.dynamodb.conditions.Key('GameId').eq('NumberSortGame'),
ScanIndexForward=True, # 昇順(タイムアタックの早い方から)
Limit=20
)
items = response.get('Items', [])
# Decimal を文字列に変換して JSON に対応
for item in items:
if isinstance(item['Score'], Decimal):
item['Score'] = float(item['Score'])
return {
'statusCode': 200,
'headers': {
'Access-Control-Allow-Origin': 'https://infra-engineering.com',
'Access-Control-Allow-Headers': 'Content-Type',
'Access-Control-Allow-Methods': 'POST, OPTIONS'
},
'body': json.dumps(items)
}
query で GameId が "NumberSortGame" のデータを取得。
Limit=20 で上位20件だけ取得。
【search-rank】
import boto3
import json
from decimal import Decimal
from boto3.dynamodb.conditions import Key
# DynamoDB テーブル設定
TABLE_NAME = 'Numbersort-ranking-table'
INDEX_NAME = 'PlayerName-Score-index'
dynamodb = boto3.resource('dynamodb')
table = dynamodb.Table(TABLE_NAME)
def lambda_handler(event, context):
allowed_origin = 'https://infra-engineering.com'
# クエリパラメータから playerName を取得
player_name = event.get('queryStringParameters', {}).get('playerName', '')
if not player_name:
return {
'statusCode': 400,
'headers': {
'Access-Control-Allow-Origin': allowed_origin
},
'body': json.dumps('playerName を指定してください')
}
# 指定プレイヤーのスコアを GSI で取得(昇順)
response = table.query(
IndexName=INDEX_NAME,
KeyConditionExpression=Key('PlayerName').eq(player_name),
ScanIndexForward=True # 昇順
)
items = response.get('Items', [])
# 全データを取得して順位を計算
all_scores_response = table.scan()
all_scores = sorted(
all_scores_response.get('Items', []),
key=lambda x: x.get('Score', 0) # Scoreでソート
)
results = []
for item in items:
score_value = item.get('Score')
if score_value is not None:
# 自分よりスコアが良い(小さい)人の数を数える
rank = 1 + sum(1 for s in all_scores if s.get('Score', float('inf')) < score_value)
# Decimalをfloatに変換
if isinstance(score_value, Decimal):
score_value = float(score_value)
results.append({
'PlayerName': item.get('PlayerName'),
'Score': score_value,
'Rank': rank
})
return {
'statusCode': 200,
'headers': {
'Access-Control-Allow-Origin': allowed_origin,
'Access-Control-Allow-Headers': 'Content-Type',
'Access-Control-Allow-Methods': 'GET, OPTIONS'
},
'body': json.dumps(results)
}
4.2.Lambda の権限に DynamoDB へのアクセスを追加
IAMの画面を開き、ロールを開きます。
先ほど作成したLambdaの関数名で検索し、開きます。

「許可を追加」から、ポリシーをアタッチを選択

AmazonDynamoDBFullAccess を追加。
(本番では必要な権限だけ絞るのが安全です)

これを、3つのLambda関数用のロールに対して行います。
ステップ5.動作確認
では、作成したDynamoDBへの書き込み/読み込みができるか確認しましょう。
5.1. submit-scoreでの書き込み確認
前回同様に、Powershellで以下のコマンドで確認します。
$body = '{"player": "umalion", "score": 1200}'
Invoke-RestMethod -Uri "https://<マネジメントコンソールで確認したURL>/prod/scores" `
-Method POST `
-ContentType "application/json" `
-Body $body
以下の応答が返ってきました。
Score saved: umalion - 1300
マネジメントコンソールでDynamoDBを確認してみましょう。
DynamoDB管理画面の、項目を探索メニューから確認できます。

先ほどPowerShellでPOSTした内容が確認できます。
5.2. get-rankingでの読み込み確認
次に、以下のURLにブラウザからアクセスし、レスポンスを確認する。

[{"PlayerName": "", "Score": 10.0, "GameId": "NumberSortGame", "CreatedAt": ""}, {"PlayerName": "umalion", "Score": 1300.0, "GameId": "NumberSortGame", "ScoreTimestamp": "1300#2025-08-14T11:33:59.812992", "CreatedAt": "2025-08-14T11:33:59.812992"}]
ちゃんと昇順でレスポンスが返ってきたことが確認できました。
5.3. search-rankでの読み込み確認
以下のURLにアクセスして確認してみます。
https://<API_GATEWAY_URL>/ranking/search?playerName=umalion

[{'PlayerName': 'umalion', 'Score': Decimal('1300'), 'Rank': 1}]
いい感じに返ってきています。
ステップ6.Lambda エンドポイントの静的サイトへの記載
HTML 側で、ランキングボード用の JS を修正。
POST(スコア送信)や GET(ランキング取得)の URL に API Gateway のエンドポイント を設定。
API Gatewayのステージ画面から、「URLを呼び出す」の箇所に記載されているURLをコピーして、JavaScriptのエンドポイント呼び出しロジックに組み込みます。
冒頭で述べた通り、このステップについては詳細を割愛します。
今回の作業は、以上になります。
作成してみて
作成してみて思ったこととして、あまり詳細を詰めずに作り始めたので、構築作業しながら、「この機能を追加したいな」「実際に一連の処理を行った際にこうなったらどうしよう」などの追加要件がボロボロと出てきました。
この辺りは、どれだけ完成時のイメージができているかだと思うので、次に何か作る際は気を付けておこうと思いました。
あとは、月に各サービスがどれだけ利用されるかだとは思いますが、これだけの機能を(個人では使いきれないくらい)贅沢に利用できるのは素晴らしいと感じました。
自分で必要な機器や場所を含む環境の用意をしないで済むのは、クラウドのとてもいい点だと思います。
最後に
もし、バグやおかしい点があれば、ご連絡いただければ幸いです。
今回の記事作成はかなり時間が掛かりましたが、自分が考えた通りに作れた時の達成感はとてもいいものですね。
いずれまたなにか作成し、公開するような企画を考えてみたいと思います。
(できるだけお金のかからない範囲で)
Comments