cloudformation-dsl 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,37 @@
1
+ # -*- encoding: utf-8 -*-
2
+
3
+ # Copyright 2013-2014 Bazaarvoice, Inc.
4
+ #
5
+ # Licensed under the Apache License, Version 2.0 (the "License");
6
+ # you may not use this file except in compliance with the License.
7
+ # You may obtain a copy of the License at
8
+ #
9
+ # http://www.apache.org/licenses/LICENSE-2.0
10
+ #
11
+ # Unless required by applicable law or agreed to in writing, software
12
+ # distributed under the License is distributed on an "AS IS" BASIS,
13
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ # See the License for the specific language governing permissions and
15
+ # limitations under the License.
16
+
17
+ lib = File.expand_path('../lib', __FILE__)
18
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
19
+ require 'cloudformation-dsl/version'
20
+
21
+ Gem::Specification.new do |gem|
22
+ gem.name = "cloudformation-dsl"
23
+ gem.version = CloudFormationDSL::VERSION
24
+ gem.authors = ["Shawn Smith", "Dave Barcelo", "Morgan Fletcher", "Csongor Gyuricza", "Igor Polishchuk", "Nathaniel Eliot", "Jona Fenocchi", "Tony Cui", 'Ho-Sheng Hsiao']
25
+ gem.email = ["Shawn.Smith@bazaarvoice.com", "Dave.Barcelo@bazaarvoice.com", "Morgan.Fletcher@bazaarvoice.com", "Csongor.Gyuricza@bazaarvoice.com", "Igor.Polishchuk@bazaarvoice.com", "Nathaniel.Eliot@bazaarvoice.com", "Jona.Fenocchi@bazaarvoice.com", "Tony.Cui@bazaarvoice.com", 'talktohosh@gmail.com']
26
+ gem.description = %q{Ruby DSL library that provides a wrapper around the CloudFormation.}
27
+ gem.summary = %q{Ruby DSL library that provides a wrapper around the CloudFormation. Written by [Bazaarvoice](http://www.bazaarvoice.com).}
28
+ gem.homepage = "http://github.com/hosh/cloudformation-dsl-rb"
29
+
30
+ gem.files = `git ls-files`.split($/)
31
+ gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
32
+ gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
33
+ gem.require_paths = %w{lib bin}
34
+
35
+ gem.add_runtime_dependency 'json'
36
+ gem.add_runtime_dependency 'bundler'
37
+ end
@@ -0,0 +1,21 @@
1
+ # Contributing
2
+
3
+ Thanks for your interest in contributing! Here are some things you should know.
4
+
5
+ ## Getting started
6
+
7
+ To get started:
8
+
9
+ - fork this project on github
10
+ - create a new branch named after the change you want to make; i.e., `git checkout -b mynewfeature`
11
+ - make your changes and commit them
12
+ - send a pull request to this project from your fork/branch
13
+
14
+ Once you've sent your pull request, one of the project collaborators will review it and provide feedback. Please accept this commentary as constructive! It is intended as such.
15
+
16
+ ## git
17
+
18
+ We're opinionated about git. Don't be surprised if we ask you to update your pull request to meet the following standards.
19
+
20
+ - rebase+squash your branch into a single commit. For clean git history, we'd prefer we merged in just 1 commit that contains the entire set of changes.
21
+ - don't write commit messages longer than 50 characters. See [How to Write a Git Commit Message](http://chris.beams.io/posts/git-commit/) for some examples of how to achieve this, and why.
data/docs/Releasing.md ADDED
@@ -0,0 +1,20 @@
1
+ # Releasing
2
+
3
+ ## Performing releases
4
+
5
+ 0. Merge the desired commits to master. But merge them cleanly! See: [merging](#merging)
6
+ 1. Edit and commit the version file in `lib/cloudformation-ruby-dsl/version.rb`. Bump the version based on the [version specification](#versioning-specification)
7
+ 2. `git push` to origin/master
8
+ 3. `rake release`
9
+
10
+ ## Versioning specification
11
+
12
+ For this project, we will follow the methodology proposed by http://semver.org/spec/v2.0.0.html.
13
+
14
+ 1. Major versions break existing interfaces.
15
+ 2. Minor versions are additive only.
16
+ 3. Patch versions are for backward-compatible bug fixes.
17
+
18
+ ## Merging
19
+
20
+ When you use the shiny green "Merge" button on a pull request, github creates a separate commit for the merge (because of the use of the `--no-ff` option). This is noisy and makes git history confusing. Instead of using the green merge button, merge the branch into master using [git-land](https://github.com/bazaarvoice/git-land#git-land) (or manually follow the steps described in the project).
@@ -0,0 +1,204 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ # Copyright 2013-2014 Bazaarvoice, Inc.
4
+ #
5
+ # Licensed under the Apache License, Version 2.0 (the "License");
6
+ # you may not use this file except in compliance with the License.
7
+ # You may obtain a copy of the License at
8
+ #
9
+ # http://www.apache.org/licenses/LICENSE-2.0
10
+ #
11
+ # Unless required by applicable law or agreed to in writing, software
12
+ # distributed under the License is distributed on an "AS IS" BASIS,
13
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ # See the License for the specific language governing permissions and
15
+ # limitations under the License.
16
+
17
+ require 'bundler/setup'
18
+ require 'cloudformation-ruby-dsl/cfntemplate'
19
+ require 'cloudformation-ruby-dsl/table'
20
+
21
+ # Note: this is only intended to demonstrate the cloudformation-ruby-dsl. It compiles
22
+ # and validates correctly, but won't produce a viable CloudFormation stack.
23
+
24
+ template do
25
+
26
+ parameter 'Label',
27
+ :Description => 'The label to apply to the servers.',
28
+ :Type => 'String',
29
+ :MinLength => '2',
30
+ :MaxLength => '25',
31
+ :AllowedPattern => '[_a-zA-Z0-9]*',
32
+ :ConstraintDescription => 'Maximum length of the Label parameter may not exceed 25 characters and may only contain letters, numbers and underscores.',
33
+ # The :Immutable attribute is a Ruby CFN extension. It affects the behavior of the '<template> update ...'
34
+ # operation in that a stack update may not change the values of parameters marked w/:Immutable => true.
35
+ :Immutable => true
36
+
37
+ parameter 'InstanceType',
38
+ :Description => 'EC2 instance type',
39
+ :Type => 'String',
40
+ :Default => 'm2.xlarge',
41
+ :AllowedValues => %w(t1.micro m1.small m1.medium m1.large m1.xlarge m2.xlarge m2.2xlarge m2.4xlarge c1.medium c1.xlarge),
42
+ :ConstraintDescription => 'Must be a valid EC2 instance type.'
43
+
44
+ parameter 'ImageId',
45
+ :Description => 'EC2 Image ID',
46
+ :Type => 'String',
47
+ :Default => 'ami-255bbc4c',
48
+ :AllowedPattern => 'ami-[a-f0-9]{8}',
49
+ :ConstraintDescription => 'Must be ami-XXXXXXXX (where X is a hexadecimal digit)'
50
+
51
+ parameter 'KeyPairName',
52
+ :Description => 'Name of KeyPair to use.',
53
+ :Type => 'String',
54
+ :MinLength => '1',
55
+ :MaxLength => '40',
56
+ :Default => parameters['Label']
57
+
58
+ parameter 'EmailAddress',
59
+ :Type => 'String',
60
+ :Description => 'Email address at which to send notification events.'
61
+
62
+ mapping 'InlineExampleMap',
63
+ :team1 => {
64
+ :name => 'test1',
65
+ :email => 'test1@example.com',
66
+ },
67
+ :team2 => {
68
+ :name => 'test2',
69
+ :email => 'test2@example.com',
70
+ }
71
+
72
+ # Generates mappings from external files with various formats.
73
+ mapping 'JsonExampleMap', 'maps/map.json'
74
+
75
+ mapping 'RubyExampleMap', 'maps/map.rb'
76
+
77
+ mapping 'YamlExampleMap', 'maps/map.yaml'
78
+
79
+ # Loads JSON mappings dynamically from example directory.
80
+ Dir.entries('maps/more_maps').each_with_index do |path, index|
81
+ next if path == "." or path == ".."
82
+ mapping "ExampleMap#{index - 1}", "maps/more_maps/#{path}"
83
+ end
84
+
85
+ # Selects all rows in the table which match the name/value pairs of the predicate object and returns a
86
+ # set of nested maps, where the key for the map at level n is the key at index n in the specified keys,
87
+ # except for the last key in the specified keys which is used to determine the value of the leaf-level map.
88
+ text = Table.load 'maps/table.txt'
89
+ mapping 'TableExampleMap',
90
+ text.get_map({ :column0 => 'foo' }, :column1, :column2, :column3)
91
+
92
+ # Shows how to create a table useful for looking up subnets that correspond to a particular env/region for eg. vpc placement.
93
+ vpc = Table.load 'maps/vpc.txt'
94
+ mapping 'TableExampleMultimap',
95
+ vpc.get_multimap({ :visibility => 'private', :zone => ['a', 'c'] }, :env, :region, :subnet)
96
+
97
+ # The tag type is a DSL extension; it is not a property of actual CloudFormation templates.
98
+ # These tags are excised from the template and used to generate a series of --tag arguments which are passed to CloudFormation when a stack is created.
99
+ # They do not ultimately appear in the expanded CloudFormation template.
100
+ # The diff subcommand will compare tags with the running stack and identify any changes, but a stack update will do the diff and throw an error on any
101
+ # changes. The tags are propagated to all resources created by the stack, including the stack itself.
102
+ #
103
+ # Amazon has set the following restrictions on CloudFormation tags:
104
+ # => limit 10
105
+ # => immutable (you may not update a stack with new tags or different values for existing tags -- they will be rejected)
106
+ #
107
+ tag :MyTag => 'MyValue'
108
+ tag :MyOtherTag => 'My Value With Spaces'
109
+
110
+ resource 'SecurityGroup', :Type => 'AWS::EC2::SecurityGroup', :Properties => {
111
+ :GroupDescription => 'Lets any vpc traffic in.',
112
+ :SecurityGroupIngress => {:IpProtocol => '-1', :FromPort => '0', :ToPort => '65535', :CidrIp => "10.0.0.0/8"}
113
+ }
114
+
115
+ resource "ASG", :Type => 'AWS::AutoScaling::AutoScalingGroup', :Properties => {
116
+ :AvailabilityZones => 'us-east-1',
117
+ :HealthCheckType => 'EC2',
118
+ :LaunchConfigurationName => ref('LaunchConfig'),
119
+ :MinSize => 1,
120
+ :MaxSize => 5,
121
+ :NotificationConfiguration => {
122
+ :TopicARN => ref('EmailSNSTopic'),
123
+ :NotificationTypes => %w(autoscaling:EC2_INSTANCE_LAUNCH autoscaling:EC2_INSTANCE_LAUNCH_ERROR autoscaling:EC2_INSTANCE_TERMINATE autoscaling:EC2_INSTANCE_TERMINATE_ERROR),
124
+ },
125
+ :Tags => [
126
+ {
127
+ :Key => 'Name',
128
+ # Grabs a value in an external map file.
129
+ :Value => find_in_map('TableExampleMap', 'corge', 'grault'),
130
+ :PropagateAtLaunch => 'true',
131
+ },
132
+ {
133
+ :Key => 'Label',
134
+ :Value => parameters['Label'],
135
+ :PropagateAtLaunch => 'true',
136
+ }
137
+ ],
138
+ }
139
+
140
+ resource 'EmailSNSTopic', :Type => 'AWS::SNS::Topic', :Properties => {
141
+ :Subscription => [
142
+ {
143
+ :Endpoint => ref('EmailAddress'),
144
+ :Protocol => 'email',
145
+ },
146
+ ],
147
+ }
148
+
149
+ resource 'WaitConditionHandle', :Type => 'AWS::CloudFormation::WaitConditionHandle', :Properties => {}
150
+
151
+ resource 'WaitCondition', :Type => 'AWS::CloudFormation::WaitCondition', :DependsOn => 'ASG', :Properties => {
152
+ :Handle => ref('WaitConditionHandle'),
153
+ :Timeout => 1200,
154
+ :Count => "1"
155
+ }
156
+
157
+ resource 'LaunchConfig', :Type => 'AWS::AutoScaling::LaunchConfiguration', :Properties => {
158
+ :ImageId => parameters['ImageId'],
159
+ :KeyName => ref('KeyPairName'),
160
+ :IamInstanceProfile => ref('InstanceProfile'),
161
+ :InstanceType => ref('InstanceType'),
162
+ :InstanceMonitoring => 'false',
163
+ :SecurityGroups => [ref('SecurityGroup')],
164
+ :BlockDeviceMappings => [
165
+ {:DeviceName => '/dev/sdb', :VirtualName => 'ephemeral0'},
166
+ {:DeviceName => '/dev/sdc', :VirtualName => 'ephemeral1'},
167
+ {:DeviceName => '/dev/sdd', :VirtualName => 'ephemeral2'},
168
+ {:DeviceName => '/dev/sde', :VirtualName => 'ephemeral3'},
169
+ ],
170
+ # Loads an external userdata script with an interpolated argument.
171
+ :UserData => base64(interpolate(file('userdata.sh'), time: Time.now)),
172
+ }
173
+
174
+ resource 'InstanceProfile', :Type => 'AWS::IAM::InstanceProfile', :Properties => {
175
+ # use cfn intrinsic conditional to choose the 2nd value because the expression evaluates to false
176
+ :Path => fn_if(equal(3, 0), '/unselected/', '/'),
177
+ :Roles => [ ref('InstanceRole') ],
178
+ }
179
+
180
+ resource 'InstanceRole', :Type => 'AWS::IAM::Role', :Properties => {
181
+ :AssumeRolePolicyDocument => {
182
+ :Statement => [
183
+ {
184
+ :Effect => 'Allow',
185
+ :Principal => { :Service => [ 'ec2.amazonaws.com' ] },
186
+ :Action => [ 'sts:AssumeRole' ],
187
+ },
188
+ ],
189
+ },
190
+ :Path => '/',
191
+ }
192
+
193
+ # add conditions that can be used elsewhere in the template
194
+ condition 'myCondition', fn_and(equal("one", "two"), not_equal("three", "four"))
195
+
196
+ output 'EmailSNSTopicARN',
197
+ :Value => ref('EmailSNSTopic'),
198
+ :Description => 'ARN of SNS Topic used to send emails on events.'
199
+
200
+ output 'MappingLookup',
201
+ :Value => find_in_map('TableExampleMap', 'corge', 'grault'),
202
+ :Description => 'An example map lookup.'
203
+
204
+ end.exec!
@@ -0,0 +1,9 @@
1
+ {
2
+ "Mappings": {
3
+ "JsonExampleMap": {
4
+ "foo": "bar"
5
+ }
6
+ }
7
+
8
+ }
9
+
@@ -0,0 +1,5 @@
1
+ {
2
+ 'Mappings' => {
3
+ 'RubyExampleMap' => { 'baz' => 'blah' }
4
+ }
5
+ }
@@ -0,0 +1,5 @@
1
+ ---
2
+ Mappings:
3
+ YamlExampleMap:
4
+ foo:
5
+ bar: yo
@@ -0,0 +1,8 @@
1
+ {
2
+ "Mappings": {
3
+ "ExampleMap1": {
4
+ "gregory": "smith"
5
+ }
6
+ }
7
+
8
+ }
@@ -0,0 +1,8 @@
1
+ {
2
+ "Mappings": {
3
+ "ExampleMap2": {
4
+ "smith": "john"
5
+ }
6
+ }
7
+
8
+ }
@@ -0,0 +1,8 @@
1
+ {
2
+ "Mappings": {
3
+ "ExampleMap3": {
4
+ "john": "gregory"
5
+ }
6
+ }
7
+
8
+ }
@@ -0,0 +1,5 @@
1
+ column0 column1 column2 column3 column4
2
+ foo baz qux quux
3
+ foo corge grault garply
4
+ bar waldo fred
5
+ bar plugh xyzzy thud
@@ -0,0 +1,25 @@
1
+ env region zone visibility subnet
2
+ qa us-east-1 a public subnet-aaaa
3
+ qa us-east-1 a private subnet-bbbb
4
+ qa us-east-1 b public subnet-cccc
5
+ qa us-east-1 b private subnet-dddd
6
+ qa us-east-1 c public subnet-eeee
7
+ qa us-east-1 c private subnet-ffff
8
+ qa eu-west-1 a public subnet-gggg
9
+ qa eu-west-1 a private subnet-hhhh
10
+ qa eu-west-1 b public subnet-iiii
11
+ qa eu-west-1 b private subnet-jjjj
12
+ qa eu-west-1 c public subnet-kkkk
13
+ qa eu-west-1 c private subnet-llll
14
+ prod us-east-1 a public subnet-mmmm
15
+ prod us-east-1 a private subnet-nnnn
16
+ prod us-east-1 b public subnet-oooo
17
+ prod us-east-1 b private subnet-pppp
18
+ prod us-east-1 c public subnet-qqqq
19
+ prod us-east-1 c private subnet-rrrr
20
+ prod eu-west-1 a public subnet-ssss
21
+ prod eu-west-1 a private subnet-tttt
22
+ prod eu-west-1 b public subnet-uuuu
23
+ prod eu-west-1 b private subnet-vvvv
24
+ prod eu-west-1 c public subnet-wwww
25
+ prod eu-west-1 c private subnet-xxxx
@@ -0,0 +1,4 @@
1
+ #!/usr/bin/env bash
2
+ echo "put initialization script here"
3
+ echo "the time is {{ locals[:time] }}"
4
+ echo "the aws region is {{ ref('AWS::Region') }}"
@@ -0,0 +1,5 @@
1
+ Several people were instrumental in building this project during its incubation stage. Because the process of open sourcing involved squashing the repository to remove confidential information, some of their contributions have been lost from the history that Github displays.
2
+
3
+ - [Shawn Smith](https://github.com/shawnsmith): initial implementation
4
+ - [Nathaniel Eliot](https://github.com/temujin9): converted from raw library to gem, refactored table code, cleaned and prepared code for open sourcing
5
+ - [Jona Fenocchi](http://github.com/jonaf): added support for CloudFormation tags
@@ -0,0 +1,14 @@
1
+ require "cloudformation-dsl/version"
2
+
3
+ module CloudFormationDSL
4
+ autoload :Template, 'cloudformation-dsl/template'
5
+ autoload :Helpers, 'cloudformation-dsl/helpers'
6
+
7
+ def self.describe(&block)
8
+ CloudFormationDSL::Template.new(&block)
9
+ end
10
+
11
+ def self.load_from(filename)
12
+ CloudFormationDSL::Template.new(filename)
13
+ end
14
+ end
@@ -0,0 +1,118 @@
1
+ module CloudFormationDSL
2
+ module Helpers
3
+ def load_from_file(filename)
4
+ file = File.open(filename)
5
+ # Figure out what the file extension is and process accordingly.
6
+ case File.extname(filename)
7
+ when ".rb"; eval(file.read, nil, filename)
8
+ when ".json"; JSON.load(file)
9
+ when ".yaml"; YAML::load(file)
10
+ else
11
+ raise("Do not recognize extension of #{filename}.")
12
+ end
13
+ ensure
14
+ file.close
15
+ end
16
+
17
+ def find_in_map(map, key, name)
18
+ # Eagerly evaluate mappings when all keys are known at template expansion time
19
+ if map.is_a?(String) && key.is_a?(String) && name.is_a?(String)
20
+ # We don't know whether the map was built with string keys or symbol keys. Try both.
21
+ def get(map, key) map[key] || map.fetch(key.to_sym) end
22
+ get(get(@dict.fetch(:Mappings).fetch(map), key), name)
23
+ else
24
+ { :'Fn::FindInMap' => [ map, key, name ] }
25
+ end
26
+ end
27
+
28
+ # Formation helpers
29
+
30
+ def base64(value) { :'Fn::Base64' => value } end
31
+
32
+ def find_in_map(map, key, name) { :'Fn::FindInMap' => [ map, key, name ] } end
33
+
34
+ def get_att(resource, attribute) { :'Fn::GetAtt' => [ resource, attribute ] } end
35
+
36
+ def get_azs(region = '') { :'Fn::GetAZs' => region } end
37
+
38
+ def join(delim, *list)
39
+ case list.length
40
+ when 0 then ''
41
+ when 1 then list[0]
42
+ else join_list(delim,list)
43
+ end
44
+ end
45
+
46
+ # Variant of join that matches the native CFN syntax.
47
+ def join_list(delim, list) { :'Fn::Join' => [ delim, list ] } end
48
+
49
+ def equal(one, two) { :'Fn::Equals' => [one, two] } end
50
+
51
+ def fn_not(condition) { :'Fn::Not' => [condition] } end
52
+
53
+ def fn_or(*condition_list)
54
+ case condition_list.length
55
+ when 0..1 then raise "fn_or needs at least 2 items."
56
+ when 2..10 then { :'Fn::Or' => condition_list }
57
+ else raise "fn_or needs a list of 2-10 items that evaluate to true/false."
58
+ end
59
+ end
60
+
61
+ def fn_and(*condition_list)
62
+ case condition_list.length
63
+ when 0..1 then raise "fn_and needs at least 2 items."
64
+ when 2..10 then { :'Fn::And' => condition_list }
65
+ else raise "fn_and needs a list of 2-10 items that evaluate to true/false."
66
+ end
67
+ end
68
+
69
+ def fn_if(cond, if_true, if_false) { :'Fn::If' => [cond, if_true, if_false] } end
70
+
71
+ def not_equal(one, two) fn_not(equal(one,two)) end
72
+
73
+ def select(index, list) { :'Fn::Select' => [ index, list ] } end
74
+
75
+ def ref(name) { :Ref => name } end
76
+
77
+ def aws_account_id() ref("AWS::AccountId") end
78
+
79
+ def aws_notification_arns() ref("AWS::NotificationARNs") end
80
+
81
+ def aws_no_value() ref("AWS::NoValue") end
82
+
83
+ def aws_stack_id() ref("AWS::StackId") end
84
+
85
+ def aws_stack_name() ref("AWS::StackName") end
86
+
87
+ # deprecated, for backward compatibility
88
+ def no_value()
89
+ warn_deprecated('no_value()', 'aws_no_value()')
90
+ aws_no_value()
91
+ end
92
+
93
+ # Read the specified file and return its value as a string literal
94
+ def file(filename) File.read(File.absolute_path(filename, File.dirname($PROGRAM_NAME))) end
95
+
96
+ # Interpolates a string like "NAME={{ref('Service')}}" and returns a CloudFormation "Fn::Join"
97
+ # operation to collect the results. Anything between {{ and }} is interpreted as a Ruby expression
98
+ # and eval'd. This is especially useful with Ruby "here" documents.
99
+ # Local variables may also be exposed to the string via the `locals` hash.
100
+ def interpolate(string, locals={})
101
+ list = []
102
+ while string.length > 0
103
+ head, match, string = string.partition(/\{\{.*?\}\}/)
104
+ list << head if head.length > 0
105
+ list << eval(match[2..-3], nil, 'interpolated string') if match.length > 0
106
+ end
107
+
108
+ # Split out strings in an array by newline, for visibility
109
+ list = list.flat_map {|value| value.is_a?(String) ? value.lines.to_a : value }
110
+ join('', *list)
111
+ end
112
+
113
+ def join_interpolate(delim, string)
114
+ $stderr.puts "join_interpolate(delim,string) has been deprecated; use interpolate(string) instead"
115
+ interpolate(string)
116
+ end
117
+ end
118
+ end