【CloudFormation Tips】全リージョン・全アカウント対応型テンプレート

この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。

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

CloudFormationテンプレートの理想 *1は、同じテンプレートを使って、あらゆるリージョン・あらゆるAWSアカウントで同じ構成が展開できることです。しかし、何も考えずにテンプレートを作ってしまうと、特定のリージョンでしか動かない、特定のAWSアカウントでしか動かないテンプレートが出来てしまいがちです。

AMIの参照方法

例えば、AMIというのは特定のリージョン内に存在するエンティティです。なので、テンプレートの中に固定でAMI IDを記述してしまうと、そのAMIがあるリージョンでしか使えないテンプレートとなってしまいます。また、公開状態ではないプライベートなAMIを使った場合、そのAMIが参照できるAWSアカウントでしか使えないテンプレートになってしまいます。

そこで、Amazon LinuxのAMI(2014.03版)であれば、このようなMappingを用意して、テンプレート内部からは下記のような参照を記述することによって、リージョン依存を防ぐことができます。

  "Mappings": {
    "AWSAmazonLinuxAMI": {
      "us-east-1":      { "201403": "ami-2f726546" },
      "us-west-2":      { "201403": "ami-b8f69f88" },
      "us-west-1":      { "201403": "ami-84f1cfc1" },
      "eu-west-1":      { "201403": "ami-a921dfde" },
      "ap-southeast-1": { "201403": "ami-787c2c2a" },
      "ap-southeast-2": { "201403": "ami-0bc85031" },
      "ap-northeast-1": { "201403": "ami-a1bec3a0" },
      "sa-east-1":      { "201403": "ami-89de7c94" }
    }
  }
{ "Fn::FindInMap": [ "AWSAmazonLinuxAMI", { "Ref": "AWS::Region" }, "201403" ]}

あと、身も蓋もないんですが、プライベートなAMIはなるべく使わないようにします。Amazon Linuxスタートで、UserDataやcfn-initを使って、自己組織化に勤しみますw

Availability Zoneの参照

次に、Multi-AZの構成を組む際に、2つのAZを指定しなければならない場合があります。例えばVPC構成の場合は、2つのサブネットを複数のAZに渡って展開したいことがしばしばあります。それを実現するために、従来私は下記のようなMappingを使っていました。("従来" ということは、もっと良い解決策があるんですが、それは後ほど)

    "AZ": {
      "us-east-1":      { "primary": "us-east-1d",      "secondary": "us-east-1a" },
      "us-west-2":      { "primary": "us-west-2a",      "secondary": "us-west-2b" },
      "us-west-1":      { "primary": "us-west-1a",      "secondary": "us-west-1b" },
      "eu-west-1":      { "primary": "eu-west-1a",      "secondary": "eu-west-1b" },
      "ap-southeast-1": { "primary": "ap-southeast-1a", "secondary": "ap-southeast-1b" },
      "ap-southeast-2": { "primary": "ap-southeast-2a", "secondary": "ap-southeast-2b" },
      "ap-northeast-1": { "primary": "ap-northeast-1a", "secondary": "ap-northeast-1c" },
      "sa-east-1":      { "primary": "sa-east-1a",      "secondary": "sa-east-1b" }
    },

このMappingを下記のように参照すれば、Multi-AZのサブネットが出来上がります。

    "Subnet1": {
      "Type": "AWS::EC2::Subnet",
      "Properties": {
        "VpcId": { "Ref": "VPC" },
        "AvailabilityZone": { "Fn::FindInMap": [ "AZ", { "Ref": "AWS::Region" }, "primary" ]},
        "CidrBlock": "10.0.0.0/24"
      }
    },
    "Subnet2": {
      "Type": "AWS::EC2::Subnet",
      "Properties": {
        "VpcId": { "Ref": "VPC" },
        "AvailabilityZone": { "Fn::FindInMap": [ "AZ", { "Ref": "AWS::Region" }, "secondary" ]},
        "CidrBlock": "10.0.1.0/24"
      }
    },

しかしこの方法には問題点があります。例えば、東京リージョンには、ap-northeast-1a, 1b, 1cという3つのAZがあります。しかし、この中の1つ(多くの場合は1b)は新規リソースの割り当てが停止しているようで、新しいサブネットが定義できないことが知られています。ここでは便宜的にこのようなAZを「無効AZ」と呼びましょう。逆に1aと1cは「有効AZ」と呼ぶことにします。

そして、東京にある3つのAZは、アカウントによって実体が異なる、ということも一部で知られています。つまり、AWSアカウントAと、AWSアカウントBがあったとして、それぞれのap-northeast-1aは同じAZを表しているとは限らない *2のです。というわけで、1bが無効AZであることが多いようですが、1bが無効だとは限らないんですね。1aが無効AZであるAWSアカウントも確認されています。

という状況下で、前述のようなMappingを利用してしまうと、"1aが無効AZであるAWSアカウント" では使えないテンプレートになってしまいます。

これを避けるための手法を最近思いつたでのご紹介します。

    "Subnet1": {
      "Type": "AWS::EC2::Subnet",
      "Properties" : {
        "VpcId": { "Ref": "VPC" },
        "AvailabilityZone" : { "Fn::Select" : [ "0", { "Fn::GetAZs" : { "Ref" : "AWS::Region" }}]},
        "CidrBlock": "10.0.0.0/24"
      }
    },
    "FrontendSubnet2": {
      "Type": "AWS::EC2::Subnet",
      "Properties": {
        "VpcId": { "Ref": "VPC" },
        "AvailabilityZone" : { "Fn::Select" : [ "1", { "Fn::GetAZs" : { "Ref" : "AWS::Region" }}]},
        "CidrBlock": "10.0.1.0/24"
      }
    },

Fn::GetAZsは、私は勝手に各リージョンにおける「全AZのリスト」を返すものだと思い込んでいましたが、実は「有効AZのリスト」を返すようです。そこで、上記のような記述によって、複数の有効AZを取得できる、というわけです。

一応検証のために、私の持っているアカウントにおいて全リージョンでそれぞれ、下記のテンプレートのoutputを確認してみました。

{
  "AWSTemplateFormatVersion": "2010-09-09",
  "Resources": {
    "DummyWaitHandle" : {
      "Type" : "AWS::CloudFormation::WaitConditionHandle"
    }
  },
  "Outputs": {
    "AvailabilityZones": {
      "Value": { "Fn::Join" : [ ",", { "Fn::GetAZs" : { "Ref" : "AWS::Region" }}]}
    }
  }
}

その結果は下記です。

"eu-west-1a,eu-west-1b,eu-west-1c"
"sa-east-1a,sa-east-1b"
"us-east-1a,us-east-1c,us-east-1d"
"ap-northeast-1a,ap-northeast-1c"
"us-west-2a,us-west-2b,us-west-2c"
"us-west-1a,us-west-1b"
"ap-southeast-1a,ap-southeast-1b"
"ap-southeast-2a,ap-southeast-2b"

各リージョンにおいて、最低2つのAZが返ってきていますね。

まとめ

というわけで、上記のTipsを意識して、再利用可能なテンプレートを量産していきましょう。

脚注

  1. あくまでも理想ですので、状況によってスタンスは崩します。
  2. スポットインスタンスの価格推移を見比べると確認できるようです。