ちょっと話題の記事

CloudFormation入門

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

よく訓練されたアップル信者、都元です。CloudFormationカコイイ…。ということでもう一本いきます。

CloudFormationとは

要するに「S3バケット作って、VPC作って、サブネットはこんな感じでー、インスタンスここにつくってー、で、出来上がったらWebサーバのURL教えてくれる?」っていうような、AWSを利用したシステム全体のレシピを処理してくれるサービスです。無料です。(但し、生成したサーバ等の費用はお客様がご負担ください)

このようなレシピのことを、CloudFormation用語で「テンプレート」と言います。そして、CloudFormationではEC2インスタンスやSecurityGroup、VPC、EIPをはじめ、LoadBalancer、AutoScalingGroup、IAMユーザに至るまで、様々なものを作成できます。これらの要素を「リソース」と呼びます。テンプレートを使って作られる一連のリソースのまとまりのことを「スタック」と呼びます。

つまり、テンプレートというのは基本的にリソースの定義(組み立て方)を列挙したもの、ということになります。

テンプレートの基本「リソース」

ここで小さいテンプレートの例を見てみましょう。

{
  "AWSTemplateFormatVersion": "2010-09-09",
  "Resources": {
    "SSHGroup" : {
      "Type" : "AWS::EC2::SecurityGroup",
      "Properties" : {
        "GroupDescription" : "Enable SSH access via port 22",
        "SecurityGroupIngress": [{
          "IpProtocol" : "tcp",
          "CidrIp" : "0.0.0.0/0",
          "FromPort" : "22", "ToPort" : "22"
        }]
      }
    },
    "WebGroup" : {
      "Type" : "AWS::EC2::SecurityGroup",
      "Properties" : {
        "GroupDescription" : "Enable HTTP access via port 80",
        "SecurityGroupIngress": [{
          "IpProtocol" : "tcp",
          "CidrIp" : "0.0.0.0/0",
          "FromPort" : "80", "ToPort" : "80"
        }]
      }
    },
    "WebServer" : {
      "Type" : "AWS::EC2::Instance",
      "Properties" : {
        "ImageId" : "ami-54cf5c3d",
        "InstanceType" : "t1.micro",
        "SecurityGroupIds" : [
          { "Ref" : "SSHGroup" },
          { "Ref" : "WebGroup" }
        ]
      }
    },
    "WebServerEip": {
      "Type": "AWS::EC2::EIP",
      "Properties": {
        "InstanceId": { "Ref": "WebServer" }
      }
    }
  }
}

テンプレートに最低限必要な要素は、AWSTemplateFormatVersion(今の所固定)とResourcesというリソース定義のリストだけです。ここでは以下の4つのリソースを定義しています。ちなみに、作成したSecurityGroupとEIPはWebServerに関連づくように指定してあります。

  • SSHGroup … 22/tcpを全IPに対して解放するSecurityGroup
  • WebGroup … 80/tcpを全IPに対して解放するSecurityGroup
  • WebServer … Webサーバとして使うEC2-Classicインスタンス
  • WebServerEip … WebServer用ElasticIP

SSHGroupWebServerEip等は、自分で命名するリソースの名前です。テンプレートの中で一意である必要があります。悲しきかな、リソースの名前は[a-zA-Z0-9]+しか使えないようです。最初、ハイフンとかアンスコを駆使した名前でテンプレートを作ってしまい、地獄を見ました。使えればいいのになぁ。あと、リソース名は大文字から始めるのが慣例のようです。

リソースの定義順は任意で構いません。CloudFormationが依存関係を計算し、適切な順番でリソースを作成してくれます。逆に言えば、リソースの作成順序を制御することはできません。

ランタイムの設定ができる「パラメータ」

さて、上で示したテンプレートでスタックを作ったとしても、WebServerにSSHアクセスはできません。鍵の指定をしていませんから…。しかし、テンプレートは汎用的であることに価値があるので、テンプレートの中に鍵名をベタ書きしてしまうのも残念な感じです。という時に使うのがパラメータです。JSONを以下のように変えてみましょう。

{
  "AWSTemplateFormatVersion": "2010-09-09",
  "Parameters" : {
    "KeyName" : {
      "Description" : "Name of an existing EC2 KeyPair to enable SSH access to the web server",
      "Type" : "String"
    }
  },
  "Resources": {
(略)
    "WebServer" : {
      "Type" : "AWS::EC2::Instance",
      "Properties" : {
        "ImageId" : "ami-54cf5c3d",
        "InstanceType" : "t1.micro",
        "KeyName" : { "Ref" : "KeyName" },
        "SecurityGroupIds" : [
          { "Ref" : "SSHGroup" },
          { "Ref" : "WebGroup" }
        ]
      }
    },
(略)
  }
}

Resourcesと並んでParametersというものを定義しています。ここに定義したパラメータは、スタック作成時に動的に指定し、各リソースのパラメータに利用することができます。Refという関数を用いて、パラメータの値を参照できるようになっています。

cfn-parameter

もう一つのランタイム設定「マッピング」

