aws-cft-tools 0.1.0 → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 32c8025214a49ca0ed274d45c3c6ee266dcbfb71
4
- data.tar.gz: 135e9ef53e1081cc471bf095dd3a5e218364162e
3
+ metadata.gz: e2c4148f5b7aa3cf1465fcc735a1b3ebd8b559d9
4
+ data.tar.gz: 654d8ffb95b10000a1e3d364803fc014aaf2788a
5
5
  SHA512:
6
- metadata.gz: bae7aae8cdc869ed5fcf53e9dd8e8419765d6ed475f8035ed482fbbdb9042a185df1323639a2263ef52e0169d0607a55ba11fe473eb0667a6327e4f28f016579
7
- data.tar.gz: 8f06dceea86f02f3be1b3507a09da24d257484160437eb9d109c044965e3652084937bd6eadff37e7f64da92bf2534520ccc52a7aa17fdd599d2c560e71b9715
6
+ metadata.gz: 31d7266d177d7cc7359b35ddea6271b4c4f1d14ef8e4fdf850056e6a7823ffc4bef9ee802c85513e99944bab14110098c33e568ca1faa58883a41ae44efc6e60
7
+ data.tar.gz: 12d6742f6050e73e68a0a54d10138ba21d33fc3ac10be49e2b150ee248651b7be6f6337d34558e8a5ea51153e174b2a59e9487977e968897f74debb1652a5913
data/.rubocop.yml CHANGED
@@ -1,6 +1,9 @@
1
1
  ---
2
2
  require: rubocop-rspec
3
3
 
4
+ AllCops:
5
+ TargetRubyVersion: 2.4
6
+
4
7
  Metrics/BlockLength:
5
8
  Exclude:
6
9
  - 'aws-cft-tools.gemspec'
data/.travis.yml CHANGED
@@ -3,3 +3,6 @@ language: ruby
3
3
  rvm:
4
4
  - 2.4.1
5
5
  before_install: gem install bundler -v 1.15.3
