CloudFormationで作成するリソースにはRoute 53で独自DNS名を付けよ

よく訓練されたアップル信者、都元です。

CloudFormationで環境を定義する際、作成済み *1のRoute 53のHostedZone名をパラメータとして与えて、各要素(EC2インスタンス, ELB, RDS等)に名前付けを行うのは、常套手段です。【AWS】VPC環境構築ノウハウ社内資料 2014年4月版でもご紹介しましたが、VPC内のインスタンスに対するプライベートIPアドレスは制御しないポリシーです。また、パブリックなIPアドレスやDNS名も、そもそも制御できるものではありません。したがって、参照が必要なインスタンスには、Route 53を用いて適宜名前を付けてやると、運用が楽になります。

オブジェクト指向プログラミングに慣れた人に対しては、オブジェクト(AWSから割り当てられたIPアドレスやDNS名)を直接触るんじゃなくて、インターフェイス(独自ドメインのDNS名)を介して触ることによって、コンポーネント間が疎結合になる、といった表現で伝わるかもしれません。

EC2編

例えば、踏み台 (bastion) インスタンスを立てる場合。以下のようにして、EIPに対するAレコードと、(自動付与の)プライベートIPアドレスに対するAレコードを両方つけたりします。まぁ後者は必要なければ省略しても全く問題ありません。こうすることにより、HostedZoneパラメータとしてexample.comを受け取った場合、このインスタンスにはbastion.example.comという名前が付き、この名前で外からアクセスできるようになります。

{
  "AWSTemplateFormatVersion": "2010-09-09",
  "Parameters" : {
    "HostedZone" : {
      "Default" : "",
      "Description" : "The alternative domain name of this distribution.",
      "Type" : "String"
    },
  },
  "Resources": {
    "BastionInstance": {
      "Type": "AWS::EC2::Instance",
      "Properties": {
        ...略...
      }
    },
   "BastionInstanceEIP": {
      "Type": "AWS::EC2::EIP",
      "Properties": {
        "Domain": "vpc",
        "InstanceId": { "Ref" : "BastionInstance" }
      }
    },
    "BastionDNSRecord" : {
      "Type" : "AWS::Route53::RecordSet",
      "Properties" : {
        "HostedZoneName" : { "Fn::Join" : [ "", [ { "Ref" : "HostedZone" }, "." ]]},
        "Comment" : "A record for the Bastion instance.",
        "Name" : { "Fn::Join" : [ "", [ "bastion.", { "Ref" : "HostedZone" }, "." ]]},
        "Type" : "A",
        "TTL" : "300",
        "ResourceRecords" : [
          { "Ref" : "BastionInstanceEIP" }
        ]
      }
    },
    "BastionLocalDNSRecord" : {
      "Type" : "AWS::Route53::RecordSet",
      "Properties" : {
        "HostedZoneName" : { "Fn::Join" : [ "", [ { "Ref" : "HostedZone" }, "." ]]},
        "Comment" : "A record for the private IP address of Bastion instance.",
        "Name" : { "Fn::Join" : [ "", [ "bastion.local.", { "Ref" : "HostedZone" }, "." ]]},
        "Type" : "A",
        "TTL" : "300",
        "ResourceRecords" : [
          { "Fn::GetAtt" : [ "BastionInstance", "PrivateIp" ] }
        ]
      }
    }
  }
}

RDS編

これも何も難しい話ではありません。概ねEC2インスタンスと同じです。最も大きな違いとしては、AレコードではなくCNAMEレコードとしている点です。この定義により、db.local.example.comでRDSに接続できるようになります。

"DatabaseInstance" : {
      "Type" : "AWS::RDS::DBInstance",
      "DeletionPolicy" : "Snapshot",
      "Properties" : {
        ...略...
      }
    },
    "DatabaseDNSRecord" : {
      "Type" : "AWS::Route53::RecordSet",
      "Properties" : {
        "HostedZoneName" : { "Fn::Join" : [ "", [ { "Ref" : "HostedZone" }, "." ]]},
        "Comment" : "CNAME record for the db.",
        "Name" : { "Fn::Join" : [ "", [ "db.local.", { "Ref" : "HostedZone" }, "." ]]},
        "Type" : "CNAME",
        "TTL" : "300",
        "ResourceRecords" : [
          { "Fn::GetAtt" : [ "DatabaseInstance", "Endpoint.Address" ] }
        ]
      }
    },

