About

AWSでメンテナンスページを出す3つの方法 — ALBルール・WAF・DNS切替を比べて、DNSをやめた理由

アプリが止まっていても出せて、普段はお金がかからず、1秒で戻せるのはどれか。ドキュメントで確かめた本文の上限と、keep-aliveの落とし穴

「ただいまメンテナンス中です」の画面をAWSの中だけで完結させたかった。条件は2つ。普段はお金がかからないこと、作業がこじれたらすぐ戻せること。この記事を読むと、ALBリスナールール・WAF・Route 53のDNS切替の3つのうちどれを選ぶか、それぞれ本文をどこまで書けるか、DNS切替がなぜ思ったより遅いのかが分かる。

ALB(Application Load Balancer)はリクエストを受けて、ECSなどの後ろのサーバーに振り分けるロードバランサーだ。WAFはその手前でリクエストを検査して、止めたり通したりするファイアウォールだ。

アプリのミドルウェアで作ってはいけない

最初に思いつくのは、Railsのミドルウェアなどでフラグを見てメンテ画面を返すやり方だ。でもメンテナンスは、アプリを止める場面で使うものだ。DBマイグレーション中だったり、タスクを全部落としていたりすれば、そのミドルウェアも一緒に止まっている。アプリの手前、つまりALBかWAFで返す必要がある。

もう1つ勘違いしやすいところ。「503ルール」と聞くと「アプリが503を返したらメンテ画面に差し替える」と読みたくなる。逆だ。こちらから意図的に503を返すスイッチで、アプリが元気でも503になる。

3つを並べると

ALBリスナールール WAFデフォルトアクション API Gateway + Lambda + Route 53
追加リソース 0 0(WAFを既に使っていれば) うちの構成で13個
切替・切り戻し 即時 即時 DNS次第 — 下を参照
本文の上限 1,024文字 4 KB 気にしなくていい
オフィスのIPだけ通す 優先度の高いforwardルールを足す IPセット + Allowルール 自前で実装
アプリ停止中 出る 出る 出る

本文の上限はドキュメントで確かめた。ALBのfixed-responseは、APIリファレンスにMessageBodyの最大長1,024とある。コンテンツタイプはtext/htmlを含む5種類から選ぶ。1,024文字なら、CSSを少し入れた短いHTMLがちょうど収まる。ロゴ画像をbase64で埋め込むのは無理だ。

WAFは少し面白かった。APIリファレンス(CustomResponseBody)ではContentの最大長が10,240になっている。ところがクォータの表では「カスタムレスポンス1つの本文は最大4 KB」だ。最初のメモには10,240と書いていたが、ドキュメント同士で食い違っている。実際に10 KBを入れて試してはいない。4 KBに収めるのが安全だ。

Browser keep-alive Route 53 DNS switch slow to undo: open connections never ask DNS again switch here: instant, both ways WAF default: Block 503 ALB rule fixed-response 503 App (ECS) may be stopped not behind either switch ALB health checks: ALB → target directly ECS Exec: SSM Session Manager deploys: ECR push + ecs update-service (AWS API) body size limit ALB fixed-response: 1,024 WAF custom body: 4 KB (quota table) API Gateway + Lambda: not a concern

DNS切替が負ける理由 — 戻すのが遅い

メンテ用のAPI Gateway + Lambdaを別に用意しておき、Route 53のレコードをそちらに向ける方法だ。一番わかりやすく見えて、最初はこれが正解だと思っていた。

問題は、DNSがこちらの思い通りにすぐには変わらないことだ。ALBに向けたaliasレコードはTTLを自分で決められず、ELBのTTL 60秒が使われる(ドキュメント)。60秒待てばいいように見えるが、その上にこれが乗る。

  • 企業・ISPのリゾルバには、最小TTLを強制的に延ばすところがある

  • ブラウザもDNSの結果を別にキャッシュする

  • すでに開いているHTTP接続は、DNSを引き直さない

最後がいちばん効く。ブラウザはkeep-aliveで1本の接続を使い回す。その接続が切れるまで、リクエストは古い向き先に行く。ALB側の設定を見ると、クライアントのkeep-alive持続時間はデフォルト3,600秒だ。接続を開いた時点から数え、通信があってもリセットされない(ドキュメント)。休まず操作している人なら、最大1時間近く古いALBにつながったままになりうる。もちろん60秒何もリクエストがなければ、アイドルタイムアウトで先に切れる。

感覚をつかむために、ローカルで真似してみた。偽のDNS表をアプリ → メンテに切り替えて、新しい接続とkeep-aliveの接続がそれぞれどこに行くかを出した。

