👻

夏の怪談「リダイレクトさせただけなのに」〜301と302は正しく使い分けよう〜

2024/08/06に公開

はじめに

すでに夏バテしています、レバテック開発部のかないです。

梅雨も明けて夏本番なので、夏恒例の怖い話をして涼しくしたいなと思う所存です。
テーマは、「メンテナンスページにリダイレクトさせただけなのに」です。

どうやってメンテナンス状態を実現したか

メンテナンスの方針はいくつか選択肢がありました

  • WAFを使ってメンテナンスページを表示する
  • フロントエンドのミドルウェアでメンテナンスページにリダイレクトさせる処理を入れる
  • ALBのリスナールールでメンテナンスページにリダイレクトさせるルールを追加する

その中で、ALBを使ったリダイレクト設定は、別件(古いページを新しいページにリダイレクトさせるなど)で過去にもやったことがあり、慣れているやり方だったので、ALBでメンテ状態を実現しようとチーム内で決まりました。

後述しますが、「過去にもやったことがあるから」という理由で甘く見ていたのが大きな落とし穴となりました。

具体的にどう設定したか

具体的には以下のようなリスナールールを設定しました。
priorityの順番を間違えると想定の挙動にならないので、以下のコードのように設定しました。

# 社内からのアクセスは普通に見れるようにするルール(動作確認のために社内のIPからのアクセスは許可していた)
resource "aws_lb_listener_rule" "maintenance_1" {
  listener_arn = aws_lb_listener.example.arn
  priority     = 1

  action {
    type             = "forward"
    target_group_arn = aws_lb_target_group.example.arn
  }

  condition {
    host_header {
      values = [
        "ホスト名"
      ]
    }
  }

  condition {
    http_header {
      http_header_name = "X-Forwarded-For"
      values = [
        社内IP
      ]
    }
  }

  condition {
    path_pattern {
      values = [
        "/*"
      ]
    }
  }
}

# メンテナンスページのURLにアクセスがあったらターゲットグループに転送するルール
resource "aws_lb_listener_rule" "maintenance_2" {
  listener_arn = aws_lb_listener.example.arn
  priority     = 2

  action {
    type             = "forward"
    target_group_arn = aws_lb_target_group.example.arn
  }

  condition {
    host_header {
      values = [
        "ホスト名"
      ]
    }
  }

  condition {
    path_pattern {
      values = [
        "/メンテナンス画面のパス/",
      ]
    }
  }
}

# メンテナンスページ以外のURLにアクセスがあったらメンテナンスページにリダイレクトさせるルール
resource "aws_lb_listener_rule" "maintenance_3" {
  listener_arn = aws_lb_listener.example.arn
  priority     = 3

  action {
    type = "redirect"

    redirect {
      host        = "ホスト名"
      path        = "/メンテナンス画面のパス/"
      port        = "#{port}"
      protocol    = "HTTPS"
      query       = "#{query}"
      # 今回のやらかしポイント
      status_code = "HTTP_301"
    }
  }

  condition {
    host_header {
      values = [
        "ホスト名"
      ]
    }
  }

  condition {
    path_pattern {
      values = [
        "/*"
      ]
    }
  }
}

メンテナンス当日に何が起こったか

当日、予定されていた作業を手順通り進めて、メンテナンス状態を解除した時、リモートで作業していたメンバーから一報が入りました。

「メンテナンスページにリダイレクトされ続けるんですが…」

雲行きが怪しくなってきました。メンテナンス状態を解除したら、社内以外からも普通にアクセスできるはずなのに、まだリダイレクトされ続けるとのこと。
しかし、事象を確認しようと思っても社内のメンバーでは再現しません。
そこで、とあるメンバーから確認が入りました。

「もしかして301でリダイレクト設定しました…?」

確認したところ、302ではなく恒久リダイレクトでお馴染みの301で設定をしていました…

余談

社内のメンバーで、本事象が再現しなかったのは、社内からは動作確認のために普通に見れるようにしたことで、メンテ期間中もメンテナンスページにリダイレクトされることがなかったため、ブラウザにキャッシュが残らなかったことが要因でした。