ELB編

ELBにも名前をつけておきましょう。この定義でelb.example.comでロードバランサにアクセスできます。

これはAレコードでもCNAMEレコードでもなく、ALIASレコードとして定義しているのが特徴です。ALIASレコードについて、詳しくはAmazon Route 53のALIASレコード利用のススメを御覧ください。

"ElasticLoadBalancer" : {
      "Type" : "AWS::ElasticLoadBalancing::LoadBalancer",
      "Properties" : {
        ...略...
      }
    },
    "LoadBalancerDNSRecord" : {
      "Type" : "AWS::Route53::RecordSet",
      "Properties" : {
        "HostedZoneName" : { "Fn::Join" : [ "", [ { "Ref" : "HostedZone" }, "." ]]},
        "Comment" : "ALIAS targeted to LoadBalancer.",
        "Name" : { "Fn::Join" : [ "", [ "elb", { "Ref" : "HostedZone" }, "." ]]},
        "Type" : "A",
        "AliasTarget" : {
          "HostedZoneId" : { "Fn::GetAtt" : [ "ElasticLoadBalancer", "CanonicalHostedZoneNameID" ] },
          "DNSName" : { "Fn::GetAtt" : [ "ElasticLoadBalancer","CanonicalHostedZoneName" ] }
        }
      }
    },

CloudFront編

CloudFrontも、Distributionに対してAWSからDNS名が割り当てられます。これも直接使うのではなく、独自ドメインを挟んでおきましょう。この定義でassets.example.comがCloudFrontに繋がります。

ポイントは2つ。CloudFrontで独自ドメインを使う場合は、AliasesプロパティにDNS名を指定する必要があります(5〜7行目)。また、CloudFrontもALIASレコードに対応しているため、AliasTargetを指定するのですが、cloudfront.netのHosted Zone IDは、現状Z2FDTNDATAQYW2で固定です。ハードコーディングしてしまいましょう。

"AssetsDistribution" : {
      "Type" : "AWS::CloudFront::Distribution",
      "Properties" : {
        "DistributionConfig" : {
          "Aliases" : [
            { "Fn::Join" : [ "", [ "assets.", { "Ref" : "HostedZone" } ]]}
          ],
          ...略...
        }
      }
    },
    "AssetsGlobalDNSRecord" : {
      "Type" : "AWS::Route53::RecordSet",
      "Properties" : {
        "HostedZoneName" : { "Fn::Join" : [ "", [ { "Ref" : "HostedZone" }, "." ]]},
        "Comment" : "ALIAS record for the assets distribution.",
        "Name" : { "Fn::Join" : [ "", ["assets.", { "Ref" : "HostedZone" }, "." ]]},
        "Type" : "A",
        "AliasTarget" : {
          "HostedZoneId" : "Z2FDTNDATAQYW2",
          "DNSName" : { "Fn::GetAtt" : [ "AssetsDistribution", "DomainName" ]}
        }
      }
    },

Elastic Beanstalk 妄想編

さて、豆の木ですよ。Elastic BeanstalkのEnvironmentをCloudFormationに依らず普通に作成すると、その環境には*.elasticbeanstalk.comというDNS名が与えられます。本来は CloudFront のケースと同様に、このDNS名に対するALIASレコードを構成できるのがベストな状況です。しかし残念ながら現状、Route 53及びElastic BeanstalkはこのDNS名のエイリアスに対応していません。(対応お待ちしております!)

また、CloudFormationの "AWS::ElasticBeanstalk::Environment" 型のリソースに対し、{ "Fn::GetAtt" : [ "EBEnvironment", "EndpointURL" ]}とすることで、その環境にアクセスするためのDNS名を得ることができます。ここで取得できるDNS名は、本来はElastic BeanstalkのDNS名が返ってくるのがベストな状況です。しかし残念ながら現状、*.elasticbeanstalk.comではなく、Beanstalkの内部的に作られるELBのDNS名(例: awseb-myst-myen-132MQC4KRLAMD-1371280482.us-east-1.elb.amazonaws.com)となっています。(対応お待ちしております!)

参考ドキュメント

というわけで理想は下記のようなものですが、まぁあくまでも妄想なので動きません。

これは妄想
    "EBEnvironment" : {
      "Type" : "AWS::ElasticBeanstalk::Environment",
      "Properties" : {
        ...略...
      }
    },
