1
0

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でサーバーレスな「未達成なら寄付Todoアプリ」を作ってみた #1(設計〜バックエンド基礎編)

1
Posted at

AWSでサーバーレスな「未達成なら寄付Todoアプリ」を作ってみた #1(設計〜バックエンド基礎編)

はじめに

AWS SAA(Solutions Architect Associate)の学習を兼ねて、以下のようなアプリを個人開発することにしました。

  • タスクに「期限」「重要度(小/中/大)」を設定する
  • 期限までに達成できなければ、重要度に応じた金額(1,000円/5,000円/10,000円)が自動で寄付として引き落とされる
  • 判定は自己申告(完了ボタンを押すだけ)

いわゆる「セルフコミットメント系アプリ」ですが、実際に手を動かしながらAWSのサーバーレス構成(Lambda / API Gateway / DynamoDB / Cognito)を一通り触れるので、学習素材としてちょうどよいボリュームだと感じています。

この記事はシリーズ1本目、設計〜バックエンドの基礎API(作成・取得・完了)ができるまでの記録です。


全体構成

決済(Stripe)以外はすべてAWSのサーバーレスサービスで構成しています。「使った分だけ課金される」ため、個人開発でもコストを抑えられるのが利点です。

image.png
ブラウザ → S3+CloudFront → API Gateway → Lambda → DynamoDB が主要な流れ。Cognitoが認証、EventBridgeが定期実行、Stripeが寄付の決済を担当。

  • フロントエンド:S3 + CloudFront で配信するReact製SPA(まだ未着手)
  • 認証:Cognitoでユーザープールを作成(ログイン画面はホストされたUIを利用)
  • バックエンド:API Gateway(HTTP API)+ Lambda(Node.js)
  • データ:DynamoDB(オンデマンドモード)
  • 決済:Stripe(カード情報は一切自前のシステムで扱わない設計。未達成が確定したタスクの金額を寄付として引き落とす)

DynamoDBのテーブル設計

複数人利用への拡張も見据えて、最初からuserIdを持たせる設計にしました。

テーブル パーティションキー 主な項目
Users userId stripeCustomerId, email
Tasks taskId userId, title, dueDate, importance, penaltyAmount, status, createdAt
PaymentHistory paymentId userId, taskId, amount, stripePaymentIntentId, chargedAt

importance(重要度)からpenaltyAmount(寄付額)への変換は、Lambda側で以下のような対応表を持たせて自動計算しています。

const importanceToPenalty = {
  small: 1000,
  medium: 5000,
  large: 10000,
};

作ったLambda関数(3本)

1. createTask - タスク作成

POSTで受け取った内容をDynamoDBに保存します。taskIdcrypto.randomUUID()でサーバー側で発行。

import { DynamoDBClient } from "@aws-sdk/client-dynamodb";
import { PutCommand, DynamoDBDocumentClient } from "@aws-sdk/lib-dynamodb";
import { randomUUID } from "crypto";

const client = new DynamoDBClient({});
const docClient = DynamoDBDocumentClient.from(client);

export const handler = async (event) => {
  const body = JSON.parse(event.body);
  const taskId = randomUUID();
  const createdAt = new Date().toISOString();

  const importanceToPenalty = { small: 1000, medium: 5000, large: 10000 };

  const params = {
    TableName: "Tasks",
    Item: {
      taskId,
      userId: body.userId,
      title: body.title,
      dueDate: body.dueDate,
      importance: body.importance,
      penaltyAmount: importanceToPenalty[body.importance],
      status: "pending",
      createdAt,
    },
  };

  try {
    await docClient.send(new PutCommand(params));
    return {
      statusCode: 201,
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({ taskId, message: "タスクを作成しました" }),
    };
  } catch (error) {
    console.error(error);
    return {
      statusCode: 500,
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({ message: "エラーが発生しました" }),
    };
  }
};

2. getTasks - タスク一覧取得

まずはシンプルにScanで全件取得し、Lambda側でuserIdをフィルタする実装にしました(パーティションキーがtaskIdのため、userId検索にはGSIが必要になりますが、これは今後の改善課題としています)。

const result = await docClient.send(new ScanCommand({ TableName: "Tasks" }));
const userTasks = result.Items.filter((task) => task.userId === userId);

メモ: DynamoDBの検索にはScan(全件走査)とQuery(索引を使った効率的な検索)があり、実務では基本Queryが推奨されます。今回はGSI設計を後回しにして、まずは動く形を優先しました。

3. completeTask - タスク完了

UpdateCommandを使い、指定したtaskIdstatusだけを部分更新します。

const params = {
  TableName: "Tasks",
  Key: { taskId },
  UpdateExpression: "SET #s = :newStatus",
  ExpressionAttributeNames: { "#s": "status" },
  ExpressionAttributeValues: { ":newStatus": "completed" },
};

statusはDynamoDBの予約語のため、#sというプレースホルダー経由で指定する必要がある点がハマりポイントでした。


つまずいたポイント:curlのクォート問題

completeTaskをテストした際、以下のエラーに遭遇しました。

"errorMessage": "\"undefined\" is not valid JSON"

原因は、Windowsのコマンドプロンプトではシングルクォート'が正しく解釈されないことでした。Mac/Linuxのターミナルでは動くコマンドが、Windowsではevent.bodyundefinedのまま届いてしまいます。

# Mac/Linuxはこれで動くが、Windows cmdでは失敗する
curl -X POST https://xxxx/completeTask -d '{"taskId":"xxxx"}'

# Windows cmdではダブルクォート+エスケープが必要
curl -X POST https://xxxx/completeTask -d "{\"taskId\":\"xxxx\"}"

環境依存の落とし穴として記録しておきます。


Lambdaの権限まわりで学んだこと

Lambda関数を新規作成すると、デフォルトではCloudWatch Logsへの書き込み権限しか付与されません(最小権限の原則)。DynamoDBを操作するには、関数ごとに作成されるIAMロールへ個別にAmazonDynamoDBFullAccessポリシーをアタッチする必要があります。

重要なのは、Lambda関数ごとにIAMロールが別々に作られるという点です。1つの関数で権限を追加しても、他の関数には引き継がれないため、createTaskgetTaskscompleteTaskと作るたびに同じ作業を3回繰り返しました。


現在の進捗

  • DynamoDB 3テーブル作成(Users / Tasks / PaymentHistory)
  • Cognitoユーザープール作成
  • createTask Lambda(動作確認済み)
  • getTasks Lambda(動作確認済み)
  • completeTask Lambda(動作確認済み)
  • checkOverdueTasks(期限切れ判定)
  • EventBridge Scheduler設定
  • Stripe連携(カード登録・自動課金)
  • フロントエンド(React + Cognito連携)
  • API GatewayのセキュリティをCognito認証に変更

次回予告

次回は、EventBridge Schedulerで期限切れタスクを毎日自動検知し、Stripeで実際に寄付を実行する仕組みを実装していきます。

1
0
1

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
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?