CloudFront のディストリビューションに、独自ドメイン(この記事では例として blog.example.com)を設定します。 ドメインの DNS が別の AWS アカウントにあり、こちらのアカウントでは証明書と CloudFront の設定だけを行って、DNS には「このレコードを登録してください」と依頼する場合を想定します。

  1. 登場するものと、つながりの仕組み
  2. 全体の流れ
  3. 方法 A: CloudFormation
  4. 方法 B: AWS CLI
  5. DNS に登録するレコード
  6. 登録の順番
  7. A レコードや AAAA レコードではだめなのか

登場するものと、つながりの仕組み#

独自ドメインの設定は、次の 3 つを同じドメイン名で結ぶ作業です。

登場するもの場所役割
DNS のレコードドメインの DNS(別アカウント)blog.example.com を CloudFront に向ける。証明書の所有確認にも使う
ACM 証明書us-east-1blog.example.com の HTTPS 通信の証明
CloudFront の別名(Alias)CloudFront ディストリビューション「このドメイン名のアクセスを受け付ける」という宣言

ブラウザが https://blog.example.com/ を開いたとき、次のようにつながります。

 ブラウザ
   │ ① blog.example.com の IP は?
   ▼
 DNS ── CNAME: blog.example.com → xxxx.cloudfront.net ──┐
                                                        │ ② CloudFront の IP が返る
   ┌────────────────────────────────────────────────────┘
   ▼
 CloudFront(ディストリビューション xxxx)
   │ ③ TLS の接続で「blog.example.com」と名乗る(SNI)
   │ ④ 別名(Alias)に blog.example.com が入っているか?  ← 入っていないと 403
   │ ⑤ ACM 証明書(blog.example.com 用)を提示する
   ▼
 ブラウザ: 証明書のドメイン名と、開いたドメイン名が一致 → HTTPS で表示

つまり、次の 3 つが同じ名前でそろって初めて繋がります。

  • DNS: blog.example.com が CloudFront の xxxx.cloudfront.net を指している
  • CloudFront の別名: blog.example.com が入っている
  • ACM 証明書: blog.example.com 用で、CloudFront に設定されている

どれか 1 つが欠けると、表示されないか、証明書のエラーになります。

もう 1 本、別のつながりがあります。DNS の検証用 CNAMEです。

 ACM  ──「このドメインはあなたのものですか?」──▶  DNS の検証用 CNAME(_xxxx.blog.example.com)
 ACM  ◀──────── レコードがあれば、所有者と確認 ────────┘ → 証明書を発行(ISSUED)

ACM は、ドメインの DNS に指定のレコードがあるかどうかで所有者かどうかを判断します。 こちらのアカウントからは DNS を触れないので、このレコードも依頼して登録してもらいます。

全体の流れ#

 ① 証明書をリクエストする       us-east-1 に ACM 証明書(DNS 検証)を作る
          │
 ② 検証用の CNAME を登録       DNS 側で登録 → 証明書が ISSUED になる
          │
 ③ CloudFront を更新           別名と証明書を設定する
          │
 ④ サイト用の CNAME を登録     blog.example.com → xxxx.cloudfront.net
          │
 ⑤ 表示を確認する

①と③の方法が 2 通りあります。CloudFormation で管理する方法(A)と、AWS CLI で直接設定する方法(B)です。②と④の DNS の依頼は、どちらでも同じです。

証明書は us-east-1 に作る#

CloudFront に使う ACM 証明書は、us-east-1(バージニア北部)にあるものに限られます。 CloudFront のスタックなどが東京リージョンなどにあっても、証明書だけは us-east-1 に作ります。

方法 A: CloudFormation#

① 証明書のスタック(us-east-1)#

証明書だけを、別のスタックにします。CloudFormation は 1 つのスタックを 1 つのリージョンに作るため、us-east-1 の証明書は、他のリージョンのスタックには入れられません。

# cert.yaml
AWSTemplateFormatVersion: "2010-09-09"
Parameters:
  DomainName:
    Type: String
    Default: blog.example.com

Resources:
  Certificate:
    Type: AWS::CertificateManager::Certificate
    Properties:
      DomainName: !Ref DomainName
      ValidationMethod: DNS

Outputs:
  CertificateArn:
    Value: !Ref Certificate
aws cloudformation create-stack --region us-east-1 \
  --stack-name cert --template-body file://cert.yaml

ValidationMethod: DNS の証明書は、DNS の検証が通るまでスタックの作成が終わりません(CREATE_IN_PROGRESS のまま待ちます)。 待っている間に、検証用のレコードを取り出して依頼します。

CERT=$(aws cloudformation describe-stack-resources --region us-east-1 \
  --stack-name cert \
  --query 'StackResources[0].PhysicalResourceId' --output text)

aws acm describe-certificate --region us-east-1 --certificate-arn "$CERT" \
  --query 'Certificate.DomainValidationOptions[].ResourceRecord'

レコードが登録されると、数分で証明書が ISSUED、スタックが CREATE_COMPLETE になります。

