この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。
こんにちは、せーのです。個人的には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」を見てみましょう。
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を吐くスクリプトを書いて流します。
test.rb
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が出てきました。
実践的なサンプルを作ってみる
もう少し実践的なサンプルを動かしてみましょう。こちらもドキュメントに載っていたサンプルです。
website.rb
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で並べるサンプルですね。先程より少し複雑になりました。このサンプルをコンパイルするスクリプトはこちらになります。
test2.rb
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、と役割別にファイルを分けると
components/base.rb
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
dynamics/elb.rb
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
これらをコンパイルするにはこのようなテンプレートを書きます。
template/website_new.rb
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