CloudFormation入門
よく訓練されたアップル信者、都元です。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
SSHGroupやWebServerEip等は、自分で命名するリソースの名前です。テンプレートの中で一意である必要があります。悲しきかな、リソースの名前は[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という関数を用いて、パラメータの値を参照できるようになっています。
もう一つのランタイム設定「マッピング」
よし、鍵指定に関して汎用的に作れたぞ、というところで思い出してください。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には、WebServerのAvailabilityZone属性(Fn::GetAtt関数によって取得)を出力します。SSHToWebServerには、SSH接続するためのコマンド例を出力します。コマンド例はFn::Join関数によって複数の文字列を連結しながら組み立てています。
その結果、スタックが完成した後、CloudFormationの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テンプレートが転がっています。自分でテンプレートを書く際には、色々参考になると思います。