どう対応したか

301リダイレクトの仕様として、ブラウザのdisk cacheにリダイレクトのキャッシュが残ってしまうので、disk cacheからキャッシュを消さないとリダイレクトされ続けてしまうという問題点がありました。

ただ、ネット等で調べてもブラウザの検証ツールからキャッシュ削除をするなどの、ユーザー頼みの解決法しか出てこず、上長とも頭を悩ませながら対応方針を考えていました。

ひとまず検証環境でメンテナンス時と同じ状況を再現させていろいろ試した結果、リダイレクトループを発生させたら解消されそうということが見えてきました。

具体的には、メンテナンスページはキャッシュされてないので、内部遷移なら遷移元、外部からの流入ならトップページへ302リダイレクトさせる設定を行いました。
実装箇所としては、フロントエンドでNuxtを使っていたので、NuxtのMiddlewareに実装した形になります。

export default defineEventHandler(({ node }) => {
    if (node.req.url?.startsWith("/メンテナンス画面のパス/")) {
        const referrer = node.req.headers.referer
        if (referrer) {
            node.res.writeHead(302, { Location: referrer })
            node.res.end()
        }
        else {
            node.res.writeHead(302, { Location: "/" })
            node.res.end()
        }
    }
})

なぜ今回の事象を起こしてしまったか

直接的な要因としては、301でリダイレクト設定してしまったことですが、なぜ301で設定してしまったのかという間接的な要因に関しては、どのような仕組みでどのような挙動になるのかという理解が浅かったことが一番大きい要因だったかなと振り返って思いました。

別件でALBを使ったリダイレクト設定をやったことがあると先述しましたが、その時は301で設定していました。ただ、古いページを新しいページにリダイレクトさせるという要件で設定を行なったので302ではなく、むしろ恒久的なリダイレクト設定が求められていました。

その設定を今回のメンテナンスでも横展開してしまったことにより、301のまま設定してしまったという背景がありました。
ステータスコードの仕様を正確に理解できていれば、「前は301だったけど今回は302だな」という使い分けができたかなと思いました。

リダイレクト設定のユースケース

今回の事象を機に、ステータスコードによるリダイレクトの仕様やユースケースを、改めて調べたのでまとめてみます。

  • 恒久的リダイレクト
    • 301
      • 一般的に広く使われている永久リダイレクト
      • SEOのリンク評価を新しいURLに引き継げる
      • 使用例
        • Webサイトのドメイン変更
          • http://example.com から https://www.example.comに変更する場合
        • URLの構造変更
          • http://example.com/products から http://example.com/shop/products に変更する場合
    • 308
      • GETメソッド以外のリンクや操作を含むWebサイトで使われる
      • 使用例
        • POSTリクエストをそのまま新しいURLに転送したい場合
  • 一時的リダイレクト
    • 302
      • 使用例
        • サイトのメンテナンス中に一時的なページに誘導する場合
        • A/Bテストやキャンペーンなどの期間中、特定のページに誘導する場合
    • 303
      • ページの再読み込みによって操作が再度実施されることを防ぐために、PUT や POST の後のリダイレクトで使用する
        • 使用例
          • フォームの送信後にユーザーを確認ページへリダイレクトさせる場合
    • 307
      • リクエストメソッドの保持が必要な場合に、302ではなく307を使用する
      • ユーザーが送信したリクエストデータが保持されたままリダイレクトされる

他にも特殊リダイレクトというものもあるみたいです。

参考記事
https://developer.mozilla.org/ja/docs/Web/HTTP/Redirections

まとめ

301リダイレクトは、URLを新しくしたいとか、旧URLのSEO評価を引き継ぎたいなどのケースで使用するもので、今回のような一時的なリダイレクト設定には絶対に使うべきではありません。

ステータスコードに限らずですが、どのような仕組みでどのような挙動になっているのかを理解することの大切さを痛感しました…


次にリダイレクトをミスるのはあなたかもしれません

レバテック開発部

Discussion