My External Storage

Nov 7, 2020 - 7 minute read - Comments - terraform aws

TerraformでAWS上にHTTPS化したサブドメインを定義する

個人のAWS環境でTerraformを使ってHTTPS化したサブドメインを定義した。
普段なかなかしないことで忘れてしまうので手順をまとめおく。

TL;DR

  • Terraformを使ってAWS上でHTTPS化したサブドメインを構成したい
  • ルートドメインのホストゾーンをTerrformで作っても登録済みドメインのネームサーバの設定は手動になるので注意する
  • ワイルドカード付きの証明書をTerffaformで生成するときは少しテクニックが必要になる
  • ハマりどころを解決できれば少々の定義で構築できた

なお、本記事で利用しているTerraformとAWS Providerのバージョンは以下となる。

terraform {
  required_version = ">= 0.13.4"
}

provider "aws" {
  version = "~> 3.6.0"
}

HTTPS化したサブドメインを作成したい

この記事の前提とやりたいことは次の通り。

  • 前提条件
    • example.com というドメインをRoute53の登録済みドメインとして登録済み
  • example.comまたは*.example.comをHTTPS化するSSL証明書を発行する
  • https://subdomain.example.com というようなHTTPS化されたサブドメインを定義する。

本記事で利用するAWSのサービスはRoute53とAWS Certificate Manager(ACM)だ。
Route53はご存知の通りAWS上でドメインを使ってルーティングを構成するDNSサービスだ。
ACMはSSL/TLS証明書を管理するサービスで、今回はHTTPS通信に利用する証明書を自動管理させる。

Route53でホストゾーンを定義する

まずRoute53でsubdomain.example.com(サブドメイン)をホストゾーンで定義する。

なお、Route53でexample.comを登録済みドメインにしている場合、すでにホストゾーンが作成されているはずである。
Terraform上では、既成のexample.com(ルートドメイン)に対応するホストゾーンをデータソースで参照する。

data aws_route53_zone example_com {
  name = "example.com"
}

resource aws_route53_zone subdomain_example_com {
  name = "subdomain.example.com"
}

ルートドメインをTerraformで定義しようとしないこと

ルートドメインのホストゾーンはTerraformで作成しないこと

AWS Provider 3.6.0系でもRoute53の「登録済みドメイン」をTerraformで管理することはできない。
Terraformでルートドメインを定義した場合、定義したルートドメインに割り当てられるNSコード(ネームサーバ)のIPと、登録済みドメインのネームサーバが一致しなくなるので証明書のDNS認証などができなくなる。

ホストゾーンを作成する場合、Route 53 はホストゾーンに一連の 4 つのネームサーバーを割り当てます。ホストゾーンを削除して新しいゾーンを作成すると、Route 53 は別の一連の 4 つのネームサーバーを割り当てます。通常、新しいホストゾーンのネームサーバーは、以前のホストゾーンのネームサーバーと一致しません。新しいホストゾーンのネームサーバーを使用するようにドメイン設定を更新しないと、ドメインはインターネット上で利用できなくなります。

サブドメインとルートドメインのホストゾーンを関連付ける

次に、作成するサブドメインをルートドメインに関連付ける。
具体的には、ルートドメイン (example.com) のホストゾーンにサブドメインのホストゾーン割り当てられる4つのネームサーバーをNSレコードとして登録する。

TerraforrmでNSレコードを作成するコードは次の通り。

resource aws_route53_record ns_record_for_subdomain {
  name    = aws_route53_zone.example_com.name
  zone_id = data.aws_route53_zone.example_com.id
  records = [
    aws_route53_zone.subdomain_example_com.name_servers[0],
    aws_route53_zone.subdomain_example_com.name_servers[1],
    aws_route53_zone.subdomain_example_com.name_servers[2],
    aws_route53_zone.subdomain_example_com.name_servers[3]
  ]
  ttl  = 300
  type = "NS"
}

これでリクエストの流れはできた。

ACMでHTTPS化用のSSL証明書を作成する

次にHTTPS化の準備をする。冒頭で述べた通りACMを使って証明書を発行する。
AWSでHTTPS用の証明書を発行する場合、DNS認証を使っておけば証明書の自動更新が実現できる。
当然今回もこれを利用する設定を行う。

ワイルドカード付きの証明書を発行する

ルートドメイン、サブドメインすべてをHTTPS化したいため、ワイルドカードを含んだSSL証明書を発行する必要がある。

まず、SSL証明書はの定義は次の通り。subject_alternative_namesを使うことでサブドメインもSSL証明書の対象に含めることができる。

resource aws_acm_certificate example_com {
  domain_name               = data.aws_route53_zone.example_com.name
  subject_alternative_names = [format("*.%s", data.aws_route53_zone.example_com.name)]
  validation_method = "DNS"

  lifecycle {
    create_before_destroy = true
  }
}

format関数はTerraformの標準関数のひとつで、ざっくりいうとGoのfmt.Printfのように文字列を生成できる。

