Techfirm Cloud Architect Blog

テックファーム株式会社クラウドインフラグループのブログ

Apacheで指定時間にメンテナンス画面に切替える方法

ApacheをつかっているWEBサイトで、メンテナンス開始時間になったら自動的にメンテナンスページに切り替わるようにする設定を紹介します。

Apacheの設定サンプル

早速Apacheの設定サンプルを紹介します。

RewriteEngine on

RewriteCond %{TIME} >=20240901020000
RewriteCond %{REMOTE_ADDR} !^127\.0\.0\.1
RewriteCond %{REMOTE_ADDR} !^192\.168\.0\.
RewriteCond %{REQUEST_URI} !^/error/.* [NC]
RewriteCond %{REQUEST_URI} !=/favicon.ico
RewriteRule ^ - [R=503,L,E=MAINTENANCE:yes,E=ERROR_DOCUMENT:error/maintenance.html]

RewriteCond %{ENV:MAINTENANCE} !^yes$
RewriteRule ^ - [E=ERROR_DOCUMENT:error/503.html]

ErrorDocument 503 /%{ENV:ERROR_DOCUMENT}
Header always set Retry-After "Sat, 31 Aug 2024 19:00:00 GMT" env=REDIRECT_MAINTENANCE

この設定では次のようなメンテナンスページの切替を行うことができます。

  • %{TIME}以降のリクエストに503 service temporarily unavailableをレスポンスする
  • メンテナンスページは/error/maintenance.htmlを表示する
  • Retry-Afterヘッダーをレスポンスに付与する
  • 除外設定
    • 接続元IPアドレス
    • URLパス
  • メンテナンスより前の503 service temporarily unavailableに対するエラーページは/error/503.htmlを表示する

動作確認環境

  • OS: AlmaLinux 9.4
  • Apache: Apache/2.4.57 (httpd-2.4.57-11.el9_4.1.x86_64)

メンテナンスページのお作法

メンテナンスページ表示の目的は、メンテナンス中にアプリケーションにリクエストが流入することを防ぐことや、
ユーザーにメンテナンス中であることを分かりやすく伝えることが挙げられます。
ただし、メンテナンスページを表示させるために、ApacheでURLリライトして表示を替えるだけでは、不十分とされています。
ユーザーから見えるブラウザ上の見た目としては同じですが、Webサーチエンジンのクローラーからは、
対象のURLパスが更新されたと判断され、メンテナンスページが検索インデックスに登録されてしまうからです。

検索クローラーのために、以下の2つの設定でメンテナンスページを表示することが推奨されています。

  • HTTPステータスコードとして503 service temporarily unavailableを返す
  • Retry-Afterヘッダーを利用することで、あらかじめ分かっているダウンタイムの時間やサイトの復旧日時について指定する

サイトのダウン タイムへの対処の仕方 | Google 検索セントラル ブログ | Google for Developers

メンテナンスページを表示する条件設定の解説

メンテナンスページを503 service temporarily unavailableでレスポンスするための設定は次の部分です。

RewriteCond %{TIME} >=20240901020000
RewriteCond %{REMOTE_ADDR} !^127\.0\.0\.1
RewriteCond %{REMOTE_ADDR} !^192\.168\.0\.
RewriteCond %{REQUEST_URI} !^/error/.* [NC]
RewriteCond %{REQUEST_URI} !=/favicon.ico
RewriteRule ^ - [R=503,L,E=MAINTENANCE:yes,E=ERROR_DOCUMENT:error/maintenance.html]

RewriteCondの条件を満たすリクエストをRewriteRuleで強制的にHTTPステータスコード503にしています。

%{TIME} >=20240901020000はメンテナンス開始時間の条件です。
TIMERewriteCondで利用可能なサーバー変数で、メンテナンス開始時刻を指定します。
指定する時刻はサーバーのタイムゾーンになりますのでご注意ください。
TIMEの他にも、曜日を表すTIME_WDAYなどがあるので、曜日指定の定期メンテナンスも表現可能です。

Variables | Expressions in Apache HTTP Server - Apache HTTP Server Version 2.4

その他の条件として、REMOTE_ADDR/REQUEST_URIを使ってメンテナンス除外リクエストを表現しています。
除外を検討する必要がある主なリクエストは以下のようなものがあります。

  • メンテナンスページ自体へのリクエスト
    • メンテナンスページ設定について詳しくは後述しますが、URLリライトで表示するページへのリクエストは内部リクエストとなるため、接続元IPアドレスがローカルIPアドレスになることに注意してください
  • メンテナンスページを構成する画像やCSS、JSへのリクエスト
    • メンテナンスページのHTMLから呼び出されるリクエストも503の対象にすると、ページの表示が崩れてしまいます
    • 例ではメンテナンスページ関連のパーツはすべて/error/に配置されている想定になっています
  • 監視システムやLBからのヘルスチェックリクエスト
    • ヘルスチェックリクエストが503になると、サーバーが切り離されてリクエストが到達しなくなる構成の場合は、除外が必要です
    • ヘルスチェックURLが、メンテナンス対象範囲のリソースが利用できなくても、正常(200 OK)を返すような準備が別で必要になります

メンテナンスページの指定

503 service temporarily unavailableレスポンスで表示されるエラーページは、ErrorDocumentで設定します。

メンテナンス開始時にApache設定変更を反映するのであれば、ErrorDocument 503 /error/maintenance.htmlのような固定値で設定できます。
今回の場合、事前にメンテナンス設定を行うため、メンテナンス以前に503が発生した場合に、メンテナンスページが表示されてしまいます。
サンプルでは通常時のエラーページを/error/503.htmlとした場合の、環境変数による出し分けをおこなっています。

