AWS初心者が記事を書いています。
はじめに
React Router + Lambda で Todo アプリを作成します。
構成概要
フロント
- React Router の SPA を S3 でホストします。
- CloudFront 経由で配信します。
バックエンド〜DB
- Lambda で実装します。API Gateway を使って API 化します。
- Cognite による認証認可を通った場合のみ API にアクセス化とします。
- DB は RDS(PostgreSQL)を使います。
今回は バックエンド〜DB を実装します。
実装手順
1. VPC を作成する
Lambda と RDS を格納するための VPC を作成します。
設定
| 項目 | 設定 |
|---|---|
| 作成するリソース | VPCなど |
| AZ | 2 |
| パブリックサブネット | 0 |
| プライベートサブネット | 2 |
| NATゲートウェイ | なし |
| VPC エンドポイント | なし |
2. セキュリティグループを作成する
Lambda と RDS 間のやり取りをセキュアにするため、それぞれに設定するセキュリティグループを作成します。
作るもの
-
lambda-sg
- Lambda のアウトバウンドを RDS のみに制限
-
rds-sg
- RDS のインバウンドを Lambda のみに制限
2-1. lambda-sg を作成する
VPCのダッシュボードを開き、セキュリティグループ を選択します。

設定
| 項目 | 設定 |
|---|---|
| セキュリティグループ名 | lambda-sg |
| 説明 | 任意のテキスト |
| VPC | 1 で作成したVPC |
インバウンドルール
設定不要です。
アウトバウンドルール
2-2 で rds-sg を作成した後に対応可能です。
次のルールを追加します。
| 項目 | 設定 |
|---|---|
| タイプ | PostgreSQL |
| 送信先 | rds-sg |
2-2. rds-sg を作成する
次の設定で作成します。
| 項目 | 設定 |
|---|---|
| セキュリティグループ名 | rds-sg |
| 説明 | 任意のテキスト |
| VPC | 1 で作成したVPC |
インバウンドルール
次のルールを追加します。
| 項目 | 設定 |
|---|---|
| タイプ | PostgreSQL |
| ソース | lambda-sg |
アウトバウンドルール
設定不要です。
3. RDS を作成する
Aurora and RDS のダッシュボードを開き、データベースを作成する を選択します。

設定
| 項目 | 設定 |
|---|---|
| エンジンのタイプ | PostgreSQL |
| テンプレート | 開発/テスト |
| デプロイオプション | シングルAZ DBインスタンスデプロイ |
| DB インスタンス識別子 | 任意の識別子 |
| 認証情報管理 | セルフマネージド |
| マスターパスワード | お好きなパスワード |
| DBインスタンスクラス | db.t4g.micro ※動作検証用なので一番軽いやつで |
| ストレージタイプ | 汎用SSD |
| コンピューティングリソース | EC2に接続しない |
| VPC | 1で作成したVPC |
| パブリックアクセス | なし |
| VPCセキュリティグループ | 既存の選択 |
| 既存の VPC セキュリティグループ | rds-sg |
| アヴェイラビリティゾーン | 指定なし |
| RDS Proxy | チェックなし |
| 最初のデータベース名 | お好きなDB名 |
4. Lambda を作成する
次の用途の Lambda をそれぞれ作成します。
- DB にテーブルを作成する Lambda
- バックエンドとして動かす Lambda
4-1. DB にテーブルを作成する Lambda
todos テーブルを作成します。
テーブル定義
- id: bigserial
- title: text
- completed bool
- created_at: timestamp
4-1-1. ファイル用意(ローカル)
ローカル環境にて、以下の構成でファイルを用意します。
- createTable
- index.js
- package.json
const { Client } = require("pg");
const MIGRATIONS = [
{
id: "001_create_todos",
sql: `
create table if not exists todos (
id bigserial primary key,
title text not null,
completed boolean not null default false,
created_at timestamptz not null default now()
);
create index if not exists idx_todos_completed on todos(completed);
create index if not exists idx_todos_created_at on todos(created_at desc);
`,
},
];
function dbConfig() {
return {
host: process.env.DB_HOST,
port: Number(process.env.DB_PORT || "5432"),
database: process.env.DB_NAME,
user: process.env.DB_USER,
password: process.env.DB_PASSWORD,
ssl: { rejectUnauthorized: false },
};
}
exports.handler = async () => {
const client = new Client(dbConfig());
await client.connect();
try {
await client.query(`
create table if not exists schema_migrations (
id text primary key,
applied_at timestamptz not null default now()
);
`);
const { rows } = await client.query("select id from schema_migrations");
const applied = new Set(rows.map((r) => r.id));
const toApply = MIGRATIONS.filter((m) => !applied.has(m.id));
for (const m of toApply) {
await client.query("begin");
try {
await client.query(m.sql);
await client.query("insert into schema_migrations(id) values($1)", [m.id]);
await client.query("commit");
console.log(`applied: ${m.id}`);
} catch (e) {
await client.query("rollback");
throw e;
}
}
return {
ok: true,
applied: toApply.map((m) => m.id),
skipped: MIGRATIONS.filter((m) => applied.has(m.id)).map((m) => m.id),
};
} finally {
await client.end();
}
};
{
"name": "create-table",
"private": true,
"type": "commonjs",
"dependencies": {
"pg": "^8.0.0"
}
}
作成したら以下のコマンドを実行します。
npm i --omit=dev
zip -r function.zip .
作成した zip ファイルは後ほど使います。
4-1-2. Lambda 作成(AWS)
Lambda のダッシュボードを開き、関数の作成 を選択します。

