はじめに
本記事では、AWSを利用した、低コストで安定感のある静的サイトの作り方をまとめています。
実際の案件での経験を元に書いているので、実践的な情報が欲しい方は、ぜひご一読ください。
というわけで、早速ですが…。
個人・小規模チームでの公式サイトやLP制作では、こんな要件が多いのではないでしょうか。
- できるだけ低コストで運用したい
- 独自ドメイン + HTTPSで公開したい
- 画像やHTMLを安定して配信したい
- サーバー管理の手間は増やしたくない
今回は、これらをすべて満たせる構成として、配信基盤に S3 + CloudFront + OAC を採用したときの構成や運用方針をまとめます。
サイト本体は静的ファイルとして管理し、S3をオリジン、CloudFrontをCDNとして公開窓口にする構成です。
静的サイトの構成としてはよくある形ですが、意外と気を付けないといけない点や、AWSだからこそ出来ることもあるので、一緒に見ていきましょう。
用語解説
この記事で出てくる用語を軽く解説しておきます。
| 用語 | この記事での扱い |
|---|---|
| Amazon S3 | 静的ファイルを置くオブジェクトストレージ。 高い耐久性があり、Versioningも設定しやすいです。 |
| CloudFront | ユーザーからのアクセスを受けるAWSのCDNサービス。 S3の前段に置き、CloudFront Functionsでエッジの軽い処理も実行できます。 |
| OAC / OAI | CloudFrontからS3へアクセスするときの制御方式。 OACはOrigin Access Control、OAIはOrigin Access Identityの略で、OAIの後継がOACと捉えておくと読みやすいです。 |
| Route 53 | AWSのDNSサービス。 |
| ACM | AWS Certificate Managerの略。 CloudFrontでHTTPS配信するためのTLS証明書を管理します。 |
| AWS WAF | AWSが提供するWAF(Web Application Firewall)。 必須ではありませんが、CloudFrontの前段防御として使えます。 |
| Response Headers Policy | CloudFrontでレスポンスヘッダーを付与するための設定。 セキュリティヘッダーをまとめて付けたいときに使います。 |
| Custom Error Response | CloudFrontでエラー時のレスポンスを調整する設定。 S3由来の AccessDeniedをそのまま見せず、サイト用のエラーページへ寄せるときに使います。 |
| DryRun | 実際には変更せず、実行した場合の差分だけを確認する考え方。aws s3 sync --dryrun のように、デプロイ前の確認で使います。 |
構成図
今回の構成を図にすると、以下のようなイメージです。
AWSサービスアイコンは、AWS公式のArchitecture Iconsをお借りしています。
(社内構成図など作るときにも便利なので、とてもオススメです。)
採用した構成
S3+OAC+CloudFrontをベースとして構成しています。
オンプレやVPSと比べても、設計~運用までかなりシンプルにできます。
簡単に全体設計をまとめておきます。
- S3に静的ファイルを配置
- CloudFrontでHTTPS配信
- OACでS3を非公開化
- CloudFront FunctionsでURL補正やリダイレクト
- Response Headers Policyでセキュリティヘッダーを付与
- S3 Versioningで復旧しやすくする
この構成で運用がシンプルになる理由は簡単で、AWSマネージドで動く部分を使わせてもらえるからです。
安定感が出るのも同じく、配信基盤がAWSのマネージドサービスを活用しているから。
コストが軽く済むのは、クラウドのメリットとしてよくあげられる従量課金制の恩恵です。
最初の設定さえ、しっかり詰めておけば、運用保守はかなり楽にしていけます。
なぜS3単体ではなくCloudFrontを挟むのか
S3単体でも、ホスティング機能を使えば、静的ウェブサイトとして公開はできるでしょ、と思う方がいるかもしれません。
もちろん、その通りです。
ただ、例えば公式サイトや企業のサイトとして運用するなら、CloudFrontを前段に置いた方が扱いやすくなります。
いくつか理由を考えてみましょう。
- 独自ドメイン + HTTPSを扱いやすい
- キャッシュにより配信が速くなりやすい
- S3バケットを直接公開しなくてよい
- セキュリティヘッダーを付けやすい
- 403 / 404のエラーページ制御がしやすい
- 将来的にパスごとの制御を追加しやすい
パッと思いつくだけでも、これだけ出てきます。
この中で特に重要なのは、S3を直接公開しないでよい という点ですね。
OACを使うことで、ユーザーはCloudFront経由でのみコンテンツにアクセスします。
S3バケット自体はPrivateのままでいいので、セキュアに運用できます。
OACでS3を非公開にする
S3側ではBlock Public Accessを有効にし、CloudFrontのDistributionからのみ読み取りを許可していきます。
S3に、以下のようなバケットポリシーを入れていきます。
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "AllowCloudFrontServicePrincipalReadOnly",
"Effect": "Allow",
"Principal": {
"Service": "cloudfront.amazonaws.com"
},
"Action": "s3:GetObject",
"Resource": "arn:aws:s3:::<BUCKET_NAME>/*",
"Condition": {
"StringEquals": {
"AWS:SourceArn": "arn:aws:cloudfront::<AWS_ACCOUNT_ID>:distribution/<DISTRIBUTION_ID>"
}
}
}
]
}
この形にしておくと、S3バケットを直接公開せずに、CloudFront経由の配信だけを許可できます。
公開経路をCloudFrontに寄せられるので、キャッシュ、ヘッダー、エラーページの制御もしやすくなります。
~ちょこっと補足~
「S3のBlock Public Accessが有効なままなのに、なんでサイト公開できているの?」
こんな質問を受けたことがあります。とてもわかります。ややこしいですよね。
解説すると、Block Public Accessは、S3バケットやオブジェクトが「インターネット上の誰でも読める状態」になることを防ぐための設定です。
しかし、今回の構成ではユーザーがS3へ直接アクセスするわけではありません。
ユーザーはCloudFrontへアクセスし、CloudFrontがOACを使って署名付きのリクエストとしてS3へオブジェクトを取りに行きます。
そのため、S3側のバケットポリシーでも、全体公開するのではなく、対象のCloudFront Distributionからのアクセスだけを許可するものにしています。
これが、Block Public Accessのまま、サイト公開が成り立つ理由です。
※ちなみに以前の構成例ではOACではなく、OAIを見かけることもありますが、新しく作るならOACを前提に考えるのをオススメします。
wwwあり・なしの整理
公式サイトでは、example.com と www.example.com の両方をどう扱うかも、最初に決めておきたいところです。
今回は、CloudFrontを1本にまとめ、www はCloudFront Functionsでapexドメインへ301リダイレクトする方針にしました。
別のS3バケットや別のCloudFront Distributionを用意する方法もあります。
但し、管理対象が増えやすくなるため、今回は不採用にしました。
具体的な設定としては、以下の通りです。
-
example.comを正規URLにする -
www.example.comは同じCloudFrontで受ける - CloudFront Functionsで
https://example.comに301リダイレクト
ちなみに、CloudFront Functionsでは、拡張子なしURLを /index.html に寄せる処理も一緒に入れています。
静的サイトでも、URLの見え方を少し整えたいという、ちょっとしたこだわりです。
セキュリティヘッダーを付与する
静的サイトでも、最低限のセキュリティヘッダーは付けておきましょう。
CloudFrontでは、Response Headers Policyを使って、ヘッダーを付与できます。
付けたいヘッダーの例としては、
Strict-Transport-SecurityContent-Security-PolicyX-Content-Type-Options: nosniffReferrer-Policy-
X-Frame-Optionsまたはframe-ancestors
といったヘッダーが挙げられます。
要件や環境にもよりますが、静的サイトだから安全とは言い切れない時代だからこそ、外部リンク・画像・CMS由来のHTML・埋め込み要素など、意図しない表示や読み込みを防ぐ設計も必要になってきます。
最初から完璧なCSPを書くのは大変ですが、少なくともどの外部リソースを許可しているのかは把握できる状態にしておくのをオススメしておきます。
エラーページを整える
S3をPrivateにしてCloudFront経由にすると、設定やパスによってはS3由来のAccessDeniedが見えることがあります。
公式サイトで生のXMLエラーが表示されるのは避けたいので、CloudFrontのCustom Error Responseでエラーページへ誘導しました。
この設定を入れると、403や404になったときに、共通のerror.htmlを返すようにできます。
<html lang="ja">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>ページが見つかりません</title>
<style>
/* 見た目の調整 */
</style>
</head>
<body>
<main>
<h1>ページが見つかりませんでした</h1>
<p>URLが変更されたか、ページが削除された可能性があります。</p>
<p><a href="/">トップページへ戻る</a></p>
</main>
</body>
</html>
このくらいの簡単なHTMLを差し込んでおくだけでも、ちゃんと作ってる感がでてきます。
デプロイ時に気をつけたこと
S3を使った静的サイトのデプロイでは、aws s3 sync を使うことが多いと思います。
aws s3 sync .\site\ s3://example-site-bucket --delete
--delete は便利ですが、扱いには注意が必要です。
S3上にある自動生成ファイルや運用上残したいファイルまで消してしまう可能性があります。
そのため、実運用では以下を意識しました。
- デプロイ前にDryRunする
- 自動生成されるファイルは除外する
- 先に全削除してからアップロードしない
- 問題発生時に戻せるようS3 Versioningを有効化する
- CloudFront Invalidationは必要なパスに絞る
例えば、まずは以下のように差分だけを確認します。
aws s3 sync .\site\ s3://example-site-bucket --delete --dryrun
いきなり本番反映せず、まず差分を見るだけでも事故をかなり減らせます。
また、実際の運用では、LambdaなどでJSONやHTMLを自動生成する場合も多くあります。
静的デプロイと自動生成ファイルが混ざると、事故も起こりやすくなるのが現実です。
そのため、実務で扱うときは、将来的な運用も考慮して、事前にルールを決めておくと良いと思います。
この後にお話するDesign for Failure的な考え方も大切ですが、事故を起こさないような最低限の取り決めはしておくに越したことはありません。
ロールバック設計と考え方
静的サイトは構成をシンプルにしやすい分、ロールバックもシンプルに考えられます。
今回の方針は以下のようにしました。
- S3 Versioningを有効にする
- 直前の成果物を保持する
- 問題があれば前回成果物を再同期する
- CloudFrontの対象パスだけInvalidationする
ロールバック設計では、「壊さない」「壊されない」ではなく、「壊れてもすぐ戻せる」ことを前提に置きます。
AWS Well-Architected Frameworkの信頼性の柱でも、設計原則として Automatically recover from failure や Test recovery procedures が挙げられています。
また、Failure managementの中でも、バックアップを取得するだけでなく、復旧できることを確認する考え方が整理されています。
これらは、いわゆるDesign for Failureに近い考え方だと捉えています。
Design for Failureとは「障害が発生しない前提」ではなく、「障害は発生しうる前提」でシステムを設計する考え方です。
発生を完全に防ぐことだけでなく、発生したときにどう検知し、どう復旧し、どこまで影響を小さくできるかに重きを置くわけです。
今回の構成では、メインの復旧手段として、S3 Versioningを採用しています。
静的サイトの障害で多いファイルの誤削除や、意図しない内容で上書きされたケースに対して、前のバージョンを追いやすくすることで対策を入れています。
DryRunでの差分チェックなどでガードレールを引くことも大切ですが、Design for Failureの考え方をベースにして、どうしたら素早く確実に戻せるかを重要視しました。
監視は最小限から始めてみる
最初から高機能な監視を入れると、サイト規模に対してコストや管理負荷が大きくなることがあります。
そんなわけで、監視は3つだけの最小構成を提案しました。
- 外形監視でトップページを見る
- 自動処理はCloudWatch Alarmで見る
- 障害時はログとS3上の成果物を確認する
もちろん、サイトの性質によっては監視対象を増やした方がよいです。
例えば、ニュースや問い合わせフォームなど、動的処理が入ってきたら、それに合わせてエラーの監視なども必要になってきます。
ビジネス規模が大きくなれば、SLA/SLOなどの観点から、監視は重要性を増しますし、アプリケーションが複雑に絡むようなシステムではAPMなどを活用した可観測性(Observability)の概念も考えていく必要があります。
以前、EC系の現場に関わっていたとき、「監視・測定・分析・評価」の改善サイクル設計をしていましたが、監視は突き詰めていくと、本当にキリがなかったです。
そのため、「見るべきものを絞る」ということも、大切な考え方だと思っています。
コストや時間は、無限に確保できるわけではないので、やらないことを決める力は、案外エンジニアとして生きていく上では、重要なスキルなのかもしれませんね。
まとめ
S3 + CloudFront + OACの構成は、公式サイトやLPのような静的コンテンツ配信と相性が良い構成です。
今回のポイントをまとめると、以下のようになります。
- S3はPrivateにする
- CloudFront + OAC経由でのみ配信する
-
wwwリダイレクトなどはCloudFront Functionsで軽く処理する - セキュリティヘッダーとカスタムエラーを整える
- Design for Failureで考える
- 監視や運用は最小構成から始める
この構成にしておくと、静的サイトとして低コストに運用しつつ、後から自動更新の仕組みも足しやすくなります。
次回は、「静的サイトでも自動化したい」 をテーマに、LambdaやEventBridgeを使ってニュース機能、配信スケジュールの同期、外部API由来のJSONを自動更新する仕組みについてまとめます。
注意喚起
ご自身の環境で試される場合は、AWSのアカウントIDやS3のバケット名などは取り扱いに注意してください。
特にGitHubへの公開やQiitaで記事にする際の混入は事故の元になります。
生成AIに読ませるのも、セキュリティの観点から非推奨とされています。
