AWS Blocks ワークショップを作ってみた

AWS Blocks ワークショップを作ってみた

AWS Blocksの概念をハンズオンで学びながら、ローカル実行からAWSデプロイまでを1つのコードで完結させる方法を試してみました。認証付きTodoアプリを段階的に構築することで、このバックエンドツールキットの実力を確認していきます。
2026.07.18

はじめに

本稿は、AWS Blocks(Preview) の概念を使いながら理解していくためのワークショップです。前半ではBlocksの概念について説明し、後半では実際に動かしてどんな動きをするものなのか確認していきます。こちらは AWS CDK Conference Japan 2026. presented by JAWS-Uで使用した資料です。

AWS Blocks とは

AWS 上でフルスタックアプリケーションを構築するためのバックエンドツールキットです。それぞれの機能がブロックとして完結しており、それらをブロックのように組み合わせることでバックエンドを実装できます。

実装したコードでローカル環境での実行/テスト、アプリ実行のためのクラウド上のインフラ構築までを完結することができます。

https://docs.aws.amazon.com/blocks/latest/devguide/what-is-blocks.html

ブロックのおおまかな種類

ブロックには以下のような種類のものがあります。これらをローカル上での実行とクラウド上での構築のための機能がパッケージで提供されます。

  • 認証
  • コンピューティングとバックグラウンド
  • データストレージ
  • AI Agent
  • コミュニケーション
  • アプリ用設定
  • 可観測性
  • ホスティングとデプロイメント

例えばAPIを作るのに認証をつけて、リクエストされたデータを保存するのにデータストレージを使うという構成が簡易な記述で実現できます。

https://docs.aws.amazon.com/blocks/latest/devguide/what-is-blocks.html

上記を使いながらどんな風に実装を書けるのか、後半のワークショップで確認していきます。

Blocks の開発フロー

Blocks は1つのコードを書くだけでローカル実行、Sandbox環境デプロイ、本番環境デプロイを実現できます。

architecture-開発ワークフロー.jpg

検証用のコード、本番用のコードというような切り替えを極力減らすことができます。これは12 Factor Appの開発/本番一致にも通じる部分です。検証環境と本番環境の差分をどう埋めるかは毎回議論になるので、公式の方法がマッチすれば議論を減らせてよさそうです。

https://12factor.net/ja/dev-prod-parity

1つのTypeScriptのコードで文脈によって、3つの展開先が分かれるようになっています。例えば以下のようにデータストレージDistributedTableを定義すると、ローカル実行時はメモリやJSONファイルとして保持されます。Sandboxデプロイ時は、CloudFormationのスタック名にprefixが付与されAWS環境にデプロイされます。本番デプロイ時には、本番用のStackIdがついてAWS環境にデプロイされます。

architecture-メンタルモデル.jpg

ワークショップ

ここからはワークショップで実際にコードを実行して、動作を確認してみます。

ワークショップのゴール

  • AWS Blocks をまず動かしてみる
  • どんな動きをするのか触って知る
  • (できれば)Sandboxで実行してみる

前提条件

  • Node.js 22以上がインストールされていること
  • ローカルの動作確認だけなら AWS アカウントは不要です
  • 発展課題のデプロイのみ、各自の AWS 環境を利用します

注記: npm createはテンプレートと依存パッケージをネットワークから取得します。パッケージ取得などが問題なく行えるPCで実行してください。

このワークショップの進め方

aws-blocks/index.tsには、認証付き Todo アプリの完成例がコメントアウトで同梱されています。これを上から順に有効化し、各行の役割を確認しながら進めます。

各ステップでは次の流れを繰り返します。

  1. 対象のコードを有効化します
  2. そのコードが何をしているかを読みます
  3. npm run devnpm run test:e2eで挙動を確かめます

全体のStepの流れは以下です。

architecture-ハンズオンの流れ.jpg

作られるサービスのアーキテクチャ全体の構造は以下になります。

architecture-アーキテクチャ.jpg

sandboxへデプロイする際は、API Gateway+Lambda+DynamoDBの部分がバックエンドとしてデプロイされます。本番にデプロイする際は、CloudFront経由でフロントエンドアプリもデプロイされます。

APIもCloudFront経由で呼び出されますが、キャッシュは無効化されており、よくあるキャッシュにより他のユーザーの情報が見えてしまう事故は防止されています。このように安全よりで自然と組み上げられるようになっています。

Step 0: プロジェクトを作成して起動する

まず、次のコマンドでサンプルアプリを作成します。テンプレートはbare(最低限の構成のみのテンプレート)を指定します。