これは妄想
    "EBLoadBalancerDNSRecord" : {
      "Type" : "AWS::Route53::RecordSet",
      "Properties" : {
        "HostedZoneName" : { "Fn::Join" : [ "", [ { "Ref" : "HostedZone" }, "." ]]},
        "Comment" : "ALIAS record for EB load balancer.",
        "Name" : { "Fn::Join" : [ "", [ "eb.", { "Ref" : "HostedZone" }, "." ]]},
        "Type" : "A",
        "AliasTarget" : {
          "HostedZoneId" : { "Fn::GetAtt" : [ "EBEnvironment", "HostedZoneId" ]}, ← ここが妄想! "example.elasticbeanstalk.com" が返る妄想!
          "DNSName" : { "Fn::GetAtt" : [ "EBEnvironment", "DNSName" ]} ← ここが妄想! "elasticbeanstalk.com" のHosted Zone IDが返る妄想!
        }
      }
    },
これは妄想

Elastic Beanstalk ワークアラウンド編

ではどうすればいいか。よく訓練されたアップル信者、都元が頑張りました。{ "Fn::GetAtt" : [ "EBEnvironment", "EndpointURL" ]}で取得できるDNS名が、ELBのDNS名であることが幸いでした。

先ほど、「cloudfront.netのHosted Zone IDは、現状Z2FDTNDATAQYW2で固定です」と説明しました。同じように、ap-northeast-1.elb.amazonaws.comのHosted Zone IDは、Z2YN17T5R711GTで固定であることが分かりました。

ただし、CloudFrontはリージョンに対するサービスではないので、グローバルにHosted Zone IDが1つしかありません。一方、ELBはリージョン毎にゾーンが異なるため、それぞれにHosted Zone IDがありました。リージョン毎に違う値を選ぶ場合はMappingsですね! 各リージョンにおけるELBの Hosted Zone ID を調べました!

"Mappings" : {
    "ELBDomain": {
      "us-east-1":      { "HostedZoneId": "Z3DZXE0Q79N41H" },
      "us-west-2":      { "HostedZoneId": "Z33MTJ483KN6FU" },
      "us-west-1":      { "HostedZoneId": "Z1M58G0W56PQJA" },
      "eu-west-1":      { "HostedZoneId": "Z3NF1Z3NOM5OY2" },
      "ap-southeast-1": { "HostedZoneId": "Z1WI8VXHPB1R38" },
      "ap-southeast-2": { "HostedZoneId": "Z2999QAZ9SRTIC" },
      "ap-northeast-1": { "HostedZoneId": "Z2YN17T5R711GT" },
      "sa-east-1":      { "HostedZoneId": "Z2ES78Y61JGQKS" }
    }
  }

このようにMappingsを定義し、以下のように書けば、Elastic Beanstalkが内部的に生成するELBに対するALIASレコードが定義できます。うほほい。

"EBEnvironment" : {
      "Type" : "AWS::ElasticBeanstalk::Environment",
      "Properties" : {
        ...略...
      }
    },
    "EBLoadBalancerDNSRecord" : {
      "Type" : "AWS::Route53::RecordSet",
      "Properties" : {
        "HostedZoneName" : { "Fn::Join" : [ "", [ { "Ref" : "HostedZone" }, "." ]]},
        "Comment" : "ALIAS record for EB load balancer.",
        "Name" : { "Fn::Join" : [ "", [ "eb.", { "Ref" : "HostedZone" }, "." ]]},
        "Type" : "A",
        "AliasTarget" : {
          "HostedZoneId" : { "Fn::FindInMap" : [ "ELBDomain", { "Ref": "AWS::Region" }, "HostedZoneId" ]},
          "DNSName" : { "Fn::GetAtt" : [ "EBEnvironment", "EndpointURL" ]}
        }
      }
    },

というわけで、結構ワークアラウンドな感じですが、綺麗に動いています。一つだけ注意点として、この方法を使ったとしてもElastic BeanstalkのSwap Environment URL機能は使えません。残念orz

まとめ

なんだか無駄に見えるかもしれませんが、とにかく名前は付けておきましょう。独自ドメインで。従って、ある程度まとまったシステムのテンプレートには、パラメータとして必ずHostedZoneがある、と思っておくと良いでしょう。

脚注

  1. そして権威DNSとして上位に登録済み。