作成時の設定
| 項目 | 設定 |
|---|---|
| 関数名 | お好きな関数名 |
| ランタイム | Node.js 24.x |
| VPC | 有効化して、1で作成したVPCを選択 |
| サブネット | 1で作ったサブネットを選択 |
| セキュリティグループ | lambda-sg |
関数作成後、アップロード元 で .zipファイル を選択し、先ほど作成した zip ファイルをアップロードします。

4-1-3. 環境変数の設定
アップロード後、設定 > 環境変数 に以下の項目を設定します。

| 項目 | 設定 |
|---|---|
| DB_HOST | DBインスタンスのエンドポイント ※データベースインスタンスの 接続とセキュリティ から確認できます。
|
| DB_NAME | DBインスタンス作成時に指定したDB名 |
| DB_USER | インスタンス作成時に指定したユーザ名 |
| DB_PASSWORD | インスタンス作成時に指定したパスワード |
| DB_PORT | 5432 |
4-1-4. 動作確認
この関数は今後使わないので、ここで削除して OK です。
4-2. バックエンドとして動かす Lambda
もう一つの Lambda 関数を作成します。こちらはアプリケーションのバックエンドとして動くものになります。
4-2-1. ファイル作成(ローカル)
4-1 と同じ階層でファイルを作成します。
package.json は 4-1 と同様です。
index.js は以下の通りです。
const { Pool } = require("pg");
let pool;
function getPool() {
if (!pool) {
pool = new Pool({
host: process.env.DB_HOST,
port: Number(process.env.DB_PORT || "5432"),
database: process.env.DB_NAME,
user: process.env.DB_USER,
password: process.env.DB_PASSWORD,
ssl: { rejectUnauthorized: false }
});
}
return pool;
}
function json(statusCode, obj) {
return {
statusCode,
headers: { "content-type": "application/json" },
body: JSON.stringify(obj)
};
}
exports.handler = async (event) => {
const method = event.requestContext?.http?.method || "GET";
const path = event.rawPath || "/";
try {
const p = getPool();
if (method === "GET" && path === "/api/todos") {
const { rows } = await p.query(
"select id, title, completed, created_at from todos order by id desc limit 200"
);
return json(200, { items: rows });
}
if (method === "POST" && path === "/api/todos") {
const body = event.body ? JSON.parse(event.body) : {};
const title = String(body.title || "").trim();
if (!title) return json(400, { message: "title is required" });
const { rows } = await p.query(
"insert into todos(title) values($1) returning id, title, completed, created_at",
[title]
);
return json(201, { item: rows[0] });
}
const m = path.match(/^\/api\/todos\/(\d+)$/);
if (m && method === "PATCH") {
const id = Number(m[1]);
const body = event.body ? JSON.parse(event.body) : {};
const completed = Boolean(body.completed);
const { rows } = await p.query(
"update todos set completed=$2 where id=$1 returning id, title, completed, created_at",
[id, completed]
);
if (!rows[0]) return json(404, { message: "not found" });
return json(200, { item: rows[0] });
}
if (m && method === "DELETE") {
const id = Number(m[1]);
await p.query("delete from todos where id=$1", [id]);
return json(204, {});
}
return json(404, { message: "not found" });
} catch (e) {
console.error(e);
return json(500, { message: "internal error" });
}
};
4-2-2. zip 化・Lambda 作成
zip を作成したら、4-1 と同じ手順で関数を作成してアップロードします。
環境変数も同様に設定します。
5. API Gateway を作成する
バックエンド用の Lambda 関数を API 化するため、API Gateway を作成します。
また、Cognito による認証認可も実装します。
5-1. API Gateway を作成する
API Gateway のダッシュボードを開き、APIを作成 を選択します。