6
+ script:
7
+ - bundle exec rake spec
8
+ - bundle exec rubocop
data/CHANGELOG.md ADDED
@@ -0,0 +1,23 @@
1
+ # Changelog
2
+
3
+ ## [0.1.1] - 2017-12-15
4
+
5
+ ### Added
6
+
7
+ * The `-d` and `--debug` options for `deploy` show more narrative. This can be useful if running parallel
8
+ jobs in a deployment (`-j` option with a value greater than one). This option is available for all
9
+ runbooks, but only the `deploy` runbook uses it in this release.
10
+ * Documentation on why `aws-cft-tools` is better than plain vanilla CloudFormation or Terraform. (Issue #10)
11
+
12
+ ### Changed
13
+
14
+ * Parallel deployments use threads. We weren't always handling output properly, so some output could be lost
15
+ if there was an error. We've reworked how we coordinate output from threads to reduce the chance of this
16
+ happening. (Issue #7)
17
+ * Deployments special case the `-j1` option to avoid threads. If you run into issues with `-j` and a value
18
+ greater than one, run without the option or with `-j1` to make sure that threads aren't the source of the
19
+ problem. (Issue #7)
20
+
21
+ ## [0.1.0] - 2017-11-29
22
+
23
+ Initial release.
data/README.md CHANGED
@@ -1,9 +1,18 @@
1
1
  # AwsCftTools
2
2
 
3
+ [![Gem Version](https://badge.fury.io/rb/aws-cft-tools.svg)](https://badge.fury.io/rb/aws-cft-tools)
4
+ [![Build Status](https://travis-ci.org/USSBA/aws-cft-tools.svg?branch=master)](https://travis-ci.org/USSBA/aws-cft-tools)
5
+
3
6
  CloudFormation and related services provide a way to manage infrastructure state in "the cloud." This
4
7
  gem and its included command (`aws-cft`) build on top of this state management system to create an
5
8
  infrastructure management solution native to the AWS environment.
6
9
 
10
+ `aws-cft-tools` empowers users to organize their CloudFormation templates using any form of directory
11
+ structure, without the need to tediously deploy their templates in a specific order or create quickly
12
+ outdated scripts to manage the deployment thereof. This project links together templates using the
13
+ Export/ImportValue features of CloudFormation to determine the order of operations, manages stack
14
+ names, and supports multiple parallel "Environments" within a single AWS account.
15
+
7
16
  ## Installation
8
17
 
9
18
  Add this line to your application's Gemfile:
@@ -71,6 +80,62 @@ update the version number in `version.rb`, and then run `bundle exec rake releas
71
80
  tag for the version, push git commits and tags, and push the `.gem` file to
72
81
  [rubygems.org](https://rubygems.org).
73
82
 
83
+ ## Why `aws-cft-tools`?
84
+
85
+ `aws-cft-tools` is designed to work in an "infrastructure as code" DevOps environment. Infrastructure is
86
+ software that is developed, tested, peer reviewed, and finally merged and deployed.
87
+
88
+ ### "Vanilla" CloudFormation
89
+
90
+ When first using CloudFormation, it is very easy to launch a single stack and get off the ground quickly.
91
+ As you move forward, users quickly find out that their Templates need to be managed in source control.
92
+ Later, users want to test their infrastructure changes in a different Environment, so a "dev" layer is
93
+ created, then an "integration", then a "staging", etc. Before too long, launching stacks is a nightmare
94
+ due to dependency conflicts, manual naming failures of Stacks, typos, and so on. On top of that,
95
+ remembering which Stacks have been deployed for which environment becomes impossible, so infrastructure
96
+ drift is inevitable.
97
+
98
+ This tool builds on top of the normal progression of teams using CloudFormation, enabling managed
99
+ Environments using parameters on templates. It offers simple deployments to roll out a full stack in
100
+ a new environment with a single command. It allows developers to continue to use CloudFormation for all
101
+ their infrastructure, while vastly simplifying the deployment and retraction process.
102
+
103
+ ### Ansible
104
+
105
+ [Ansible](https://www.ansible.com/) provides features that are a mix of infrastructure management and
106
+ instance configuration. For example, Ansible can do the work of TerraForm and Chef, combined. However,
107
+ Ansible works best when working with an expected inventory of resources. It makes changes to bring
108
+ infrastructure in line with the inventory. `aws-cft-tools` only manages CloudFormation templates and leaves
109
+ configuration of instances to other tools such as Chef or Ansible.
110
+
111
+ #### Using Ansible with `aws-cft-tools`
112
+
113
+ Ansible can manage the production of an Amazon Machine Image (AMI). It can spin up a temporary EC2 instance
114
+ and install all of the necessary system packages, make any configuration changes, and trigger the creation
115
+ of a tagged AMI. If the AMI is tagged with an Environment and Role, then `aws-cft-tools` can discover the
116
+ AMI and provide it as a parameter to any CloudFormation stacks that require the image. For example, creating
117
+ a new AMI and then using `aws-cft-tools` to deploy the CloudFormation Template for an auto-scaling group
118
+ that uses that AMI can result in the deployment of a new version of an application.
119
+
120
+ ### TerraForm
121
+
122
+ [TerraForm](https://www.terraform.io/) and `aws-cft-tools` are solving similar problems with fundamentally
123
+ different approaches. TerraForm is designed to work with multiple cloud providers while `aws-cft-tools` is
124
+ specific to AWS. So TerraForm can't depend on features that aren't provided by all cloud providers. Thus,
125
+ TerraForm requires a state file that introduces some complexity into managing infrastructure.
126
+
127
+ Using `aws-cft-tools` doesn't mean infrastructure management is less complex than when using TerraForm. Only
128
+ that the complexity is different. Instead of managing a state file outside of AWS, `aws-cft-tools` assumes
129
+ that AWS is the source of all state information.
130
+
131
+ Rather than computing changes, for example, `aws-cft-tools` requests a list of changes from AWS for a given
132
+ change in template and parameters. This does take more time than if all of that information was in a local
133
+ state file, but it ensures that any changes reflect the current deployment.
134
+
135
+ In exchange for taking a little more time to make changes (e.g., pull requests and code reviews after
136
+ initial development), teams can work on different parts of the infrastructure without having to coordinate
137
+ with each other.
138
+
74
139
  ## Building Gem for Local Use
75
140
 
76
141
  ```shell
data/code.json CHANGED
@@ -16,7 +16,7 @@
16
16
  "exemptionText": null
17
17
  },
18
18
  "vcs": "git",
19
- "laborHours": 1,
19
+ "laborHours": 182,
20
20
  "tags": ["SBA", "DevOps", "AWS"],
21
21
  "contact": { "email": "Andrew.Davy@sba.gov" }
22
22
  }
data/exe/aws-cft CHANGED
@@ -21,6 +21,7 @@ Clamp do
21
21
  option ['-T', '--tag'], 'NAME:VALUE', 'require a tag have the given value (may be given more than once)',
22
22
  multivalued: true
23
23
  option ['-v', '--[no-]verbose'], :flag, 'verbose narration of actions'
24
+ option ['-D', '--debug'], :flag, 'extra verbosity to aid in debugging'
24
25
  option '--version', :flag, 'Show version' do
25
26
  puts AwsCftTools::VERSION
26
27
  exit(0)
@@ -160,10 +161,16 @@ Clamp do
160
161
  profile: profile,
161
162
  root: root,
162
163
  region: region,
164
+ tags: tag_hash
165
+ }.merge(flag_options)
166
+ end
167
+
168
+ def flag_options
169
+ {
163
170
  noop: noop?,
164
171
  check: check?,
165
172
  verbose: verbose?,
166
- tags: tag_hash
173
+ debug: debug?
167
174
  }
168
175
  end
169
176
 
data/lib/aws_cft_tools.rb CHANGED
@@ -23,6 +23,7 @@ module AwsCftTools
23
23
  require 'aws_cft_tools/deletion_change'
24
24
  require 'aws_cft_tools/client'
25
25
  require 'aws_cft_tools/dependency_tree'
26
+ require 'aws_cft_tools/threaded_output'
26
27
  require 'aws_cft_tools/stack'
27
28
  require 'aws_cft_tools/template'
28
29
  require 'aws_cft_tools/template_set'
@@ -53,16 +53,36 @@ module AwsCftTools
53
53
  id = id_params(params)
54
54
 
55
55
  aws_client.create_change_set(params)
56
-
57
- aws_client.wait_until(:change_set_create_complete, id)
58
-
56
+ return [] if wait_for_changeset(id) == :nochanges
59
57
  mapped_changes(AWSEnumerator.new(aws_client, :describe_change_set, id, &:changes).to_a)
60
- rescue Aws::Waiters::Errors::FailureStateError
61
- []
62
58
  ensure
63
59
  aws_client.delete_change_set(id)
64
60
  end
65
61
 
62
+ def wait_for_changeset(id, times_waited = 0)
63
+ times_waited += 1
64
+ aws_client.wait_until(:change_set_create_complete, id)
65
+ rescue Aws::Waiters::Errors::FailureStateError
66
+ status = check_failure(id)
67
+ return status unless status == :retry
68
+ raise_if_too_many_retries(params, times_waited)
69
+ sleep(2**times_waited + 1)
70
+ retry
71
+ end
72
+
73
+ def raise_if_too_many_retries(params, retries)
74
+ return if retries < 5
75
+ raise CloudFormationError, "Error waiting on changeset for #{params[:stack_name]}"
76
+ end
77
+
78
+ def check_failure(id)
79
+ status = aws_client.describe_change_set(id)
80
+ return :retry unless status.status == 'FAILED'
81
+ return :no_changes if status.status_reason.match?(/didn't contain changes/)
82
+ raise CloudFormationError,
83
+ "Error creating changeset for #{params[:stack_name]}: #{status.status_reason}"
84
+ end
85
+
66
86
  def id_params(params)
67
87
  {
68
88
  change_set_name: params[:change_set_name],
@@ -25,7 +25,7 @@ module AwsCftTools
25
25
  def update_stack(template)
26
26
  aws_client.update_stack(update_stack_params(template))
27
27
  # we want to wait for the update to complete before we proceed
28
- aws_client.wait_until(:stack_update_complete, stack_name: template.name)
28
+ wait_for_stack_operation(:stack_update_complete, template.name)
29
29
  rescue Aws::CloudFormation::Errors::ValidationError => exception
30
30
  raise exception unless exception.message.match?(/No updates/)
31
31
  end
@@ -39,7 +39,7 @@ module AwsCftTools
39
39
  def create_stack(template)
40
40
  aws_client.create_stack(create_stack_params(template))
41
41
  # we want to wait for the create to complete before we proceed
42
- aws_client.wait_until(:stack_create_complete, stack_name: template.name)
42
+ wait_for_stack_operation(:stack_create_complete, template.name)
43
43
  end
44
44
 
45
45
  ##
@@ -55,6 +55,20 @@ module AwsCftTools
55
55
 
56
56
  private
57
57
 
58
+ def wait_for_stack_operation(op, stack_name, times_waited = 0)
59
+ times_waited += 1
60
+ aws_client.wait_until(op, stack_name: stack_name)
61
+ rescue Aws::Waiters::Errors::FailureStateError
62
+ raise_if_too_many_retries(stack_name, times_waited)
63
+ sleep(2**times_waited + 1)
64
+ retry
65
+ end
66
+
67
+ def raise_if_too_many_retries(stack_name, retries)
68
+ return if retries < 5
69
+ raise CloudFormationError, "Error waiting on stack operation for #{stack_name}"
70
+ end
71
+
58
72
  def update_stack_params(template)
59
73
  common_stack_params(template).merge(
60
74
  use_previous_template: false,
@@ -48,6 +48,7 @@ module AwsCftTools
48
48
  def initialize(configuration = {})
49
49
  @options = configuration
50
50
  @client = AwsCftTools::Client.new(options)
51
+ @stdout = $stdout
51
52
  end
52
53
 
53
54
  # @!group Callbacks
@@ -137,6 +138,17 @@ module AwsCftTools
137
138
  end
138
139
  end
139
140
 
141
+ ##
142
+ # @param note [String] a debug note
143
+ #
144
+ # Prints the given content to stdout if running in +debug+ mode. Debug statements are output
145
+ # without any capture when running multiple threads.
146
+ #
147
+ def debug(note = nil)
148
+ return unless note && options[:debug]
149
+ @stdout.puts "DEBUG\nDEBUG " + note.split(/\n/).join("\nDEBUG ") + "\nDEBUG"
150
+ end
151
+
140
152
  ##
141
153
  # @param description [String] an optional verbose description
142
154
  # @yield runs the block if in +verbose+ mode
@@ -19,6 +19,7 @@ module AwsCftTools
19
19
  # provide a tabular report of changeset actions
20
20
  #
21
21
  def narrate_changes(changes)
22
+ TablePrint::Config.io = $stdout
22
23
  tp(
23
24
  changes.map(&:to_narrative),
24
25
  %i[action logical_id physical_id type replacement scopes]
@@ -48,19 +48,36 @@ module AwsCftTools
48
48
  end
49
49
 
50
50
  def process_slice(templates)
51
- old_stdout = $stdout
52
- threads = create_threads(templates) { |template| process_template(template) }
53
- threads.map(&:thread).map(&:join)
54
- puts threads.map(&:output).map(&:string)
51
+ jobs = options[:jobs]
52
+ if jobs && jobs > 1
53
+ process_slice_threaded(templates)
54
+ else
55
+ templates.each(&method(:process_template))
56
+ end
57
+ end
58
+
59
+ def process_slice_threaded(templates)
60
+ original_stdout = $stdout
61
+ $stdout = ThreadedOutput.new(original_stdout)
62
+ template_list = templates.map(&:name).join(', ')
63
+ debug("Creating threads for #{template_list}")
64
+ threads = create_threads(templates, &method(:process_template))
65
+ debug("Waiting on threads for #{template_list}")
66
+ threads.map(&:join)
55
67
  ensure
56
- $stdout = old_stdout # just in case!
68
+ $stdout = original_stdout
57
69
  end
58
70
 
59
71
  def process_template(template)
72
+ ThreadedOutput.prefix = template_name = template.name
73
+ debug("Processing #{template_name}")
60
74
  is_update = deployed_templates.include?(template)
61
- operation("#{is_update ? 'Updating' : 'Creating'}: #{template.name}") do
75
+ operation("#{is_update ? 'Updating' : 'Creating'}: #{template_name}") do
62
76
  exec_template(template: template, type: is_update ? :update : :create)
63
77
  end
78
+ ensure
79
+ $stdout.flush
80
+ debug("Finished processing #{template_name}")
64
81
  end
65
82
 
66
83
  def exec_template(params) # template:, type:
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'thread'
4
+
3
5
  module AwsCftTools
4
6
  module Runbooks
5
7
  class Deploy
@@ -9,27 +11,8 @@ module AwsCftTools
9
11
  module Threading
10
12
  private
11
13
 
12
- # FIXME: things don't always work out well when capturing output
13
- # for now, we don't, and output gets mangled a bit when running with
14
- # multiple jobs in parallel
15
- def with_captured_stdout(capture)
16
- old_stdout = $stdout
17
- old_table_io = TablePrint::Config.io
18
- TablePrint::Config.io = $stdout = capture
19
- yield
20
- ensure
21
- $stdout = old_stdout
22
- TablePrint::Config.io = old_table_io
23
- end
24
-
25
14
  def create_threads(list, &_block)
26
- list.map { |item| threaded_process { yield item } }
27
- end
28
-
29
- def threaded_process(&block)
30
- output = StringIO.new
31
- thread = Thread.new { with_captured_stdout(output, &block) }
32
- OpenStruct.new(output: output, thread: thread)
15
+ list.map { |item| Thread.new { yield item } }
33
16
  end
34
17
  end
35
18
  end
@@ -0,0 +1,103 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'forwardable'
4
+ require 'thread'
5
+
6
+ module AwsCftTools
7
+ ##
8
+ # Provides a way to process output and prefix with a thread identifier. The object is shared by
9
+ # threads. Each thread should set its own prefix.
10
+ #
11
+ class ThreadedOutput
12
+ extend Forwardable
13
+
14
+ #
15
+ # @param real_stdout [IO] The file object that should be written to with prefixed text.
16
+ #
17
+ def initialize(real_stdout)
18
+ @stdout = real_stdout
19
+ @buffer = Hash.new { |hash, key| hash[key] = '' }
20
+ @semaphore = Mutex.new
21
+ end
22
+
23
+ def_delegator :@semaphore, :synchronize, :guarded
24
+ def_delegators ThreadedOutput, :prefix
25
+
26
+ ##
27
+ # The prefix for output from the current thread.
28
+ #
29
+ def self.prefix
30
+ Thread.current['output_prefix'] || ''
31
+ end
32
+
33
+ ##
34
+ # @param prefix [String] The prefix for each line of text output by the calling thread.
35
+ #
36
+ def self.prefix=(prefix)
37
+ Thread.current['output_prefix'] = prefix + ': '
38
+ end
39
+
40
+ ##
41
+ # Ensure all buffered text is output. If any text is output, a newline is output as well.
42
+ #
43
+ def flush
44
+ guarded { @stdout.puts prefix + buffer } if buffer != ''
45
+ self.buffer = ''
46
+ end
47
+
48
+ ##
49
+ # Write the string to the output with prefixes as appropriate. If the string does not end in a
50
+ # newline, then the remaining text will be buffered until a newline is seen.
51
+ #
52
+ def write(string)
53
+ print(string)
54
+ end
55
+
56
+ ##
57
+ # Writes all of the arguments to the output with prefixes. Appends a newline to each argument.
58
+ #
59
+ # @param args [Array<String>]
60
+ #
61
+ def puts(*args)
62
+ print(args.join("\n") + "\n")
63
+ end
64
+
65
+ ##
66
+ # Writes all of the arugments to the output without newlines. Will output the prefix after each
67
+ # newline.
68
+ #
69
+ # @param args [Array<String>]
70
+ #
71
+ def print(*args)
72
+ append(args.join(''))
73
+ printable_lines.each do |line|
74
+ guarded { @stdout.puts prefix + line }
75
+ end
76
+ end
77
+
78
+ private
79
+
80
+ def printable_lines
81
+ lines = buffer.split(/\n/)
82
+ if buffer[-1..-1] == "\n"
83
+ self.buffer = ''
84
+ else
85
+ self.buffer = lines.last
86
+ lines = lines[0..-2]
87
+ end
88
+ lines
89
+ end
90
+
91
+ def buffer
92
+ @buffer[prefix]
93
+ end
94
+
95
+ def append(value)
96
+ @buffer[prefix] += value
97
+ end
98
+
99
+ def buffer=(string)
100
+ @buffer[prefix] = string
101
+ end
102
+ end
103
+ end
@@ -4,5 +4,5 @@ module AwsCftTools
4
4
  ##
5
5
  # Version of AwsCftTools.
6
6
  #
7
- VERSION = '0.1.0'
7
+ VERSION = '0.1.1'
8
8
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: aws-cft-tools
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 0.1.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Small Business Administration
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2017-11-29 00:00:00.000000000 Z
11
+ date: 2017-12-15 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: aws-sdk
@@ -235,6 +235,7 @@ files:
235
235
  - ".travis.yml"
236
236
  - ".yardopts"
237
237
  - BEST-PRACTICES.md
238
+ - CHANGELOG.md
238
239
  - CONTRIBUTING.md
239
240
  - Gemfile
240
241
  - LICENSE
@@ -292,6 +293,7 @@ files:
292
293
  - lib/aws_cft_tools/template_set/closure.rb
293
294
  - lib/aws_cft_tools/template_set/dependencies.rb
294
295
  - lib/aws_cft_tools/template_set/each_slice_state.rb
296
+ - lib/aws_cft_tools/threaded_output.rb
295
297
  - lib/aws_cft_tools/version.rb
296
298
  - rubycritic.reek
297
299
  homepage: https://github.com/USSBA/aws-cft-tools