ALBとTerraformで作るHTTPリダイレクタ
なにこれ
LAPRAS株式会社でSREをしていますyktakaha4と申します🐧
実務で最近おこなった対応について、いい感じに実装できたな~と思ったため備忘録として書き残すこととしました📝
作った理由
Webサービス運用に長く携わっていると、過去作ったWebアプリやLPなどのクローズに立ち会うことがままあります
この時、削除対象のサービスに関連するリソースを消していくことになりますが、サービスのドメイン(DNSレコード)を単純に削除すると、以下のようなエラー画面がユーザーに表示されます
削除されたドメインにアクセス
ユーザーにしてみれば、この表示だとサービスが終了したのか、一時的なシステムエラー等が生じているのか判断ができず、望ましい挙動とは言えないものと思います
例えば、 サービスは終了しました
といった内容の簡易なHTMLページを表示したり、コーポレートサイト等へリダイレクトできるとよいですが、
そのためにはなにかしらのWebサービスを常時立ち上げておく必要があり、またサービス終了のたびにそうしたものを都度作るのも、運用担当者としては避けたいところです
弊社ではサービスのインフラにAWSを利用しているのですが、ロードバランサーのマネージドサービスであるApplication Load Balancerは柔軟な設定が可能でありつつも管理コストがほとんどかからず、今回のユースケースにちょうどよさそうです
また、実装については、弊社でも利用しているTerraformを使ってIaC管理したいです
ということで、ALBとTerraformを使ってシンプルなリダイレクタを構築することとしました☕
どのようなものを作るか
実装にあたって必要そうな仕様を整理し、以下のような機能が実現できるとよいと考えました
- あるドメインにアクセスした際に、別のドメインにリダイレクトさせる
- リダイレクトの際に、パスやクエリパラメータを維持or破棄するかを選択できる
- 要件に応じて任意のステータスコードを返却できる
- 特定のドメインについては、デザイン付きの簡易なHTMLファイルを返却できる
- HTTP / HTTPS共に同一のレスポンスを返却する
- 昨今はHTTPSによるサービス構築が前提となっているものの、ユーザーがアドレスバーに
http://~
と手入力してアクセスして来るケースも考えられます
- 昨今はHTTPSによるサービス構築が前提となっているものの、ユーザーがアドレスバーに
また、システム管理上の観点からは、以下の仕様を満たせるとよさそうです
- リダイレクト対象のドメインが増えてもコストのかかるリソース(ALB本体など)を追加作成しなくてよくする
- OSやミドルウェアのバージョンアップなどのメンテナンスコストを極力排する
- これは、ALBだけで実装すれば概ね達成することができそうです💡
- アクセスログを取得する
- 今回の本筋とは離れますが、作成するS3はサーバサイド暗号化やライフサイクルルールの設定といった一般的な内容を踏襲するものとします
上記の仕様を考えたうえで、今回の検証に用いるドメイン test-penguin.net
を取得し、以下に示すようなビジネス要求を想定します
-
test-penguin.net
ドメインにアクセスした際は、https://example.com/
にリダイレクトする- パスやクエリパラメータは維持する
-
subdomain.test-penguin.net
ドメインにアクセスした際は、https://example.net/
にリダイレクトする- パスやクエリパラメータは維持しない
-
fixed.subdomain.test-penguin.net
ドメインにアクセスした際は、簡易なHTMLページを表示する- CSSによるスタイリングを可能とする
前述の仕様を簡単なアーキテクチャ図に起こしたものが以下です
今回作るもののアーキテクチャ図
それではやっていきましょう🍮
作ったもの
今回作成したものをGitHubに公開しました🐙
利用にあたっては、 terraform apply
の過程で作成される aws_route53_zone
のネームサーバーをご自身の持っているドメインに紐づけてください
また、 variable.tf
についても各自で変更をお願いします
https://us-east-1.console.aws.amazon.com/route53/domains/home#/
いくつか、ポイントになる部分を見ていきます
今回作成されるALBは aws_lb.main
ひとつだけです
resource "aws_lb" "main" {
name = "${var.service}-${var.env}-main"
load_balancer_type = "application"
internal = false
security_groups = [
aws_security_group.main.id,
]
subnets = data.aws_subnets.default.ids
access_logs {
enabled = true
bucket = aws_s3_bucket.logs.bucket
prefix = "access-log"
}
tags = {
"Name" = "${var.service}-${var.env}-main"
}
}
本稿執筆時点でのALBのコストは 0.0243USD/h
+ LCUによる従量課金なので、月額としては2500円~程度になります
これをサービス終了のたびに作成していたら結構なコストになってしまいますが、今回作るものは単一のALBで複数ドメインを処理するため、コスト増を抑えられるのでないかと思います
ALBに対して、HTTPおよびHTTPSのリスナーを設定します
リスナーはデフォルトルールをひとつ指定する必要があるので、実務ではコーポレートサイトへのリダイレクトなど、汎用的に利用されそうなものをデフォルトとしておくとよいでしょう
また、HTTPについてはHTTPSとしてリダイレクトされるように redirect
ブロックを設定します
resource "aws_lb_listener" "main_http" {
load_balancer_arn = aws_lb.main.arn
port = "80"
protocol = "HTTP"
default_action {
type = "redirect"
redirect {
port = "443"
host = var.redirector_default_destination_domain_name
protocol = "HTTPS"
status_code = "HTTP_301"
}
}
}
resource "aws_lb_listener" "main_https" {
load_balancer_arn = aws_lb.main.arn
port = "443"
protocol = "HTTPS"
ssl_policy = "ELBSecurityPolicy-TLS13-1-2-2021-06"
certificate_arn = aws_acm_certificate.source.arn
default_action {
type = "redirect"
redirect {
port = "443"
host = var.redirector_default_destination_domain_name
protocol = "HTTPS"
status_code = "HTTP_301"
}
}
}
リスナールールは、HTTP / HTTPS両方のものを作成する必要があるため、for_eachを利用します
今回は host_header
を利用して、どのドメイン名からアクセスしてきたかによって同一ALBでリダイレクト先を変更します
加えて、簡易HTMLページ用のCSSを fixed.subdomain.test-penguin.net/fixed.css
で配信するために path_pattern
を利用します
ALBでリダイレクタを作成するメリットとして、上記以外にもかなり柔軟な条件判定ができますので、詳細は以下を確認してください
条件にマッチした際のアクションについても、パス・クエリパラメータの維持だけでなく、追加の内容で上書きといった操作も事由に行えます
resource "aws_lb_listener_rule" "main_subdomain" {
for_each = toset([
aws_lb_listener.main_https.arn,
aws_lb_listener.main_http.arn,
])
listener_arn = each.value
priority = 10
condition {
host_header {
values = [local.redirector_source_subdomain_name]
}
}
action {
type = "redirect"
redirect {
port = "443"
host = var.redirector_subdomain_destination_domain_name
protocol = "HTTPS"
status_code = "HTTP_301"
path = "/"
query = ""
}
}
}
resource "aws_lb_listener_rule" "main_fixed_style" {
for_each = toset([
aws_lb_listener.main_https.arn,
aws_lb_listener.main_http.arn,
])
listener_arn = each.value
priority = 20
condition {
host_header {
values = [local.redirector_fixed_domain_name]
}
}
condition {
path_pattern {
values = ["/fixed.css"]
}
}
action {
type = "fixed-response"
fixed_response {
content_type = "text/css"
status_code = "200"
message_body = templatefile("${path.module}/templates/fixed.css", {})
}
}
}
resource "aws_lb_listener_rule" "main_fixed" {
for_each = toset([
aws_lb_listener.main_https.arn,
aws_lb_listener.main_http.arn,
])
listener_arn = each.value
priority = 25
condition {
host_header {
values = [local.redirector_fixed_domain_name]
}
}
action {
type = "fixed-response"
fixed_response {
content_type = "text/html"
status_code = "410"
message_body = templatefile(
"${path.module}/templates/fixed.html",
{
domain_name = local.redirector_fixed_domain_name
}
)
}
}
}
HTML返却時の小技として、templatefile関数を利用することで、HTMLファイルにTerraformのレイヤから任意の文字列を注入することができます
実務においては、サービス名やURLの情報をmapに持たせておいて for_each
で参照させて、同一HTMLテンプレートで複数のサービス終了ページを実装する…といったこともできるものと思います
<html lang="en">
<head>
<meta charset="utf-8">
<title>${domain_name} has been permanently removed.</title>
<link rel="stylesheet" href="fixed.css">
</head>
<body>
<span class="domain_name">${domain_name}</span> has been permanently removed.
</body>
</html>
.domain_name {
font-weight: bold;
color: red;
}
静的ファイル返却の注意点としては、 レスポンスできるコンテンツのサイズは1024文字以下に収める
必要がある…というものがあります
比較的厳しい制限ではありますが、内容を工夫すればそれなりのデザインのものを最小限の維持コストで配信できるので、デザイナーの方と相談しながら実装できるとよいものと思います
今回、複数のサーバー証明書を同一ALBに設定するサンプルを作るため、 test-penguin.net
および *.subdomain.test-penguin.net
の2種類の証明書をACMで作成しています
ALBに紐づける際は、ひとつ目の証明書を aws_lb_listener
に設定し、2つ目以降は aws_lb_listener_certificate
を追加で作成して紐づけることとなります
resource "aws_lb_listener_certificate" "source_subdomain" {
listener_arn = aws_lb_listener.main_https.arn
certificate_arn = aws_acm_certificate.source_subdomain.arn
}
最後に小ネタですが、今回ALBをデフォルトVPCおよびデフォルトサブネットに作成していますが、これらのIDを variable.tf
に記載するとサンプルコードとして使い勝手が悪くなります
これを動的に取得するには、以下のようにaws_vpcやaws_subnetsといったデータソースが活用できます
同名のリソースもあるので利用に際しては注意してください
また、ALBから自己所有しているS3バケットにアクセスログ書き込みを許可するために、リージョンごとのELBのサービスアカウントのIDを指定する必要があったのですが、これについてはaws_elb_service_accountを使うと、適用先のリージョンに合わせて動的に設定値を決定出来て大変便利です
data "aws_elb_service_account" "current" {}
data "aws_vpc" "default" {
default = true
}
data "aws_subnets" "default" {
filter {
name = "vpc-id"
values = [data.aws_vpc.default.id]
}
}
動作確認
最後に、作ったものを簡単に動作確認してみます
test-penguin.net
にアクセスすると、ちゃんと example.com
にリダイレクトされています
パスやクエリパラメータも維持されておりよさそうです🍎
$ curl --include "https://test-penguin.net/path?query=abc"
HTTP/2 301
server: awselb/2.0
date: Sun, 06 Aug 2023 06:27:43 GMT
content-type: text/html
content-length: 134
location: https://example.com:443/path?query=abc
<html>
<head><title>301 Moved Permanently</title></head>
<body>
<center><h1>301 Moved Permanently</h1></center>
</body>
</html>
対して、 subdomain.test-penguin.net
へのアクセスは、 example.net
にリダイレクトされます
こちらはパスやクエリパラメータは破棄します
また、HTTPアクセスした場合も Location
ヘッダはHTTPSにアクセスするよう指示されておりよさそうです
$ curl --include "http://subdomain.test-penguin.net/path?query=abc"
HTTP/1.1 301 Moved Permanently
Server: awselb/2.0
Date: Sun, 06 Aug 2023 06:28:54 GMT
Content-Type: text/html
Content-Length: 134
Connection: close
Location: https://example.net:443/
<html>
<head><title>301 Moved Permanently</title></head>
<body>
<center><h1>301 Moved Permanently</h1></center>
</body>
</html>
最後に、 fixed.subdomain.test-penguin.net
にアクセスしてみます
fixed.html
では ${domain_name}
となっていたパラメータ部分に実際のドメイン名が埋め込まれています
$ curl --include "https://fixed.subdomain.test-penguin.net"
HTTP/2 410
server: awselb/2.0
date: Sun, 06 Aug 2023 06:31:56 GMT
content-type: text/html; charset=utf-8
content-length: 309
<html lang="en">
<head>
<meta charset="utf-8">
<title>fixed.subdomain.test-penguin.net has been permanently removed.</title>
<link rel="stylesheet" href="fixed.css">
</head>
<body>
<span class="domain_name">fixed.subdomain.test-penguin.net</span> has been permanently removed.
</body>
</html>
HTMLだけでなくCSSも固定レスポンスで配信しているため、EC2やCloudFrontといった配信の仕組みを追加で持つ必要がなくなり、可用性の向上も期待できそうです
サービス終了ページのサンプル
おわりに
サービスのクローズは残念な出来事ではありますが、いい加減な対応で負債を積み増さないようにしていきたいですね🐕
Discussion