統合 から、4-2 で追加した Lambda 関数を設定します。

作成後、Stages を選択して API のエンドポイントを確認できます。

この状態で、ブラウザから https://<APIエンドポイント>/api/todos にアクセスすると、DB に登録されている todo の一覧を取得できます。

ただし、今は無条件にアクセスできてしまうので、Cognito による認証認可を追加します。
5-2. ユーザプールとアプリケーションクライアントを作成する
Cognito のダッシュボードから、ユーザープールを作成 を選択します。

設定
| 項目 | 設定 |
|---|---|
| アプリケーションタイプ | SPA |
| サインイン識別子のオプション | メールアドレス ※それ以外でも可 |
| 自己登録 | オン |
| サインアップのための必須属性 | なし |
| リターン URL | 入力しない |
テストユーザを追加
この後の動作確認のため、ユーザプールにテストユーザを追加しておきます。
メールアドレスを入力し、適当なパスワードを設定しておきます。

5-3. API Gateway にオーサライザーを設定する
オーサライザーとは
API Gateway によって呼び出される認可チェックの仕組みです。
API を呼ぼうとするクライアントからのリクエストを受けたとき、オーサライザーを通すことで「このクライアントがこの API を呼んでよいか」を判断できます。
API Gateway に戻り、Authorization > オーソライザーを管理 > 作成 を選択します。

設定
| 項目 | 設定 |
|---|---|
| オーソライザーのタイプ | JWT |
| 名前 | 任意の名前 |
| IDソース | $request.header.Authorization |
| 発行者URL | https://cognito-idp.<リージョン>.amazonaws.com/<5-2で作成したユーザプールのユーザプールID> |
| 対象者 | 5-2 で作成したアプリケーションクライアントのクライアントID |
オーソライザーをルートにアタッチ から、各ルートにアタッチします。

これで、API のアクセスに認可が必須になりました。
試しに 5-1 と同様に API エンドポイントへアクセスすると、Unauthorized でエラーとなります。

5-4. アクセストークンを取得して API にアクセスする
API にアクセスするにはアクセストークンが必要です。
そして、アクセストークンを取得するには、ユーザ認証を行って認可コードを取得する必要があります。
5-4-1. 認可コードを取得する
ユーザ認証を行うため、アプリケーションクライアントのダッシュボードを開き、ログインページを表示 を選択します。

サインイン後、今回はパスワード強制変更する設定にしていたので、新パスワードを入力します。

サインイン完了画面が表示されます。アドレスバーで、認可コードを確認できます。

5-4-2. 認可コードからトークンを取得する
取得した認証コードを使ってトークンを取得します。
Cognite プレフィックスドメイン は、ユーザプールのダッシュボードから 認証方法 を選択し、パスキー の項目から確認できます。

curl -sS -X POST "https://<Cognito プレフィックスドメイン>/oauth2/token" \
-H "Content-Type: application/x-www-form-urlencoded" \
--data-urlencode "grant_type=authorization_code" \
--data-urlencode "client_id=<クライアントID>" \
--data-urlencode "code=<取得した認可コード>" \
--data-urlencode "redirect_uri=<redirect_url>"
うまくいくと、アクセストークンや ID トークンが返ってきます。
5-4-3. アクセストークンで API にアクセスする
取得したアクセストークンを使って API エンドポイントにアクセスします。
curl -sS '<APIエンドポイント>/api/todos' \
-H "Authorization: Bearer <取得したアクセストークン>"
以下のようなレスポンスが返ってくれば OK です。
"items":[]
おわりに
以上で、バックエンドの実装はひとまず完了です。
次回はフロントエンドアプリを作成していきます。












