🐍

【Terraform】ACMを使用してALBにHTTPSでアクセスする

に公開

どのサイトでも HTTPS 化することが求められる時代になっています。

今回はTerraformAWSを使用して、ALB に HTTPS アクセスできるようにします!

また、HTTP でアクセスした場合には HTTPS へリダイレクトするようにもします

現状

  1. Route53 で ALB を Alias レコードとして登録している
  2. ALB は Nginx をインストールしている EC2 へリクエストを振り分ける
  3. 1.と 2.より example.com でアクセスすると、Nginx の画面が表示される
  4. ALB に SSL 証明書は適用していないため、HTTP のみでアクセスできる

コマンドで現状を確認すると

// HTTPリクエスト
// レスポンスヘッダ
$ curl -I http://example.com
HTTP/1.1 200 OK
Date: Wed, 01 Nov 2023 13:37:21 GMT
Content-Type: text/html
Content-Length: 615
Connection: keep-alive
Server: nginx/1.24.0
Last-Modified: Fri, 13 Oct 2023 13:33:26 GMT
ETag: "65294726-267"
Accept-Ranges: bytes

// レスポンスボディ
$ curl http://example.com
<!DOCTYPE html>
<html>
<head>
<title>Welcome to nginx!</title>
----略------------------------------


// HTTPSリクエスト
$ curl https://example.com
^C
// 応答ないので終了する
$ telnet example.com 443
Trying ×××.×××.×××.×××...
Trying ×××.×××.×××.×××...
telnet: Unable to connect to remote host: Connection timed out

ゴール

ACM で SSL 証明書を作成し ALB に適用することで

  1. HTTPS でアクセス
  2. HTTP⇒HTTPS のリダイレクト

をできるようにします

Terraform で構築していく

ACM で SSL 証明書を作成

aws_acm_certificate で SSL 証明書を作成します

プロパティ 説明
domain_name 証明書のドメイン名。
validation_method 検証方法。今回は DNS 認証としています。
後ほど Route53 に検証用レコードを作成します。
subject_alternative_names SANs の設定。今回はワイルドカードを設定しておきます。
create_before_destroy 旧リソースを削除する前に新リソースを作成するか。
true にしておくと、証明書を作り変えるときに 旧証明書が ALB にアタッチされたまま削除しようとしてエラーになることを防げます。
# ACMでSSL証明書を作成
resource "aws_acm_certificate" "cert" {
  domain_name               = "example.com"
  validation_method         = "DNS"
  subject_alternative_names = ["*.example.com"]

  lifecycle {
    create_before_destroy = true
  }
}

SSL 証明書を Route53 で検証

このパートはハマりポイントが多いので注意してください!

aws_route53_record でDNS 検証用レコードを Route53 に作成します

aws_acm_certificate.cert.domain_validation_options は Set 型のため、
for 文で map 型へ変換し、for_each で 1 つずつ取り出しながらレコードを作成しています。

プロパティ 説明
name レコード名。map 型の name プロパティに入っている
type レコードタイプ。type プロパティに入っている
records レコード値。List 型。record プロパティに入っている
zone_id ホストゾーンの ID。
ttl TTL。
allow_overwrite レコードの上書きを許すか。
証明書にワイルドカードを含めた場合には true にしないとエラーになる

また、aws_acm_certificate_validation を使用して DNS 検証が完了するまで待ちます

aws_route53_record.cert を Set→List へ変換して validation_record_fqdns に指定します

# ホストゾーン
# 事前に作成されているものとします
resource "aws_route53_zone" "hostzone" {
  name = "example.com"
}

# SSL証明書のDNS検証用レコード
resource "aws_route53_record" "cert" {
  for_each = {
    for dvo in aws_acm_certificate.cert.domain_validation_options : dvo.domain_name => {
      name   = dvo.resource_record_name
      record = dvo.resource_record_value
      type   = dvo.resource_record_type
    }
  }

  name            = each.value.name
  type            = each.value.type
  records         = [each.value.record]
  zone_id         = aws_route53_zone.hostzone.zone_id
  ttl             = 60
  allow_overwrite = true
}

# SSL証明書の検証が終わるまで待つ
resource "aws_acm_certificate_validation" "cert" {
  certificate_arn         = aws_acm_certificate.cert.arn
  validation_record_fqdns = flatten([values(aws_route53_record.cert)[*].fqdn])
}