npm create @aws-blocks/blocks-app@latest my-app -- --template bare
cd my-app

続いて、ローカル開発サーバーを起動します。

npm run dev

ブラウザでhttp://localhost:3000を開きます。Hello, World!と現在時刻が表示されれば成功です。

ローカルで動く土台が完成です。ここまでで、次の3ファイルを確認しておきます。

  • aws-blocks/index.ts: バックエンド。API とブロックを定義します
  • src/index.ts: フロントエンド。バックエンドの API を直接 import します

ポイント: この1リポジトリで、ローカル実行から AWS 構築までが完結します。

ここからはベースの内容のコメントアウトを外して、ソースを読んでいきます。

Step 1: import を確認する

aws-blocks/index.tsの冒頭付近に、次の import があります。

import { AuthBasic, DistributedTable, Realtime } from '@aws-blocks/blocks';
import { z } from 'zod';

今日使う3つのブロックと、スキーマ定義用の Zod を読み込んでいます。ブロックは import してnewするだけで使えます。

Step 2: 認証ブロックを作成する

Examplesより下にある、次のauthの定義を有効化します。

const auth = new AuthBasic(scope, 'auth', {
  passwordPolicy: { minLength: 8 },
  crossDomain: process.env.BLOCKS_SANDBOX === 'true',
});

認証ブロックのインスタンスを1つ作っています。passwordPolicyはパスワードの最小長を指定します。crossDomainは、フロントと API が別ドメインになる sandbox 向けに Cookie 属性を切り替える設定です。

ポイント: この1行が、ローカルでは擬似的な認証、AWS 上では JWT セッション基盤になります。第2引数の'auth'はブロックの ID です。デプロイ後の ID 変更はリソースの作り直しになるため、変更しません。

Step 3: 認証 API を公開する

次の1行を有効化して、認証の状態遷移 API をフロントとテストへ公開します。

export const authApi = auth.createApi();

authApiはサインアップやサインインの操作をまとめた API です。直下のコメントから使い方も確認できます。

// authApi.setAuthState({ action: 'signUp', username: '...', password: '...' })
// authApi.setAuthState({ action: 'signIn', username: '...', password: '...' })
// authApi.setAuthState({ action: 'signOut' })
// authApi.getAuthState() → { state: 'signedIn', user: { username } }

ポイント: authApiを export した瞬間、開発サーバーが型付きクライアント(client.js)を再生成します。これでimport { authApi } from 'aws-blocks'が使えるようになります。バックエンドの型がフロントまで自動で伝わります。コード生成のための追加手順はありません。

Step 4: データのスキーマを定義する

保存するデータの形を Zod スキーマで定義します。次のitemSchemaを有効化します。

const itemSchema = z.object({
  userId: z.string(),
  itemId: z.string(),
  title: z.string(),
  createdAt: z.number(),
});

このスキーマ1つが、APIリクエスト時のバリデーションと TypeScript の型の両方の元になります。

Step 5: DistributedTable を追加する

次のitemsテーブルを有効化します。

const items = new DistributedTable(scope, 'items', {
  schema: itemSchema,
  key: { partitionKey: 'userId', sortKey: 'itemId' },
});

構造化データを保存するブロックです。partitionKeysortKeyでキーの構成を決めます。ここはDynamoDBの構造を意識する必要があります。もし意識したくない場合は、DatabaseでAurora Serverless V2を使ったり、DistributedDatabaseでAurora DSQLも選択できます。

ポイント: この1行が、ローカルでは.bb-data/配下の JSON、AWS 上では DynamoDB テーブルになります。書き換えは不要で、同じコードのまま両方で動きます。

Step 6: 認証で保護した API を実装する

いよいよアプリケーションの本体です。まず、同梱されているAPI の例を読みます。

// async createItem(title: string) {
//   const user = await auth.requireAuth(context);
//   const itemId = Date.now().toString(36);
//   const item = { userId: user.username, itemId, title, createdAt: Date.now() };
//   await items.put(item);
//   return item;
// },
// async listItems() {
//   const user = await auth.requireAuth(context);
//   return await Array.fromAsync(items.query({ where: { userId: { equals: user.username } } }));
// },

先頭にある greet の部分をコメントアウトします。コード下部の記載を次の内容に置き換えます。greet は残したまま、2つのメソッドを追加します。

export const api = new ApiNamespace(scope, 'api', (context) => ({
  async greet(name: string) {
    return { message: `Hello, ${name}!`, timestamp: Date.now() };
  },
  async createItem(title: string) {
    const user = await auth.requireAuth(context);
    const itemId = Date.now().toString(36);
    const item = { userId: user.username, itemId, title, createdAt: Date.now() };
    await items.put(item);
    return item;
  },
  async listItems() {
    const user = await auth.requireAuth(context);
    return await Array.fromAsync(
      items.query({ where: { userId: { equals: user.username } } }),
    );
  },
}));

