aws-cft-tools 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.editorconfig +10 -0
- data/.gitignore +52 -0
- data/.rspec +2 -0
- data/.rubocop.yml +19 -0
- data/.travis.yml +5 -0
- data/.yardopts +1 -0
- data/BEST-PRACTICES.md +136 -0
- data/CONTRIBUTING.md +38 -0
- data/Gemfile +8 -0
- data/LICENSE +15 -0
- data/README.md +118 -0
- data/Rakefile +17 -0
- data/USAGE.adoc +404 -0
- data/aws-cft-tools.gemspec +53 -0
- data/bin/console +15 -0
- data/bin/setup +8 -0
- data/code.json +24 -0
- data/exe/aws-cft +176 -0
- data/lib/aws-cft-tools.rb +3 -0
- data/lib/aws_cft_tools.rb +31 -0
- data/lib/aws_cft_tools/aws_enumerator.rb +55 -0
- data/lib/aws_cft_tools/change.rb +66 -0
- data/lib/aws_cft_tools/client.rb +84 -0
- data/lib/aws_cft_tools/client/base.rb +40 -0
- data/lib/aws_cft_tools/client/cft.rb +93 -0
- data/lib/aws_cft_tools/client/cft/changeset_management.rb +109 -0
- data/lib/aws_cft_tools/client/cft/stack_management.rb +85 -0
- data/lib/aws_cft_tools/client/ec2.rb +136 -0
- data/lib/aws_cft_tools/client/templates.rb +84 -0
- data/lib/aws_cft_tools/deletion_change.rb +43 -0
- data/lib/aws_cft_tools/dependency_tree.rb +109 -0
- data/lib/aws_cft_tools/dependency_tree/nodes.rb +71 -0
- data/lib/aws_cft_tools/dependency_tree/variables.rb +37 -0
- data/lib/aws_cft_tools/errors.rb +25 -0
- data/lib/aws_cft_tools/runbook.rb +166 -0
- data/lib/aws_cft_tools/runbook/report.rb +30 -0
- data/lib/aws_cft_tools/runbooks.rb +16 -0
- data/lib/aws_cft_tools/runbooks/common/changesets.rb +30 -0
- data/lib/aws_cft_tools/runbooks/common/templates.rb +38 -0
- data/lib/aws_cft_tools/runbooks/deploy.rb +107 -0
- data/lib/aws_cft_tools/runbooks/deploy/reporting.rb +50 -0
- data/lib/aws_cft_tools/runbooks/deploy/stacks.rb +109 -0
- data/lib/aws_cft_tools/runbooks/deploy/templates.rb +37 -0
- data/lib/aws_cft_tools/runbooks/deploy/threading.rb +37 -0
- data/lib/aws_cft_tools/runbooks/diff.rb +28 -0
- data/lib/aws_cft_tools/runbooks/diff/context.rb +86 -0
- data/lib/aws_cft_tools/runbooks/diff/context/reporting.rb +87 -0
- data/lib/aws_cft_tools/runbooks/hosts.rb +43 -0
- data/lib/aws_cft_tools/runbooks/images.rb +43 -0
- data/lib/aws_cft_tools/runbooks/init.rb +86 -0
- data/lib/aws_cft_tools/runbooks/retract.rb +69 -0
- data/lib/aws_cft_tools/runbooks/retract/templates.rb +44 -0
- data/lib/aws_cft_tools/runbooks/stacks.rb +43 -0
- data/lib/aws_cft_tools/stack.rb +83 -0
- data/lib/aws_cft_tools/template.rb +177 -0
- data/lib/aws_cft_tools/template/dsl_context.rb +14 -0
- data/lib/aws_cft_tools/template/file_system.rb +62 -0
- data/lib/aws_cft_tools/template/metadata.rb +144 -0
- data/lib/aws_cft_tools/template/properties.rb +129 -0
- data/lib/aws_cft_tools/template_set.rb +120 -0
- data/lib/aws_cft_tools/template_set/array_methods.rb +63 -0
- data/lib/aws_cft_tools/template_set/closure.rb +77 -0
- data/lib/aws_cft_tools/template_set/dependencies.rb +55 -0
- data/lib/aws_cft_tools/template_set/each_slice_state.rb +58 -0
- data/lib/aws_cft_tools/version.rb +8 -0
- data/rubycritic.reek +3 -0
- metadata +321 -0
@@ -0,0 +1,30 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'table_print'
|
4
|
+
|
5
|
+
module AwsCftTools
|
6
|
+
class Runbook
|
7
|
+
##
|
8
|
+
# A subclass of the Runbook designed for reporting out status or other information about resources.
|
9
|
+
#
|
10
|
+
class Report < Runbook
|
11
|
+
def run
|
12
|
+
tp(items, columns)
|
13
|
+
end
|
14
|
+
|
15
|
+
###
|
16
|
+
# @return [Array<Object>]
|
17
|
+
#
|
18
|
+
def items
|
19
|
+
[]
|
20
|
+
end
|
21
|
+
|
22
|
+
###
|
23
|
+
# @return [Array<String>]
|
24
|
+
#
|
25
|
+
def columns
|
26
|
+
[]
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module AwsCftTools
|
4
|
+
##
|
5
|
+
# Namespace for runbooks
|
6
|
+
#
|
7
|
+
module Runbooks
|
8
|
+
require_relative 'runbooks/deploy'
|
9
|
+
require_relative 'runbooks/diff'
|
10
|
+
require_relative 'runbooks/hosts'
|
11
|
+
require_relative 'runbooks/images'
|
12
|
+
require_relative 'runbooks/init'
|
13
|
+
require_relative 'runbooks/retract'
|
14
|
+
require_relative 'runbooks/stacks'
|
15
|
+
end
|
16
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module AwsCftTools
|
4
|
+
module Runbooks
|
5
|
+
module Common
|
6
|
+
##
|
7
|
+
# Changesets - operations on changesets in the deploy runbook
|
8
|
+
#
|
9
|
+
module Changesets
|
10
|
+
private
|
11
|
+
|
12
|
+
# @todo store this somewhere so we can have an "active" changeset to be reviewed and committed.
|
13
|
+
#
|
14
|
+
def changeset_set
|
15
|
+
@changeset_set ||= SecureRandom.hex(16)
|
16
|
+
end
|
17
|
+
|
18
|
+
##
|
19
|
+
# provide a tabular report of changeset actions
|
20
|
+
#
|
21
|
+
def narrate_changes(changes)
|
22
|
+
tp(
|
23
|
+
changes.map(&:to_narrative),
|
24
|
+
%i[action logical_id physical_id type replacement scopes]
|
25
|
+
)
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
@@ -0,0 +1,38 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module AwsCftTools
|
4
|
+
module Runbooks
|
5
|
+
module Common
|
6
|
+
##
|
7
|
+
# Templates - operations on templates in multiple runbooks
|
8
|
+
#
|
9
|
+
module Templates
|
10
|
+
private
|
11
|
+
|
12
|
+
def filtered_templates(set)
|
13
|
+
filtered_by_environment(
|
14
|
+
filtered_by_role(
|
15
|
+
filtered_by_selection(options[:templates], set)
|
16
|
+
)
|
17
|
+
)
|
18
|
+
end
|
19
|
+
|
20
|
+
def filtered_by_role(set)
|
21
|
+
set.select { |template| template.role?(options[:role]) }
|
22
|
+
end
|
23
|
+
|
24
|
+
def filtered_by_environment(set)
|
25
|
+
set.select { |template| template.environment?(options[:environment]) }
|
26
|
+
end
|
27
|
+
|
28
|
+
def filtered_by_selection(templates, set)
|
29
|
+
if templates && templates.any?
|
30
|
+
set.select { |template| templates.include?(template.filename.to_s) }
|
31
|
+
else
|
32
|
+
set
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
@@ -0,0 +1,107 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'forwardable'
|
4
|
+
|
5
|
+
module AwsCftTools
|
6
|
+
module Runbooks
|
7
|
+
##
|
8
|
+
# Deploy - manage CloudFormation stack deployment
|
9
|
+
#
|
10
|
+
# @example
|
11
|
+
# % aws-cft deploy -e QA # deploy all templates to the QA environment
|
12
|
+
# % aws-cft deploy -e Staging -n -v # narrate the templates that would be used for Staging
|
13
|
+
# % aws-cft deploy -e Production -c -v # narrate the changes that would go into Production
|
14
|
+
#
|
15
|
+
class Deploy < Runbook
|
16
|
+
require_relative 'common/changesets'
|
17
|
+
require_relative 'common/templates'
|
18
|
+
require_relative 'deploy/reporting'
|
19
|
+
require_relative 'deploy/stacks'
|
20
|
+
require_relative 'deploy/templates'
|
21
|
+
require_relative 'deploy/threading'
|
22
|
+
|
23
|
+
extend Forwardable
|
24
|
+
|
25
|
+
include Common::Changesets
|
26
|
+
include Common::Templates
|
27
|
+
include Reporting
|
28
|
+
include Stacks
|
29
|
+
include Templates
|
30
|
+
include Threading
|
31
|
+
|
32
|
+
def_delegators :client, :images
|
33
|
+
def_delegator :client, :all_stacks, :stacks
|
34
|
+
|
35
|
+
def run
|
36
|
+
run_reports
|
37
|
+
|
38
|
+
detail 'Updating template parameters...'
|
39
|
+
update_parameters
|
40
|
+
|
41
|
+
process_templates(options[:jobs] || 1)
|
42
|
+
end
|
43
|
+
|
44
|
+
private
|
45
|
+
|
46
|
+
def process_templates(slice_size)
|
47
|
+
templates_in_folder_order.each_slice(slice_size, &method(:process_slice))
|
48
|
+
end
|
49
|
+
|
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)
|
55
|
+
ensure
|
56
|
+
$stdout = old_stdout # just in case!
|
57
|
+
end
|
58
|
+
|
59
|
+
def process_template(template)
|
60
|
+
is_update = deployed_templates.include?(template)
|
61
|
+
operation("#{is_update ? 'Updating' : 'Creating'}: #{template.name}") do
|
62
|
+
exec_template(template: template, type: is_update ? :update : :create)
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
def exec_template(params) # template:, type:
|
67
|
+
checking { exec_template_check(**params) }
|
68
|
+
doing { exec_template_for_real(**params) }
|
69
|
+
end
|
70
|
+
|
71
|
+
def exec_template_check(template:, type:)
|
72
|
+
narrate_changes(client.send(:"changes_on_stack_#{type}", template, changeset_set))
|
73
|
+
rescue Aws::CloudFormation::Errors::ValidationError => error
|
74
|
+
puts "Error checking #{template.filename}: #{error.message}"
|
75
|
+
end
|
76
|
+
|
77
|
+
def exec_template_for_real(template:, type:)
|
78
|
+
client.send(:"#{type}_stack", template)
|
79
|
+
rescue Aws::CloudFormation::Errors::ValidationError => error
|
80
|
+
raise AwsCftTools::CloudFormationError, "Error processing #{template.filename}: #{error}"
|
81
|
+
end
|
82
|
+
|
83
|
+
##
|
84
|
+
# update_parameters - notate templates with region and image id as appropriate
|
85
|
+
#
|
86
|
+
def update_parameters
|
87
|
+
templates.each { |template| update_template_with_image_id(template) }
|
88
|
+
end
|
89
|
+
|
90
|
+
##
|
91
|
+
# the set of templates corresponding to deployed CloudFormation stacks
|
92
|
+
#
|
93
|
+
def deployed_templates
|
94
|
+
@deployed_templates ||= templates_in_folder_order.select do |template|
|
95
|
+
deployed_stack_names.include?(template.name)
|
96
|
+
end
|
97
|
+
end
|
98
|
+
|
99
|
+
##
|
100
|
+
# the set of templates with no corresponding deployed CloudFormation stack
|
101
|
+
#
|
102
|
+
def new_templates
|
103
|
+
@new_templates ||= templates_in_folder_order - deployed_templates
|
104
|
+
end
|
105
|
+
end
|
106
|
+
end
|
107
|
+
end
|
@@ -0,0 +1,50 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'table_print'
|
4
|
+
|
5
|
+
module AwsCftTools
|
6
|
+
module Runbooks
|
7
|
+
class Deploy
|
8
|
+
##
|
9
|
+
# module with various reporting functions for deployment
|
10
|
+
#
|
11
|
+
module Reporting
|
12
|
+
private
|
13
|
+
|
14
|
+
def run_reports
|
15
|
+
report_available_images
|
16
|
+
report_undefined_variables
|
17
|
+
|
18
|
+
detail do
|
19
|
+
tp(templates, ['filename'])
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
##
|
24
|
+
# report_available_images - provide tabular report of available images
|
25
|
+
#
|
26
|
+
def report_available_images
|
27
|
+
detail('Available Images') { tp(report_available_images_data) }
|
28
|
+
end
|
29
|
+
|
30
|
+
def report_available_images_data
|
31
|
+
available_images.map { |role_env, ami| role_env.split(/:/).reverse + [ami] }
|
32
|
+
.compact
|
33
|
+
.sort
|
34
|
+
.map { |role_env_ami| %w[role environment ami].zip(role_env_ami).to_h }
|
35
|
+
end
|
36
|
+
|
37
|
+
##
|
38
|
+
# report_undefined_image - provide list of undefined imports that block stack deployment
|
39
|
+
#
|
40
|
+
def report_undefined_variables
|
41
|
+
vars = templates_in_folder_order.undefined_variables
|
42
|
+
return unless vars.any?
|
43
|
+
puts '*** Unable to update or create templates.'
|
44
|
+
puts 'The following variables are referenced but not defined: ', vars
|
45
|
+
exit 1 # TODO: convert to a raise
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
@@ -0,0 +1,109 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module AwsCftTools
|
4
|
+
module Runbooks
|
5
|
+
class Deploy
|
6
|
+
##
|
7
|
+
# Stacks - operations on stacks in the deploy runbook
|
8
|
+
#
|
9
|
+
module Stacks
|
10
|
+
private
|
11
|
+
|
12
|
+
##
|
13
|
+
# Provides the next environment to look at if the current one fails.
|
14
|
+
#
|
15
|
+
# This is mainly used to find an AMI that is appropriate for the environment. Since we want
|
16
|
+
# to be able to promote AMIs from one environment to the next without rebuilding them, we
|
17
|
+
# should use the AMI that is most appropriate even if it's not tagged with the environment
|
18
|
+
# in scope.
|
19
|
+
#
|
20
|
+
# This can be configured in the configuration file with the +:environment_successors+ key.
|
21
|
+
#
|
22
|
+
def successor_environment(env)
|
23
|
+
(options[:environment_successors] || {})[env]
|
24
|
+
end
|
25
|
+
|
26
|
+
##
|
27
|
+
# list the available images by role and environment
|
28
|
+
#
|
29
|
+
# An image for a role/environment is the most recent AMI tagged with that role and environment.
|
30
|
+
#
|
31
|
+
def available_images
|
32
|
+
@available_images ||= images.sort_by(&:created_at).each_with_object({}) do |image, mapping|
|
33
|
+
key = image_key(image)
|
34
|
+
mapping[key] = image.image_id if key != ':'
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
def image_key(image)
|
39
|
+
(image.environment || '') + ':' + (image.role || '')
|
40
|
+
end
|
41
|
+
|
42
|
+
##
|
43
|
+
# retrieve the appropriate AMI identifier for an environment/role
|
44
|
+
#
|
45
|
+
# This takes into account environment succession (see #successor_environment) to find the
|
46
|
+
# best image for an environment/role as determined by #available_images.
|
47
|
+
#
|
48
|
+
def find_image(role, env)
|
49
|
+
key = "#{env}:#{role}"
|
50
|
+
image = available_images[key]
|
51
|
+
return image if image
|
52
|
+
next_env = successor_environment(env)
|
53
|
+
find_image(role, next_env) if next_env
|
54
|
+
end
|
55
|
+
|
56
|
+
##
|
57
|
+
# list the template files containing `Region` metadata
|
58
|
+
#
|
59
|
+
def files_with_region_param
|
60
|
+
@files_with_region_param ||=
|
61
|
+
templates.select { |template| template.allowed_regions.any? }.map(&:filename)
|
62
|
+
end
|
63
|
+
|
64
|
+
##
|
65
|
+
# list the template source filenames for deployed stacks
|
66
|
+
#
|
67
|
+
# This returns the filename as reported by the `Source` tag on the CloudFormation stack.
|
68
|
+
#
|
69
|
+
def deployed_stack_names
|
70
|
+
stacks.map(&:name).compact
|
71
|
+
end
|
72
|
+
|
73
|
+
##
|
74
|
+
# list the filenames of all of the in-scope templates
|
75
|
+
#
|
76
|
+
def files
|
77
|
+
templates.map(&:filename)
|
78
|
+
end
|
79
|
+
|
80
|
+
##
|
81
|
+
# add `ImageId` parameter to templates as-needed
|
82
|
+
#
|
83
|
+
def update_template_with_image_id(template)
|
84
|
+
params = template.parameters
|
85
|
+
params.each do |key, value|
|
86
|
+
update_params_with_image_id(params, key, value)
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
def update_params_with_image_id(params, key, value)
|
91
|
+
return unless value.is_a?(Hash)
|
92
|
+
role = value['Role']
|
93
|
+
image = find_image(role, options[:environment])
|
94
|
+
params[key] = image if role
|
95
|
+
report_undefined_image(role) if role && !image
|
96
|
+
end
|
97
|
+
|
98
|
+
##
|
99
|
+
# utility function to create an error message about undefined AMIs
|
100
|
+
#
|
101
|
+
def report_undefined_image(role)
|
102
|
+
puts format('Unable to find image for %s suitable for %s',
|
103
|
+
role,
|
104
|
+
options[:environment])
|
105
|
+
end
|
106
|
+
end
|
107
|
+
end
|
108
|
+
end
|
109
|
+
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module AwsCftTools
|
4
|
+
module Runbooks
|
5
|
+
class Deploy
|
6
|
+
##
|
7
|
+
# module with methods to manage ordering of templates
|
8
|
+
#
|
9
|
+
module Templates
|
10
|
+
##
|
11
|
+
# list the templates in-scope for this deployment
|
12
|
+
#
|
13
|
+
def templates
|
14
|
+
@templates ||= begin
|
15
|
+
candidates = client.templates
|
16
|
+
|
17
|
+
candidates.closure(
|
18
|
+
filtered_templates(
|
19
|
+
candidates
|
20
|
+
)
|
21
|
+
)
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
def template_folder_order
|
26
|
+
options[:template_folder_priorities] || []
|
27
|
+
end
|
28
|
+
|
29
|
+
private
|
30
|
+
|
31
|
+
def templates_in_folder_order
|
32
|
+
templates.in_folder_order(template_folder_order)
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module AwsCftTools
|
4
|
+
module Runbooks
|
5
|
+
class Deploy
|
6
|
+
##
|
7
|
+
# module with methods to manage threading
|
8
|
+
#
|
9
|
+
module Threading
|
10
|
+
private
|
11
|
+
|
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
|
+
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)
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|