🔧

HTTP 503 を返すメンテナンス画面を CloudFront と S3 を使って構築する方法

2023/09/04に公開

最近こんなことをつぶやきました。

つぶやいた背景

Web サービスなどのメンテナンスの際に、メンテナンス画面を表示させることがあります。
今運用に関わっている Web サービスでメンテナンスページの HTML や画像を S3 に保存し、CloudFront を使って表示することになりました。

このつぶやきでは、それに加えてレスポンスコードを 200 ではなく 503 にする、という要件があったんですが、それがうまく行かなかったのでぼやいた。というのが背景です。(原因は後述)
この実装はちょっとハマりました。結論としては公式ドキュメントをよく見るべし。ということになるのですが、調査の過程、実装方法は試行錯誤しました。

CloudFront と S3 を使った静的サイトのホスティングの構成としては、あまり同様の構成の例がなかったので、今回はメンテナンス用の HTML を S3 に保存し、CloudFront でアクセスさせる構成。かつレスポンスコードを 503 に書き換える方法を紹介したいと思います。

S3 での静的サイトホスティングの構成について

S3 では静的サイトをホスティングすることができますが、S3 単体では HTTPS でアクセスすることができません。HTTPS 化しているWebサービスでメンテナンス画面だけ HTTP アクセスなのは困りものです。

この注意書きにもある通り、HTTPS を使って S3 に保存した HTML を表示させたい場合は、CloudFront を使うといった構成をとらないといけません。

https://docs.aws.amazon.com/ja_jp/AmazonS3/latest/userguide/WebsiteEndpoints.html

レスポンスコードについて

では次に、CloudFront 経由で S3 に保存された HTML ファイルにアクセスすると、どのようなレスポンスコードが返ってくるでしょうか。

その答えは 200 が返ってきます。

正常に HTML が取得できているわけなので 200 でも問題ないと思いますが、MDN で HTTP レスポンスコードについて確認すると、メンテナンスの際は 503 を返すのほうが、よりベターのように思います。

503 Service Unavailable

503 Service Unavailable
サーバーはリクエストを処理する準備ができていないことを示します。 一般的な原因は、サーバーがメンテナンスや過負荷でダウンしていることです。 このレスポンスとともに問題について説明する、ユーザーにわかりやすいページを送信するべきであることに注意してください。

要件のまとめ

ということで、今回の要件をまとめると以下のとおりです。

  • メンテナンスページの HTML を正常に取得できたら 200 ではなく 503 を返すようにしたい。
  • 存在しないページにアクセスした際も 503 としたい。
  • 存在しないページにアクセスしてもメンテナンスページを表示させたい。

ちなみに、S3 に HTML が保存されている構成で、存在しないページにアクセスした場合 404 ではなく、直感に反して 403 が返ってきます。

これは存在しないパスへのアクセスが発生するのでパーミッションエラーとなるためのようです。

今回の構成

前置きがだいぶ長くなりましたが、今回の構成を図にしてみました。

S3 に保存されているメンテナンスページを CloudFront を使って HTTPS でアクセスできるようにします。200 のレスポンスコードは CloudFront Functions を使って 503 に書き換えを行い、403 のレスポンスコードは CloudFront のカスタムエラーページの設定で書き換えます。

設定方法

前提条件

CloudFront の証明書は ACM でリクエストしたものを使用しました。ACM で管理された証明書を CloudFront で使う場合、証明書はバージニア北部リージョンに存在しなければならないので注意してください。

今回は Route 53 を使用して、ACM での証明書リクエストに必要な CNAME を作成したり、CloudFront のエイリアスレコードを作成してカスタムドメインでアクセスできるようにしています。

S3 バケットの作成と設定

バケットポリシーや CloudFront でのカスタムヘッダーの追加等で、CloudFront をバイパスして S3 にアクセスされることを防ぐこともできますが、そもそもパブリックに公開されていなければより安全です。

パブリックアクセスをブロックしたまま静的サイトをホストするには、CloudFront のオリジンアクセスコントロールを使用してアクセスします。(OAC の設定は後述します)

https://docs.aws.amazon.com/ja_jp/AmazonS3/latest/userguide/HostingWebsiteOnS3Setup.html

