CloudFormationのtemplateをRubyで作成 – 「SparkleFormation」を触ってみた。
こんにちは、せーのです。個人的には2015年に入ってから4つめのCloudFormationネタとなります。どうしたんでしょう。 今日はRubyのDSLでCloudFormationのtemplateが書けるrubyist垂涎のツールをご紹介します。名前は「SparkleFormation」といいます。
SparkleFormation - github | document
CloudFormation templateに対する根本的な不満
やっぱりなんと言っても特徴は「CloudFormationのtemplateをRubyのコードで書ける」という部分にあります。逆に言うと「CloudFormationのtemplateをRubyのコードで書けるだけのツール」とも言えます。CloudFormationのtemplateというのは本来はJSONで書かれています。JSONはRubyに比べれば決して難しくない言語です。Rubyを覚える1/50くらいの学習コストといえるでしょう。先日ご紹介した「knife-cloudformation」もknifeでCloudFormationを動かす「だけ」のツールといえるかと思います。ではなぜわざわざRubyで書くのでしょうか。なぜわざわざknifeで動かすのでしょうか。 それは皆がCloudFormationのtemplateに対して思っているうっすらとした不満の現れかと思います。SparkleFormationの作者はなぜJSONだけで書かないのか、という問いに対して
逆にみんな、本当に本心からこのテンプレートを全てJSONで書きたいなんて思ってるのか?
という回答をしています。なんでしょう、胸が抉られるようです。 CloudFormationは構成がコード化出来て便利だから、今流行りのInfrastructure as a codeだから、色々な言語に対して汎用的でテキストベースだから扱いやすいから、jqとかですぐに加工できるから、だからこれでいいんだ。JSONが正しい姿なんだ。使いづらくない、めんどくさくなんか無い! そう思い込もうとしていた私の心にグッサリ刺さりました。もう無理しなくていいんだよ。泣いてもいいんだよ。心にダムはあるのかい。
特徴 = Rubyに出来てJSONに出来ないこと全て。
ということでこのツールの強みとしては「JSONで表現出来ない書き方が出来る」というところにあります。 CloudFormationのtemplateは地味にコメントが使えません(正確にはDescriptionアトリビュートに書けば使えると言えば使えるのですが所謂プログラムコードに書く「コメント」が書けません)。これは地味にイタいです。何の目的でこういうものを作るのか、どうしてこういうセキュリティグループなのか、という説明が書けなかったのです。でもSparkleFormationは書けます。だってRubyだもの。 他にもループ処理が書けたりよくあるパターンをまとめてComponent化して使いまわす等CloudFormationをしばらく使っていると「ああ、これが出来ればなあ」というような機能が詰まっています。例えるならCloudFormationのフレームワーク、と言った感じでしょうか。 またrubyを使っているということで色々な連携が使えます。上に書かれているknife-cloudforamtionのテンプレートとしても使えるため
$ knife cloudformation test --file templates/sparkleformationtest.rb --defaults
みたいな書き方ができます。他にもRedis, RabbitMQ, Sensu Enterprise, Uchiwa等との連携、またChefとの連携にも使えます。
中の人も注目している?
皆さんは「AWS Pop-Up Loft」という名前を聞いたことがあるでしょうか。これはサンフランシスコにあるAWSを使ったスタートアップ等があつまるコミュニティースペースです。こちらには軽食も完備されていて、みんなリラックスした雰囲気で色々な情報交換や勉強会が行われています。 この「SparkleFormation」は先日の1月8日になんとこのAWS Pop-Up Loftにて勉強会が行われており、AWSテクニカルチームも注目のツールになっています。日本ではまだ知らない人も多いこのツールをいち早くマスターすると、後々AWS系で一歩先を行けること間違いなし、ですね!
インストール
では早速インストールしてみましょう。githubからダウンロードしてgem、もしくはgemのBundlerでインストールします。私はgemそのままで入れてみました。
Tsuyoshis-Air:dev Tsuyoshi$ git clone https://github.com/sparkleformation/sparkle_formation.git sparkleformation Cloning into 'sparkleformation'... remote: Counting objects: 1714, done. remote: Compressing objects: 100% (12/12), done. remote: Total 1714 (delta 2), reused 0 (delta 0) Receiving objects: 100% (1714/1714), 1.14 MiB | 557.00 KiB/s, done. Resolving deltas: 100% (1078/1078), done. Checking connectivity... done. Tsuyoshis-Air:dev Tsuyoshi$ cd sparkleformation/ Tsuyoshis-Air:sparkleformation Tsuyoshi$ ls -l total 80 -rw-r--r-- 1 Tsuyoshi staff 898 1 12 03:08 CHANGELOG.md -rw-r--r-- 1 Tsuyoshi staff 558 1 12 03:08 CONTRIBUTING.md -rw-r--r-- 1 Tsuyoshi staff 97 1 12 03:08 Gemfile -rw-r--r-- 1 Tsuyoshi staff 10850 1 12 03:08 LICENSE -rw-r--r-- 1 Tsuyoshi staff 8433 1 12 03:08 README.md drwxr-xr-x 5 Tsuyoshi staff 170 1 12 03:08 bin drwxr-xr-x 11 Tsuyoshi staff 374 1 12 03:08 docs drwxr-xr-x 4 Tsuyoshi staff 136 1 12 03:08 examples drwxr-xr-x 4 Tsuyoshi staff 136 1 12 03:08 lib -rw-r--r-- 1 Tsuyoshi staff 706 1 12 03:08 sparkle_formation.gemspec drwxr-xr-x 4 Tsuyoshi staff 136 1 12 03:08 test Tsuyoshis-Air:sparkleformation Tsuyoshi$ gem install sparkle_formation Fetching: sparkle_formation-0.2.6.gem (100%) ERROR: While executing gem ... (Gem::FilePermissionError) You don't have write permissions for the /Library/Ruby/Gems/2.0.0 directory. Tsuyoshis-Air:sparkleformation Tsuyoshi$ sudo gem install sparkle_formation Password: Successfully installed sparkle_formation-0.2.6 Parsing documentation for sparkle_formation-0.2.6 1 gem installed Tsuyoshis-Air:sparkleformation Tsuyoshi$
動かしてみる
早速サンプルを動かしてみましょう。サンプルとしてセットされている「ec2_example.rb」を見てみましょう。
SparkleFormation.new('ec2_example') do description "AWS CloudFormation Sample Template EC2InstanceSample..." parameters do key_name do description 'Name of an existing EC2 KeyPair to enable SSH access to the instance' type 'String' end end mappings.region_map do _set('us-east-1', :ami => 'ami-7f418316') _set('us-east-1', :ami => 'ami-7f418316') _set('us-west-1', :ami => 'ami-951945d0') _set('us-west-2', :ami => 'ami-16fd7026') _set('eu-west-1', :ami => 'ami-24506250') _set('sa-east-1', :ami => 'ami-3e3be423') _set('ap-southeast-1', :ami => 'ami-74dda626') _set('ap-northeast-1', :ami => 'ami-dcfa4edd') end resources do my_instance do type 'AWS::EC2::Instance' properties do key_name _cf_ref(:key_name) image_id _cf_map(:region_map, 'AWS::Region', :ami) user_data _cf_base64('80') end end end outputs do instance_id do description 'InstanceId of the newly created EC2 instance' value _cf_ref(:my_instance) end az do description 'Availability Zone of the newly created EC2 instance' value _cf_attr(:my_instance, :availability_zone) end public_ip do description 'Public IP address of the newly created EC2 instance' value _cf_attr(:my_instance, :public_ip) end private_ip do description 'Private IP address of the newly created EC2 instance' value _cf_attr(:my_instance, :private_ip) end public_dns do description 'Public DNSName of the newly created EC2 instance' value _cf_attr(:my_instance, :public_dns_name) end private_dns do description 'Private DNSName of the newly created EC2 instance' value _cf_attr(:my_instance, :private_dns_name) end end end
Rubyを書いたことがある方ならなんとなく書こうとしている意図がわかると思います。ParameterとしてSSH鍵の名前、MappingとしてAMIのID、ResourceとしてEC2インスタンスが一つありOUTPUTが色々指定されている、という感じですね。では実行してみましょう。コンパイルしてJSON Templateを吐くスクリプトを書いて流します。
require 'sparkle_formation' require 'json' puts JSON.pretty_generate( SparkleFormation.compile('ec2_example.rb') )
Tsuyoshis-Air:sparkleformation Tsuyoshi$ cd examples/allinone/cloudformation/ Tsuyoshis-Air:cloudformation Tsuyoshi$ chmod 700 test.rb Tsuyoshis-Air:cloudformation Tsuyoshi$ ruby test.rb { "Description": "AWS CloudFormation Sample Template EC2InstanceSample...", "Parameters": { "KeyName": { "Description": "Name of an existing EC2 KeyPair to enable SSH access to the instance", "Type": "String" } }, "Mappings": { "RegionMap": { "Us-east-1": { "Ami": "ami-7f418316" }, "Us-west-1": { "Ami": "ami-951945d0" }, "Us-west-2": { "Ami": "ami-16fd7026" }, "Eu-west-1": { "Ami": "ami-24506250" }, "Sa-east-1": { "Ami": "ami-3e3be423" }, "Ap-southeast-1": { "Ami": "ami-74dda626" }, "Ap-northeast-1": { "Ami": "ami-dcfa4edd" } } }, "Resources": { "MyInstance": { "Type": "AWS::EC2::Instance", "Properties": { "KeyName": { "Ref": "KeyName" }, "ImageId": { "Fn::FindInMap": [ "RegionMap", { "Ref": "AWS::Region" }, "Ami" ] }, "UserData": { "Fn::Base64": "80" } } } }, "Outputs": { "InstanceId": { "Description": "InstanceId of the newly created EC2 instance", "Value": { "Ref": "MyInstance" } }, "Az": { "Description": "Availability Zone of the newly created EC2 instance", "Value": { "Fn::GetAtt": [ "MyInstance", "AvailabilityZone" ] } }, "PublicIp": { "Description": "Public IP address of the newly created EC2 instance", "Value": { "Fn::GetAtt": [ "MyInstance", "PublicIp" ] } }, "PrivateIp": { "Description": "Private IP address of the newly created EC2 instance", "Value": { "Fn::GetAtt": [ "MyInstance", "PrivateIp" ] } }, "PublicDns": { "Description": "Public DNSName of the newly created EC2 instance", "Value": { "Fn::GetAtt": [ "MyInstance", "PublicDnsName" ] } }, "PrivateDns": { "Description": "Private DNSName of the newly created EC2 instance", "Value": { "Fn::GetAtt": [ "MyInstance", "PrivateDnsName" ] } } } } Tsuyoshis-Air:cloudformation Tsuyoshi$
綺麗にJSONが出てきました。
実践的なサンプルを作ってみる
もう少し実践的なサンプルを動かしてみましょう。こちらもドキュメントに載っていたサンプルです。
SparkleFormation.new('website') do set!('AWSTemplateFormatVersion', '2010-09-09') description 'Supercool Website' resources.cfn_user do type 'AWS::IAM::User' properties.path '/' properties.policies _array( -> { policy_name 'cfn_access' policy_document.statement _array( -> { effect 'Allow' action 'cloudformation:DescribeStackResource' resource '*' } ) } ) end resources.cfn_keys do type 'AWS::IAM::AccessKey' properties.user_name ref!(:cfn_user) end parameters.web_nodes do type 'Number' description 'Number of web nodes for ASG.' default 2 end resources.website_autoscale do type 'AWS::AutoScaling::AutoScalingGroup' properties do availability_zones({'Fn::GetAZs' => ''}) launch_configuration_name ref!(:website_launch_config) min_size ref!(:web_nodes) max_size ref!(:web_nodes) end end resources.website_launch_config do type 'AWS::AutoScaling::LaunchConfiguration' properties do image_id 'ami-123456' instance_type 'm3.medium' end end resources.website_elb do type 'AWS::ElasticLoadBalancing::LoadBalancer' properties do availability_zones._set('Fn::GetAZs', '') listeners _array( -> { load_balancer_port '80' protocol 'HTTP' instance_port '80' instance_protocol 'HTTP' } ) health_check do target 'HTTP:80/' healthy_threshold '3' unhealthy_threshold '3' interval '5' timeout '15' end end end end
ELBに80ポートでEC2を2つAuto Scalingで並べるサンプルですね。先程より少し複雑になりました。このサンプルをコンパイルするスクリプトはこちらになります。
require 'sparkle_formation' require 'json' puts JSON.pretty_generate( SparkleFormation.compile('website.rb') )
Tsuyoshis-Air:examples Tsuyoshi$ chmod 700 test2.rb Tsuyoshis-Air:examples Tsuyoshi$ ruby test2.rb { "AWSTemplateFormatVersion": "2010-09-09", "Description": "Supercool Website", "Resources": { "CfnUser": { "Type": "AWS::IAM::User", "Properties": { "Path": "/", "Policies": [ { "PolicyName": "cfn_access", "PolicyDocument": { "Statement": [ { "Effect": "Allow", "Action": "cloudformation:DescribeStackResource", "Resource": "*" } ] } } ] } }, "CfnKeys": { "Type": "AWS::IAM::AccessKey", "Properties": { "UserName": { "Ref": "CfnUser" } } }, "WebsiteAutoscale": { "Type": "AWS::AutoScaling::AutoScalingGroup", "Properties": { "AvailabilityZones": { "Fn::GetAZs": "" }, "LaunchConfigurationName": { "Ref": "WebsiteLaunchConfig" }, "MinSize": { "Ref": "WebNodes" }, "MaxSize": { "Ref": "WebNodes" } } }, "WebsiteLaunchConfig": { "Type": "AWS::AutoScaling::LaunchConfiguration", "Properties": { "ImageId": "ami-123456", "InstanceType": "m3.medium" } }, "WebsiteElb": { "Type": "AWS::ElasticLoadBalancing::LoadBalancer", "Properties": { "AvailabilityZones": { "Fn::GetAZs": "" }, "Listeners": [ { "LoadBalancerPort": "80", "Protocol": "HTTP", "InstancePort": "80", "InstanceProtocol": "HTTP" } ], "HealthCheck": { "Target": "HTTP:80/", "HealthyThreshold": "3", "UnhealthyThreshold": "3", "Interval": "5", "Timeout": "15" } } } }, "Parameters": { "WebNodes": { "Type": "Number", "Description": "Number of web nodes for ASG.", "Default": 2 } } } Tsuyoshis-Air:examples Tsuyoshi$
こちらを更に環境の環境の初期定義分として使いまわす目的のbase, パラメータ値を定義するcomponents, 引数を持たせて1テンプレート内で複数回使えるように設定するdynamics、と役割別にファイルを分けると
SparkleFormation.build do set!('AWSTemplateFormatVersion', '2010-09-09') resources.cfn_user do type 'AWS::IAM::User' properties.path '/' properties.policies _array( -> { policy_name 'cfn_access' policy_document.statement _array( -> { effect 'Allow' action 'cloudformation:DescribeStackResource' resource '*' } ) } ) end resources.cfn_keys do type 'AWS::IAM::AccessKey' properties.user_name ref!(:cfn_user) end end
SparkleFormation.dynamic(:elb) do |_name, _config={}| resources("#{_name}_elb".to_sym) do type 'AWS::ElasticLoadBalancing::LoadBalancer' properties do availability_zones._set('Fn::GetAZs', '') listeners _array( -> { load_balancer_port _config[:load_balancer_port] || '80' protocol _config[:protocol] || 'HTTP' instance_port _config[:instance_port] || '80' instance_protocol _config[:instance_protocol] || 'HTTP' } ) health_check do target _config[:target] || 'HTTP:80/' healthy_threshold _config[:healthy_threshold] || '3' unhealthy_threshold _config[:unhealthy_threshold] || '3' interval _config[:interval] || '5' timeout _config[:timeout] || '15' end end end end
これらをコンパイルするにはこのようなテンプレートを書きます。
SparkleFormation.new(:website).load(:base).overrides do description 'Supercool Website' parameters.web_nodes do type 'Number' description 'Number of web nodes for ASG.' default 2 end resources.website_autoscale do type 'AWS::AutoScaling::AutoScalingGroup' properties do availability_zones({'Fn::GetAZs' => ''}) launch_configuration_name ref!(:website_launch_config) min_size ref!(:web_nodes) max_size ref!(:web_nodes) end end resources.website_launch_config do type 'AWS::AutoScaling::LaunchConfiguration' properties do image_id 'ami-123456' instance_type 'm3.medium' end end dynamic!(:elb, 'website') end
なかなかわかりにくくなってきましたね。baseの部分には使い回ししやすいIAM Userについてまとめてあり、dynamicsはELBが入っています。こちらにEC2系を入れてしまうのも手かと思います。オブジェクト指向プログラミングが身についている方であればbaseがabstractな感じに見えるかと思います。そう見えれば何を入れたら良いかも大体わかってきますね。
まとめ
いかがでしたでしょうか。上でご紹介したのはこのライブラリのほんのさわりの部分です。例えばこのテンプレートをknife-cloudformationで流しやすいようにまとめてあるsparkle starter kitというツールがあったり、octets = ENV['VPC_SUBNET].split('.').slice(0,2).join('.')">みたいな書き方をすることで既存のVPCに対してサブネットを追加する、等この世界はまだまだ奥が深そうです。まずはドキュメントを読んでみてください。
参考サイト
- https://speakerdeck.com/portertech/sparkleformation-build-infrastructure-with-cloudformation-and-keep-your-sanity
- https://www.youtube.com/watch?v=JnNWn3BoAcM&t=2h40m50s
- https://github.com/hw-labs/sparkleformation-starter-kit
- http://awsadvent.tumblr.com/post/105009176470/aws-advent-2014-sparkleformation-build