ALB の HTTPS リスナーを作成

ALB の 443 ポートへ来たアクセスをターゲットグループの 80 ポートへながします

プロパティ 説明
load_balancer_arn ALB の ARN。
port リクエストを受け取るリスナーのポート。443 とします
protocol リスナーが受け取るプロトコル。HTTPS とします
ssl_policy セキュリティポリシー。 AWSによると ELBSecurityPolicy-TLS13-1-0-2021-06 が推奨。
certificate_arn SSL 証明書の ARN。
aws_acm_certificate で作成したものを指定。
default_action リクエストが来た時の ALB の挙動を設定。
type デフォルトアクションのタイプ。
ターゲットグループにリクエストを流すのでforward を指定
target_group_arn リクエストを流すターゲットグループの ARN。
# ALB
# すでに作成済みとします
resource "aws_lb" "alb" {
  name               = "alb"
  internal           = false
  load_balancer_type = "application"

  security_groups = [aws_security_group.alb_sg.id]
  subnets         = [
    aws_subnet.subnet_1a.public_subnet_id,
    aws_subnet.subnet_1c.public_subnet_id
  ]

  # 削除保護
  enable_deletion_protection = true
}

# HTTPSリスナー
# ALBは443ポートで受けてEC2の80ポートへリクエストをながす
resource "aws_lb_listener" "alb_https" {
  load_balancer_arn = aws_lb.alb.arn
  port              = "443"
  protocol          = "HTTPS"
  ssl_policy        = "ELBSecurityPolicy-TLS13-1-2-2021-06"
  certificate_arn   = aws_acm_certificate.cert.arn

  default_action {
    type             = "forward"
    target_group_arn = aws_lb_target_group.tg.arn
  }
}

# ターゲットグループ
# 80ポートにHTTPプロトコルでリクエストをながす
# すでに作成済みとします
resource "aws_lb_target_group" "tg" {
  name     = "alb-tg"
  port     = 80
  protocol = "HTTP"
  vpc_id   = aws_vpc.main.id
}

HTTP リスナーのアクションをリダイレクトへ変更

HTTP リスナーを修正して、HTTPS へリダイレクトするようにします

具体的にはdefault_action の type を redirectにして、
リダイレクト先のポート、プロトコル、スタータスコードを指定します

# HTTPリスナー
# HTTPSへリダイレクトする
resource "aws_lb_listener" "alb_http" {
  load_balancer_arn = aws_lb.alb.arn
  port              = "80"
  protocol          = "HTTP"

  default_action {
    type = "redirect"

    redirect {
      port        = "443"
      protocol    = "HTTPS"
      status_code = "HTTP_301"
    }
  }
}

(必要に応じて)ALB の 443 ポートへの通信を許可

設定内容はインバウンドルールにインターネットからの 443 ポートへの通信を許可すること。

# ALBのセキュリティグループ
# すでに作成済みとします
resource "aws_security_group" "alb_sg" {
  name        = "alb_sg"
  vpc_id      = aws_vpc.main.id
}

# インターネットからの443ポートへの通信を許可
resource "aws_security_group_rule" "alb_sg_ingress_https" {
  security_group_id = aws_security_group.alb_sg.id
  type              = "ingress"
  from_port         = "443"
  to_port           = "443"
  protocol          = "tcp"
  cidr_blocks       = ["0.0.0.0/0"]
}

完成したコード

# ACMでSSL証明書を作成
resource "aws_acm_certificate" "cert" {
  domain_name               = "example.com"
  validation_method         = "DNS"
  subject_alternative_names = ["*.example.com"]

  lifecycle {
    create_before_destroy = true
  }
}


# ホストゾーン
# 事前に作成されているものとします
resource "aws_route53_zone" "hostzone" {
  name = "example.com"
}

# SSL証明書のDNS検証用レコード
resource "aws_route53_record" "cert" {
  for_each = {
    for dvo in aws_acm_certificate.cert.domain_validation_options : dvo.domain_name => {
      name   = dvo.resource_record_name
      record = dvo.resource_record_value
      type   = dvo.resource_record_type
    }
  }

  name            = each.value.name
  type            = each.value.type
  records         = [each.value.record]
  zone_id         = aws_route53_zone.hostzone.zone_id
  ttl             = 60
  allow_overwrite = true
}