証明書の検証につかうレコードはこのように定義する。

resource aws_route53_record certificate {
  for_each = {
    for dvo in aws_acm_certificate.example_com.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
  records         = [each.value.record]
  ttl             = 60
  type            = each.value.type
  zone_id         = data.aws_route53_zone.example_com.id
  allow_overwrite = true
}

最後にapply時SSL証明書の検証が完了するまで待機する設定を書いておく。

resource aws_acm_certificate_validation cert {
  certificate_arn         = aws_acm_certificate.example_com.arn
  validation_record_fqdns = [for record in aws_route53_record.certificate : record.fqdn]
}

terraform apply中に「Tried to create resource record set….but it already exists」が出て失敗する

ワイルドカードを使った証明書を作成しようとすると、ワイルドカードとルートドメインの検証用レコードが同じ内容になるためエラーが発生する。
これを回避するにはallow_overwritetrueにしておくのを忘れないこと

本記事に記載した先ほどのaws_route53_record certificateはAWS Provider v3の定義方法だ。
AWS Provider v2などで同様の現象を回避したい場合は次のブログが参考になる。

DNS認証が終わらない

正しく設定できているように見えるのにDNS認証が通らない場合はネームサーバの設定がおかしく、検証用リクエストがさばけていない可能性がある。
前述したとおり、ルートドメインをTerraformで新しく作った場合などに発生する。
ルートドメインのホストゾーンのNSレコードに記載されているネームサーバのIPを登録済みドメインのネームサーバのIPとして登録すること。

ALBへ通信を流す

ここまでできればあとはサブドメインのホストゾーンからALBへ通信を流し込むAレコードを作成すればよい。

resource "aws_route53_record" "subdomain_example_com" {
  zone_id = aws_route53_zone.subdomain_example_com.zone_id
  name    = aws_route53_zone.subdomain_example_com.name
  type    = "A"

  # 実際に通信をさばくALBの情報
  alias {
    name                   = aws_lb.xxx.dns_name
    zone_id                = aws_lb.xxx.zone_id
    evaluate_target_health = true
  }
}

これで、 https://subdomain.example.comの通信がALBへ流し込まれるようになる。

AレコードはAWS独自拡張DNSレコードタイプのエイリアスレコードのことだ(よくわかっていないが速いらしい)。

TerraformのApplyが終わればローカルからhttps://subdomain.example.comへの通信が可能になっているはずだ(当然ALB以降に何かしらレスポンスする定義がないとダメだが)。

最後に

私はアプリケーションエンジニアなので、普段ALBらへんまではTerraformで書いたり、覗いたことがあった。
今回いつもSREの方々に任せているところを自分ではじめてRoute53やACMを自分で設定してみてネットワークについて少し理解が深まった。
ただ、アプリケーションを作ってObservabilityの勉強をしたいのが目的なので、ゴール(スタート?)はまだまだ遠い…

最後に今回使ったサンプルコードをまとめておく。

terraform {
  required_version = ">= 0.13.4"
}

provider "aws" {
  version = "~> 3.6.0"
}

data aws_route53_zone example_com {
  name = "example.com"
}

resource aws_route53_zone subdomain_example_com {
  name = "subdomain.example.com"
}

resource aws_route53_record ns_record_for_subdomain {
  name    = aws_route53_zone.example_com.name
  zone_id = data.aws_route53_zone.example_com.id
  records = [
    aws_route53_zone.subdomain_example_com.name_servers[0],
    aws_route53_zone.subdomain_example_com.name_servers[1],
    aws_route53_zone.subdomain_example_com.name_servers[2],
    aws_route53_zone.subdomain_example_com.name_servers[3]
  ]
  ttl  = 300
  type = "NS"
}

resource aws_acm_certificate example_com {
  domain_name               = data.aws_route53_zone.example_com.name
  subject_alternative_names = [format("*.%s", data.aws_route53_zone.example_com.name)]
  validation_method = "DNS"

  lifecycle {
    create_before_destroy = true
  }
}

resource aws_route53_record certificate {
  for_each = {
    for dvo in aws_acm_certificate.example_com.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
  records         = [each.value.record]
  ttl             = 60
  type            = each.value.type
  zone_id         = data.aws_route53_zone.example_com.id
  allow_overwrite = true
}

resource aws_acm_certificate_validation cert {
  certificate_arn         = aws_acm_certificate.example_com.arn
  validation_record_fqdns = [for record in aws_route53_record.certificate : record.fqdn]
}

resource "aws_route53_record" "subdomain_example_com" {
  zone_id = aws_route53_zone.subdomain_example_com.zone_id
  name    = aws_route53_zone.subdomain_example_com.name
  type    = "A"

  # 実際に通信をさばくALBの情報
  alias {
    name                   = aws_lb.xxx.dns_name
    zone_id                = aws_lb.xxx.zone_id
    evaluate_target_health = true
  }
}

参考

関連記事