RewriteRule ^ - [R=503,L,E=MAINTENANCE:yes,E=ERROR_DOCUMENT:error/maintenance.html]

RewriteCond %{ENV:MAINTENANCE} !^yes$
RewriteRule ^ - [E=ERROR_DOCUMENT:error/503.html]

ErrorDocument 503 /%{ENV:ERROR_DOCUMENT}

RewriteRuleでHTTPステータスコード503にする際にフラグで環境変数を設定します。

  • MAINTENANCE: エラーページを表示する条件を満たすかどうかのフラグ変数
  • ERROR_DOCUMENT: ErrorDocumentに指定するページのパス文字列変数

MAINTENANCEがyesでない場合は、ERROR_DOCUMENTerror/503.htmlとなり、 ErrorDocument 503 /error/503.htmlとなるので、メンテナンス前のリクエストが503になっても通常のエラーページが表示されます。

メンテナンスページHTMLの注意点

メンテナンスページのHTMLファイルから呼び出す、画像やCSS、JSのパスは、絶対パスにしてください。

HTTPレスポンスコードが503になることにより、レスポンスされるファイルは/error/maintenance.htmlですが、
Apache内部のリダイレクトにより表示されているため、ブラウザで表示されているURLパスは、リクエストした時のものになります。
このため、相対パスで画像やCSS、JSを指定していると、リクエストしたURLパスからの相対パスを参照することになり、存在しないURLパスにリクエストすることになります。

絶対パス
<img src="/error/images/banner.jpg" />
相対パス
<img src="./images/banner.jpg" />

/error/maintenance.htmlに上記の表記がされていて、メンテナンス中に/entry/2024/09/10/001.htmlにリクエストした場合、ブラウザがリクエストする画像ファイルはそれぞれ以下のようなパスになります。

  • 絶対パス: /error/images/banner.jpg
  • 相対パス: /entry/2024/09/10/images/banner.jpg

Retry-Afterヘッダーの付与

検索エンジンのクローラーにメンテナンスの終了時刻を通知するためにRetry-Afterヘッダーを付与します。

Header always set Retry-After "Sat, 31 Aug 2024 19:00:00 GMT" env=REDIRECT_MAINTENANCE

Retry-Afterヘッダーの値として記載する日時は、HTTP-dateフォーマットです。

Retry-After: <day-name>, <day> <month> <year> <hour>:<minute>:<second> GMT

GMTに変換して記載する必要があるのでご注意ください。

Linuxのdateコマンドで変換する場合、次のコマンドを利用ください。

$ echo "2024-09-01 04:00:00 JST" |  date -f - +"%a, %d %b %Y %H:%M:%S GMT" -u
Sat, 31 Aug 2024 19:00:00 GMT

Retry-After - HTTP | MDN
7.1.1.1. Date/Time Formats | RFC 7231 - Hypertext Transfer Protocol (HTTP/1.1): Semantics and Content

メンテナンス開始以降にRetry-Afterヘッダーを付与する

Headerディレクティブでは、環境変数が存在するかどうかを判定して、設定の有効・無効を制御できます。
メンテナンスページを表示する条件を満たすかどうかのフラグ変数として、MAINTENANCEを用意していますが、
サンプルではREDIRECT_MAINTENANCEという環境変数を使って判定しています。

MAINTENANCEを使うと、メンテナンス時刻以降のレスポンスであってもRetry-Afterヘッダーが付与されません。

設定を検証するなかで、以下のような挙動となっていることがわかりました。

  • 内部リダイレクト(URLリライト)された場合、Headerディレクティブは、Apacheが内部的に行うリクエストへのレスポンスヘッダに適用される
  • 内部リダイレクト(URLリライト)された場合、元リクエストの環境変数はREDIRECT_というプリフィックスが付いた状態で、内部で発生するリクエスト処理に引き継がれる

Apacheが内部的に行う/error/maintenance.htmlへのレスポンスにRetry-Afterヘッダーを付与する必要があるので、
元リクエストのMAINTENANCEが引き継がれた、REDIRECT_MAINTENANCEが存在する場合を条件としています。

カスタムエラーレスポンスとリダイレクト | カスタムエラーレスポンス - Apache HTTP サーバ バージョン 2.4
When setting environment variables in Apache RewriteRule directives, what causes the variable name to be prefixed with "REDIRECT_"? - Stack Overflow

まとめ

事前に設定した時刻ピッタリにメンテナンスページに切替える方法について紹介してきました。
今回検証をするなかで、ディレクティブごとに環境変数がどう使えるかや、知らなかったApacheの挙動を知ることができてとても学習になりました。
サンプルでは条件設定にRewriteCondをつかっていますが、<If>ディレクティブを使ったバージョンも検証してみて、
使い慣れているのと、視認性が高かったRewriteCondのほうを紹介しました。
他設定の挙動に差異があるので、機会があれば<If>ディレクティブバージョンも紹介できればと思います。

思いのほか複雑な設定となったので、OSやバージョンによって動作の差異がある可能性があります。
あくまで参考としてご利用いただき、ご自身の環境での動作確認は万全におこなうようお願いします。
また、.htaccessに記載した場合の挙動については検証していないため、多少の手直しが必要かもしれません。

今回紹介した設定をベースにして、切替のトリガーをファイルの有無にしたり、特定のCookieがあれば除外するといった
いろいろな要件へのアレンジもできたらいいなと思います。