amplesscms

article

サーバレス構成の全体像

Amplify Gen 2 を土台に、AppSync・DynamoDB・S3・Cognito・Lambda をどう組み合わせているか。公開リクエスト、書き込み、非同期処理、キャッシュの考え方を追います。

ampless は、AWS Amplify Gen 2 を土台にしたサーバレス CMS です。管理サーバーを固定で持たず、Amplify が作るマネージドサービスを組み合わせて動かします。

ホームのアーキテクチャパネルにも、同じ考え方をレイヤーごとに分けて描いています。

Runtime map — Cognito / AppSync / S3 / Next.js

レイヤー構成

レイヤー サービス 担当
認証 Cognito 管理者・編集者のログイン、グループ制御
管理 API AppSync GraphQL コンテンツ CRUD、サイト設定の読み書き
データ DynamoDB 投稿、ページ、メディア、サイト設定(KvStore)、PostTag、McpToken
メディア S3 アップロード画像、静的バンドル、サイト設定キャッシュ、プラグイン公開アセット
プラグイン実行 Lambda(trust_level 別) フック、Webhook、画像処理
イベント DynamoDB Streams + SQS 非同期フック・キャッシュ再生成
公開サイト Next.js App Router SSR + middleware ルーティング
CDN CloudFront キャッシュ、エッジ配信

これらは Amplify が CDK で生成し、構成変更も Amplify 側の流れに乗せて反映します。利用者が CDK を直接書く場面を、普段の運用からできるだけ外しているのがポイントです。

投稿のリクエスト経路

/blog-post のような公開 URL にアクセスが来たときは、だいたい次の順番で処理されます。

ブラウザ
  ↓ HTTPS
CloudFront(Amplify Hosting が自動構成)
  ↓ cache miss なら SSR
Next.js middleware
  ├── format: tiptap/markdown/html → テーマレンダリング
  ├── format: html + no_layout    → /raw/<slug> に書き換え(ベア HTML)
  └── format: static              → /static/<slug> に書き換え(S3 へ 302)
  ↓
AppSync GraphQL(API キー、draft 除外カスタムリゾルバ)
  ↓
DynamoDB(Post / Page / Media)

middleware は、投稿の { format, metadata, updatedAt } だけを読む小さな射影を引きます。その結果を Lambda のウォームキャッシュに 60 秒だけ残し、format に応じて内部ハンドラへ振り分けます。CloudFront のキャッシュに当たれば、SSR Lambda は起動しません。

書き込みのリクエスト経路

管理 UI、MCP、公開サイトは、同じ AppSync スキーマを見ています。違うのは認証モードです。管理 UI は Cognito、MCP Lambda は IAM / SigV4、公開サイトは API キーを使います。

管理 UI (Next.js)        → AppSync (Cognito User Pool: admin/editor) ─┐
MCP Lambda (HTTP)        → AppSync (IAM / SigV4: allow.resource)       ├→ DynamoDB / S3
公開サイト / テーマ      → AppSync (API キー: draft 除外リゾルバ)      ─┘

ampless パッケージ本体には、投稿 CRUD の実装を持たせていません。公開しているのは型定義、プラグイン契約、フォーマット変換ヘルパー、PostsProvider のインターフェースなどです。CRUD の実体は AppSync 側にあります。

非同期処理(DynamoDB Streams → SQS、trust_level で fan-out)

投稿公開後の副作用は、書き込みの同期処理から切り離しています。dispatcher がイベントをキューに振り分け、trust_level ごとのプロセッサ Lambda がそれぞれの IAM ロールで処理します。

DynamoDB Stream(Post + KvStore[siteconfig])
  ↓
event-dispatcher Lambda(イベント種別判定 + fan-out)
  ├── SQS: TrustedEventsQueue    → processor-trusted   Lambda(DynamoDB / S3 アクセス権あり)
  └── SQS: UntrustedEventsQueue  → processor-untrusted Lambda(外部 HTTP のみ、AWS 権限なし)
                                                  ↓ リトライ 3 回失敗
                                            EventsDlq(共有、14 日保持)

組み込みの trusted ハンドラは、いまのところ次の二つを担当します。

  • post.index.refreshPostTag 非正規化インデックスの差分更新
  • site.settings.updatedpublic/site-settings.json の再生成(公開サイトはこれを 60 秒キャッシュで読む)

Webhook、RSS 再生成、OG 画像の生成のような処理は、コアに直接抱え込まず、プラグインが trusted / untrusted のどちらかの枠でイベントを購読します。

SQS を間に置くと、メッセージ単位のリトライ、DLQ 退避、流量制御を扱いやすくなります。Stream を直接処理する構成だと、1 件の失敗でバッチ全体が何度もリトライされることがあります。

1 デプロイ = 1 サイトの理由

ampless は、1 Amplify デプロイ = 1 サイトに固定しています。1 デプロイで複数サイトを Host ヘッダで振り分けるモードは入れていません。

理由はキャッシュです。Amplify Hosting の内側にある CloudFront は、キャッシュキーに Host を含めない構成になっています。複数ドメインを束ねると、SSR レスポンスを安全にキャッシュしにくくなります。サイトごとにデプロイを分ければ、この問題を避けられます。

複数サイトを並走させたい場合は、Amplify プロジェクトごと分ける運用にしてください。

なぜ Lambda@Edge / CloudFront Functions を使わないか

Amplify が自動生成する CloudFront に、カスタムエッジ関数を差し込む正規の方法は、いまのところ用意されていません。そのため、プラグイン実行はリージョン Lambda 側に寄せ、エッジでは CloudFront のキャッシュを使う方針にしています。

テキスト変換のような軽い処理なら、リージョン Lambda 側でも 1〜2 ms 程度で終わります。CloudFront のキャッシュが効いているリクエストでは、その Lambda も呼ばれません。

コストが伸び縮みする仕組み

公開ページは静的配信にかなり近い形で返します。書き込み API も、AppSync や DynamoDB などのマネージドサービスの従量課金です。月の固定費として目立つものは、AppSync の API Key 更新と、DynamoDB の最低限の保管料くらいに収まります。

金額感はホームの料金テーブルと、その下のシナリオも見てください。小さなブログなら無料枠に収まりやすく、突発的に読まれた月もサーバレスと CDN 側で受け止めます。

関連記事