なぜ構成を公開しようと思ったか
自社のコーポレートサイトを作り直したとき、正直なところ「これ、ちょっと変わった構成になったな」と感じていました。WordPressを捨て、Astroで静的生成し、S3とCloudFrontで配信し、コンテンツの更新はGitベースのPRフローで回す。説明すると大体「え、CMS使ってないんですか?」と聞き返される構成です。
この構成で半年ほど運用してみて、月額のランニングコストが数百円で安定し、Lighthouseスコアも90点台後半をキープしている。このあたりのリアルな数字と設計判断の根拠を、一度まとめておくべきだと考えました。
構成の全体像
全体のアーキテクチャはシンプルです。
[Astro (静的生成)] → [GitHub Actions (CI/CD)] → [S3 (ホスティング)] → [CloudFront (CDN)] → エンドユーザー
Astroがビルド時にHTMLを生成し、GitHub Actionsがテスト・デプロイを担い、S3に静的ファイルを配置、CloudFrontがCDNとしてエッジから配信する。サーバーは一切動いていません。
運用を考えると、動的処理が不要なサイトにわざわざサーバーを立てるのはコストとリスクの両面で損だと考えています。コーポレートサイトの更新頻度は日に何度もあるわけではなく、ビルド時にHTMLを生成するだけで十分です。
コンテンツ更新:PRベースで効率よく回す
ここが一番「変わってる」と言われる部分です。
担当者がMDXで記事を執筆 → ブランチにpush → PR作成
↓
レビュー → マージ
↓
GitHub Actions → ビルド → S3デプロイ
担当者がMDXファイルで記事を書き、PRを出す。レビューを経てマージされれば、GitHub Actionsが自動でビルド・デプロイします。CMSの管理画面を介さない分、Gitに慣れたチームであればこちらのほうが圧倒的に効率が良いと感じています。
さらに、このフロー自体がGit標準のワークフローなので、AIエージェントとの連携も容易です。たとえば定型的な更新作業をAIに任せてPRを出させ、人間がレビューするという運用も組み合わせられます。PRという関所がある以上、人間がApproveしない限り本番には反映されないので、品質面の安心感もあります。
パフォーマンス:なぜLighthouseで90点台後半が出るのか
静的サイト × CDN配信の組み合わせは、パフォーマンスに関してはほぼ最適解です。
Lighthouseのスコアは常に90点台後半、ページによっては100点が出ます。WordPress時代は60〜70点台をうろうろしていたので、体感できるレベルで違う。理由は明確で、HTMLがビルド済みなのでサーバーサイドの処理待ちがゼロ、CloudFrontのエッジキャッシュからレスポンスが返るのでTTFBが数十ミリ秒です。Astroの「必要なJavaScriptだけを送る」という設計思想も大きく効いています。
# Cache-Controlの設計
# HTML → 常に最新を返す
Cache-Control: no-cache
# CSS/JS(ハッシュ付きファイル名)→ 長期キャッシュ
Cache-Control: public, max-age=31536000, immutable
ここは好みが分かれるところですが、私はHTMLをno-cacheにしてassetsをimmutableにする方針を選びました。HTMLのキャッシュを長くすると更新がユーザーに届くまでラグが出るので、運用を考えるとこちらが安心です。
運用コスト:月額8,000円が数百円になった話
月額8,000〜10,000円 vs 月額300〜500円。ここが一番インパクトがあるので、内訳を出します。
| 項目 | WordPress時代 | 現在(Astro + S3 + CF) |
|---|---|---|
| サーバー / ホスティング | 月額 約8,000円 | S3ストレージ: 数円 |
| CDN | 別途契約 or なし | CloudFront: 200〜400円 |
| SSL証明書 | 別途管理 | ACM: 無料 |
| CMS保守 | WordPress運用込み | なし(Git管理) |
| 合計 | 8,000〜10,000円 | 300〜500円 |
ランニングコストの実態はCloudFrontのリクエスト料とS3のストレージ料だけです。最初に試算したとき「本当にこれだけ?」と疑ったのですが、半年分のAWS請求書を見ても実際にこの水準で推移しています。
ただし、これはアクセス規模が小〜中程度のコーポレートサイトの話です。月間数百万PVを超えるサイトではCloudFrontのデータ転送料が積み上がるので、その点は注意が必要です。
この構成が向くサイト、向かないサイト
今思えば、フィットするかどうかの判断基準はわりと明確でした。
向いているケース:
- コーポレートサイト、LP、ブログなど更新頻度が低〜中程度のサイト
- リアルタイムな動的処理(ログイン、カート等)が不要
- すでにAWSインフラを持っている組織
向いていないケース:
- ECサイトのようにユーザーごとに表示が異なるもの
- 数分単位で更新が走るニュースメディア
- 非エンジニアがGit以外の手段でコンテンツ更新したい場合(ただしAIエージェント経由でカバーできる範囲は意外と広い)
---
// Astroのビルド時データ取得
// ビルド時にすべて解決されるのでランタイムのAPIコールはゼロ
const news = await getCollection('news');
const published = news
.filter(n => n.data.status === 'published')
.sort((a, b) => b.data.publishedAt.getTime() - a.data.publishedAt.getTime());
---
<ul>
{published.map(item => (
<li>{item.data.title}</li>
))}
</ul>
このコードはビルド時に完結するので本番ではただのHTMLです。改善の余地としては、記事数が増えたときのページネーションがまだ入っていないこと。今のところ問題になっていませんが、そのうち対応が必要になるはずです。
振り返って
この構成で半年運用して一番実感したのは、「動的処理が要らないなら最初から静的にしておくのが圧倒的に楽」ということでした。当たり前の結論なのですが、WordPress時代はプラグイン更新やセキュリティパッチ対応に毎月数時間取られていた現実があったので、そこから解放されたのは大きい。
今後はプレビュー環境の改善や、AIとの連携による更新作業のさらなる効率化を進めていきたいと考えています。完璧な構成ではないですが、コストとパフォーマンスのバランスという点では、今のところかなり満足しています。