このブログは、月$12のLightsailインスタンスの前にCloudFrontを置いて配信しています。CDNにかかっている費用は0円です。
LightsailのコンソールにはCDNを追加するボタンがあり、そこから作ると月$2.50かかります。ところが同じCloudFrontを普通のAWSサービスとして自分で組むと、無料枠に収まります。中身は同じエッジネットワークで、違いは設定を誰がやるかだけです。
この構成にして一番助けられたのは、CloudFrontが無料で録り続けているメトリクスです。サイトの障害が実は2ヶ月続いていたと後から突き止められたのは、この記録のおかげでした。組み方と、やってみて分かったハマりどころをまとめます。

入り口で値段が違う
まず料金の整理です(Lightsail料金表)。Lightsailの「ディストリビューション」は定額制で、最小プランが月$2.50(転送50GB)、以降$10(200GB)、$35(500GB)と階段が上がります。Lightsailらしい分かりやすさですが、0円のプランはありません。
一方、素のCloudFrontは2つの選び方があります(CloudFront料金表)。
- 従量制:常時無料枠が毎月1TB転送+1,000万リクエスト。個人ブログでこれを使い切るのはまず無理です
- 定額プラン(2025年11月開始):Freeプランが$0で月100GB・100万リクエスト。WAFとDDoS対策とDNSが同梱で、超過課金なし(一時的なスパイクは月間枠の3倍まで吸収)
このブログの直近30日の実測は、転送が約2GB、リクエストが約10万件でした。従量制の無料枠の0.2%、定額Freeプランの枠で見ても2%です。どちらを選んでも0円圏内です。厳密にはRoute 53のホストゾーン代$0.50/月がかかりますが、独自ドメインを運用していれば元々払っているものです。
「ボタン一つで足せる分かりやすさ」に月$2.50を払うか、半日の設定作業で0円とフル機能を取るか。この記事は後者の記録です。
構成
訪問者
│ https://blog.hiroaki.jp
▼
Route 53(ALIASレコード)
▼
CloudFront ─ 証明書はACM(無料・自動更新)
│ https + 秘密ヘッダ
▼
Lightsailインスタンス(オリジン)─ 証明書はLet's Encrypt
流れは図のとおりです。訪問者のアクセスはRoute 53のDNSでCloudFrontへ向けられ、訪問者に近い配信拠点(エッジ)にページのキャッシュがあれば、そこから即座に返ります。キャッシュにないページだけ、CloudFrontがオリジンのLightsailへ取りに行きます。取ってきたページは訪問者へ返すと同時にエッジへ保存されるので、以降の同じページへのアクセスはオリジンには届きません。
WordPress側で必要になるのは、このキャッシュの後始末です。CloudFrontは一度配ったページを有効期限までエッジに持ち続けるので、何もしないと、記事を公開したり修正したりしても、しばらく訪問者には更新前のページが見えてしまいます。これを防ぐのがC3 Cloudfront Clear Cacheというプラグインで、記事の公開・更新に合わせて該当ページのキャッシュを自動で消してくれます。あわせて、管理画面とログインページはキャッシュの対象から外しておきます。ログイン中の画面がキャッシュされて他の訪問者に配られては困るからです。
作業自体はACM証明書の発行(バージニア北部リージョン指定に注意)、ディストリビューション作成、Route 53の切り替えで半日でした。ここから、やってみて分かったハマりどころを3つ書きます。
ハマりどころ1:証明書は2枚必要になる
CDNを挟むと、TLSの区間が2つに分かれます。訪問者とCloudFrontの間はACM証明書(無料・自動更新)で済みますが、CloudFrontとオリジンの間にも別の証明書が要ります。
ここでACMは使えません。AWSの公開ACM証明書はエクスポートできず、Lightsailのインスタンスにインストールする手段がないからです。オリジン側は従来どおりLet’s Encryptで取り、自動更新を仕込みます。CDNを入れても、Let’s Encryptからは離れられません。
オリジンへの接続をhttpにすれば証明書は1枚で済みますが、おすすめしません。次のハマりどころで書く秘密ヘッダが、平文でインターネットを流れることになるからです。
ハマりどころ2:オリジンは隠れない
CDNを挟んでも、オリジンのサーバは今までどおりインターネットに公開されたままです。そしてインターネットには、公開サーバの弱点を自動で探して回るプログラムが常に巡回しています。この記事ではスキャナと呼びます。このブログのアクセスログにも毎日残っています。しかもオリジン用のホスト名は、Let’s Encryptで証明書を取った瞬間に、誰でも検索できる証明書の公開記録(Certificate Transparencyログ)へ載ります。つまりスキャナは、CDNを素通りしてオリジンを直接叩けます。実際、404の記事で書いたスキャンのバーストは、すべてこの経路でオリジンに直撃していました。
対策は共有シークレットです。CloudFrontがオリジンへのリクエストに秘密の値を持つカスタムヘッダを付け、オリジン側のApacheはそのヘッダがない接続を403で断ります。
設定は2箇所です。CloudFront側は、ディストリビューションのオリジン設定にある「カスタムヘッダーの追加」で、ヘッダ名と秘密の値を登録します(公式ドキュメント)。値はopenssl rand -base64 36などで作った、長いランダム文字列にします。
オリジン側のApacheには、そのヘッダを検査する設定を入れます。ヘッダ名と値は例です。
# CloudFrontだけが知る秘密ヘッダを持つリクエストだけを通す
SetEnvIf X-Origin-Secret "^CloudFrontに登録したのと同じ値$" from_cloudfront
<Directory /var/www/html>
Require expr %{reqenv:from_cloudfront} == '1'
</Directory>
注意点が2つあります。まず、値は2箇所に存在するので、変えるときは必ず両方を更新します。片方だけ変えると全訪問者が403になります。無停止で入れ替えるなら、Apache側に新旧2つのSetEnvIf行を並べてからCloudFront側を新しい値にし、配信への反映を確認してから古い行を消します。もう1つ、Let’s Encryptの更新確認(HTTP-01)はCloudFrontを通らず認証局から直接届くので、/.well-known/acme-challengeのパスだけは、このヘッダ検査の対象から外しておきます。ここを塞ぐと証明書の自動更新が失敗します。
ハマりどころ3:WordPressの404はキャッシュされない
CloudFrontを置けばオリジンへのリクエストはほぼなくなる、と思っていたら、404だけは違いました。WordPressは404応答にキャッシュ禁止のヘッダを付けるので、スキャナが叩く存在しないURLは全部オリジンに届きます。この話は長くなったので別記事にしました。

