Skip to content

サーバレスアーキテクチャ

ひとまずDynamoDBを操作するLambdaを作ってみる

python
import json
import boto3
import time
from boto3.dynamodb.conditions import Key
from decimal import Decimal

def _resp(status, body=None):
    return {
        "statusCode": status,
        "headers": {
            "Content-Type": "application/json",
            # "Access-Control-Allow-Origin": "*",
            # "Access-Control-Allow-Methods": "GET,POST,OPTIONS",
            # "Access-Control-Allow-Headers": "Content-Type",
        },
        "body": json.dumps(body or {})
        }

def _get_method(event):
    return (event.get("requestContext", {}).get("http", {}).get("method")
            or event.get("httpMethod") or "GET")

def _parse_body(event):
    body_raw = event.get("body") or ""
    return json.loads(body_raw)


def _to_jsonable(x):
    if isinstance(x, list):
        return [_to_jsonable(i) for i in x]
    if isinstance(x, dict):
        return {k: _to_jsonable(v) for k, v in x.items()}
    if isinstance(x, Decimal):
        # 1758384246206 のような整数なら int に、そうでなければ float に
        return int(x) if x == int(x) else float(x)
    return x

dynamodb = boto3.resource("dynamodb")
TABLE_NAME = "Messages"
table = dynamodb.Table(TABLE_NAME)

def lambda_handler(event, context):
    method = _get_method(event)

    # if method == "OPTIONS":
    #     return _resp(200, {})   # CORSプリフライト
    
    if method == "GET":
        qs = event.get("queryStringParameters") or {}
        roomId = (qs.get("roomId") or "general").strip()
        try:
            limit = int(qs.get("limit") or "50")
        except (ValueError, TypeError):
            limit = 50
        res = table.query(
            KeyConditionExpression=Key("roomId").eq(roomId),
            ScanIndexForward=True, # 古→新
            Limit=limit
        )
        items = res.get("Items", [])
        items = _to_jsonable(items)
        return _resp(200, {"ok": True, "items": items})

    if method == "POST":
        try:
            body = _parse_body(event)
        except json.JSONDecodeError:
            return _resp(400, {"error": "Invalid JSON"})
        
        roomId = (body.get("roomId") or "default").strip()
        user = (body.get("user") or "annonymous").strip()
        text = (body.get("text") or "").strip()
        if not text:
            return _resp(400, {"error": "Text is empty"})
        
        ts = int(time.time() * 1000)
        item = {
            "roomId": roomId,
            "ts": ts,
            "user": user,
            "text": text,
        }
        table.put_item(Item=item)
        return _resp(200, {"ok": True, "item": item})
    
    return _resp(405, {"error": "Method not allowd"})

Lambda関数URLも有効化しておく。CORSの設定をちゃんとしておかないと動かない。

alt text

フロントエンドとなるhtmlはこんな感じ。

html
<!doctype html>
<meta charset="utf-8" />
<title>Serverless Chat (mini)</title>
<style>
  body { font-family: sans-serif; max-width: 600px; margin: 40px auto; }
  #messages { border: 1px solid #ddd; padding: 8px; min-height: 200px; }
  .msg { margin: 4px 0; padding: 6px; border-radius: 6px; background: #f7f7f7; }
</style>
<h1>Serverless Chat</h1>
<div id="messages"></div>
<form id="form">
  <input id="user" placeholder="name" value="noim">
  <input id="text" placeholder="message">
  <button>Send</button>
</form>
<script>
const URL = "https://2smlcb36562yoidtepzyyskcve0zqnyb.lambda-url.ap-northeast-1.on.aws";
const messagesDiv = document.getElementById("messages");
const form = document.getElementById("form");
const userInput = document.getElementById("user");
const textInput = document.getElementById("text");

async function load() {
  const res = await fetch(`${URL}?roomId=general&limit=20`);
  const data = await res.json();
  messagesDiv.innerHTML = data.items.map(m => 
    `<div class="msg"><b>${m.user}</b>: ${m.text}</div>`
  ).join("");
}

form.addEventListener("submit", async e => {
  e.preventDefault();
  const body = { roomId: "general", user: userInput.value, text: textInput.value };
  await fetch(URL, { method: "POST", headers: {"Content-Type":"application/json"}, body: JSON.stringify(body) });
  textInput.value = "";
  await load();
});

load();
setInterval(load, 5000); // 5秒ごとに更新
</script>

index.htmlを作成したら、ローカルで簡単なwebサーバを立てて上記のhtmlを表示させる。 index.htmlがあるディレクトリで以下を実行。

bash
python3 -m http.server 8000

ブラウザでアクセスして動作を確認する。

これからやりたいこと

https://tmokmss.hatenablog.com/entry/serverless-fullstack-webapp-architecture-2025

ここらへんを参考に、これからやりたいことは

  • 認証
  • フロントエンドをリッチに(Nextjsとか?)
  • フロントエンドをCloudFrontとかから配信