よし、鍵指定に関して汎用的に作れたぞ、というところで思い出してください。AMIというのはリージョン毎にIDが異なります。WebServerに指定したami-54cf5c3dはus-east-1のAMIです。つまりこのテンプレートは現状us-east-1でしか上手くスタックを作り上げられません。ここを何とかしたい。ただ、ユーザにAMIのIDを入力させるとか、ないわー。

という時に使えるのがマッピングです。

{
  "AWSTemplateFormatVersion": "2010-09-09",
  "Parameters" : {
    (略)
  },
  "Mappings" : {
    "RegionMap" : {
      "us-east-1" : {"AMI" : "ami-54cf5c3d"},
      "us-west-1" : {"AMI" : "ami-b63210f3"},
      "us-west-2" : {"AMI" : "ami-8e27adbe"},
      "eu-west-1" : {"AMI" : "ami-3c5f5748"},
      "ap-southeast-1" : {"AMI" : "ami-ba7538e8"},
      "ap-southeast-2" : {"AMI" : "ami-b6df4e8c"},
      "ap-northeast-1" : {"AMI" : "ami-5d7dfa5c"},
      "sa-east-1" : {"AMI" : "ami-89c81394"}
    }
  },
  "Resources": {
    (略)
        "ImageId" : { "Fn::FindInMap" : [ "RegionMap", { "Ref" : "AWS::Region" }, "AMI" ]},
    (略)
  }
}

挙動は想像つきますね。AWS::Regionは疑似パラメータと呼び、CloudFormationが自動的に設定するパラメータです。ここに、CloudFormationが実行されたリージョン名が入っているので、そのリージョンに対応するAMIのIDを引き当てる、というのがFn::FindInMap関数の役割です。

スタックを作成した結果分かったことを教えて欲しい「アウトプット」

スタックを生成してみないと分からない「結果」というものがあります。例えば、Webサーバに割り当てられたIPアドレス値や、場合によってはWebサーバの立ち上がったAZや、SecurityGroupのID等も知りたいことがあります。

まぁ、CloudFormationで生成されるリソース群は、(タグに色々メタデータが付いているものの)普通のリソースなので、個別のページを見て回ったり、APIを叩けば情報は手に入るのですが、欲しい結果をまとめて表示してくれると便利ですね。

そこで、テンプレートに以下のような記述を追加します。

{
  "AWSTemplateFormatVersion": "2010-09-09",
  "Parameters" : {
    (略)
  },
  "Mappings" : {
    (略)
  },
  "Resources" : {
    (略)
  },
  "Outputs" : {
    "WebServerIpAddress" : {
      "Value" : { "Ref" : "WebServerEip" },
      "Description" : "IP Address of WebServer"
    },
    "WebServerAvailabilityZone" : {
      "Value" : { "Fn::GetAtt" : [ "WebServer", "AvailabilityZone" ]},
      "Description" : "AvailabilityZone of WebServer"
    },
    "SSHToWebServer" : {
      "Value" : { "Fn::Join" :["", [
        "ssh -i /path/to/", { "Ref" : "KeyName" }, ".pem",
        " ec2-user@", { "Ref" : "WebServerEip" }
      ]] }, 
      "Description" : "SSH command to connect WebServer"
    }
  }
}

WebServerIpAddressには、WebServerEipのIPアドレスを出力します。WebServerAvailabilityZoneには、WebServerAvailabilityZone属性(Fn::GetAtt関数によって取得)を出力します。SSHToWebServerには、SSH接続するためのコマンド例を出力します。コマンド例はFn::Join関数によって複数の文字列を連結しながら組み立てています。

その結果、スタックが完成した後、CloudFormationのOutputsというタブで、この情報が確認できるようになります。

cfn-outputs

そういや、このサーバってWebサーバですよね

httpdインストールしましょうか?

    "WebServer" : {
      "Type" : "AWS::EC2::Instance",
      "Properties" : {
        "ImageId" : { "Fn::FindInMap" : [ "RegionMap", { "Ref" : "AWS::Region" }, "AMI" ]},
        "InstanceType" : "t1.micro",
        "KeyName" : { "Ref" : "KeyName" },
        "SecurityGroupIds" : [
          { "Ref" : "SSHGroup" },
          { "Ref" : "WebGroup" }
        ],
        "UserData" : { "Fn::Base64" : { "Fn::Join" : ["", [
          "#!/bin/bash\n",
          "yum -y update\n",
          "yum -y install httpd\n",
          "chkconfig httpd on\n",
          "service httpd start\n"
        ]]}}
      }
    },

このように、User Dataも当然のように指定できます。User Dataの指定方法"UserData" : { "Fn::Base64" : { "Fn::Join" : ["", []]}}というのはイディオムなので覚えておきましょう。

cfn-initという仕組みを使えば、もう少しスマートに色々できますが、それはまた別のお話。

参考

以下のページに、アマゾン謹製のCloudFormationテンプレートが転がっています。自分でテンプレートを書く際には、色々参考になると思います。

AWS CloudFormation サンプルテンプレート - アジアパシフィック(東京)リージョン