キャッシュの寿命を延ばして、障害に備える
「ハマりどころ2」のとおりオリジンは隠れません。その帰結を2026年9月5日に体験しました。スキャナの並列接続にApacheのワーカーを占有され、17分間、キャッシュにないページだけが応答しなくなったのです(顛末は上でリンクした404の記事の「対策3」に書きました)。裏を返すと、キャッシュにあったページは無事でした。それなら、キャッシュの寿命を延ばせばそのまま障害への備えになります。
WordPressはHTMLにCache-Controlヘッダを付けないため、それまでキャッシュ期間はCloudFrontのデフォルトTTLである24時間任せでした。これをApache側で明示するようにしました。本番の設定はこの2行です。
Header setifempty Cache-Control "public, max-age=300, s-maxage=604800, stale-while-revalidate=86400, stale-if-error=604800" "expr=%{REQUEST_STATUS} == 200 && %{CONTENT_TYPE} =~ m#^text/html#"
Header setifempty Cache-Control "public, max-age=300, s-maxage=3600, stale-while-revalidate=600, stale-if-error=604800" "expr=%{REQUEST_STATUS} == 200 && %{CONTENT_TYPE} =~ m#^application/(rss|atom)\+xml#"
| ディレクティブ | 意味 |
|---|---|
| s-maxage=604800 | CloudFrontに7日間保持させる |
| stale-while-revalidate=86400 | 期限切れでも古いキャッシュを即座に返し、裏で再取得する |
| stale-if-error=604800 | オリジンがエラーや無応答なら、期限切れキャッシュを最長7日まで配る |
| max-age=300 | ブラウザには5分しか持たせない |
要はstale-if-errorです。CloudFrontはこのディレクティブを解釈するので、オリジンが応答しない間は手元の古いキャッシュを配り続けます。先の17分の障害のとき、この設定が入っていれば読者には何も見えなかったはずです。ブラウザ側のmax-ageを5分に留めているのは、ブラウザのキャッシュはあとから消す手段がないからです。
7日間もキャッシュして更新が遅れないのかというと、鮮度の管理をTTLからキャッシュ無効化(invalidation)に切り替えたので問題ありません。CloudFront連携プラグインのC3 Cloudfront Cache Controllerが、記事の公開・更新のたびにトップページ・記事・カテゴリアーカイブを自動で無効化します。7日という数字は「更新が何もなければ」の上限でしかありません。無効化のCloudFront無料枠は月1,000パスあるので、個人ブログの更新頻度なら課金は発生しません。
例外はフィードで、C3の無効化対象に入っていません。ここはコードを書かず、上の2行目のとおり1時間キャッシュの自然更新に割り切りました。RSSリーダーのポーリング間隔は数十分から1時間程度なので、実害はないと判断しています。
公平のために書くと、この構成ではサイドバーの人気記事ランキングのような動的要素も、次の無効化まで凍結します。我が家の場合は記事を公開するたびにトップページが無効化されるので、実際の凍結期間は公開間隔と同じです。ここが許容できないサイトでは、s-maxageを短くするか、動的部分をJavaScriptで別読みする構成が要ります。
無料のメトリクスが、2ヶ月の障害を録っていた
素のCloudFrontを選んで一番得だったのは、実はメトリクスでした。CloudFrontはエラー率やリクエスト数を無料で記録し続けていて、1時間粒度なら15ヶ月分を遡れます。
この記録が働いたのは、先日サイトが落ちたときです。調べ始めた時点では、数日の障害だと思っていました。ところがメトリクスを遡ると、日平均の5xxエラー率が20%を下回らない日が、7月4日から57日間続いていました。「実質2ヶ月落ちていた」と確定させてくれたのは、設定した覚えもないこの無料の記録です。
今はこのメトリクスにCloudWatchアラームを2本立てて、5xx増加と無トラフィックをメールで受けています。アラームは10本まで無料枠内です。ダウンタイム監視のサービスを別途契約しなくても、CDNに元々付いている機能で足りました。
ただし万能ではありません。9月のワーカー枯渇障害では、キャッシュが200を返し続けたため5xx率がなかなか閾値に届かず、アラームが鳴ったのは復旧の直前でした。エラー率のような統計監視は、トラフィックの少ないサイトでは遅れて鳴ります。今はサーバ内に毎分のヘルスチェックと自動再起動を併設して、この穴を塞いでいます。
まとめ
Lightsailの手軽さはそのままに、CDNだけ素のAWSで組む。この構成の価値は$2.50/月の節約そのものより、フル機能が使えることにあります。カスタムヘッダによるオリジン保護、エラーキャッシュ、障害時に期限切れキャッシュを配るstale-if-error、無料のメトリクスとアラーム。純正CDNのAPI仕様も確認しましたが、設定できるのはTTLと、転送するヘッダ・Cookie・クエリの指定までで、オリジンへのカスタムヘッダやエラー応答のキャッシュ制御はありません。メトリクスは純正CDNにもあるものの、15ヶ月分の保持とCloudWatchアラームの組み合わせは素のCloudFrontならではです。
半日の設定作業と、証明書2枚・秘密ヘッダ2箇所という管理の増分を許容できるなら、個人ブログのCDNはこれで十分だと思います。


コメント