# SSL証明書の検証
resource "aws_acm_certificate_validation" "cert" {
  certificate_arn         = aws_acm_certificate.cert.arn
  validation_record_fqdns = flatten([values(aws_route53_record.cert)[*].fqdn])
}


# ALB
# すでに作成済みとします
resource "aws_lb" "alb" {
  name               = "alb"
  internal           = false
  load_balancer_type = "application"

  security_groups = [aws_security_group.alb_sg.id]
  subnets         = [
    aws_subnet.subnet_1a.public_subnet_id,
    aws_subnet.subnet_1c.public_subnet_id
  ]

  # 削除保護
  enable_deletion_protection = true
}

# HTTPリスナー
# HTTPSへリダイレクトする
resource "aws_lb_listener" "alb_http" {
  load_balancer_arn = aws_lb.alb.arn
  port              = "80"
  protocol          = "HTTP"

  default_action {
    type = "redirect"

    redirect {
      port        = "443"
      protocol    = "HTTPS"
      status_code = "HTTP_301"
    }
  }
}

# HTTPSリスナー
# ALBは443ポートで受けてEC2の80ポートへリクエストをながす
resource "aws_lb_listener" "alb_https" {
  load_balancer_arn = aws_lb.alb.arn
  port              = "443"
  protocol          = "HTTPS"
  ssl_policy        = "ELBSecurityPolicy-TLS13-1-2-2021-06"
  certificate_arn   = aws_acm_certificate.cert.arn

  default_action {
    type             = "forward"
    target_group_arn = aws_lb_target_group.tg.arn
  }
}

# ターゲットグループ
# 80ポートにHTTPプロトコルでリクエストをながす
# すでに作成済みとします
resource "aws_lb_target_group" "tg" {
  name     = "alb-tg"
  port     = 80
  protocol = "HTTP"
  vpc_id   = aws_vpc.main.id
}

# ALBのセキュリティグループ
# すでに作成済みとします
resource "aws_security_group" "alb_sg" {
  name        = "alb_sg"
  vpc_id      = aws_vpc.main.id
}

# インターネットからの443ポートへの通信を許可
resource "aws_security_group_rule" "alb_sg_ingress_https" {
  security_group_id = aws_security_group.alb_sg.id
  type              = "ingress"
  from_port         = "443"
  to_port           = "443"
  protocol          = "tcp"
  cidr_blocks       = ["0.0.0.0/0"]
}

動作確認

terraform apply を実行して正常終了したら、動作確認をしていきます

HTTPS でのアクセス

$ curl -I https://example.com
HTTP/2 200
date: Wed, 01 Nov 2023 15:32:10 GMT
content-type: text/html
content-length: 615
server: nginx/1.24.0
last-modified: Fri, 13 Oct 2023 13:33:26 GMT
etag: "65294726-267"
accept-ranges: bytes

$ curl https://example.com
<!DOCTYPE html>
<html>
<head>
<title>Welcome to nginx!</title>
<style>
html { color-scheme: light dark; }
body { width: 35em; margin: 0 auto;
font-family: Tahoma, Verdana, Arial, sans-serif; }
</style>
</head>
<body>
<h1>Welcome to nginx!</h1>
<p>If you see this page, the nginx web server is successfully installed and
working. Further configuration is required.</p>

<p>For online documentation and support please refer to
<a href="http://nginx.org/">nginx.org</a>.<br/>
Commercial support is available at
<a href="http://nginx.com/">nginx.com</a>.</p>

<p><em>Thank you for using nginx.</em></p>
</body>
</html>

HTTPS に対して 200 と Nginx の画面も返ってきています

やったー

HTTP⇒HTTPS のリダイレクト

$ curl -I http://example.com
HTTP/1.1 301 Moved Permanently
Server: awselb/2.0
Date: Wed, 01 Nov 2023 15:32:24 GMT
Content-Type: text/html
Content-Length: 134
Connection: keep-alive
Location: https://example.com:443/

HTTP に curl を投げると、301 でhttps://example.comにリダイレクトされています

無事終わりましたー

参考

https://docs.aws.amazon.com/ja_jp/elasticloadbalancing/latest/application/create-https-listener.html#describe-ssl-policies

https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/acm_certificate

https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/lb_listener

https://dev.classmethod.jp/articles/dnsrecord-acm-alb-with-terraform/#toc-2

https://zenn.dev/kuuki/articles/error-aws-terraform-acm-dns-auth/

Discussion