AWS Blocks とは
AWS Blocks | Build applications locally in seconds, deploy to AWS when ready
AWS Blocks は、オープンソースの TypeScript フレームワークです。このフレームワークを使用すると、事前に構築されたモジュールからバックエンド機能を構成し、コードからインフラストラクチャを生成することで、インフラストラクチャの定型コードではなくビジネスロジックに集中できます。
簡単にいうと、AWS 製の IfC (Infrastructure from Code) ツールです。
ちなみに、IfC とは アプリのコードを書くと、必要なインフラがそこから自動生成される」という考え方で IaC (Infrastructure as Code) を発展させたものです。筆者は初めて知りました。
利点
- ローカル開発では AWS アカウントが不要です。Postgres、認証、リアルタイム処理、ファイルストレージ、バックグラウンドジョブを備えたローカル環境を構築できます
- データスキーマを一度定義すれば、フロントエンド側の型が自動的に更新されます
- コーディングエージェント向けのステアリングファイルが同梱されています
- より高度なカスタマイズが必要な場合は、AWS CDK のレイヤーで基盤となるリソースを直接設定できます
- 既存リソースと統合できます。外部の Postgres サービス、API、認証プロバイダーにも接続できます
AWS Blocks を始める
インストール
プロジェクトのひな形は以下のコマンドで作成できます。今回は 単一のAPIエンドポイントを備えた最小限のスターター(デフォルト)テンプレートを利用します。
npm create @aws-blocks/blocks-app@latest my-app
コマンドを実行すると、 ToDo アプリ一式が生成されます。
生成されたディレクトリ構成は以下のとおりです。
my-app
├── AGENTS.md
├── README.md
├── aws-blocks # Blocks のバックエンド定義を置くディレクトリ
│ ├── client.js # フロントエンドから API を呼び出す自動生成ファイル(通常は編集しない)
│ ├── index.cdk.ts # Blocks が生成する構成に、CDK で AWS リソース設定を追加する場所
│ ├── index.handler.ts # index.ts の API 実装を AWS 上の Lambda で実行するためのエントリポイント
│ ├── index.ts # Block 定義、DB スキーマ、API 実装を書く中心的なファイル
│ ├── package.json
│ └── scripts # sandbox、deploy、destroy などの Blocks 用スクリプト
│ ├── cleanup.ts
│ ├── console.ts
│ ├── deploy.ts
│ ├── destroy.ts
│ ├── sandbox-destroy.ts
│ ├── sandbox-id.ts
│ ├── sandbox.ts
│ └── server.ts
├── cdk.json
├── index.html
├── package-lock.json
├── package.json
├── src # フロントエンドの実装を置くディレクトリ
│ └── index.ts
├── test
│ └── e2e.test.ts # API クライアントを直接呼び出す E2E テスト
├── tsconfig.json
└── vite.config.ts
私の環境では TypeScript の型エラーが出ていたため、追加で以下を実行しています。
npm install -D @types/node
{
"compilerOptions": {
"target": "ES2022",
"module": "ES2022",
"moduleResolution": "bundler",
"lib": [
"ESNext",
"DOM"
],
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"resolveJsonModule": true,
"outDir": "build-temp",
+ "types": [
+ "node"
+ ]
},
"include": [
"src/**/*",
"aws-blocks/**/*",
"test/**/*"
]
}
また、最近のフレームワークと同様に、AGENTS.md が同梱されています。
AGENTS.md
# Agent Guide
## Quick Reference
- **Backend:** `aws-blocks/index.ts` — APIs, auth, data models
- **Frontend:** `src/` — imports backend APIs via `import { api } from 'aws-blocks'`
- **Tests:** `test/e2e.test.ts` — run with `npm run test:e2e`
- **Full guide:** `node_modules/@aws-blocks/blocks/README.md` — architecture, workflow, best practices, common mistakes
- **Block catalog + decision tree:** `node_modules/@aws-blocks/blocks/docs/index.md`
- **Per-block docs:** `node_modules/@aws-blocks/blocks/docs/<package-name>.md`
## Workflow
1. Make changes to backend (`aws-blocks/index.ts`) or frontend (`src/`)
2. Test with `npm run test:e2e` — starts a dev server automatically if one isn't running
3. For faster iteration: run `npm run dev &` in the background, then run `npm run test:e2e` repeatedly (reuses the running server)
4. Do NOT use curl/fetch against the API unless troubleshooting connectivity
## Rules
- **Use Building Blocks** for all persistence and cloud abstractions — never local files, in-memory arrays, or local databases.
- **Read block docs** at `node_modules/@aws-blocks/blocks/docs/<package-name>.md` before using a block.
- **The JSON-RPC transport is invisible** — do not construct RPC payloads manually. Import and call the typed API directly.
## Deploying (requires AWS credentials)
- `npm run sandbox` — deploy backend to AWS, serve frontend locally
- `npm run deploy` — full production deploy to AWS
- `npm run sandbox:destroy` — tear down sandbox resources
サンプルアプリの構成
バックエンドの本体は、aws-blocks/index.ts に集約されています。
Block の宣言と API の定義が同居しているのが特徴的です。
aws-blocks/index.ts
/**
* Backend — aws-blocks/index.ts
*
* Real-time todo app with per-user isolation, optimistic locking, and secondary indexes.
*
* This file defines your API, auth, data model, and real-time channels.
* The frontend imports these exports directly via `import { ... } from 'aws-blocks'`.
*
* ─── IMPORTANT ───────────────────────────────────────────────────────────────
* Do NOT use local files, in-memory arrays, or local databases for persistence.
* Use Building Blocks for cloud persistence and other common cloud abstractions.
* They work locally with automatic mocks and deploy to AWS with zero configuration.
*
* For the full list of blocks and how to use them, see:
* node_modules/@aws-blocks/blocks/README.md
* ─────────────────────────────────────────────────────────────────────────────
*/
import { ApiNamespace, Scope, AuthBasic, DistributedTable, Realtime } from '@aws-blocks/blocks';
import { z } from 'zod';
const scope = new Scope('my-app');
// ─── Auth ────────────────────────────────────────────────────────────────────
const auth = new AuthBasic(scope, 'auth', {
passwordPolicy: { minLength: 8 },
crossDomain: process.env.BLOCKS_SANDBOX === 'true',
});
export const authApi = auth.createApi();
// ─── Data ────────────────────────────────────────────────────────────────────
// Zod schema = runtime validation + TypeScript types + DynamoDB table shape.
const todoSchema = z.object({
userId: z.string(), // partition key — per-user isolation
todoId: z.string(), // sort key — unique within a user
title: z.string(),
completed: z.boolean(),
priority: z.number(), // 1=high, 2=medium, 3=low
version: z.number(), // optimistic locking — incremented on each update
createdAt: z.number(),
});
const todos = new DistributedTable(scope, 'todos', {
schema: todoSchema,
key: { partitionKey: 'userId', sortKey: 'todoId' },
indexes: {
// Secondary indexes: query todos sorted by priority or title.
// The partition key is always userId (per-user isolation), the sort key varies.
byPriority: { partitionKey: 'userId', sortKey: 'priority' },
byTitle: { partitionKey: 'userId', sortKey: 'title' },
},
});
// ─── Realtime ────────────────────────────────────────────────────────────────
const rt = new Realtime(scope, 'live', {
namespaces: {
todos: Realtime.namespace(z.object({
action: z.enum(['created', 'updated', 'deleted']),
todoId: z.string(),
})),
},
});
// ─── API ─────────────────────────────────────────────────────────────────────
export const api = new ApiNamespace(scope, 'api', (context) => ({
async subscribeTodos() {
const user = await auth.requireAuth(context);
return rt.getChannel('todos', user.username);
},
async createTodo(title: string, priority: number = 2) {
const user = await auth.requireAuth(context);
const todoId = `${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`;
const todo = {
userId: user.username,
todoId,
title,
completed: false,
priority,
version: 1,
createdAt: Date.now(),
};
await todos.put(todo);
await rt.publish('todos', user.username, { action: 'created' as const, todoId });
return todo;
},
/** List todos, optionally sorted by a secondary index. */
async listTodos(sortBy?: 'priority' | 'title') {
const user = await auth.requireAuth(context);
if (sortBy) {
const index = sortBy === 'priority' ? 'byPriority' : 'byTitle';
return await Array.fromAsync(
todos.query({ index, where: { userId: { equals: user.username } } })
);
}
// Default: sorted by todoId (creation order)
return await Array.fromAsync(
todos.query({ where: { userId: { equals: user.username } } })
);
},
/**
* Toggle todo completion with optimistic locking.
* Uses `ifFieldEquals` to detect concurrent writes. On conflict,
* throws ConditionalCheckFailedException — caller should re-read and retry.
*/
async toggleTodo(todoId: string) {
const user = await auth.requireAuth(context);
const todo = await todos.get({ userId: user.username, todoId });
if (!todo) throw new Error('Todo not found');
await todos.put(
{ ...todo, completed: !todo.completed, version: todo.version + 1 },
{ ifFieldEquals: { version: todo.version } },
);
await rt.publish('todos', user.username, { action: 'updated' as const, todoId });
return { success: true };
},
/** Update a todo's priority with optimistic locking. */
async updatePriority(todoId: string, priority: number) {
const user = await auth.requireAuth(context);
const todo = await todos.get({ userId: user.username, todoId });
if (!todo) throw new Error('Todo not found');
await todos.put(
{ ...todo, priority, version: todo.version + 1 },
{ ifFieldEquals: { version: todo.version } },
);
await rt.publish('todos', user.username, { action: 'updated' as const, todoId });
return { success: true };
},
/** Delete a todo. Broadcasts 'deleted' to all connected clients. */
async deleteTodo(todoId: string) {
const user = await auth.requireAuth(context);
await todos.delete({ userId: user.username, todoId });
await rt.publish('todos', user.username, { action: 'deleted' as const, todoId });
return { success: true };
},
}));
ソースコードを確認すると、複数の Block から構成されていることがわかります。
| Block | 役割 |
|---|---|
Scope |
Block を格納する名前空間コンテナ |
AuthBasic |
プロトタイプ、内部ツール、MVP 向けのシンプルなユーザー名/パスワード認証 |
DistributedTable |
複数のアクセスパターンを持つエンティティや、インデックス付きの構造化データ |
Realtime |
WebSocket を介したライブアップデート、チャット、通知、コラボレーション機能 |
ApiNamespace |
フロントエンドから直接呼び出せる型安全なバックエンドメソッド |
特に ApiNamespace を利用すると、フロントエンドのソースコードから次のように型情報を利用できます。
// Backend: define the API
export const api = new ApiNamespace(scope, 'api', (context) => ({
async greet(name: string) {
return { message: `Hello, ${name}!` };
},
}));
// Frontend: call the API directly
import { api } from 'aws-blocks';
const result = await api.greet('World');
// result.message === 'Hello, World!'
// TypeScript knows the return type
バックエンドのメソッドシグネチャを変更すると、TypeScript がフロントエンド側で即座にエラーを報告します。
ローカル起動
ローカルでアプリを起動します。
npm run dev
ブラウザで http://localhost:3000 を開くと、認証や CRUD 機能を持つ ToDo アプリがすでに動いています。
AWS アカウントなしで迅速にローカル起動できます。
ローカル起動時は、各 Block に対応するローカルモックが使用されます。
従来、AWS を前提にしたアプリケーションをローカルで動かす場合は、LocalStack などで AWS サービスを模倣する環境を別途用意することがよくありました。AWS Blocks では、Block ごとにローカル実装が用意されているため、最初から AWS 互換環境を組み立てなくても開発を始められます。
ただし、これは AWS API を完全に模倣する環境ではなく、Blocks の抽象化に対応したローカル実装です。そのため、IAM やサービス固有の挙動まで確認したい場合は、sandbox で AWS 上にデプロイして検証します。
また、ローカル起動時のデータは、.bb-data/ 配下に格納されます。
デプロイ
AWS Blocks は内部で AWS CDK を利用するため、初回だけセットアップが必要です。
aws sts get-caller-identity
npx cdk bootstrap aws://<ACCOUNT_ID>/<REGION>
デプロイ方法は 2 種類あります。
| コマンド | 用途 |
|---|---|
npm run sandbox |
Lambda ホットスワップでデプロイ。実サービスに対する高速な検証用 |
npm run deploy |
ホスティング(CloudFront + S3)込みのフルデプロイ。ステージング/本番向け |
ローカルではモックだった各 Block が、デプロイすると DynamoDB や API Gateway などに解決されます。
デプロイすると、100 個程度のリソースが作成されます。(なお、私の環境では 20 分程度かかりました。)
完了後、AWS コンソールでリソースを確認します。
API Gateway では、REST と WebSocket で API が作成されています。
DynamoDB では、テーブルがいくつか作成されていました。
CloudFront 経由でブラウザからアクセスすると、アプリがデプロイされていることを確認できました。
検証後は、不要なリソースを残さないように npm run sandbox:destroy または npm run destroy で削除します。
気になったことの深掘り
ローカルモックと AWS リソースの対応関係
AWS Blocks の特徴的な点は、同じコードを使ったまま、実行する文脈によって実装が切り替わることです。
ローカル開発では、AWS アカウントなしでアプリケーションを起動できます。このとき各 Block は、ローカルモックとして動作します。
一方、デプロイ時には CDK の Construct や、それらを組み合わせたインフラ定義として合成されます。AWS 上では、Lambda ランタイムから実際の AWS サービスを呼び出す形になります。
今回のサンプルで使われている Block と AWS リソースの対応は、以下のように整理できます。
| Block | 役割 | ローカル開発時 | AWS デプロイ後 |
|---|---|---|---|
AuthBasic |
ユーザー名/パスワード認証 | ローカルで動く認証実装 | KVStore(DynamoDB)+ AppSetting(SSM Parameter Store)+ JWT |
DistributedTable |
インデックス付きの構造化データ | ローカルのデータストア | DynamoDB |
Realtime |
WebSocket による Pub/Sub | ローカルのリアルタイム通信 | API Gateway WebSocket + DynamoDB |
ApiNamespace |
型安全なバックエンド API | ローカル API サーバー | Lambda + API Gateway |
開発者が書いているのは new DistributedTable(...) や new Realtime(...) といった Block の宣言です。一方で、その裏側では「ローカルで動く実装」「CloudFormation に変換されるインフラ定義」「本番実行時に AWS SDK 経由で呼び出される実装」が切り替わっています。
この仕組みによって、ローカルでは手軽に試せる一方で、デプロイ時には DynamoDB、API Gateway、Lambda などの AWS ネイティブな構成に変換されます。
ローカルモックと本番実装を切り替える仕組み
実装を追ってみると、切り替えの中心にあるのは Node.js の conditional exports でした。
例えば @aws-blocks/bb-distributed-table の package.json では、同じパッケージに対して複数のエントリポイントが定義されています。
{
"exports": {
".": {
"browser": "./dist/index.browser.js",
"cdk": {
"types": "./dist/index.cdk.d.ts",
"default": "./dist/index.cdk.js"
},
"aws-runtime": "./dist/index.aws.js",
"types": "./dist/index.mock.d.ts",
"default": "./dist/index.mock.js"
}
}
}
つまり、アプリケーション側では常に次のように同じ import を書きます。
import { DistributedTable } from '@aws-blocks/blocks';
ただし、実行時に付与される condition によって、読み込まれる実装が変わります。
| 実行タイミング | condition | 読み込まれる実装 |
|---|---|---|
npm run dev |
なし |
default、つまりローカルモック |
| CDK synth / deploy | cdk |
CDK Construct |
| Lambda 実行時 | aws-runtime |
AWS SDK を使う実装 |
| ブラウザ |
browser または生成クライアント |
ブラウザ向けのクライアント実装 |
実際、npm run sandbox では CDK を起動するときに NODE_OPTIONS=--conditions=cdk が付与されます。これにより、aws-blocks/index.ts を CDK synth 用に読み込んだときは、DistributedTable や Realtime がローカルモックではなく CDK Construct として解決されます。
一方で、Lambda 実行用のクライアント生成では --conditions=aws-runtime が使われます。このときは、同じ Block が DynamoDB や API Gateway などの実サービスを呼び出す実装として読み込まれます。
この仕組みによって、アプリケーションコード上では new DistributedTable(...) と 1 回書くだけで済みます。ローカル開発、CDK によるインフラ生成、Lambda 実行時の AWS SDK 呼び出しという 3 つの文脈を、パッケージの解決先を切り替えることで実現しています。
ApiNamespace による型安全な API
ApiNamespace は、AWS Blocks の開発体験を特徴づける重要な要素です。
通常の Web API では、バックエンド側で REST API や GraphQL API を定義し、フロントエンド側では fetch や生成されたクライアントを使って呼び出します。その場合、API のパス、リクエスト型、レスポンス型を別々に管理する必要があります。バックエンドの変更にフロントエンドが追従できていないと、実行時エラーになりがちです。
AWS Blocks では、バックエンド側で定義した ApiNamespace のメソッドを、フロントエンドから型付きの関数として呼び出せます。
例えば、バックエンドで次のように API を定義します。
export const api = new ApiNamespace(scope, 'api', (context) => ({
async createTodo(title: string, priority: number = 2) {
const user = await auth.requireAuth(context);
const todo = {
userId: user.username,
todoId: crypto.randomUUID(),
title,
completed: false,
priority,
version: 1,
createdAt: Date.now(),
};
await todos.put(todo);
return todo;
},
}));
フロントエンド側では、この API を直接呼び出します。
import { api } from 'aws-blocks';
const todo = await api.createTodo('AWS Blocks を試す', 1);
// createTodo の型はおおよそ次のように推論される
// (title: string, priority?: number) => Promise<{
// userId: string,
// todoId: string,
// title: string,
// completed: boolean,
// priority: number,
// version: number,
// createdAt: number
// }>
このとき、createTodo の第 1 引数が string、第 2 引数が number であることや、戻り値に title や completed が含まれることを TypeScript が理解できます。
例えば、バックエンド側で createTodo(title: string, priority: number) を createTodo(input: { title: string; priority: number }) のように変更すると、古い呼び出し方をしているフロントエンドのコードは TypeScript のエラーになります。
これは、API の変更に対するフィードバックが実行時ではなくコンパイル時に得られるということです。小さいサンプルでは地味に見えますが、画面数や API 数が増えるほど効いてくるポイントです。
Blocks で始めて、必要なら CDK に降りる設計
AWS Blocks では、AWS の細かい設定を最初からすべて書かなくても、認証、データストア、API、リアルタイム通信などを組み合わせてアプリケーションを作成できます。
ただし、完全に AWS を隠すためのフレームワークではありません。生成されたプロジェクトには aws-blocks/index.cdk.ts が含まれており、Blocks だけでは表現しきれない設定が必要になった場合は、CDK のレイヤーで調整できます。
生成された aws-blocks/index.cdk.ts を見ると、BlocksStack は通常の CDK Construct として扱えるようになっています。
export const blocksStack = await BlocksStack.create(app, stackName, {
backendHandlerPath: join(__dirname, 'index.handler.ts'),
backendCDKPath: join(__dirname, 'index.ts')
});
BlocksStack.create(...) に、Lambda ハンドラ用のファイルと、Block 定義を含む index.ts を渡しています。ここで index.ts が CDK 用の condition で読み込まれるため、AuthBasic、DistributedTable、Realtime などが AWS リソースとして合成されます。
さらに、作成された blocksStack には handler や gateway が公開されています。
blocksStack.handler; // NodejsFunction
blocksStack.gateway; // RestApi
例えば、Blocks で作った API から、CDK で追加した SQS キューへメッセージを送る構成を考えます。
まず、aws-blocks/index.cdk.ts で SQS キューを追加します。
import * as cdk from 'aws-cdk-lib';
import * as logs from 'aws-cdk-lib/aws-logs';
import * as sqs from 'aws-cdk-lib/aws-sqs';
export const blocksStack = await BlocksStack.create(app, stackName, {
backendHandlerPath: join(__dirname, 'index.handler.ts'),
backendCDKPath: join(__dirname, 'index.ts')
});
// Blocks 管理外の追加リソースとして、監査ログ送信用の SQS キューを作成する。
const auditQueue = new sqs.Queue(blocksStack, 'AuditQueue', {
// メッセージ処理中に他のコンシューマーから見えない時間。
visibilityTimeout: cdk.Duration.seconds(30),
});
// Blocks のバックエンド Lambda に、この SQS キューへメッセージ送信する権限を付与する。
auditQueue.grantSendMessages(blocksStack.handler);
// Lambda 側で送信先キューを参照できるよう、キュー URL を環境変数として渡す。
blocksStack.handler.addEnvironment('AUDIT_QUEUE_URL', auditQueue.queueUrl);
// Blocks バックエンド Lambda の CloudWatch Logs 保持期間を 1 か月に設定する。
new logs.LogRetention(blocksStack, 'BlocksHandlerLogRetention', {
logGroupName: `/aws/lambda/${blocksStack.handler.functionName}`,
retention: logs.RetentionDays.ONE_MONTH,
});
ここで行っていることは、通常の CDK と同じです。
-
new sqs.Queue(...)で SQS キューを追加する。 -
auditQueue.grantSendMessages(blocksStack.handler)で、Blocks が作成した Lambda に SQS 送信権限を付与する。 -
blocksStack.handler.addEnvironment(...)で、Lambda にキュー URL を環境変数として渡す。 -
logs.LogRetention(...)で、Lambda のログ保持期間を設定する。
aws-blocks/index.ts に、通常の Blocks API としてメソッドを追加します。
import { SendMessageCommand, SQSClient } from '@aws-sdk/client-sqs';
export const api = new ApiNamespace(scope, 'api', (context) => ({
async recordAudit(message: string) {
const user = await auth.requireAuth(context);
const queueUrl = process.env.AUDIT_QUEUE_URL;
if (!queueUrl) {
return { queued: false, reason: 'AUDIT_QUEUE_URL is not configured' };
}
const sqs = new SQSClient({});
await sqs.send(new SendMessageCommand({
QueueUrl: queueUrl,
MessageBody: JSON.stringify({
userId: user.username,
message,
createdAt: new Date().toISOString(),
}),
}));
return { queued: true };
},
}));
この例で重要なのは、アプリケーションの API は aws-blocks/index.ts に置いたまま、追加の AWS リソースや権限設定だけを index.cdk.ts に分離できる点です。
recordAudit という API メソッド自体は Blocks の ApiNamespace として定義しています。一方で、その API が利用する SQS キュー、Lambda への環境変数、SQS 送信権限、ログ保持期間は CDK で追加しています。
つまり、アプリケーションの主要部分は Block で簡潔に書きつつ、Block の抽象化では直接指定しないインフラ設定を CDK 側で補える構造になっています。
この構造から、AWS Blocks では以下のような段階的な使い方が想定されていると考えられます。
- 最初は Block を組み合わせて、ローカルでアプリケーションを作成する
- そのまま AWS にデプロイして、実サービス上で動作させる
- 必要な場合は CDK に降りて、より細かい AWS リソース設定を追加する
この「最初は簡単に始められるが、必要になったら CDK で拡張できる」という逃げ道があるのは重要です。
例えば、単純な ToDo アプリや社内ツールであれば、Blocks の抽象化だけでも進めやすいはずです。一方で、本番運用に近づくと、ログ、監視、IAM、ドメイン、ネットワーク、セキュリティ設定など、細かく制御したい部分が出てきます。そのときに CDK の知識がそのまま使えるなら、抽象化に閉じ込められにくくなります。
まとめると AWS Blocks は、「AWS を知らなくても本番アプリが作れる」というより、「AWS の定型的な構成を最初は Block に任せ、必要なところから CDK で深く触れる」ためのツールと捉えると、理解しやすいです。


