cloudformation-tool 1.2.1 → 1.3.4

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 06e60f20bd426c7bc9a5a4155b245e9c821a04efc9d160f38698597c65540786
4
- data.tar.gz: 42c4c4591c884a273cc5e5afc4987d96f6455faa1fe2e0bae09fadff58112072
3
+ metadata.gz: 5e68236a0ceae7d8423b08ad878d8800398f6f3d4035883961369db5cce9e69a
4
+ data.tar.gz: 2f0d5633efd0cd1a20caa428135e1e81f483855dfa77dedaabc9d3b8657a2d01
5
5
  SHA512:
6
- metadata.gz: 5965d055430f3015261c79585a42fc7fe4f45bc42c982a243b3aa161a883dee49066db543c7a793da60050c07181bc33ca5b3cc505d68b21af6477f9611ae720
7
- data.tar.gz: 57486ffee6db6beb3ac404de21ff40023e6dd712b492a577416c68c6dbca1383be521e3e2e6604e4ed9aef9824031687e39b4f2c29529a6b019e388405886c27
6
+ metadata.gz: cfc5b645933403a7ec6b66151bcd0bb47e74324573ebe61455cbe10505e46c1ff7867960fd108b91f9a6302da8ae165ddd981f633f96175a149a74fcae6b8e70
7
+ data.tar.gz: 0ea95fa0ca26a6b3106f7ecd31b659025f7d7f7d76d15d0b19fd47475102135883165c5e8e655a4ceadade2ddde17ecf65fea73a1771a4a8c34f45d6f85f11b4
data/README.md CHANGED
@@ -216,6 +216,102 @@ specifying the S3 bucket and object key, either of the following fields may be u
216
216
  Role: !GetAtt [ LambdaExecutionRole, Arn ]