読みどころは次の2点です。

  • auth.requireAuth(context)が唯一の認証ゲートです。AWS Blocks の API は既定ですべて公開です。この1行がないと、誰でも呼び出せてしまいます。
  • items.putで保存し、items.queryで取得します。queryの結果はArray.fromAsyncで配列にまとめます。

注意: 同梱例のcreateItemにはrt.publish(...)の行もありますが、ここではまだ含めません。Realtime は発展課題(Step 8)で扱います。データメソッドは必ず API のメソッド内で呼びます。ファイル最上位で呼ぶと、デプロイ時にエラーになります。

動作確認

test/e2e.test.tsを変更します。既存のテストを以下のコードに置き換えます。

import { test } from 'node:test';
import assert from 'node:assert';
import { installCookieJar } from '@aws-blocks/blocks/utils';
import { api, authApi } from 'aws-blocks';

// Node の fetch はブラウザと違いセッションCookieを保持しないため、
// サインイン後の認証付き呼び出しにはCookieジャーの導入が必要
installCookieJar();

test('createItem は認証で保護されている', async () => {
  // 未ログインでは 401 で弾かれる
  await assert.rejects(() => api.createItem('secret'));

  // サインアップしてサインインする(再実行時はユーザー重複を避ける)
  const username = `bob_${process.hrtime.bigint().toString(36)}`;
  await authApi.setAuthState({ action: 'signUp', username, password: 'password123' });
  await authApi.setAuthState({ action: 'signIn', username, password: 'password123' });

  // ログインに成功する
  const item = await api.createItem('secret');
  assert.strictEqual(item.userId, username);
  const list = await api.listItems();
  assert.ok(list.some((i) => i.title === 'secret'));
});

npm run test:e2eを実行して、テストが緑になることを確認します。緑になったら、次のコマンドで保存されたデータを確認します。

ls -l .bb-data/

JSON ファイルが生成されていれば、ローカルの擬似ストレージにも実際にデータが残っています。開発サーバーを再起動してもデータは残ります。データを消したいときはrm -rf .bb-dataを実行します。

Step 7: フロントエンドを書き換える

最後に、追加した API を使うフロントエンドを実装します。src/index.tsの中身を、次の内容に置き換えます。

import { api, authApi } from 'aws-blocks';
import { AccountMenuBar, Authenticator, onAuthChange } from '@aws-blocks/blocks/ui';
import { html, render } from 'lit-html';

const app = document.getElementById('app')!;
document.body.prepend(AccountMenuBar(authApi)); // サインインとサインアウトのバー

async function renderList() {
  const items = await api.listItems(); // ログイン中のみ成功する
  render(
    html`
      <h1>My Items</h1>
      <form @submit=${onAdd}>
        <input id="title" placeholder="New item..." />
        <button>Add</button>
      </form>
      <ul>
        ${items.map((i) => html`<li>${i.title}</li>`)}
      </ul>
    `,
    app,
  );
}

async function onAdd(e: Event) {
  e.preventDefault();
  const input = document.getElementById('title') as HTMLInputElement;
  if (!input.value.trim()) return;
  await api.createItem(input.value); // 認証で保護した API を呼ぶ
  input.value = '';
  await renderList();
}

// サインイン中はリスト、未サインインはログインフォームを表示する
onAuthChange(authApi, async (user) => {
  if (user) {
    await renderList();
  } else {
    render(html``, app);
    app.appendChild(Authenticator(authApi));
  }
});

UI 部品は AWS Blocks 同梱の@aws-blocks/blocks/uiから読み込みます。AccountMenuBarはサインインとサインアウトのバーを、Authenticatorはログインフォームをそのまま提供します。

onAuthChangeは、表示直後に現在のログイン状態でコールバックを呼びます。これを使って、初期表示を自動で切り替えます。

フロントエンドの動作確認

ブラウザでhttp://localhost:3000を開き、次の流れを試します。

  1. サインアップして、続けてサインインします
  2. 入力欄にテキストを入れて Add を押します
  3. 一覧にアイテムが追加されます
  4. ページを再読み込みしてもデータが残ります

別のユーザーでサインインすると、そのユーザーのアイテムだけが見えます。requireAuthで得た
user.usernameをキーにして、データが分離されているためです。

ここまでが本編のゴールです。同じ1行のコードが、ローカルでは擬似実装、AWS 上では実サービスとして動きます。

