aws-cft-tools 0.1.0 → 0.1.1

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
  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