before switch  new-conn=app         keep-alive=app
after switch 1 new-conn=maintenance keep-alive=app
after switch 2 new-conn=maintenance keep-alive=app
after switch 3 new-conn=maintenance keep-alive=app
after reconnect new-conn=maintenance keep-alive=maintenance

当たり前の結果なのに、出してみるとしっくりきた。開いている接続は、つなぎ直すまでアプリに行き続ける。

メンテの開始が数分遅れるのは我慢できる。でも作業に失敗して急いで戻したいときに、数分〜数十分待たされるのは困る。ALBルールやWAFならAPIを1回叩けば次のリクエストから変わる。接続はもうALBにつながっているので、keep-aliveも関係ない。

なのでDNS切替は「ALBそのものが使えない」ときだけ、と整理した。リージョン障害のような場面だ。

ALBルールで切り替える

優先度1にfixed-response 503のルールを入れると、デフォルトのforwardより先に当たる。

aws elbv2 create-rule --listener-arn <listener-arn> --priority 1 --conditions '[{"Field":"path-pattern","PathPatternConfig":{"Values":["/*"]}}]' --actions '[{"Type":"fixed-response","FixedResponseConfig":{"StatusCode":"503","ContentType":"text/html","MessageBody":"<h1>メンテナンス中です</h1><p>22:00に戻ります</p>"}}]'

戻すときはdelete-ruleだ。オフィスからだけ先に確認したいなら、その手前(優先度0はないので、503ルールを2に下げて1に)に、source-ip条件で本来のターゲットグループへforwardするルールを置く。source-ipには「このIP以外」という否定がないので、こうして2本に分ける。

注意が1つ。source-ipはALBに直接つないできたアドレスを見る。手前にCloudFrontがあれば、見えるのはCloudFrontのIPだ。X-Forwarded-Forはこの条件に当たらず、http-header条件で別に見る必要があるとドキュメントにある。

WAFで切り替える

WAFを既にALBに付けているなら、web ACLのデフォルトアクションをAllow → Blockに変え、カスタムレスポンスで503と本文を指定する。オフィスのIPはIPセットに入れ、Allowルールを1つ置けば終わりだ。ALBルールと違ってレスポンスヘッダーを付けられるので(content-type以外)、Retry-Afterも入れられる。

WAFを使っていなかったなら、web ACLそのものに月額料金がかかる。メンテ画面1つのために新しく入れる理由としては弱い。そのときはALBルールがいい。

503を返している間も、中の作業は全部できる

これも誤解しやすい。503はインターネットから入ってくるHTTPだけを止める門で、その裏の経路は生きている。

  • ECS Exec — SSM Session Manager経由。ALBもWAFも通らない

  • デプロイ — ECR push → ecs update-service。全部AWS APIだ

  • ALBのヘルスチェック — ALBがターゲットに直接送る。リスナールールもWAFも通らないので、ECSが「ヘルスチェック失敗」でタスクを殺したりロールバックしたりしない

  • 内部ALB経由のサービス間の呼び出し — WAFを公開ALBにだけ付けていれば関係ない

逆に外形監視(Syntheticsのように外から叩く監視)は503を受けて赤くなる。切り替える前にミュートしておかないと、アラートが積み上がる。

選び方

  • 短いHTMLで足りる → ALBリスナールール。リソース0、設定1行

  • デザインした画面・オフィスIPの迂回・Retry-Afterが要り、WAFを既に使っている → WAFデフォルトアクション

  • ALBそのものが使えない → そのときだけDNS切替。TTLは守られない前提で設計する

メンテ画面は年に数回しか使わないので、いざというときに動かないことが多い。普段からステージングで一度オン・オフしてみておくのをすすめる。

ECS・ALBの上で無停止の作業やメンテナンス手順を組みたいが手が足りない、というときは https://www.negabaro.com/contact から聞いてほしい。

動作フロー

1

メンテ画面はアプリの手前(ALB・WAF)で返す — アプリのミドルウェアはアプリと一緒に止まる

2

短いHTMLならALBリスナールールのfixed-response 503(本文1,024文字まで)

3

WAFを既に使っているならデフォルトアクションをBlock + 503に — 本文4 KB・IPセット・Retry-After

4

DNS切替はkeep-alive接続がDNSを引き直さないので戻すのが遅い — ALBが使えないときだけ

5

切り替える前に外形監視をミュートする。ヘルスチェック・ECS Exec・デプロイはそのまま動く

ユースケース

DBマイグレーション・インフラ入れ替えのように、アプリをしばらく止める作業 ECS + ALB構成に初めてメンテのスイッチを作るとき Route 53でメンテを切り替えていて、戻すのが遅いと感じているとき
← ECS Execのセッションログが1行も残らないとき — まずイメージにscriptがあるかを見る