本ブログ「yagibrary」は、これまで 「Next.js + AWS Amplify Hosting」 で運用されていました。
しかし、個人ブログとしてのセキュリティ向上、運用コストの極限までの削減、そして何よりも「ページの表示速度」を極限まで高めるため、今回 「Astro + AWS S3 + AWS CloudFront」 の完全な静的ホスティング(Jamstack)構成へ移行しました。
移行にあたり、 「ウェブの管理画面(ログインやAI投稿支援機能)」をいっそ完全に廃止する という思い切った意思決定も行いました。
本記事では、このマイグレーションの背景から、具体的なシステム構成、そして移行中・移行後に直面した技術的な課題(サブディレクトリのAccess Denied問題やCloudCannon連携)の解決策までを詳細に共有します。
🛠 移行前の構成と抱えていた課題
移行前のシステムは、フロントエンドにNext.js(App Router)を使用し、AWS Amplify Hosting で動的(あるいはSSR/ISRを交えた)ホスティングを行っていました。また、AIを活用した投稿アシスタントや、管理者が直接ログインしてブラウザ上で執筆できる管理画面・APIも本番環境に同居させていました。
この構成には以下の3つの課題がありました。
- 管理画面に伴うセキュリティリスク 本番環境(Amplify)上に管理者用のログイン画面やセッション管理API、AI用のアクセストークンが露出していたため、不正アクセスやトークン漏洩に対する継続的なセキュリティ管理(心理的負荷)が必要でした。
- SSR(サーバーサイドレンダリング)のオーバーヘッドとビルドの警告 Next.jsの動的な機能(動的なsitemap.xmlの生成など)が原因で、ビルド時に「DYNAMIC_SERVER_USAGE」警告が発生し、完全に純粋な静的サイトとしてビルドすることが難しくなっていました。
- 運用・ビルドコスト Amplify Hosting は非常に便利ですが、ビルド時間($0.01/分)や、データ転送量、ホスティング料金が個人開発ブログとしてはやや割高に感じる部分がありました。
🌐 移行後の新アーキテクチャ
新しい構成では、余分なサーバー処理を一切持たない 「Astroによる100%静的サイト」 を作成し、AWSの堅牢なグローバルCDNである CloudFront と、静的ストレージ S3 でホスティングします。
全体アーキテクチャ図
graph TD
subgraph GitHub
Repo["GitHub リポジトリ (master)"] -->|Push検出| Actions["GitHub Actions"]
end
subgraph AWS Cloud
Actions -->|Astro静的ビルド & 同期| S3["AWS S3 (Private Access)"]
S3 -->|読み込み| CF["AWS CloudFront (CDN)"]
CF_Func["CloudFront Functions<br/>(URL自動補完)"] -.->|リクエストをリライト| CF
end
User["読者 (Webブラウザ)"] -->|HTTPSリクエスト| CF
✍️ 新しい執筆・デプロイフロー (GitOps)
管理画面を廃止したため、記事の投稿は以下のシンプルなワークフローになります。
- ローカルでMarkdown(MD)ファイルを作成・編集する(VS Codeなどで執筆)。
- Git でプッシュする(
git push origin master)。 - GitHub Actions がプッシュを検知し、クラウド上で
astro buildを実行。 - 生成された静的ファイルを
aws s3 syncコマンドで S3 へ転送。 - CloudFront のキャッシュをパージ(クリア)して、数分で世界中に公開される。
AIによるアシスタント機能が必要な場合は、ローカルのVS Code内でAI拡張機能(Copilotなど)やWebのGemini/ChatGPTを使うことで、本番環境にAPIキーを持たせることなく安全に執筆を補佐できます。
🚀 技術的移行のポイントとハマりどころの解決
今回のリプレイスで直面した技術的な問題と、そのクリア方法をまとめました。これから同様の移行をする方の参考になれば幸いです。
1. サブディレクトリの 403 / Access Denied 問題 (最重要)
S3バケットをパブリック非公開にし、CloudFrontの OAC (Origin Access Control) を使ってアクセス制限を行うセキュアな構成において、最も発生しやすい問題です。
Astroはデフォルトで /posts/my-post/index.html のように、ディレクトリ単位で静的ファイルを出力します。
ブラウザから https://yagibrary.com/posts/my-post/ にアクセスした際、CloudFront は自動的に index.html を補完してくれません。そのため、S3バケットに対して /posts/my-post/ という実在しないフォルダキーを直接読みに行き、S3の権限エラー(Access Denied)が発生します。
【解決策】CloudFront Functions によるURLリライト
AWSのベストプラクティスに基づき、CloudFrontへのリクエストが発生した瞬間にURLの末尾に自動的に index.html(または /index.html)を付与する、超軽量なエッジプログラム(CloudFront Functions)を実装しました。
function handler(event) {
var request = event.request;
var uri = request.uri;
// URLの末尾が / で終わる場合は index.html を付与
if (uri.endsWith('/')) {
request.uri += 'index.html';
}
// 拡張子がなく、末尾が / でない場合は /index.html を付与 (例: /posts/my-post)
else if (!uri.includes('.')) {
request.uri += '/index.html';
}
return request;
}
これを Viewer Request イベントに関連付けました。この関数は毎月200万回まで永久に無料で実行できるため、個人ブログの規模であれば追加コストは「ゼロ」です。
2. ビルド生成物(dist)を Git の追跡から外す
Astroはビルド成果物を /dist フォルダに、キャッシュなどの一時ファイルを /.astro フォルダに出力します。
Next.jsの .gitignore をそのまま引き継いでいると、これらのビルド用フォルダが Git の追跡対象に入ってしまい、ローカルでビルドするたびに数千件のファイル変更差分(ノイズ)が発生してしまいます。
.gitignore に忘れずに以下を追記します。
# astro
/dist/
/.astro/
3. CloudCannon (ヘッドレスCMS) 連携と日付の型エラー
GitベースのヘッドレスCMSである CloudCannon を導入し、外出先やスマホからでも記事の簡易的な執筆や管理ができるようにしました。
しかし、CloudCannonのエディタから記事を保存した際、記事のメタデータである日付がダブルクォーテーションで囲まれない date: 2026-06-11 (YAML Dateオブジェクト型) として保存されてしまいました。
Astroのスキーマ定義(src/content/config.ts)で date: z.string()(文字列型)を厳密に定義していたため、Zodの型エラーが発生してデプロイが強制終了してしまいました。
【解決策】Zodの z.coerce.string() の適用と、表示側のフォーマット統一
まず、スキーマ側の定義を z.coerce.string() に緩和しました。これにより、届いたデータが Dateオブジェクトであっても、Astroが自動的に文字列に変換(Coerce)してビルドを通すようにします。
// src/content/config.ts
import { defineCollection, z } from 'astro:content';
const postsCollection = defineCollection({
type: 'content',
schema: z.object({
title: z.string(),
date: z.coerce.string(), // 日付オブジェクトも自動で文字列に強制変換する
summary: z.string(),
tags: z.array(z.string()),
}),
});
次に、Date型から変換された日付は Tue Jun 09 2026... のような長い英語表記になり、元々の "2026-06-09" という文字列表示と表記が揺れてしまうため、Astroの各表示コンポーネント(一覧や詳細)で、日付を表示する際に必ず YYYY-MM-DD に統一するフォーマット関数(formatDate)を通すようにしました。
// src/utils/date.ts
export function formatDate(dateStr: string): string {
if (!dateStr) return '';
try {
const date = new Date(dateStr);
if (isNaN(date.getTime())) return dateStr;
const yyyy = date.getFullYear();
const mm = String(date.getMonth() + 1).padStart(2, '0');
const dd = String(date.getDate()).padStart(2, '0');
return `${yyyy}-${mm}-${dd}`;
} catch {
return dateStr;
}
}
📈 移行した結果とメリット
今回のスタック変更により、ブログは以下のように劇的に改善しました。
- パフォーマンスの向上 Astroが生成するHTMLはJavaScriptが極限まで削ぎ落とされたもの(Zero-JS)になります。Lighthouseスコアはパフォーマンス・SEOを含めほぼ「オール100点満点」をマークし、スマートフォンの回線環境からでも一瞬で表示されるようになりました。
- コストの劇的な削減 Amplifyのホスティング費用とビルド課金から解放されました。 CloudFrontはデータ転送が毎月1TBまで永久無料、S3のストレージ(数百MB)は数円、GitHub Actionsも無料枠の範囲内です。結果として、毎月のAWS費用はRoute 53のドメイン管理固定費($0.50 / 約80円)のみになりました。
- セキュリティの心配がゼロに 本番サーバー上にNode.jsの実行環境や、管理者用ログインフォーム、API、認証データベースが1つも存在しないため、物理的にハッキングや不正アクセスを受ける対象が存在しません。個人開発者にとって「管理しなくていい」というのは最大の安心材料です。
📝 まとめ
ブログのような「読み取り専用」のコンテンツが主体のWebサイトにおいて、Astro + S3 + CloudFront は現在考えられる中で最も高速、安価、そして最高にセキュアな選択肢の一つです。
Next.jsのような動的なフルスタックフレームワークは非常に強力ですが、もし「自分のブログが少し重い」「毎月のAmplifyの費用や管理画面のセキュリティが不安」と感じている場合は、思い切ってAstroへの移行と、管理画面を廃止した「GitOps」への転換を検討してみてはいかがでしょうか。