amplesscms

article

サーバレス構成の全体像

AWS Amplify Gen 2 を土台に、AppSync・DynamoDB・S3・Cognito・Lambda を組み合わせている構成。各レイヤーで何が動いて、なぜ「使った分だけ請求」のかたちに収まるのかを、図と一緒にたどっていきます。

ampless は AWS Amplify Gen 2 を土台に置いた CMS で、固定の管理サーバーを持ちません。Amplify が用意してくれるマネージドサービス(AppSync / DynamoDB / S3 / Cognito / Lambda)を組み合わせて動かす作りにしてあり、結果としてふだんは「使った分だけ請求される」シェイプに収まります。

ホームのアーキテクチャパネルにも、同じ層構造を切り出して描いてあります。

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

レイヤー構成

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

上記はすべて、Amplify が CDK で自動生成・自動更新します。利用者の側で CDK を直接触らずに済むのが、ここでは大きいです。

投稿のリクエスト経路

/blog-post のような公開 URL にリクエストが来たときの流れです。

flowchart TD
  Browser["ブラウザ"] -->|"HTTPS"| CloudFront["CloudFront<br/>Amplify Hosting"]
  CloudFront -->|"cache miss"| Middleware["Next.js middleware"]
  Middleware --> Projection["Post projection<br/>format / metadata / updatedAt"]
  Projection -->|"tiptap / markdown / html"| Themed["テーマレンダリング"]
  Projection -->|"html + no_layout"| Raw["/raw/:slug<br/>ベア HTML"]
  Projection -->|"static"| Static["/static/:slug(/...)<br/>S3 bytes を stream"]
  Static -->|"6 MB 超"| Presigned["302 presigned URL fallback"]
  Themed --> AppSync["AppSync GraphQL<br/>API key / draft 除外"]
  Raw --> AppSync
  AppSync --> DynamoDB["DynamoDB<br/>Post / Page / Media"]

middleware は、投稿の { format, metadata, updatedAt } だけを取り出した小さな射影を引いて、Lambda のウォームキャッシュに 60 秒キープしながらリクエストを内部ハンドラに振り分けています。CloudFront 側のキャッシュが効いていれば、SSR Lambda はそもそも起動すらしません。

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

管理 UI、MCP、公開サイト(こちらは読み取りのみ) — どれも同じ AppSync スキーマを叩いていて、違うのは認証モードの部分だけです。

flowchart LR
  Admin["管理 UI<br/>Cognito User Pool<br/>admin / editor"] --> AppSync["AppSync GraphQL"]
  MCP["MCP Lambda<br/>IAM / SigV4<br/>allow.resource"] --> AppSync
  Public["公開サイト / テーマ<br/>API key<br/>draft 除外リゾルバ"] --> AppSync
  AppSync --> DynamoDB["DynamoDB"]
  AppSync --> S3["S3"]

ampless パッケージそのものに、CRUD のロジックは入っていません。型定義、プラグイン契約、フォーマット変換ヘルパー、PostsProvider のインターフェース — このあたりだけを公開していて、CRUD の実体側は AppSync が抱えるかたちです。

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

公開後の副作用は、書き込みの同期処理からきれいに切り離してあります。dispatcher が両方のキューに fan-out しつつ、trust_level の分離は各プロセッサ Lambda の IAM 実行ロール側で実現する作りです。

flowchart TD
  Stream["DynamoDB Stream<br/>Post + KvStore[siteconfig]"] --> Dispatcher["event-dispatcher Lambda<br/>イベント種別判定 + fan-out"]
  Dispatcher --> TrustedQueue["SQS: TrustedEventsQueue"]
  Dispatcher --> UntrustedQueue["SQS: UntrustedEventsQueue"]
  TrustedQueue --> TrustedProcessor["processor-trusted Lambda<br/>DynamoDB / S3 access"]
  UntrustedQueue --> UntrustedProcessor["processor-untrusted Lambda<br/>CMS データ権限なし"]
  TrustedProcessor -->|"3 回失敗"| DLQ["EventsDlq<br/>14 日保持"]
  UntrustedProcessor -->|"3 回失敗"| DLQ

組み込みの trusted ハンドラが受け持っているのは、いまのところ二つです。

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

イベント駆動のプラグイン(SEO の sitemap 再生成、RSS の feed.xml 再生成、Webhook の HMAC 署名 POST など)も同じパイプラインに乗ります。Webhook のような外向き HTTP は trusted 側、外部 URL を叩くだけの軽量フックは untrusted 側、というふうに trust_level で分けて流す形です。

なお、ページの <head> への script 注入(GA4 / GTM / Plausible / Cookie consent など)や、投稿本文への JSON-LD / 読了時間バッジ追加(Schema JSON-LD / Reading time など)は、このイベントパイプラインではなくリクエスト時に走るプラグイン拡張面(publicHead / publicBodyEnd / publicBodyForPost / publicHtmlForPost)から出力されます。

間に 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 の API Key 更新と、DynamoDB の最低保管料くらいに収まります。

数字感はホームの料金テーブルと、その下のシナリオも見てみてください。小さなブログなら無料枠の範囲に収まりますし、突発的なバズもサーバレスとマネージドサービスの側で吸収していけます。

関連記事