ALB: L7ロードバランサーの基礎とHTTPでの最初の公開
前章までで、VPC と S3 ができました。この章で、Tasuku は初めて外部に公開されます。ただし、HTTPS ではありません。この章で作る ALB が受け付けるのは、HTTP (80 番ポート) だけです。Tasuku のドメインに SSL 証明書を発行し、443 番ポートで HTTPS を終端する作業は、次章で Route 53 と ACM を扱ってから行います。
6.1. L7ロードバランサーの基礎
ALB (Application Load Balancer) は、OSI 参照モデルの第 7 層 (アプリケーション層) で動作するロードバランサーです。1 HTTP のメソッドやパス、ヘッダーの中身を読んでから振り分け先を決められる点が、TCP/UDP のポート番号までしか見ない L4 ロードバランサー (NLB) との違いです。Tasuku は将来、web と api という 2 種類のアプリケーションをドメインで振り分けます。この振り分けには L7 の情報が要るため、Tasuku のロードバランサーは ALB を選びます。
ALB を構成する要素は 3 つあります。リスナーは、ALB がどのポート・プロトコルでリクエストを受け付けるかを決めます。ターゲットグループは、リクエストの転送先となるサーバー群 (この連載では ECS Fargate のタスク) をまとめた単位です。ヘルスチェックは、ターゲットグループに属する各ターゲットが実際にリクエストを処理できる状態かどうかを、ALB が定期的に確認する仕組みです。
この章の時点では、ターゲットグループもヘルスチェックも作りません。転送先となる ECS Fargate のタスクが、まだ存在しないためです。第3章で IAM ロールの実体を予告表にとどめ、使う章に実装を委ねたのと同じ考え方です。
| 要素 | この章 | 実体を作る章 |
|---|---|---|
| リスナー | 作る (80番、固定レスポンス) | — |
| ターゲットグループ | 作らない | 第10章 (web用・api用に2個) |
| ヘルスチェック | 概念のみ | 第10章 |
6.2. まずHTTPで公開する設計判断
ALB のリスナーには、本来なら HTTPS (443 番ポート) を使うべきです。平文の HTTP だけで公開する状態を長く続けるのは避けたいところです。それでもこの章が HTTP だけにとどめるのは、443 番ポートを使うには SSL 証明書が要り、証明書の発行にはドメインの検証が要るためです。ドメインの検証は次章で扱う Route 53 の仕事であり、この章の時点ではまだ済んでいません。
そのため、この章では 80 番ポートのリスナーだけを作り、ALB を起動した直後の姿を確かめます。次章でドメインと SSL 証明書の準備が整ったら、443 番ポートのリスナーを追加し、80 番ポートは 443 番へのリダイレクト専用に切り替えます。HTTP で公開してしまってから、それを HTTPS へ置き換えていく順序です。
6.3. ALB用セキュリティグループ
第4章の 4.7 節で示したセキュリティグループチェーンの図は、連載が完成した後の姿です。ALB 用のセキュリティグループは、最終的にインターネットからの 443 番ポートを受け付けます。しかしこの章の時点では、ALB 自体が 80 番ポートのリスナーしか持ちません。この章で作るセキュリティグループが許可するのも、80 番ポートだけです。443 番ポートの許可は、リスナーを追加する次章で足します。
resource "aws_security_group" "alb" {
name = "${local.name_prefix}-alb-sg"
description = "ALB用SG。80番のみ許可 (443番は第7章で追加)"
vpc_id = aws_vpc.main.id
tags = {
Name = "${local.name_prefix}-alb-sg"
}
}
resource "aws_vpc_security_group_ingress_rule" "alb_http" {
security_group_id = aws_security_group.alb.id
cidr_ipv4 = "0.0.0.0/0"
from_port = 80
to_port = 80
ip_protocol = "tcp"
description = "Internetからの80番 (HTTP)"
}
ingress をインラインのingress引数ではなく、aws_vpc_security_group_ingress_ruleという独立したリソースで書いている理由は、第5章で S3 の設定を複数の独立したリソースに分けた理由と同じです。現在のプロバイダでは、aws_security_groupのインライン引数は非推奨で、CIDR ブロックごとに独立したリソースを使う書き方が推奨されています。3
このセキュリティグループには、egress (送信) のルールを何も定義していません。ALB が登録済みのターゲットへ通信する必要があるのは、ターゲットグループができてからです。4 この章の時点ではターゲットグループがまだ存在せず、ALB が外向きに通信すべき相手がいません。egress を空のままにしておけるのは、この章だけの一時的な状態です。ECS Fargate のタスク用セキュリティグループができる第 8 章以降、そこへの egress をこのセキュリティグループに追加していくことになります。
aws_security_groupを新規に作成すると、AWS は本来、全許可の egress ルールを既定で追加します。ただし Terraform でこのリソースを新規作成する場合、egress ブロックを何も書かなければ、この既定ルールを Terraform 自身が取り消します。5 このセキュリティグループの egress が空のままなのは、消し忘れではなく、この挙動を踏まえた意図的な状態です。
6.4. アクセスログ用S3バケットとバケットポリシー
第5章の 5.2 節で、ALB のアクセスログ用バケットはこの章で扱うと予告していました。ログの送信元となる ALB が存在しない段階では、バケットポリシーの設計まで検証できなかったためです。ここでその約束を果たします。
ALB のアクセスログを S3 に送るには、バケット側に ELB (Elastic Load Balancing) への書き込みを許可するバケットポリシーが要ります。この許可の与え方には 2 つの方式があります。1 つは、リージョンごとに割り当てられた ELB の固有アカウント ID に対して許可する方式です。ap-northeast-1 の場合、このアカウント ID は582318560864です。6 もう 1 つは、logdelivery.elasticloadbalancing.amazonaws.comというログ配信専用のサービスプリンシパルに対して許可する方式です。前者は 2022 年 8 月より前からあるリージョン向けの方式として引き続きサポートされていますが、現在はサービスプリンシパルを使う後者への切り替えが推奨されています。7 この章では、推奨されている後者を採用します。
resource "aws_s3_bucket_policy" "alb_logs" {
bucket = aws_s3_bucket.alb_logs.id
policy = jsonencode({
Version = "2012-10-17"
Statement = [
{
Sid = "AllowALBAccessLogDelivery"
Effect = "Allow"
Principal = {
Service = "logdelivery.elasticloadbalancing.amazonaws.com"
}
Action = "s3:PutObject"
Resource = "${aws_s3_bucket.alb_logs.arn}/alb/AWSLogs/${data.aws_caller_identity.current.account_id}/*"
}
]
})
}
このポリシーには、送信元の ALB をaws:SourceArn条件で絞り込む追加のハードニングを加えていません。AWS の公式ドキュメントも、この条件を必須のものとしてではなく、基本のポリシーとは別枠の推奨事項として案内しています。8 条件を加えなかった理由は、セキュリティ上の判断だけではありません。aws:SourceArnに ALB の ARN を使うと、このバケットポリシーが ALB 本体を参照することになります。すると、この章の Terraform コードは「ALB のaccess_logs属性がこのバケットを参照し、バケットポリシーがその ALB を参照する」という、互いを参照し合う関係になってしまいます。第5章5.3 節で扱った、backend の設定がその backend 自身の管理下にあるバケットを要求してしまう「鶏と卵」の問題とは事情が異なりますが、これもまた順序が噛み合わなくなる一因です。バケットポリシーを先に、ALB 本体を後に作る順序を Terraform の依存関係として確定させるため、aws_lbのリソース定義にはdepends_onでこのバケットポリシーへの依存を明示します。実装は次節で示します。
暗号化には、SSE-S3 だけを使います。ALB のアクセスログ用バケットは、SSE-KMS による暗号化に対応していません。9 第5章の添付バケットとは異なり、このバケットにはバージョニングを設定しません。添付バケットでバージョニングが役に立つのは、上書きや削除で失われるはずだったオブジェクトを復旧できるからです。アクセスログのオブジェクトは、ALB が一定間隔ごとに新しいファイルとして書き出すものであり、同じキーに対する上書きが起きません。上書きが起きない以上、バージョニングによる復旧の恩恵も働きません。第5章5.1 節で述べた「原則の適用範囲は用途によって変わる」という考え方が、ここに表れています。
6.5. Terraform実装
ここまでの内容を、alb.tfという新しいファイルに書きます。ALB のアクセスログ用バケットも、S3 のリソースではありますが、ALB を作るこの章のalb.tfにまとめます。第2章以来、章ごとに主なリソースを 1 つのファイルにまとめてきた方針を、ここでも踏襲します。
セキュリティグループは前節で示した通りです。続けて、アクセスログ用バケットを作ります。バケット本体・Block Public Access・暗号化・ライフサイクル・バケットポリシーは、第5章と同じく、それぞれ独立したリソースに分けます。data.aws_caller_identity.currentは、第3章のiam.tfで宣言済みのものをそのまま参照します。
resource "aws_s3_bucket" "alb_logs" {
bucket = "${local.name_prefix}-alb-logs-${data.aws_caller_identity.current.account_id}"
tags = {
Name = "${local.name_prefix}-alb-logs"
}
}
resource "aws_s3_bucket_public_access_block" "alb_logs" {
bucket = aws_s3_bucket.alb_logs.id
block_public_acls = true
block_public_policy = true
ignore_public_acls = true
restrict_public_buckets = true
}
resource "aws_s3_bucket_server_side_encryption_configuration" "alb_logs" {
bucket = aws_s3_bucket.alb_logs.id
rule {
apply_server_side_encryption_by_default {
sse_algorithm = "AES256"
}
}
}
resource "aws_s3_bucket_lifecycle_configuration" "alb_logs" {
bucket = aws_s3_bucket.alb_logs.id
rule {
id = "expire-alb-logs"
status = "Enabled"
filter {}
expiration {
days = 180
}
}
}
ライフサイクル設定は、180 日を過ぎたログを自動的に削除します。アクセスログは日々蓄積され続けるため、第5章の添付バケットと同様に無期限に保持しない設定をあらかじめ入れておきます(ただし添付バケットは非現行バージョンの期限切れが対象で、ここでは現行オブジェクト自体を 180 日で削除する点が異なります)。
バケットポリシーは前節で示した通りです。ここまででバケットの用意が整い、ALB 本体とリスナーを作れます。
resource "aws_lb" "main" {
name = "${local.name_prefix}-alb"
internal = false
load_balancer_type = "application"
security_groups = [aws_security_group.alb.id]
subnets = [for k, v in aws_subnet.this : v.id if local.subnets[k].tier == "public"]
drop_invalid_header_fields = true
access_logs {
bucket = aws_s3_bucket.alb_logs.id
prefix = "alb"
enabled = true
}
tags = {
Name = "${local.name_prefix}-alb"
}
depends_on = [aws_s3_bucket_policy.alb_logs]
}
resource "aws_lb_listener" "http" {
load_balancer_arn = aws_lb.main.arn
port = "80"
protocol = "HTTP"
default_action {
type = "fixed-response"
fixed_response {
content_type = "text/plain"
status_code = "200"
message_body = "Tasuku ALB is up (${var.env})"
}
}
}
subnetsには、第4章で作った public サブネット 2 つを、aws_subnet.thisから直接参照して渡します。internalをfalseにしているのは、インターネットからの着信を受ける ALB だからです。
drop_invalid_header_fieldsは、不正な形式の HTTP ヘッダーを ALB が除去するかどうかを決める属性で、既定値はfalseです。10 有効にしておくと、正規のリクエストとして解釈されるべきでないヘッダーを起点にした攻撃 (HTTP デシンク攻撃) への対策になります。11 この章のリスナーは fixed-response しか返さず攻撃の実害は小さいものの、この設定は ALB 自体の属性であり、後の章でターゲットグループへの転送を始めても引き継がれます。有効にする理由は十分にあります。
idle_timeoutは明示せず、既定値の 60 秒に委ねます。12 この章のリスナーは接続を長時間維持するような処理をしないため、既定値を変える理由がありません。
リスナーのdefault_actionはfixed-responseだけです。content_typeに指定できる値は、text/plain・text/css・text/html・application/javascript・application/jsonの 5 つに限られています。13 この章では、装飾の要らないtext/plainを選びます。message_bodyには 1,024 文字までの上限がありますが、14 この章で返す短い確認メッセージには十分な余裕があります。
最後に、後続の章が参照する値をoutputs.tfに追加します。
output "alb_arn" {
description = "ALBのARN (第6章)"
value = aws_lb.main.arn
}
output "alb_dns_name" {
description = "ALBのDNS名 (第6章の動作確認、第7章のRoute 53レコード検討で参照)"
value = aws_lb.main.dns_name
}
output "alb_zone_id" {
description = "ALBの正規ホストゾーンID (第7章のRoute 53 aliasレコードのzone_idで参照)"
value = aws_lb.main.zone_id
}
output "alb_listener_http_arn" {
description = "80番リスナーのARN (第7章のHTTPS化、第10章のリスナールール追加で参照)"
value = aws_lb_listener.http.arn
}
output "alb_security_group_id" {
description = "ALB用SGのID (第8章のECSタスク用SGのingress許可元として参照)"
value = aws_security_group.alb.id
}
alb_dns_nameとalb_zone_idは、次章で Route 53 の alias レコードを作るときに使います。alb_security_group_idは、ECS Fargate のタスク用セキュリティグループを作る第 8 章で、ALB からの ingress を許可する送信元として参照します。
6.6. 動作確認
terraform fmt -check -recursive -diffとterraform validateを実行し、構文とプロバイダスキーマの整合を確認します。15 この連載の検証はvalidateまでであり、planやapplyは AWS の認証情報を要するため対象に含めていません。第5章5.9 節で述べた検証水準を、この章でも変えていません。
実際に自分の AWS 環境でapplyする場合は、terraform output alb_dns_nameで ALB の DNS 名を取得し、curlでアクセスすると、200 番のステータスとともにTasuku ALB is up (dev)という本文が返ってきます。ALB のプロビジョニングには数分かかります。アクセスログの権限が正しく設定されていれば、ログバケットの中にELBAccessLogTestFileという空のテストファイルが作成されます。これは実際のログファイルではなく、権限を確認するための印です。16
本章のまとめ
- ALB はリスナー・ターゲットグループ・ヘルスチェックの 3 要素からなりますが、この章ではリスナーだけを作り、転送先のない fixed-response で応答しました
- ドメインと SSL 証明書の準備が整っていないため、443 番ポートは次章に持ち越し、この章は HTTP (80 番) だけで公開します
- ALB 用セキュリティグループの egress は、ターゲットグループが存在しないこの章の時点では空のままにしています
- アクセスログ用バケットのポリシーには、現在推奨されているログ配信サービスプリンシパル方式を採用しました
- バケットポリシーと ALB 本体が互いを参照しないよう、
aws:SourceArn条件を使わずdepends_onで順序を明示しました
次に読む
- 第 7 章: Route 53 / ACM: ドメインの検証と SSL 証明書の発行、443 番ポートへの移行
- 第 5 章: S3: バケット設計原則と tfstate 管理の定石
- 連載の目次: 全 12 章の構成と読み進め方