完全無料!?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の画面に移動します。

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関数作成
  • スコア登録 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つとも作成すると、以下の画像のような表示になると思います。

Lambda関数の一覧画面

 

 ステップ2.2.API Gateway の設定

AWS Lambda は サーバーを持たずに動くプログラムですが、ブラウザから直接アクセスすることはできません。

そこで登場するのが API Gateway です。API Gateway は、Lambda への「入り口」のような役割を持ちます。

REST API の作成

AWS マネジメントコンソールで API Gateway を開き、「APIの作成」から作成を開始します

API Gatewayの画面

「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 関数
/scoresPOSTsubmit-score
/rankingGETget-ranking
/ranking/searchGETsearch-rank
  • URL パスの追加
    「リソースの作成」で各 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が呼ばれているかの確認をしていきます。
api endpoint
  • scoresの確認方法
    Powershellで以下のコードを入力します。
    $body = '{"player": "umalion", "score": 1200}'
    Invoke-RestMethod -Uri "https://<マネージメントコンソールで確認したURL>/prod/scores" `
      -Method POST `
      -ContentType "application/json" `
      -Body $body
    
コマンド実行結果

レスポンスが返ってきました!

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

Lambdaモニタリング画面
  • rankingの確認画面
    https://<マネジメントコンソールで確認したURL>/prod/ranking
    をブラウザで入力して確認してみましょう。
rankingレスポンス

こちらも無事にレスポンスが返ってきました!

  • ranking/searchの確認方法
    https://<マネジメントコンソールで確認したURL>/prod/ranking/search?playerName=<任意の名前>
    をブラウザで入力して確認してみましょう。
ranking/searchのレスポンス確認

問題なくレスポンスが返ってきました!

「ステージ」は、ブラウザやアプリから呼び出すときの 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のページを開きます。
「テーブルの作成」から作成を開始しましょう。

DynamoDB画面

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

DynamoDBテーブル一覧

テーブル情報を開き、アクションから、項目を作成を選択し、その他の属性の項目を作成しましょう。

GameId- パーティションキー
NumberSortGame文字列
Score- ソートキー
10.00数値型
PlayerNameブランクのまま文字列
CreatedAtブランクのまま文字列
DynamoDB項目作成
その他の属性追加

その他の属性を追加して、「項目を作成」ボタンを押しましょう。

3.2.上位20位までのランキング取得用のGSI作成

今回構築したDynamoDBの構成では、プライマリキーだけでの検索だと、GameIdとScoreTimestampの組み合わせでしか検索ができないです。
GSIで順位の取得をしやすくします。

インデックスタブから、インデックスの作成ボタンを押します。

DynamoDBインデックス作成
項目設定内容
パーティションキー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の関数名で検索し、開きます。

IAMロール管理画面

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

ポリシーをアタッチを選択

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

DynamoDBFullAccessを付与

これを、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

ranking-searchテスト結果
[{'PlayerName': 'umalion', 'Score': Decimal('1300'), 'Rank': 1}]

いい感じに返ってきています。

 

ステップ6.Lambda エンドポイントの静的サイトへの記載

HTML 側で、ランキングボード用の JS を修正。
POST(スコア送信)や GET(ランキング取得)の URL に API Gateway のエンドポイント を設定。

API Gatewayのステージ画面から、「URLを呼び出す」の箇所に記載されているURLをコピーして、JavaScriptのエンドポイント呼び出しロジックに組み込みます。

冒頭で述べた通り、このステップについては詳細を割愛します。

今回の作業は、以上になります。

作成してみて

作成してみて思ったこととして、あまり詳細を詰めずに作り始めたので、構築作業しながら、「この機能を追加したいな」「実際に一連の処理を行った際にこうなったらどうしよう」などの追加要件がボロボロと出てきました。

この辺りは、どれだけ完成時のイメージができているかだと思うので、次に何か作る際は気を付けておこうと思いました。

あとは、月に各サービスがどれだけ利用されるかだとは思いますが、これだけの機能を(個人では使いきれないくらい)贅沢に利用できるのは素晴らしいと感じました。
自分で必要な機器や場所を含む環境の用意をしないで済むのは、クラウドのとてもいい点だと思います。

最後に

もし、バグやおかしい点があれば、ご連絡いただければ幸いです。
今回の記事作成はかなり時間が掛かりましたが、自分が考えた通りに作れた時の達成感はとてもいいものですね。
いずれまたなにか作成し、公開するような企画を考えてみたいと思います。
(できるだけお金のかからない範囲で)

Comments