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 章の構成と読み進め方