217
217
  ```
218
218
 
219
+ ### Nested Stacks Modules
220
+
221
+ The CloudFormation pre-compiler supports loading local templates as "nested stacks" using the
222
+ CloudFormation `AWS::CloudFormation::Stack` resource type.
223
+
224
+ Instead of first pre-deploying a template to S3 to be used for a nested stack, use the
225
+ `Template` property (instead of the `TemplateURL` property) to point to a local
226
+ sub-template. The sub-template will be compiled separately and deployed automatically to
227
+ an S3 bucket before deploying the compiled template to CloudFormation.
228
+
229
+ The `monitor` tool (also used during `create` operation) supports nested stacks by
230
+ automatically detecting nested stack updates in the main stack's event stream and will
231
+ start streaming the nested stack events - this allows the user to more easily locate problems
232
+ with nested stacks.
233
+
234
+ Currently there's no automatic resolution of references between nested and parent stacks, so
235
+ make sure to set up nested stack parameters for all resources that should be referenced from
236
+ the parent stack.
237
+
238
+ #### Example
239
+
240
+ `cloud-formation.yaml`:
241
+
242
+ ```
243
+ AWSTemplateFormatVersion: "2010-09-09"
244
+ Description: "CloudFormation template with nested stacks"
245
+ Parameters:
246
+ DomainName:
247
+ Description: "The DNS domain name for the system"
248
+ Type: String
249
+ Default: example.com
250
+ AMI:
251
+ Description: "The AMI ID for the image to deploy"
252
+ Type: String
253
+ Default: ami-af4333cf
254
+
255
+ Resources:
256
+ VPC:
257
+ Type: AWS::EC2::VPC
258
+ Properties:
259
+ CidrBlock: 172.20.0.0/16
260
+ EnableDnsSupport: true
261
+ EnableDnsHostnames: true
262
+ SecurityGroupExample:
263
+ Type: AWS::EC2::SecurityGroup
264
+ Properties:
265
+ VpcId: !Ref VPC
266
+ GroupDescription: example security group
267
+ SecurityGroupIngress:
268
+ - { IpProtocol: icmp, CidrIp: 0.0.0.0/0, FromPort: -1, ToPort: -1 }
269
+ - { IpProtocol: tcp, CidrIp: 0.0.0.0/0, FromPort: 22, ToPort: 22 }
270
+ ServiceStack:
271
+ Type: AWS::CloudFormation::Stack
272
+ Properties:
273
+ Template: service.yaml
274
+ Parameters:
275
+ DomainName: !Ref DomainName
276
+ AMI: !Ref AMI
277
+ VPC: !Ref VPC
278
+ ```
279
+
280
+ `service.yaml`:
281
+
282
+ ```
283
+ AWSTemplateFormatVersion: "2010-09-09"
284
+ Description: "Service nested stack"
285
+ Parameters:
286
+ DomainName:
287
+ Description: "The DNS domain name for the system"
288
+ Type: String
289
+ AMI:
290
+ Description: "The AMI ID for the image to deploy"
291
+ Type: String
292
+ VPC:
293
+ Description: "The VPC into which to deploy the service"
294
+ Type: String
295
+
296
+ Resources:
297
+ Subnet:
298
+ Type: AWS::EC2::Subnet
299
+ Properties:
300
+ AvailabilityZone: !Select [ 0, !GetAZs { Ref: "AWS::Region" } ]
301
+ CidrBlock: 172.20.0.0/24
302
+ MapPublicIpOnLaunch: true
303
+ VpcId: !Ref VPC
304
+ Ec2Instance:
305
+ Type: AWS::EC2::Instance
306
+ Properties:
307
+ ImageId: !Ref AMI
308
+ KeyName: "secret"
309
+ NetworkInterfaces:
310
+ - AssociatePublicIpAddress: "true"
311
+ DeviceIndex: "0"
312
+ SubnetId: !Ref Subnet
313
+ ```
314
+
219
315
  ## Caching
220
316
 
221
317
  Some resource compilation may require uploading to S3, such as Lambda code or cloud-init setup
@@ -248,7 +344,12 @@ The following commands are supported:
248
344
  - `status` - Check if the names stack exists or not
249
345
  - `delete` - Delete the specified stack. After issuing the delete command, the tool will
250
346
  immediately start `monitor` mode until the operation has completed.
251
- - `servers` - List EC2 instances created and managed by this stack.
347
+ - `servers` - List EC2 instances created and managed by this stack, per autoscaling group, including servers in nested stacks.
348
+ - `groups` - list autoscaling groups managed by the stack, including groups in nested stacks.
349
+ - `recycle` - recycle servers in an autoscaling group in a stack by scaling the group up and down.
350
+ - `scale` - set the scale of an autoscaling group managed by a stack to a specific desired value.
351
+ - `invalidate` - send an invalidation request to a CloudFront distribution managed by a stack.
352
+ - `output` - retrieve output values from a stack.
252
353
 
253
354
  Please see the specific help for each command by running `cftool <command> --help` for
254
355
  more details and specific options.
@@ -260,3 +361,100 @@ The AWS region to be used can be select by specifying top level option (i.e. bef
260
361
  ### Credentials Selection
261
362
 
262
363
  The tool will use the standard AWS credentials selection process, except when you want to use AWS CLI configured credential profiles, you may select to use a profile other than "default" by specifying the top level option (i.e. before the command name) `-p <profile>`, by providing the standard environment variable `AWS_DEFAULT_PROFILE` or by having a file called `.awsprofile` - whose content is the name of a valid AWS REGION - in a parent directory (at any level up to the root directory).
364
+
365
+ ## Library API
366
+
367
+ The cloudformatin tool can also be consumed as a library by other applications - for example an application that needs to perform high-level business-logic oriented
368
+ operations for a specific application deployed in a stack, using the cloudformation tool abstraction of CloudFormation templates and stacks.
369
+
370
+ ### Usage as a library
371
+
372
+ To use the cloudformatin tool as a library, require `cloud_formation_tool`.
373
+
374
+ ### CloudFormation templates
375
+
376
+ The cloudformation pre-compiler can be used to manipulate pre-compiled templates.
377
+
378
+ To access the pre-compiler, initialize a `CloudFormationTool::CloudFormation` with the path to the local template resource (either a file or a directory that can be
379
+ parsed by the pre-compiler).
380
+
381
+ The initial template resource will be loaded but will not be fully parsed - and included elements will not be read - until the `compile` method is called.
382
+
383
+ The following method calls are available on the `CloudFormation` instance:
384
+
385
+ #### `compile(parameters = nil)`
386
+
387
+ Pre-compiles the template, with the provided parameter `Hash`, if provided. Returns a `Hash` repsenting the compiled template.
388
+
389
+ #### `to_yaml`
390
+
391
+ Pre-compiles the template and returns a YAML rendering of the CloudFormation template, suitable for deploying to AWS CloudFormation.
392
+
393
+ #### `each`
394
+
395
+ Yields a tuple for each defined template parameter, that includes the parameter's name and its default value (if set, `nil` otherwise).
396
+
397
+ ### CloudFormation stacks
398
+
399
+ The cloudformation tool's abstraction of a CloudFormation stack can be used to manipulate stack resouces, such as autoscaling groups or instances in a stack context.
400
+
401
+ To access the stack API, initialize a `CloudFormationTool::CloudFormation::Stack` with the name of the stack. You can then access the following methods:
402
+
403
+ #### `exist?`
404
+
405
+ Check if a stack exists.
406
+
407
+ #### `create(template, params = {})`
408
+
409
+ Create or update a stack by deploying the specified template. The template can be any local file or directory resource that can be parsed by the cloudformation pre-compiler.
410
+
411
+ #### `delete`
412
+
413
+ Deletes the stack
414
+
415
+ #### `stack_id`
416
+
417
+ Return the AWS CloudFormation stack identifier for the stack, which is the ARN of the stack.
418
+
419
+ #### `output`
420
+
421
+ Returns the output values of the stack
422
+
423
+ #### `resources`
424
+
425
+ Return a list of resources in the stack and all of its nested stacks
426
+
427
+ #### `asgroups`
428
+
429
+ Return a list of autoscaling groups in the stack and all of its nested stacks. The returned values are AWS SDK CloudFormation resources, extended with a set of methods
430
+ to help manage autoscaling groups:
431
+
432
+ ##### `group`
433
+
434
+ Returns the AWS SDK `Aws::AutoScaling::AutoScalingGroup` object for the autoscaling group.
435
+
436
+ #### `cdns`
437
+
438
+ Return a list of CloudFront CDN distributions in the stack and all of its nested stacks. The returnd values are AWS SDK CloudFormation resources, extended with a set of
439
+ methods to help manage CloudFront distributions:
440
+
441
+ ##### `distribution`
442
+
443
+ Returns the AWS SDK `Aws::CloudFront::Types::Distribution` object for the CloudFront distribution.
444
+
445
+ ##### `domain_names`
446
+
447
+ Returns the comma delimited list of the distribution aliases domain names
448
+
449
+ ##### `invalidate(path)`
450
+
451
+ Creates a new invalidation in the CloudFront distribution with the specified path expression
452
+
453
+ #### `each`
454
+
455
+ Yields CloudFormation stack events, in the order they were created. Subsequent calls to `each` will not repeat events previously yielded and will only yield additional
456
+ events created since the last call to `each`.
457
+
458
+ #### `see_event`
459
+
460
+ Mark all events since the last call to `each` (or from stack creation, if `each` was not previously called) as "seen" so they will not be yielded in future calls to `each`.
data/bin/cftool CHANGED
@@ -7,9 +7,6 @@ begin
7
7
  rescue SocketError => e
8
8
  warn "Networking error: #{e.message}"
9
9
  exit 1
10
- rescue Seahorse::Client::NetworkingError => e
11
- warn "Networking error: #{e.message}"
12
- exit 1
13
10
  rescue CloudFormationTool::Errors::BaseError => e
14
11
  warn e.message
15
12
  exit 1
@@ -31,7 +31,7 @@ module CloudFormationTool
31
31
 
32
32
  option [ "-d", "--debug" ], :flag, "Enable debug logging" do
33
33
  logger.level = Logger::Severity::DEBUG
34
- logger.formatter = logger.default_formatter
34
+ logger.formatter = nil
35
35
  end
36
36
 
37
37
  option [ "-q", "--quiet" ], :flag, "Enable debug logging" do
@@ -16,13 +16,18 @@ module CloudFormationTool
16
16
  asg_name.nil? or (res.logical_resource_id == asg_name)
17
17
  end.collect do |res|
18
18
  Thread.new do
19
- awsas.describe_auto_scaling_groups({
19
+ asg = awsas.describe_auto_scaling_groups({
20
20
  auto_scaling_group_names: [ res.physical_resource_id ]
21
- }).auto_scaling_groups.first.instances.collect do |i|
22
- Aws::EC2::Instance.new i.instance_id, client: awsec2
23
- end.collect do |i|
24
- ips = [ i.public_ip_address ] + i.network_interfaces.collect(&:ipv_6_addresses).flatten.collect(&:ipv_6_address)
25
- "#{res.logical_resource_id.ljust(30, ' ')} '#{i.public_dns_name}' (#{ips.join(', ')})"
21
+ }).auto_scaling_groups.first
22
+ if asg.nil?
23
+ []
24
+ else
25
+ asg.instances.collect do |i|
26
+ Aws::EC2::Instance.new i.instance_id, client: awsec2
27
+ end.collect do |i|
28
+ ips = [ i.public_ip_address ] + i.network_interfaces.collect(&:ipv_6_addresses).flatten.collect(&:ipv_6_address)
29
+ "#{res.logical_resource_id.ljust(30, ' ')} '#{i.public_dns_name}' (#{ips.join(', ')})"
30
+ end
26
31
  end
27
32
  end
28
33
  end
@@ -183,6 +183,12 @@ module CloudFormationTool
183
183
  else
184
184
  load_files(val, restype)
185
185
  end
186
+ when 'AWS::CloudFormation::Stack'
187
+ if key == 'Properties' and val.key?('Template')
188
+ NestedStack.new(val, self).to_cloudformation
189
+ else
190
+ load_files(val, restype)
191
+ end
186
192
  else
187
193
  load_files(val, restype)
188
194
  end
@@ -0,0 +1,25 @@
1
+ module CloudFormationTool
2
+ class CloudFormation
3
+
4
+ class NestedStack
5
+ include Storable
6
+
7
+ def initialize(props, tpl)
8
+ @tpl = tpl
9
+ @data = props
10
+ if props.key?('Template')
11
+ path = props['Template']
12
+ path = if path.start_with? "/" then path else "#{@tpl.basedir}/#{path}" end
13
+ @content = CloudFormation.new(path).to_yaml
14
+ @data['TemplateURL'] = upload(make_filename('yaml'), @content, mime_type: 'text/yaml', gzip: false)
15
+ @data.delete('Template')
16
+ end
17
+ end
18
+
19
+ def to_cloudformation
20
+ @data
21
+ end
22
+
23
+ end
24
+ end
25
+ end
@@ -15,6 +15,7 @@ module CloudFormationTool
15
15
  @name = name
16
16
  @seenev = Set.new
17
17
  @watch_timeouts = 0
18
+ @nested_stacks = Hash[]
18
19
  end
19
20
 
20
21
  def delete
@@ -95,7 +96,14 @@ module CloudFormationTool
95
96
  def resources
96
97
  begin
97
98
  awscf.list_stack_resources(stack_name: @name).each do |resp|
98
- resp.stack_resource_summaries.each { |res| yield res }
99
+ resp.stack_resource_summaries.each do |res|
100
+ yield res
101
+ if res.resource_type == 'AWS::CloudFormation::Stack'
102
+ Stack.new(res.physical_resource_id).resources do |nested_res|
103
+ yield nested_res
104
+ end
105
+ end
106
+ end
99
107
  end
100
108
  rescue Aws::CloudFormation::Errors::ValidationError => e
101
109
  raise CloudFormationTool::Errors::AppError, "Failed to get resources: #{e.message}"
@@ -133,6 +141,7 @@ module CloudFormationTool
133
141
  end
134
142
 
135
143
  def monitor(start_time = nil)
144
+ @nested_stacks = Hash[]
136
145
  done = false
137
146
  begin
138
147
  until done
@@ -140,26 +149,65 @@ module CloudFormationTool
140
149
  next if @seenev.add?(ev.event_id).nil?
141
150
  text = "#{ev.timestamp.strftime "%Y-%m-%d %H:%M:%S"}| " + %w(
142
151
  resource_type:40
143
- logical_resource_id:38
152
+ logical_resource_id:42
144
153
  resource_status
145
154
  ).collect { |field|
146
155
  (name,size) = field.split(":")
147
156
  size ||= 1
148
- ev.send(name.to_sym).ljust(size.to_i, ' ')
157
+ (if name == 'logical_resource_id' and ev.stack_name != self.name
158
+ logical_nested_stack_name(ev.stack_name) + "|"
159
+ else
160
+ ''
161
+ end + ev.send(name.to_sym)).ljust(size.to_i, ' ')
149
162
  }.join(" ")
150
163
  text += " " + ev.resource_status_reason if ev.resource_status =~ /_FAILED/
151
164
  if start_time.nil? or start_time < ev.timestamp
152
165
  puts text
153
166
  end
154
- done = (ev.resource_type == "AWS::CloudFormation::Stack" and ev.resource_status =~ /(_COMPLETE|_FAILED)$/)
167
+ check_nested_stack(ev)
168
+ done = is_final_event(ev)
155
169
  end
170
+ sleep 1
156
171
  end
157
172
  rescue CloudFormationTool::Errors::StackDoesNotExistError => e
158
173
  puts "Stack #{name} does not exist"
159
174
  end
160
175
  end
161
176
 
177
+ def logical_nested_stack_name(phys_name)
178
+ @nested_stacks[phys_name] || 'unknown'
179
+ end
180
+
181
+ def nested_stack_name(ev)
182
+ ev.physical_resource_id.split('/')[1]
183
+ end
184
+
185
+ def check_nested_stack(ev)
186
+ return unless ev.resource_type == "AWS::CloudFormation::Stack" and
187
+ ev.logical_resource_id != self.name # not nested stack
188
+ return if @nested_stacks.has_key? ev.logical_resource_id # seeing the first or last nested stack event - ignoring
189
+ @nested_stacks[nested_stack_name(ev)] = ev.logical_resource_id
190
+ end
191
+
192
+ def is_final_event(ev)
193
+ ev.resource_type == "AWS::CloudFormation::Stack" and
194
+ ev.resource_status =~ /(_COMPLETE|_FAILED)$/ and
195
+ ev.logical_resource_id == self.name
196
+ end
197
+
198
+ def tracked_stacks
199
+ [ self.name ] + @nested_stacks.keys.compact
200
+ end
201
+
162
202
  def each
203
+ tracked_stacks.each do |name|
204
+ events_for(name) do |ev|
205
+ yield ev
206
+ end
207
+ end
208
+ end
209
+
210
+ def events_for(stack_name)
163
211
  token = nil
164
212
  sleep(if @_last_poll_time.nil?
165
213
  0
@@ -172,7 +220,7 @@ module CloudFormationTool
172
220
  end
173
221
  end)
174
222
  begin
175
- resp = awscf.describe_stack_events stack_name: name, next_token: token
223
+ resp = awscf.describe_stack_events stack_name: stack_name, next_token: token
176
224
  @watch_timeouts = 0
177
225
  resp.stack_events.each do |ev|
178
226
  yield ev
@@ -188,7 +236,11 @@ module CloudFormationTool
188
236
  end
189
237
  rescue Aws::CloudFormation::Errors::ValidationError => e
190
238
  if e.message =~ /does not exist/
191
- raise CloudFormationTool::Errors::StackDoesNotExistError, "Stack does not exist"
239
+ if stack_name == self.name
240
+ raise CloudFormationTool::Errors::StackDoesNotExistError, "Stack does not exist"
241
+ end
242
+ # ignore "does not exist" errors on nested stacks - we may try to poll them before
243
+ # they actually exist. We'll just try later
192
244
  else
193
245
  raise e
194
246
  end
@@ -45,7 +45,7 @@ module CloudFormationTool
45
45
  def encode(allow_gzip = true)
46
46
  yamlout = compile
47
47
  usegzip = false
48
- if allow_gzip and yamlout.size > 16384 # max AWS EC2 user data size - try compressing it
48
+ if allow_gzip and yamlout.size > 16000 # max AWS EC2 user data size - try compressing it
49
49
  yamlout = Zlib::Deflate.new(nil, 31).deflate(yamlout, Zlib::FINISH) # 31 is the magic word to have deflate create a gzip compatible header
50
50
  usegzip = true
51
51
  end
@@ -1,3 +1,3 @@
1
1
  module CloudFormationTool
2
- VERSION = '1.2.1'
2
+ VERSION = '1.3.4'
3
3
  end
metadata CHANGED
@@ -1,29 +1,29 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: cloudformation-tool
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.2.1
4
+ version: 1.3.4
5
5
  platform: ruby
6
6
  authors:
7
7
  - Oded Arbel
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2018-12-05 00:00:00.000000000 Z
11
+ date: 2020-07-07 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rake
15
15
  requirement: !ruby/object:Gem::Requirement
16
16
  requirements:
17
- - - "~>"
17
+ - - ">="
18
18
  - !ruby/object:Gem::Version
19
- version: '12'
19
+ version: 12.3.3
20
20
  type: :runtime
21
21
  prerelease: false
22
22
  version_requirements: !ruby/object:Gem::Requirement
23
23
  requirements:
24
- - - "~>"
24
+ - - ">="
25
25
  - !ruby/object:Gem::Version
26
- version: '12'
26
+ version: 12.3.3
27
27
  - !ruby/object:Gem::Dependency
28
28
  name: clamp
29
29
  requirement: !ruby/object:Gem::Requirement
@@ -168,6 +168,7 @@ files:
168
168
  - lib/cloud_formation_tool/cloud_formation.rb
169
169
  - lib/cloud_formation_tool/cloud_formation/cloud_front_distribution.rb
170
170
  - lib/cloud_formation_tool/cloud_formation/lambda_code.rb
171
+ - lib/cloud_formation_tool/cloud_formation/nested_stack.rb
171
172
  - lib/cloud_formation_tool/cloud_formation/stack.rb
172
173
  - lib/cloud_formation_tool/cloud_init.rb
173
174
  - lib/cloud_formation_tool/errors.rb
@@ -192,8 +193,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
192
193
  - !ruby/object:Gem::Version
193
194
  version: '0'
194
195
  requirements: []
195
- rubyforge_project:
196
- rubygems_version: 2.7.8
196
+ rubygems_version: 3.1.2
197
197
  signing_key:
198
198
  specification_version: 4
199
199
  summary: A pre-compiler tool for CloudFormation YAML templates