cloudformation_wrapper 0.1.2

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 20f9a4efe8adbabb2c6d7d28b9ea9e43563fd622029fe83e6c0d719060aae5ed
4
+ data.tar.gz: bb5b22a94052890e4f1a634ded06598657c0cbf24923c0578e187595cc4b2f78
5
+ SHA512:
6
+ metadata.gz: 554bd6b6abd082f1395fd03a2d47568c4d2bb70cdb77f2fc19ea6e49563c6eb2b59dfda4d993bda5a0213287971f0bb6880d84ac61e6251eb452d164697a9a68
7
+ data.tar.gz: 2ada52243c7cb5d439f140a6fc6f87876547230ad4122209e3ec9cf653c57ea0f63f47af62c3866687a8459a7d5ccd29c3b613a1ea1a0aaaf48f7e196df2855d
@@ -0,0 +1,227 @@
1
+ module CloudFormationWrapper
2
+ # Stack Manager Class
3
+ # Class containing static convenience methods for deploying and managing CloudFormation Stacks.
4
+ # @since 1.0
5
+ class StackManager
6
+ def self.deploy(options)
7
+ unless options[:client]
8
+ access_key_id = options[:access_key_id] || ENV['AWS_ACCESS_KEY_ID'] || ENV['ACCESS_KEY'] ||
9
+ raise(ArgumentError, 'Cannot find AWS Access Key ID.')
10
+
11
+ secret_access_key = options[:secret_access_key] || ENV['AWS_SECRET_ACCESS_KEY'] || ENV['SECRET_KEY'] ||
12
+ raise(ArgumentError, 'Cannot find AWS Secret Key.')
13
+
14
+ credentials = Aws::Credentials.new(access_key_id, secret_access_key)
15
+ end
16
+
17
+ region = options[:region] || ENV['AWS_REGION'] || ENV['AMAZON_REGION'] || ENV['AWS_DEFAULT_REGION'] ||
18
+ raise(ArgumentError, 'Cannot find AWS Region.')
19
+
20
+ verified_options = verify_options(options)
21
+
22
+ cf_client = verified_options[:client] || Aws::CloudFormation::Client.new(credentials: credentials, region: region)
23
+
24
+ deploy_stack(
25
+ verified_options[:parameters],
26
+ verified_options[:name],
27
+ verified_options[:template_path],
28
+ verified_options[:wait_for_stack],
29
+ cf_client
30
+ )
31
+ end
32
+
33
+ private
34
+
35
+ def verify_options(options)
36
+ defaults = {
37
+ description: 'Deployed with CloudFormation Wrapper.', parameters: {}, wait_for_stack: true
38
+ }
39
+
40
+ options_with_defaults = options.reverse_merge(defaults)
41
+
42
+ unless options_with_defaults[:template_path] && (options_with_defaults[:template_path].is_a? String)
43
+ raise ArgumentError, 'template_path must be provided (String)'
44
+ end
45
+
46
+ unless options_with_defaults[:parameters] && (options_with_defaults[:parameters].is_a? Hash)
47
+ raise ArgumentError, 'parameters must be provided (Hash)'
48
+ end
49
+
50
+ unless options_with_defaults[:client] && (options_with_defaults[:client].is_a? Aws::CloudFormation::Client)
51
+ raise ArgumentError, 'If you\'re providing a client, it must be an Aws::CloudFormation::Client.'
52
+ end
53
+
54
+ return if options_with_defaults[:name] && (options_with_defaults[:name].is_a? String)
55
+ raise ArgumentError, 'name must be provided (String)'
56
+ end
57
+
58
+ def deploy_stack(parameters, stack_name, template_path, cf_client, _wait)
59
+ template_parameters = construct_template_parameters(parameters)
60
+ client_token = ENV.fetch('BUILD_NUMBER', SecureRandom.uuid.delete('-'))
61
+ change_set_type = describe_stack(stack_name, cf_client) ? 'UPDATE' : 'CREATE'
62
+
63
+ create_change_set_params = {
64
+ stack_name: stack_name,
65
+ template_body: File.read(template_path),
66
+ parameters: template_parameters,
67
+ change_set_name: "ChangeSet-#{client_token}",
68
+ client_token: client_token,
69
+ description: ENV.fetch('BUILD_TAG', 'Stack Updates.'),
70
+ change_set_type: change_set_type
71
+ }
72
+
73
+ change_set_id = cf_client.create_change_set(create_change_set_params).id
74
+
75
+ unless wait_for_stack_change_set_creation(change_set_id, cf_client)
76
+ puts "No changes required for #{stack_name}"
77
+ delete_change_set(change_set_id, cf_client)
78
+ return nil
79
+ end
80
+
81
+ list_changes(change_set_id, cf_client)
82
+ time_change_set_executed = Time.now
83
+ execute_change_set(change_set_id, cf_client)
84
+ updated_stack = wait_for_stack_to_complete(stack_name, time_change_set_executed, cf_client)
85
+ if updated_stack.stack_status == 'CREATE_COMPLETE' || updated_stack.stack_status == 'UPDATE_COMPLETE'
86
+ puts "Stack finished updating: #{updated_stack.stack_status}"
87
+ else
88
+ puts "Stack failed to update: #{updated_stack.stack_status} (#{updated_stack.stack_status_reason})"
89
+ return false
90
+ end
91
+ true
92
+ end
93
+
94
+ def construct_template_parameters(parameters)
95
+ template_parameters = []
96
+ parameters.each do |k, v|
97
+ template_parameters.push(
98
+ parameter_key: k.to_s,
99
+ parameter_value: v.to_s
100
+ )
101
+ end
102
+ template_parameters
103
+ end
104
+
105
+ def describe_stack(stack_name, cf_client)
106
+ response = cf_client.describe_stacks(stack_name: stack_name)
107
+ return false if response.stacks.length != 1
108
+ return response.stacks[0]
109
+ rescue Aws::CloudFormation::Errors::ServiceError
110
+ return false
111
+ end
112
+
113
+ def wait_for_stack_change_set_creation(change_set_id, cf_client)
114
+ polling_period = 1 # second
115
+
116
+ puts "Waiting for the Change Set (#{change_set_id}) to be reviewed..."
117
+
118
+ loop do
119
+ sleep(polling_period)
120
+ response = cf_client.describe_change_set(change_set_name: change_set_id)
121
+ if response.status == 'CREATE_COMPLETE'
122
+ puts "Change Set (#{change_set_id}) created."
123
+ return true
124
+ end
125
+ if response.status == 'FAILED'
126
+ puts "Change Set (#{change_set_id}) creation failed: #{response.status_reason}"
127
+ return false
128
+ end
129
+ puts '...'
130
+ end
131
+ end
132
+
133
+ def list_changes(change_set_id, cf_client)
134
+ response = cf_client.describe_change_set(change_set_name: change_set_id)
135
+ puts
136
+ puts 'Stack Set Changes:'
137
+ response.changes.each do |change|
138
+ resource_change = change.resource_change
139
+ puts "\t#{resource_change.action} - " \
140
+ "#{resource_change.logical_resource_id} " \
141
+ "aka #{resource_change.physical_resource_id} " \
142
+ "(#{resource_change.resource_type})"
143
+ puts "\t\tScope: #{resource_change.scope}"
144
+ puts "\t\tReplacment: #{resource_change.replacement}"
145
+ puts "\t\tDetails:"
146
+ resource_change.details.each do |detail|
147
+ puts "\t\t\tTarget: #{detail.target.attribute} - " \
148
+ "#{detail.target.name} - " \
149
+ "recreate:#{detail.target.requires_recreation}"
150
+ puts "\t\t\tCaused By: #{detail.causing_entity}"
151
+ puts "\t\t\tChange Source: #{detail.change_source}"
152
+ end
153
+ end
154
+ puts
155
+ end
156
+
157
+ def execute_change_set(change_set_id, cf_client)
158
+ puts 'Executing Change Set...'
159
+
160
+ client_token = ENV.fetch('BUILD_NUMBER', SecureRandom.uuid.delete('-'))
161
+
162
+ cf_client.execute_change_set(change_set_name: change_set_id, client_request_token: client_token)
163
+ end
164
+
165
+ def wait_for_stack_to_complete(stack_name, minimum_timestamp_for_events, cf_client)
166
+ timestamp_width = 30
167
+ logical_resource_width = 40
168
+ resource_status_width = 40
169
+ polling_period = 3 # seconds
170
+ most_recent_event_id = ''
171
+
172
+ puts
173
+ puts "#{'Timestamp'.ljust(timestamp_width)} " \
174
+ "#{'Logical Resource Id'.ljust(logical_resource_width)} " \
175
+ "#{'Status'.ljust(resource_status_width)} "
176
+
177
+ puts "#{'-'.center(timestamp_width, '-')} " \
178
+ "#{'-'.center(logical_resource_width, '-')} " \
179
+ "#{'-'.center(resource_status_width, '-')}"
180
+
181
+ stack = {}
182
+ loop do
183
+ sleep(polling_period)
184
+ stack = describe_stack(stack_name, cf_client)
185
+ events = get_latest_events(stack_name, minimum_timestamp_for_events, most_recent_event_id, cf_client)
186
+ most_recent_event_id = events[0].event_id unless events.empty?
187
+ events.reverse_each do |event|
188
+ line = "#{event.timestamp.to_s.ljust(timestamp_width)} " \
189
+ "#{event.logical_resource_id.ljust(logical_resource_width)} " \
190
+ "#{event.resource_status.ljust(resource_status_width)} "
191
+ if !event.resource_status.end_with?('IN_PROGRESS') && !event.resource_status_reason.nil?
192
+ line << event.resource_status_reason
193
+ end
194
+ puts line
195
+ end
196
+ break unless stack.stack_status.end_with?('IN_PROGRESS')
197
+ end
198
+ stack
199
+ end
200
+
201
+ def get_latest_events(stack_name, minimum_timestamp_for_events, most_recent_event_id, cf_client)
202
+ no_new_events = false
203
+ response = nil
204
+ events = []
205
+ loop do
206
+ params = {
207
+ stack_name: stack_name
208
+ }
209
+
210
+ params[:next_token] = response.next_token unless response.nil?
211
+
212
+ response = cf_client.describe_stack_events(params)
213
+
214
+ response.stack_events.each do |event|
215
+ if (event.event_id == most_recent_event_id) || (event.timestamp < minimum_timestamp_for_events)
216
+ no_new_events = true
217
+ break
218
+ end
219
+ events << event
220
+ end
221
+
222
+ break if no_new_events || !response.next_token
223
+ end
224
+ events
225
+ end
226
+ end
227
+ end
@@ -0,0 +1,4 @@
1
+ module CloudFormationWrapper
2
+ # @!visibility private
3
+ VERSION = '0.1.2'.freeze
4
+ end
@@ -0,0 +1,9 @@
1
+ require 'cloudformation_wrapper/version'
2
+
3
+ require 'aws-sdk-cloudformation'
4
+ require 'active_support/core_ext/hash'
5
+
6
+ require 'cloudformation_wrapper/stack_manager'
7
+
8
+ STDOUT.sync
9
+ STDERR.sync
metadata ADDED
@@ -0,0 +1,61 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: cloudformation_wrapper
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.2
5
+ platform: ruby
6
+ authors:
7
+ - Ted Armstrong
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2018-01-18 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: aws-sdk-cloudformation
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '3'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '3'
27
+ description: Deploys and Manages AWS CloudFormation.
28
+ email:
29
+ - theodorecarmstrong@gmail.com
30
+ executables: []
31
+ extensions: []
32
+ extra_rdoc_files: []
33
+ files:
34
+ - lib/cloudformation_wrapper.rb
35
+ - lib/cloudformation_wrapper/stack_manager.rb
36
+ - lib/cloudformation_wrapper/version.rb
37
+ homepage: https://github.com/Shockolate/CloudFormationWrapper
38
+ licenses:
39
+ - Apache-2.0
40
+ metadata: {}
41
+ post_install_message:
42
+ rdoc_options: []
43
+ require_paths:
44
+ - lib
45
+ required_ruby_version: !ruby/object:Gem::Requirement
46
+ requirements:
47
+ - - ">="
48
+ - !ruby/object:Gem::Version
49
+ version: '2.1'
50
+ required_rubygems_version: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ">="
53
+ - !ruby/object:Gem::Version
54
+ version: '0'
55
+ requirements: []
56
+ rubyforge_project:
57
+ rubygems_version: 2.7.1
58
+ signing_key:
59
+ specification_version: 4
60
+ summary: Easy deployment of AWS CloudFormation stacks
61
+ test_files: []