Terraform 実践 #10 ドメイン・HTTPS・完成 — Route 53、ACM 検証、CloudFront と provider alias

読了 5分

最後のピースです。今のサービスは ALB の長い自動生成アドレスで HTTP 接続しかできません。今回はドメインをつなぎ、証明書を発行して HTTPS を有効にし、静的アセットに CDN を取り付けて、#1 で描いた完成形に到達します。Terraform の観点で新しく学ぶことは 2 つです。ACM の DNS 検証をリソースで表現するイディオムと、リージョンの異なるリソースを 1 つのコードで扱う provider alias です。ドメインはすでに Route 53 にホストゾーンがある前提で進めます(購入とゾーンの概念はドメイン・DNS の基礎を参照してください)。

ホストゾーンはデータソースで #

ドメイン自体はこのプロジェクトが作ったものではないので、基礎 #4 の原則どおり、管理権限を持たずに読むだけにします。

dns.tf
# dns.tf(新規ファイル)
data "aws_route53_zone" "main" {
  name = "example.com"
}

ACM 証明書と DNS 検証イディオム #

ACM 証明書は申請だけでは発行されず、ドメイン所有の検証を経ます。コンソールでは「レコードを作成」ボタンをクリックするあの過程が、Terraform ではリソース 3 つのイディオムになります。申請、検証レコード、検証待ちです。

dns.tf
resource "aws_acm_certificate" "alb" {
  domain_name       = "app.example.com"
  validation_method = "DNS"

  lifecycle {
    create_before_destroy = true
  }
}

resource "aws_route53_record" "alb_cert_validation" {
  for_each = {
    for dvo in aws_acm_certificate.alb.domain_validation_options : dvo.domain_name => {
      name   = dvo.resource_record_name
      type   = dvo.resource_record_type
      record = dvo.resource_record_value
    }
  }

  zone_id = data.aws_route53_zone.main.zone_id
  name    = each.value.name
  type    = each.value.type
  ttl     = 60
  records = [each.value.record]
}

resource "aws_acm_certificate_validation" "alb" {
  certificate_arn         = aws_acm_certificate.alb.arn
  validation_record_fqdns = [for r in aws_route53_record.alb_cert_validation : r.fqdn]
}

真ん中のリソースの for_each は、基礎 #4 の for 式が実務で最もよく使われる形です。証明書が要求する検証レコードの一覧をマップに変換してレコードを作ります。最後の aws_acm_certificate_validation は実際のリソースを作るのではなく、検証完了まで待つ待機装置です。これを参照するリソースは、証明書の発行が完了した後に作られます。初見では戸惑いますが、ACM をコードで扱うあらゆる場面で遭遇する標準イディオムです。

HTTPS リスナーとリダイレクト #

発行された証明書を ALB に取り付けます。#4 で予告したとおり、80 番のリスナーはリダイレクトに変わります。

lb.tf
# lb.tf を修正
resource "aws_lb_listener" "https" {
  load_balancer_arn = aws_lb.main.arn
  port              = 443
  protocol          = "HTTPS"
  certificate_arn   = aws_acm_certificate_validation.alb.certificate_arn

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

resource "aws_lb_listener" "http" {
  load_balancer_arn = aws_lb.main.arn
  port              = 80
  protocol          = "HTTP"

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

443 リスナーが証明書 ARN を aws_acm_certificate_validation から取ることが、先ほどのイディオムの仕上げです。ALB のセキュリティグループに 443 のインバウンドを追加するのも忘れません。あとは https://app.example.com へ向かう最後のつなぎ、エイリアスレコードを取り付けます。

dns.tf
resource "aws_route53_record" "app" {
  zone_id = data.aws_route53_zone.main.zone_id
  name    = "app.example.com"
  type    = "A"

  alias {
    name                   = aws_lb.main.dns_name
    zone_id                = aws_lb.main.zone_id
    evaluate_target_health = true
  }
}

provider alias — CloudFront の us-east-1 要件 #

静的アセットの番になると、実務で有名な落とし穴に遭遇します。CloudFront に取り付ける証明書は、リージョンに関係なく必ず us-east-1 の ACM に存在しなければなりません。今の provider は ap-northeast-2 ですが、どうすればよいでしょうか。答えが provider alias です。同じ provider を別の設定でもう 1 セット登録する機能です。

providers.tf
# providers.tf に追加
provider "aws" {
  alias  = "us_east_1"
  region = "us-east-1"

  default_tags {
    tags = {
      Project   = "myapp"
      Env       = var.env
      ManagedBy = "terraform"
    }
  }
}
cdn.tf
resource "aws_acm_certificate" "cdn" {
  provider = aws.us_east_1

  domain_name       = "static.example.com"
  validation_method = "DNS"

  lifecycle {
    create_before_destroy = true
  }
}

リソースに provider = aws.us_east_1 を指定すると、そのリソースだけバージニアに作られます。検証レコードと待機装置は先ほどのイディオムと同じ形で付きます(レコード自体はデフォルトの provider で、証明書と待機は alias で作る点にだけ注意します)。CloudFront ディストリビューションは OAC(Origin Access Control)で #5 の閉じた assets バケットを読みます。ディストリビューションのリソースは引数が多いので全文は載せませんが、骨組みは単純です。origin が S3 バケット、証明書が今作った cdn 証明書、バケットポリシーが「このディストリビューションから来る GetObject だけ許可」です。バケットは最後までパブリックにならず、世界と接するのは CDN だけです。

完成、そして振り返り #

static.example.com のエイリアスレコードまで取り付ければ、#1 の図がすべてコードになりました。10 本の道のりをリソースではなく決定で振り返るとこうなります。タグとバックエンドを初日に立て(#1)、ネットワーク階層を基準にセキュリティを設計し(#2・#3)、トラフィックを受けながら階層を乗り換え(#4・#8)、データには安全装置をかけ(#5)、シークレットを記録から消し(#6)、権限を参照で絞り(#7)、構造を再作成なしにリファクタリングして環境を複製しました(#9)。これらの決定は Terraform の文法ではなくインフラ運用の判断であり、Terraform はその判断を、レビューと履歴のあるコードにしてくれた道具です。

実習を終えたら片付けます。dev は destroy 1 回で済み、これで料金の心配も終わりです。残る問いは 1 つです。このコードをチームで一緒に、人の手の apply なしに運用するには何が必要でしょうか。

まとめ #

この記事で扱った内容です。

  • すでにあるホストゾーンはデータソースで読みます。ACM は申請、検証レコード(for_each イディオム)、検証待ちのリソース 3 つで発行します
  • HTTPS リスナーは検証待ちリソースから証明書 ARN を受け取って発行完了後に作られ、80 番リスナーは 301 リダイレクトに変わります
  • CloudFront 用の証明書は必ず us-east-1 に必要で、provider alias で 1 つのコードから 2 つのリージョンを扱います
  • CloudFront は OAC で閉じたバケットを読みます。バケットは最後までパブリックになりません
  • 10 本の成果物はリソースの一覧ではなく、コードに残った運用判断です。実習の片付けは destroy 1 回です

次は Terraform 運用講座です。PR で plan をレビューして CI が apply を実行するチームワークフローから、ドリフト対応、ポリシーとコストのガードレールまで、7 本で扱います。

X