③ CloudFront のスタックに別名と証明書を足す#

CloudFront を作っているテンプレートに、ドメインと証明書のパラメータを足します。

Parameters:
  CustomDomainName:
    Type: String
    Default: blog.example.com
  CertificateArn:
    Type: String
    Default: arn:aws:acm:us-east-1:<アカウント ID>:certificate/<ID>

Conditions:
  UseCustomDomain: !And
    - !Not [!Equals [!Ref CustomDomainName, ""]]
    - !Not [!Equals [!Ref CertificateArn, ""]]

CloudFront の Aliases と ViewerCertificate を、条件で切り替えます。

DistributionConfig:
  Aliases: !If [UseCustomDomain, [!Ref CustomDomainName], !Ref "AWS::NoValue"]
  ViewerCertificate: !If
    - UseCustomDomain
    - AcmCertificateArn: !Ref CertificateArn
      SslSupportMethod: sni-only
      MinimumProtocolVersion: TLSv1.2_2021
    - CloudFrontDefaultCertificate: true
  • sni-only にします。専用 IP(vip)は月額の費用がかかり、今のブラウザなら SNI で困りません。
  • 2 つのパラメータを空にすると、独自ドメインをやめて、CloudFront の既定のドメインと既定の証明書に戻せます。
  • 証明書のスタックの出力は、別リージョンなので Fn::ImportValue では渡せません(エクスポートは同じリージョン内のみ)。パラメータで ARN を直接渡します。

適用の前に、変更セットで差分を確認します。

aws cloudformation deploy --region <CloudFront のスタックのリージョン> \
  --template-file template.yaml --stack-name <スタック名> \
  --capabilities CAPABILITY_NAMED_IAM --no-execute-changeset

差分が Distribution の Modify(置換なし)だけであることを確認して、実行します。

方法 A の特徴#

  • 証明書・別名・パラメータがコードに残り、作り直せる。
  • スタックを消せば、証明書もまとめて消える。
  • 証明書が別リージョンのため、スタックが 2 つになる。ARN の受け渡しは手動。

方法 B: AWS CLI#

① 証明書をリクエストする#

CERT=$(aws acm request-certificate --region us-east-1 \
  --domain-name blog.example.com \
  --validation-method DNS \
  --query CertificateArn --output text)

# 検証用のレコードが出るまで、数秒かかる
aws acm describe-certificate --region us-east-1 --certificate-arn "$CERT" \
  --query 'Certificate.DomainValidationOptions[].ResourceRecord'

CLI はリクエストするだけで、その場で戻ります。検証が終わるのを待つなら次を使います。

aws acm wait certificate-validated --region us-east-1 --certificate-arn "$CERT"

③ CloudFront に別名と証明書を設定する#

CloudFront の設定の更新は、設定全体を取得して、書き換えて、丸ごと送り返す形です。更新には ETag が要ります。

ID=<ディストリビューション ID>
aws cloudfront get-distribution-config --id "$ID" > cfg.json
ETAG=$(jq -r .ETag cfg.json)

jq --arg d "blog.example.com" --arg c "$CERT" '
  .DistributionConfig
  | .Aliases = {Quantity: 1, Items: [$d]}
  | .ViewerCertificate = {
      ACMCertificateArn: $c,
      SSLSupportMethod: "sni-only",
      MinimumProtocolVersion: "TLSv1.2_2021",
      CloudFrontDefaultCertificate: false
    }' cfg.json > new.json

aws cloudfront update-distribution --id "$ID" \
  --if-match "$ETAG" --distribution-config file://new.json

反映の状況は、ステータスが Deployed になるまで見ます。

aws cloudfront wait distribution-deployed --id "$ID"

方法 B の特徴#

  • 手順が短く、1 つのアカウントで素早く試せる。
  • 設定がコードに残らない。作り直しや引き継ぎで再現しにくい。
  • ディストリビューションを CloudFormation で作っている場合は、CLI で変えると差分(ドリフト)が出ます。次にスタックを更新したとき、CLI で足した別名と証明書が消えます。CloudFormation で作ったものは、CloudFormation で変えます。

どちらを選ぶか#

A: CloudFormationB: AWS CLI
向く場面継続して運用する・作り直す可能性がある試す・一時的・既に CLI で作っている
記録コードに残る残らない(手順書が要る)
証明書のリージョンus-east-1 に別スタックus-east-1 にそのまま作る
後始末スタックの削除証明書と設定を個別に戻す

DNS に登録するレコード#

ドメインを管理しているのは別のアカウントなので、こちらからは登録できません。次の 2 つを、順番に依頼します。どちらの方法でも同じです。

1 件目: 証明書の検証用(CNAME)#

項目値
タイプCNAME
名前_<ランダム>.blog.example.com.
値_<ランダム>.<ランダム>.acm-validations.aws.

登録すると、証明書が ISSUED になります。 このレコードは、証明書を自動更新するたびに ACM が参照するので、消さずに残します。

