debiancdn
AWS, Content Delivery Network and Debian
VPCのメタパターンもしくは怠惰な私のVPC導入。
2012/12/05
投稿者: : 皆様、昨日の松井さんの男気あふれる記事に度肝を抜かれた荒木でございます。毎度のことながら彼の軌跡は奇跡として語り継がれるといって間違いないでしょう。
さて、VPC環境構築のメタパターンと予告しました。実際のところCloudformationの出番は無限です。その中でも最も強力なのは、ある状態を保存再現する機能です。VPCのように面倒な環境構築に有効です。
時間のない人のために結論を。
- CloudFormationはある時点の状態再現に有効です。
- VPCに関しては再現を自動化するのは諦めて、(時としてつまらない)設計を再現するためにCloudFormationを使いましょう。
- 実はVPCIDを使えばClassicな環境からの移行は簡単!
というわけで、CloudFormationこそがVPC導入のポイントかもしれません。
これから述べる内容は
- ELB
- アプリケーションサーバ
- データベース(RDS)
のどこにでもある三段構えのサービスを題材にしています。
CloudFormationの意義
AWSを使ったシステム構成工数に関しては多くの方が考える問題です。
システムは机上での設計ではなく、試行錯誤を経てサービスを作るべきだとは思います。
実際のシステムでは、構成変更時にはテストが欠かせません。
また、テストの与える影響の測定や変更前への切り戻しを意識したシステム構成を求められることはまちがいありません。
そのために同一のシステムを構成し、部分的な変更を加えた後にテストを行うのが理想的な体制でしょう!
その一方で、この師走のなか、毎回同じことを繰り返す時間はない、というのが本当のところではないでしょうか。
何度でも速やかに構成(クローニング)できるようにして、ある時点でのシステムにもどせれば、どんどんチャレンジすることができるはずです。
そこで役に立つのがCloudFormationです。
テンプレート作成には3つの方法があります。
- テンプレートの文法をAWS CloudFormation User Guideを読み、いちから作成する方法
- AWS CloudFormationサンプルテンプレート(http://aws.amazon.com/jp/cloudformation/aws-cloudformation-templates/) として集められたサンプルを流用する方法
- CloudFormerを使って、既存のAWSリソースからCloudFormationテンプレートを作成する方法
正直、最初の2つの方法は「部分的に変更を加えてきたシステム」において現実的な方法ではないでしょう。そこで3番目のCloudFormerの登場です。
一旦、CloudFormer で作成した AWS CloudFormation テンプレートを使うと、AWS Management Consoleで数回クリックするだけで既存のシステムのコピーを起動可能です。
準備:まずは、CloudFormer自体を用意する。
起動すると、次のような画面があらわれるはずです。
CloudFormerはIAMのアカウントを使用するため、その確認のチェックはさくっと終わらせて前にすすみましょう。
すると、こんな具合で最終的にシステムを作っていいかどうかの確認がはいります。もちろん躊躇せずにContinueしましょう。
無事起動するとStatusがCREATE_COMPLETEに変わります。それまでは、Eventsでもみてニヤニヤしましょう。
CloudFormerの使用
CloudFormerを使用するには、CloudFormerの作成したURLにwebブラウザでアクセスします。そのURLはOutputsタブの中で確認します。
するとこんな画面があらわれます。テンプレート作成対象リージョンを選択しましょう。
あとは一本道です!
含めるドメイン名を確認したり、EC2を確認したり、RDSを確認したり、セキュリティグループを確認したり。。
数個の画面でチェックをつづけると、S3に保存するように促されます。保存されたテンプレートはJSON形式で、その頭のほうが確認できます。
ここまできたら、このテンプレートをつかってたちあげてみましょう。Congratulationsがきたら、即座にLaunch Stackです。
とはいえみなさん時間もないことですし、上がりそうだ!という雰囲気だけつかんでおきましょうか。
とりあえず、作成したテンプレートの置き場だけは確認しておきましょう。このテンプレートがスタート地点になります。
とはいえ、これで完璧か!といわれるとそんなことはありません。
CloudFormerはまりポイント1:AMIは動作時インタンスのAMIである
個人的にCloudFormerのハマリポイントはなんだと問われれば、AMI情報は動作時のインスタンスが起動した時点でのAMIであって、CloudFormerがAMIを作るわけではないところです。
"instancei2d5f6a2d": {
"Type": "AWS::EC2::Instance",
"Properties": {
"AvailabilityZone": "ap-northeast-1b",
"DisableApiTermination": "FALSE",
"ImageId": "ami-c857e5c9",
"InstanceType": "m1.small",
(以下略)
こんなテンプレートがあるときは、このAMIの内容を変更したならば、自分でつくりなおさなければなりません。あえてつくりなおさなければならないときは、マネージメントコンソールでインスタンスから右クリックして作りなおせばよいでしょう。
CloudFormerはまりポイント2:Route53で指定している値は動作時ELBのCNAMEである
こう言われてもピンとこないかもしれません。
"dnscdpshoparakiin": {
"Type": "AWS::Route53::RecordSet",
"Properties": {
"HostedZoneId": "/hostedzone/xxxxxxxxxxxx",
"Name": "xxxxxxx.araki.in.",
"Type": "CNAME",
"TTL": "300",
"Comment": "araki.in ",
"ResourceRecords": [
"xxxxxxx-444735762.ap-northeast-1.elb.amazonaws.com"
]
}
},
この、ResourceRecordsの内容はそのものずばりELBのCNAMEを指定しています。
このままでは、ELBの定義ではなく、ELBのエンドポイントそのものを指定しているため、作成したELBからFn::GetAttを使って値を得る形式に変更します。
例えばこんな具合に。
{ "Fn::GetAtt" : [ "elbeccube", "DNSName" ] }
CloudFormerはまりポイント3:RDSの内容は動作時である
これもELBと同じですが、データが消えるという意味ではもっと問題がおおきいかもしれません。
スナップショットを作成したら、DBSnapshotIdentifierパラメータで指定しましょう。
CloudFormerはまりポイント4:手修正でフォーマットを破壊する
ここまでうまく行っても、人間にはJSONやSGMLやXMLは無理だという説があります!
もちろん慎重にやれとか、ダブルチェックしろとか、トリプルチェックしろ。。N重チェックというループに陥いるのもいとおかしというところですが、そんなくだらないチェックには機械にやらせましょう。
そこで登場するのがcfn-validate-templateコマンドです。
$ cfn-validate-template –template-file テンプレートファイル名
で、テンプレートがValidと言われるまでこころのままに修正しましょう。
今時の人なら、こんなコマンドよりもVisualなアプローチのほうがいいかもしれません。たとえばMacならVisualJSONをつかってみましょう。構造も一発で確認できます。
クローニング実行
それでは準備したCloudformationを実行してみましょう。
クローニングの実行は、CloudFormationのタブ内、Create New Stackボタンを押すところからはじまります。
つづいてStackNameとJSONファイルの在り処を指定します。
後は、CloudFormation Stacksの画面から、Statusの変化と、Eventsの出力適宜Refreshしながら眺めて待ちましょう。
CloudFormationではカバーできない部分のフォローにUserData
CloudFormationによって骨格はほぼ完成したもののたとえば、アプリケーション固有の設定はカバーできません。
ここで登場するのが、UserDataです。
"instanceib3ba90b3": { "Type": "AWS::EC2::Instance", "Properties": { "AvailabilityZone": "ap-northeast-1a", "DisableApiTermination": "FALSE", "ImageId": "ami-5259eb53", "UserData": { "Fn::Base64" : { "Fn::Join" : ["", [ "#!/bin/bash \n", "sed -i \"s/yyyyyy.araki.in/", {"Ref":"SiteURL"}, "/g\" /home/ec2-user/cdp/eccube/data/config/config.php\n", "#EOF" ]]}},
この例では、/home/ec2-user/cdp/eccube/data/config/config.php に含まれる yyyyyy.araki.inをSiteURLというリファレンスに書き換えています。ある意味かなり力技ですが、UserDataの万能感は異常です。
VPCに関しては再現を自動化するのは諦めて、(時としてつまらない)設計を再現するためにCloudFormationを使いましょう。
とまあ、こんなことを書くのは、CloudFormerがVPCに対応していない試行錯誤したシステムの保存にフォーカスしていて、なおかつ、繰り返し単調な設計がつづくネットワーク再現に最適だからなのです。
たとえば、こんなことやってられますか?
1 VPCとして10.1.0.0/16を定義
2 ELB用にPublicサブネットを定義
2.1 ZoneAのELB用subnet 10.1.10.0/24を定義
2.2 ZoneBのELB用 subnet 10.1.11.0/24を定義
3 eccube用にPrivateサブネットを定義
3.1 ZoneAのeccube用subnet 10.1.20.0/24を定義
3.2 ZoneBのeccube用 subnet 10.1.21.0/24を定義
4 RDS用にPrivateサブネットを定義
4.1 ZoneAのRDS用subnet 10.1.30.0/24を定義
4.2 ZoneBのRDS用 subnet 10.1.31.0/24を定義
5 インターネットゲートウェイを定義
5.1 インターネットゲートウェイをVPCに関連づけ
6 2のPublicサブネットの経路表を定義
6.1 Publicサブネットの経路表にデフォルトゲートウェイとしてインターネットゲートウェイを向ける
6.2 ZoneAのELB用subnetに対して経路表を関連づけ
6.3 ZoneBのELB用subnetに対して経路表を関連づけ
7 2のPublicサブネットにNACLを定義
7.1 Inbound用にHTTPポートを許可
7.2 Inbound用に1024から65535のダイナミックポートを許可
7.3 Outbound用にHTTPポートを許可
7.4 Outbound用に1024から65535のダイナミックポートを許可
7.5 ZoneAのELB用subnetに対してNACLを関連づけ
7.6 ZoneBのELB用subnetに対してNACLを関連づけ
8 PrivateサブネットにNACLを定義
8.1 Inbound用にVPC内部からの接続を許可
8.2 Outbound用にVPC内部への接続を許可
8.3 ZoneAのeccube用subnetにNACLを関連づけ
8.4 ZoneBのeccube用subnetにNACLを関連づけ
8.5 ZoneAのRDS用subnetにNACLを関連づけ
8.6 ZoneBのRDS用subnetにNACLを関連づけ
あたまがクラクラしてきませんか? 正直私はこんな指示書を渡されて、マネージメントコンソールから設定しろと言われたらいやになります。正直いくららくらくページを稼げるといわれてもスクリーンショットなんてとってられません。
一方で、こういう繰り返しはCloudFormationならばコピペ一発です。なんという楽でしょう。しかも失敗もへります!
もっとも、コピペの結果はこんなダラダラとしたJSONになります。
VPCの中へのサービス配置
VPC内部のサブネット作成が完了したら、実際のサービス配置をはじめます。
CloudFormationテンプレートの入れ子呼び出し
まず最初のテクニックは、VPCサブネットを作成するために使ったCloudFormationテンプレートの呼び出しです。
呼び出しは、AWS::CloudFormation::StackのPropertiesにTemplateURLとして与えます。
"Resources" : { "vpcMake" : { "Type" : "AWS::CloudFormation::Stack", "Properties" : { "TemplateURL" : "https://s3-ap-northeast-1.amazonaws.com/arakisa /CloudFormation/eccube-vpc-05vpc.json", "TimeoutInMinutes" : "60" } },
このテンプレートでは、呼び出し元となるCloudFormationテンプレートに実行結果の値を渡すために、Outputsを定義しています。
"Outputs":{ "ELBSubnetAId":{ "Value":{"Ref": "ELBSubnetA"}, "Description":"Id of ELBSubnetA" }, "ELBSubnetBId":{ "Value":{"Ref": "ELBSubnetB"}, "Description":"Id of ELBSubnetB" }, "ECSubnetAId":{ "Value":{"Ref": "ECSubnetA"}, "Description":"Id of ECSubnetA" }, "ECSubnetBId":{ "Value":{"Ref": "ECSubnetB"}, "Description":"Id of ECSubnetB" }, "RDSSubnetAId":{ "Value":{"Ref": "RDSSubnetA"}, "Description":"Id of RDSSubnetA" }, "RDSSubnetBId":{ "Value":{"Ref": "RDSSubnetB"}, "Description":"Id of RDSSubnetB" }, "VPCID":{ "Value":{ "Ref" : "VPC" }, "Description":"Id of VPC" } }
たとえばこんな具合です。ネットワークに関連するIdをVPCID, ELBSubnetAId, ELBSubnetBId, ECSubnetAId, ECSubnetBId, RDSSubnetAId, RDSSubentBIdとして定義しています。
呼び出し元で上のVPCIDを使うためには、
{ "Fn::GetAtt" :["vpcMake", "Outputs.VPCID"] }
のように、Fn::GetAttを使っていつでも呼び出すことができます。
呼び出し元テンプレートの修正
親となるCloudFormationテンプレートでは、VPC環境に対応した修正が必要です。
今回、VPC内で使用を定義するために特別なパラメータをあたえなければならないリソースタイプは
- AWS::ElasticLoadBalancing::LoadBalancer
- AWS::EC2::SecurityGroup
- AWS::EC2::Instance
- AWS::RDS::DBSubnetGroup
- AWS::RDS::DBSecurityGroup
という5つです。
AWS::ElasticLoadBalancing::LoadBalancer
ELBはVPCにおいては、配置するサブネットと、セキュリティグループを指定します。
ELBを配置するサブネットの指定は、AWS::ElasticLoadBalancing::LoadBalancerの中で、Subnetsで指定し、セキュリティグループも同様にSecurityGroupsとして指定する。
"elbeccube": { "Type": "AWS::ElasticLoadBalancing::LoadBalancer", "Properties": { "Subnets": [ { "Fn::GetAtt" :["vpcMake", "Outputs.ELBSubnetAId"] }, { "Fn::GetAtt" :["vpcMake", "Outputs.ELBSubnetBId"] } ], "SecurityGroups" : [{"Ref" : "LoadBalancerSecurityGroup"}], ……(略)
AWS::EC2::SecurityGroup
セキュリティグループは、AWS::EC2::SecurityGroupを使って指定します。
VPC内においては、セキュリティグループも、どのVPCに所属しているかをProperties中にVpcIdとして宣言します。
"LoadBalancerSecurityGroup" : { "Type" : "AWS::EC2::SecurityGroup", "Properties" : { "GroupDescription" : "Enable HTTP access on port 80", "VpcId" : { "Fn::GetAtt" :["vpcMake", "Outputs.VPCID"] }, "SecurityGroupIngress" : [ { "IpProtocol" : "tcp", "FromPort" : "80", "ToPort" : "80", "CidrIp" : "0.0.0.0/0" } ], "SecurityGroupEgress" : [ { "IpProtocol" : "tcp", "FromPort" : "80", "ToPort" : "80", "CidrIp" : "0.0.0.0/0" } ]}},
AWS::EC2::Instance
EC2インスタンスもVPC内で起動させるためには、SubnetIdを指定します。
"instanceib3ba90b3": { "Type": "AWS::EC2::Instance", "Properties": { "SubnetId": {"Fn::GetAtt":["vpcMake", "Outputs.ECSubnetAId"]}, …… (略)
AWS::RDS::DBSubnetGroup
RDSが使用するサブネットにはSubnetIdを指定します。
"MyDBSubnetGroup" : { "Type" : "AWS::RDS::DBSubnetGroup", "Properties" : { "DBSubnetGroupDescription" : "Subnets available for the RDS DB Instance", "SubnetIds" : [ { "Fn::GetAtt" :["vpcMake", "Outputs.RDSSubnetAId"] }, { "Fn::GetAtt" :["vpcMake", "Outputs.RDSSubnetBId"] } ]}},
AWS::RDS::DBSecurityGroup
RDSはさらにDBSecurityGroupも定義します。この例では、CIDRブロックを使っています。
"dbsgdefault": { "Type": "AWS::RDS::DBSecurityGroup", "Properties":{ "GroupDescription": "RDS security group in private", "EC2VpcId" : { "Fn::GetAtt" :["vpcMake", "Outputs.VPCID"] }, "DBSecurityGroupIngress": [{ "CIDRIP": "10.1.20.0/23" } ]}}
以上の修正を加えたCloudFormationテンプレートを
https://arakisa.s3.amazonaws.com/CloudFormation/eccube-vpc-06instance.json
に置いておきます!ながーいのでニヤニヤする程度にとどめてくださいませ!
結論
- CloudFormationはある時点の状態再現に有効です。
- VPCに関しては再現を自動化するのは諦めて、(時としてつまらない)設計を再現するためにCloudFormationを使いましょう。
- 実はVPCIDを使えばClassicな環境からの移行は簡単!
というわけで、CloudFormationこそがVPC導入のポイントです。異論は大いにあるでしょうけれども、今あるシステムを移行するほうが新規に設計するよりは難しいとおもいます。そんな手助けに(主に試行錯誤のためですが)なると幸いです。