次のチュートリアルでは、[パブリックアクセスをブロック] を無効にする必要があります。[パブリックアクセスをブロック] 設定は、有効にしておくことをお勧めします。4 つすべての [パブリックアクセスをブロック] 設定を有効にしたまま、静的ウェブサイトをホストする場合は、Amazon CloudFront のオリジンアクセスコントロール (OAC) を使用できます。

S3 バケットを作成後に、メンテナンスページの HTML とページ内で使用する画像などを保存します。

一旦、S3 の作業は以上で終了です。
CloudFront のディストリビューションを作成後に、バケットポリシーの設定を変更します。

CloudFront の設定

オリジンアクセスコントロール(OAC)の作成

続いて、CloudFront の管理画面のメニューで[セキュリティ > オリジンアクセス]を選択して、OAC を作成します。

OAC 名を設定し、署名動作は推奨設定の [署名リクエスト (推奨)] を指定します。
オリジンタイプは S3 を指定します。

ディストリビューションの作成

次にディストリビューションを作成します。

  1. [オリジンドメイン]で、Amazon S3 から先ほど作成した S3 を選択します。
  2. [Origin access control settings (recommended)]で先ほど作成した OAC を選択します。
  3. デフォルトのキャッシュビヘイビアの設定や WAF の設定、CNAME(代替ドメイン名)は環境に合わせて適宜で設定します。
  4. [デフォルトルートオブジェクト - オプション] で S3 に保存したメンテナンスページの HTML ファイル名を指定します。
  5. [ディストリビューションを作成] をクリックします。
  6. ディストリビューションの作成後、オリジンタブでオリジンを選択し、[編集]をクリックします。
  7. [ポリシーをコピー]をクリックし、S3 の読み取り許可ポリシーをコピーします。
  8. [S3 バケットアクセス許可に移動]のリンクから S3 のバケットポリシーを編集し、コピーしたポリシーを適用します。
  9. CloudFront でエラーページタブを選択し、[カスタムエラーレスポンスを作成]をクリックします。
  10. 画像のように、HTTP 403 エラーが発生するときに、maintenance.html と 503 を返すように設定します。

CloudFront Functions

403 といった HTTP エラーの場合は、カスタムエラーページでレスポンスコードを書き換えることができますが、200 が返る場合は CloudFront Functions を使用して書き換えます。

関数の JavaScript は次のコードを使ってください。

function handler(event) {
    var response = event.response;
    var contentType = response.headers['content-type'].value;
    
    if (contentType.includes('image')) {
        return response;
    }
    
    if (response && response.statusCode === 200) {
        response.statusCode = 503;
        response.statusDescription = 'Service Temporarily Unavailable';
    }

    return response;
}

このコードでは CloudFront からのレスポンスを取得して、レスポンスコードの書き換えを行っています。
また、書き換えの例外として、Content-Type が image/png や image/ico などのような画像形式のものは 503 に書き換えずそのまま 200 を返すようにします。これはファビコンを取得しようとしたときに 503 になると、正常に反映されないので、その対処のためです。
あとは画像自体はメンテナンスを表す 503 ではなく、正常に取得できたことを意味する 200 のほうがベターだと個人的に考えている、ということもあります。

また、statusCode は文字列型ではなく数値型なので、if の条件の書き方も注意が必要です。(ここでハマって最初のツイートをした)
これを読まれているみなさんはドキュメントはよく見ましょう。。。

https://docs.aws.amazon.com/ja_jp/AmazonCloudFront/latest/DeveloperGuide/functions-event-structure.html#functions-event-structure-response

関数を作成したら、ディストリビューションのビヘイビアタブを選択し、[ビューワーレスポンス]に作成した関数を紐づけます。こうすることで、CloudFront のレスポンスを取得してレスポンスコードを適切に書き換える動作となります。

動作確認

ここまでの設定が完了したら、実際に動作を確認してみます。

正しい URL でアクセスした場合

想定通り、503 が返ってきています。

存在しない URL でアクセスした場合

存在しないパスにアクセスしてもメンテナンス画面が表示され、503 になっています。画像やファビコンはそのまま200が返ってきています。

まとめ

今回は少しトリッキーな要件でメンテナンス画面を表示させる構成を作りました。
適切なレスポンスコードを返すことは SEO にも影響するようです。また、よくある S3 での静的サイトホスティングではなく、CloudFront と OAC を使用してセキュアな環境を簡単に作れます。

この記事がお役に立てると幸いです。

Discussion