同じアカウントで同じドメインの証明書は、作り直しても検証用の CNAME の値が同じです。CLI で作った証明書を CloudFormation で作り直したときも、依頼したレコードをそのまま使えました。

2 件目: ドメインの向き先(CNAME)#

項目値
タイプCNAME
名前blog.example.com
値<ディストリビューション>.cloudfront.net

値の https:// や末尾の / は付けません。値は、次で確認できます。

aws cloudfront get-distribution --id "$ID" --query 'Distribution.DomainName' --output text

登録の順番#

2 件目の CNAME は、CloudFront に別名と証明書を設定した後に登録します。

順番起きること
設定してから登録(正しい)登録した時点で、すぐ HTTPS で表示できる
登録してから設定CloudFront が「この名前の別名は無い」と判断してアクセスを拒否する(403)。設定が終われば直る

前の「つながりの仕組み」の ④ にあたります。CloudFront は、アクセスされたドメイン名が別名に入っているかを見ます。入っていない名前は、DNS が正しくても受け付けません。

A レコードや AAAA レコードではだめなのか#

結論から書くと、CNAME にしました。A レコードと AAAA レコードは試していません。 理由は、固定の IP アドレスを書く A / AAAA レコードでは、CloudFront の向き先を正しく指定できないからです。

固定の IP アドレスを書けない#

A レコードは「ドメイン名 → IPv4 アドレス」、AAAA レコードは「ドメイン名 → IPv6 アドレス」の対応です。 一方、CloudFront には自分のディストリビューション専用の IP アドレスがありません。

  • 世界中のエッジロケーションが多数の IP アドレスを共有していて、どこに繋ぐかは、問い合わせた場所や混み具合で DNS が動的に返します。
  • 返される IP アドレスは、予告なく変わります。
  • AWS は CloudFront 全体の IP の範囲(ip-ranges.json)を公開していますが、これは「CloudFront が使いうる全部の範囲」で、ディストリビューションに割り当てられた値ではありません。

ある時点で返った IP アドレスを A / AAAA レコードに手で書くと、エッジが入れ替わったときに繋がらなくなったり、遠いエッジに固定されたりします。運用できません。 (専用 IP を使える有料の機能もありますが、通常の用途には過剰です。)

AAAA レコードを別に書く必要もない#

IPv6 を有効にしたディストリビューションは、CNAME の向き先(xxxx.cloudfront.net)の側で、CloudFront が A と AAAA の両方を返します。 CNAME を 1 件登録するだけで、クライアントは IPv4 でも IPv6 でも繋がります。実際に名前を引くと、IPv6 のアドレスも返ります。

getent hosts blog.example.com
# 2600:9000:...  xxxx.cloudfront.net blog.example.com

「A レコード」にできるケース: Route 53 のエイリアス#

DNS が Route 53 なら、エイリアスレコードという選択肢があります。 タイプは A(と AAAA)ですが、値は IP アドレスではなく「CloudFront のディストリビューション」を指定します。Route 53 が、その時点の正しい IP アドレスを返します。

方式使える DNS特徴
CNAMEどの DNS サービスでも可汎用。サブドメイン専用(ゾーンの頂点には置けない)
エイリアス(A / AAAA)Route 53 のみゾーンの頂点(example.com そのもの)にも置ける。AAAA も別に作る。CloudFront へのエイリアスの問い合わせは無料

サブドメイン(blog.example.com)なら、CNAME で足ります。依頼する側も、DNS の種類を気にせず登録できます。 ゾーンの頂点(example.com そのもの)で公開したい場合は、CNAME が置けない(頂点には他の種類のレコードも必要なため)ので、Route 53 のエイリアスを使います。

確認#

DNS が反映されたら、HTTPS で表示できることを確認します。

curl -sI https://blog.example.com/ | head -3
# HTTP/2 200

後始末と注意#

  • 検証用の CNAME(1 件目)は、削除しないでください。消すと、証明書の自動更新が失敗します。
  • 独自ドメインをやめるときは、CloudFront の別名と証明書を外してから、証明書を削除します。CloudFront が使用中の証明書は削除できません。順番を逆にすると、エラーになります。
  • 証明書のスタック(us-east-1)を別に作った場合は、CloudFront 側のスタックを更新してから、証明書のスタックを削除します。

まとめ#

項目内容
結ぶものDNS のレコード、ACM 証明書、CloudFront の別名を、同じドメイン名でそろえる
証明書の場所us-east-1(CloudFront の要件)
方法 A(CloudFormation)証明書は別スタック。ARN はパラメータで渡す。コードに残る
方法 B(AWS CLI)request-certificate と update-distribution。手軽だが、設定が残らず、CloudFormation 管理のものとは併用しない
DNS への依頼CNAME を 2 件。検証用が先、向き先は CloudFront の設定後
A / AAAA試していない。CloudFront に固定 IP が無いため、IP を書く方式は使えない。Route 53 のエイリアスなら A / AAAA になる