Step 8(発展): Realtime とデプロイ

時間と環境に余裕があれば、次を試してみます。

Realtime を有効化する

rtの定義と、createItem内のrt.publish(...)の行を有効化します。さらに、Subscribeするため以下のsubscribeItemsを追加します。

export const api = new ApiNamespace(scope, 'api', (context) => ({
  ...
  // 追加部分
  async subscribeItems() {
    const user = await auth.requireAuth(context);
    return rt.getChannel('items', user.username);
  },
}));

WebSocket の pub/sub も、やはり1つのブロックで実現できます。コードの変更点と周辺コードを記載します。

// Realtime:
const rt = new Realtime(scope, 'live', {
  namespaces: { items: Realtime.namespace(z.object({ action: z.string(), itemId: z.string() })) },
});
//
// Protected API method:
export const api = new ApiNamespace(scope, 'api', (context) => ({
  async createItem(title: string) {
    const user = await auth.requireAuth(context);
    const itemId = Date.now().toString(36);
    const item = { userId: user.username, itemId, title, createdAt: Date.now() };
    await items.put(item);
    // 以下のコメントアウトを解除
    await rt.publish('items', user.username, { action: 'created', itemId });
    return item;
  },
  async listItems() {
    const user = await auth.requireAuth(context);
    return await Array.fromAsync(items.query({ where: { userId: { equals: user.username } } }));
  },
  // 追加部分
  async subscribeItems() {
    const user = await auth.requireAuth(context);
    return rt.getChannel('items', user.username);
  },
}));

フロントエンドに反映するため、src/index.tsonAuthChangeを以下のように修正します。

// サインイン中はリスト、未サインインはログインフォームを表示する
let itemsSub: { unsubscribe(): void } | null = null;

onAuthChange(authApi, async (user) => {
  if (user) {
    await renderList();
    if (!itemsSub) {
      const channel = await api.subscribeItems();
      const sub = channel.subscribe(() => renderList()); // 他タブのpublishを受けて再描画
      await sub.established; // サーバー側の購読確立を待つ
      itemsSub = sub;
    }
  } else {
    itemsSub?.unsubscribe();
    itemsSub = null;
    render(html``, app);
    app.appendChild(Authenticator(authApi));
  }
});

これにより、複数のタブを開くと、片方の追加がもう片方へ即座に反映されます。

各自の AWS へデプロイする

先にローカルでサーバーを起動していたら停止してください。終わってから自分の AWS 環境へ、次のコマンドでデプロイします。

npm run sandbox

sandboxオプションで起動すると、AWS環境へデプロイ後ローカルでサーバーが起動します。フロントはローカルサーバーのみで動き、APIリクエストはローカルサーバーを通して、AWS環境にリクエストを飛ばす動きになります。デプロイ後は、同じnpm run test:e2eが実 AWS を相手に通ります。

重要: 確認が終わったら、必ず次のコマンドでリソースを撤去します。各自のアカウントで動かすため、コスト管理を忘れないようにします。

npm run sandbox:destroy

もし早めに終わったら、npm run deployで本番環境としてデプロイしたり、他のブロックを試してみてください。

Blocks への個人的所感

今までAWS公式のサービスだけだと、ローカル環境での実行は一部のサービス(SAM LocalでのLambdaやDynamoDB Localなど)でしかサポートされていなかったので、ローカル環境での実行テストが完結するのは非常に魅力的です。昨今のAIエージェントを使ったIaCやサーバレスのサービス開発では、AIエージェントをAWS環境と接続してフィードバックを与える必要があります。これは権限の与える範囲の検討やフィードバックまでの時間などいくつか問題がありましたが、このフィードバックを素早く与えられることでこの問題を解決できる可能性があります。

逆にデメリットとしては、ローカルでの動作が実際の本番環境と乖離している場合、本番にデプロイしないと動作が分からなくなります。ローカルでの検証が無駄になる可能性があるので、このローカル環境での再現性が今後重要になりそうです。

また追加の利点として抽象化のレベルが上がるのもあると考えます。AIエージェントにより構築作業が高速化され、生産性を最大化させるためには人間側で認知すべき箇所を減らす必要があります。Blocksは実質L3 Constructの上位版と言えるような抽象化レイヤーです。上位レイヤーの部分さえ読めば問題が解決するなら、人間の認知負荷を減らして価値を最大化できる可能性があります。この点でBlocksは未来のツールとも言えます。この未来のツールがどうなるのか、意見を交わせるとありがたいです。

この記事をシェアする

AWSのお困り事はクラスメソッドへ

関連記事