aws-cft-tools 0.1.0
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 +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,40 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module AwsCftTools
|
4
|
+
class Client
|
5
|
+
##
|
6
|
+
# = CloudFormation Client
|
7
|
+
#
|
8
|
+
# All of the business logic behind direct interaction with the AWS API for CloudFormation templates
|
9
|
+
# and stacks.
|
10
|
+
#
|
11
|
+
class Base
|
12
|
+
attr_reader :options
|
13
|
+
|
14
|
+
##
|
15
|
+
#
|
16
|
+
# @param options [Hash] client configuration
|
17
|
+
# @option options [String] :environment the operational environment in which to act
|
18
|
+
# @option options [String] :profile the AWS credential profile to use
|
19
|
+
# @option options [String] :region the AWS region in which to act
|
20
|
+
#
|
21
|
+
def initialize(options = {})
|
22
|
+
@options = options
|
23
|
+
end
|
24
|
+
|
25
|
+
##
|
26
|
+
# The AWS SDK client object for this part of the AwsCftTools client
|
27
|
+
def aws_client
|
28
|
+
@aws_client ||= begin
|
29
|
+
klass = self.class.aws_client_class
|
30
|
+
klass && klass.new(
|
31
|
+
region: options[:region],
|
32
|
+
profile: options[:profile]
|
33
|
+
)
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
def self.aws_client_class; end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
@@ -0,0 +1,93 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module AwsCftTools
|
4
|
+
class Client
|
5
|
+
##
|
6
|
+
# = CloudFormation Client
|
7
|
+
#
|
8
|
+
# All of the business logic behind direct interaction with the AWS API for CloudFormation templates
|
9
|
+
# and stacks.
|
10
|
+
#
|
11
|
+
class CFT < Base
|
12
|
+
require_relative 'cft/changeset_management'
|
13
|
+
require_relative 'cft/stack_management'
|
14
|
+
|
15
|
+
include ChangesetManagement
|
16
|
+
include StackManagement
|
17
|
+
|
18
|
+
##
|
19
|
+
#
|
20
|
+
# @param options [Hash] client configuration
|
21
|
+
# @option options [String] :environment the operational environment in which to act
|
22
|
+
# @option options [String] :profile the AWS credential profile to use
|
23
|
+
# @option options [String] :region the AWS region in which to act
|
24
|
+
#
|
25
|
+
def initialize(options)
|
26
|
+
super(options)
|
27
|
+
end
|
28
|
+
|
29
|
+
def self.aws_client_class
|
30
|
+
Aws::CloudFormation::Client
|
31
|
+
end
|
32
|
+
|
33
|
+
##
|
34
|
+
# Lists all exports from stacks in CloudFormation.
|
35
|
+
#
|
36
|
+
# @return [Array<Aws::CloudFormation::Types::Export>]
|
37
|
+
#
|
38
|
+
def exports
|
39
|
+
@exports ||= AWSEnumerator.new(aws_client, :list_exports, {}, &:exports).to_a
|
40
|
+
end
|
41
|
+
|
42
|
+
##
|
43
|
+
# Lists all of the stacks in CloudFormation that are specific to the selected environment.
|
44
|
+
#
|
45
|
+
# @return [Array<OpenStruct>]
|
46
|
+
#
|
47
|
+
def stacks
|
48
|
+
@stacks ||= all_stacks.select do |stack|
|
49
|
+
tags = stack.tags
|
50
|
+
satisfies_environment(tags) &&
|
51
|
+
satisfies_role(tags) &&
|
52
|
+
satisfies_tags(tags)
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
##
|
57
|
+
# List all of the stacks in CloudFormation.
|
58
|
+
#
|
59
|
+
# @return [Array<OpenStruct>]
|
60
|
+
#
|
61
|
+
def all_stacks
|
62
|
+
@all_stacks ||= AWSEnumerator.new(aws_client, :describe_stacks, &method(:map_stacks)).to_a
|
63
|
+
end
|
64
|
+
|
65
|
+
private
|
66
|
+
|
67
|
+
def map_stacks(resp)
|
68
|
+
resp.stacks.map { |stack| Stack.new(stack, aws_client) }
|
69
|
+
end
|
70
|
+
|
71
|
+
def satisfies_environment(tag_set)
|
72
|
+
env = options[:environment]
|
73
|
+
!env || tag_set['Environment'] == env
|
74
|
+
end
|
75
|
+
|
76
|
+
def satisfies_role(tag_set)
|
77
|
+
role = options[:role]
|
78
|
+
!role || tag_set['Role'] == role
|
79
|
+
end
|
80
|
+
|
81
|
+
def satisfies_tags(tag_set)
|
82
|
+
tags = options[:tags]
|
83
|
+
return true unless tags
|
84
|
+
tag_set.all? { |key, value| satisfies_tag(tags, key, value) }
|
85
|
+
end
|
86
|
+
|
87
|
+
def satisfies_tag(tags, key, value)
|
88
|
+
tag = tags[key]
|
89
|
+
!tag || tag == value
|
90
|
+
end
|
91
|
+
end
|
92
|
+
end
|
93
|
+
end
|
@@ -0,0 +1,109 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module AwsCftTools
|
4
|
+
class Client
|
5
|
+
class CFT
|
6
|
+
##
|
7
|
+
# Provides changeset management functions for the CFT client.
|
8
|
+
#
|
9
|
+
module ChangesetManagement
|
10
|
+
##
|
11
|
+
# Accepts a template for a stack and tries to create an update changeset for that stack. The stack
|
12
|
+
# must already exist in CloudFormation.
|
13
|
+
#
|
14
|
+
# @param template [AwsCftTools::Template]
|
15
|
+
# @param changeset_set [String] an identifier linking various changesets as part of the same run
|
16
|
+
# @return [Array<AwsCftTools::Change>]
|
17
|
+
#
|
18
|
+
def changes_on_stack_update(template, changeset_set)
|
19
|
+
do_changeset(update_changeset_params(template, changeset_set))
|
20
|
+
end
|
21
|
+
|
22
|
+
##
|
23
|
+
# Accepts a template for a stack and tries to create a creation changeset for the stack. The stack
|
24
|
+
# must not exist yet in CloudFormation.
|
25
|
+
#
|
26
|
+
# @param template [AwsCftTools::Template]
|
27
|
+
# @param changeset_set [String] an identifier linking various changesets as part of the same run
|
28
|
+
# @return [Array<AwsCftTools::Change>]
|
29
|
+
#
|
30
|
+
def changes_on_stack_create(template, changeset_set)
|
31
|
+
do_changeset(create_changeset_params(template, changeset_set))
|
32
|
+
end
|
33
|
+
|
34
|
+
##
|
35
|
+
# Accepts a template and creates a mock changeset listing the resources that would be removed if
|
36
|
+
# the stack were deleted. The stack must exist in CloudFormation.
|
37
|
+
#
|
38
|
+
# @param template [AwsCftTools::Template]
|
39
|
+
# @param _changeset_set [Object] ignored to maintain compatibility with the other changeset methods
|
40
|
+
# @return [Array<AwsCftTools::DeletionChange>]
|
41
|
+
#
|
42
|
+
def changes_on_stack_delete(template, _changeset_set)
|
43
|
+
mock_delete_changeset(template)
|
44
|
+
end
|
45
|
+
|
46
|
+
private
|
47
|
+
|
48
|
+
##
|
49
|
+
# perform the changeset creation/deletion and return the changes that would happen if the
|
50
|
+
# changes moved forward
|
51
|
+
#
|
52
|
+
def do_changeset(params)
|
53
|
+
id = id_params(params)
|
54
|
+
|
55
|
+
aws_client.create_change_set(params)
|
56
|
+
|
57
|
+
aws_client.wait_until(:change_set_create_complete, id)
|
58
|
+
|
59
|
+
mapped_changes(AWSEnumerator.new(aws_client, :describe_change_set, id, &:changes).to_a)
|
60
|
+
rescue Aws::Waiters::Errors::FailureStateError
|
61
|
+
[]
|
62
|
+
ensure
|
63
|
+
aws_client.delete_change_set(id)
|
64
|
+
end
|
65
|
+
|
66
|
+
def id_params(params)
|
67
|
+
{
|
68
|
+
change_set_name: params[:change_set_name],
|
69
|
+
stack_name: params[:stack_name]
|
70
|
+
}
|
71
|
+
end
|
72
|
+
|
73
|
+
def update_changeset_params(template, changeset_set)
|
74
|
+
common_changeset_params(template, changeset_set).merge(change_set_type: 'UPDATE')
|
75
|
+
end
|
76
|
+
|
77
|
+
def create_changeset_params(template, changeset_set)
|
78
|
+
common_changeset_params(template, changeset_set).merge(change_set_type: 'CREATE')
|
79
|
+
end
|
80
|
+
|
81
|
+
def mock_delete_changeset(template)
|
82
|
+
# act like we're doing a changeset, but just narrate all of the resources being removed
|
83
|
+
id_params = { stack_name: template.name }
|
84
|
+
mapped_deleted_resources(
|
85
|
+
AWSEnumerator.new(aws_client, :list_stack_resources, id_params, &:stack_resource_summaries).to_a
|
86
|
+
)
|
87
|
+
rescue Aws::CloudFormation::Errors::ValidationError
|
88
|
+
[]
|
89
|
+
end
|
90
|
+
|
91
|
+
def common_changeset_params(template, changeset_set)
|
92
|
+
params = template.stack_parameters
|
93
|
+
params.merge(
|
94
|
+
capabilities: %w[CAPABILITY_IAM CAPABILITY_NAMED_IAM],
|
95
|
+
change_set_name: "#{params[:stack_name]}-#{changeset_set}"
|
96
|
+
)
|
97
|
+
end
|
98
|
+
|
99
|
+
def mapped_deleted_resources(resources)
|
100
|
+
resources.map { |resource| AwsCftTools::DeletionChange.new(resource) }
|
101
|
+
end
|
102
|
+
|
103
|
+
def mapped_changes(changes)
|
104
|
+
changes.flat_map { |change| AwsCftTools::Change.new(change) }
|
105
|
+
end
|
106
|
+
end
|
107
|
+
end
|
108
|
+
end
|
109
|
+
end
|
@@ -0,0 +1,85 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module AwsCftTools
|
4
|
+
class Client
|
5
|
+
class CFT
|
6
|
+
##
|
7
|
+
# Provide stack management functions for the CFT client.
|
8
|
+
#
|
9
|
+
module StackManagement
|
10
|
+
##
|
11
|
+
# Accepts information about a stack and tries to update that stack. The stack must already
|
12
|
+
# exist in CloudFormation.
|
13
|
+
#
|
14
|
+
# Metadata keys:
|
15
|
+
# - filename (required)
|
16
|
+
# - name
|
17
|
+
# - parameters
|
18
|
+
# - template_file
|
19
|
+
#
|
20
|
+
# If the update would result in no changes, no error is raised. Otherwise, all errors are
|
21
|
+
# raised and halt deployment.
|
22
|
+
#
|
23
|
+
# @param template [AwsCftTools::Template]
|
24
|
+
#
|
25
|
+
def update_stack(template)
|
26
|
+
aws_client.update_stack(update_stack_params(template))
|
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)
|
29
|
+
rescue Aws::CloudFormation::Errors::ValidationError => exception
|
30
|
+
raise exception unless exception.message.match?(/No updates/)
|
31
|
+
end
|
32
|
+
|
33
|
+
##
|
34
|
+
# Accepts information about a stack and tries to create the stack. The stack must not exist in
|
35
|
+
# CloudFormation.
|
36
|
+
#
|
37
|
+
# @param template [AwsCftTools::Template]
|
38
|
+
#
|
39
|
+
def create_stack(template)
|
40
|
+
aws_client.create_stack(create_stack_params(template))
|
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)
|
43
|
+
end
|
44
|
+
|
45
|
+
##
|
46
|
+
# Accepts information about a stack and tries to remove the stack. The stack must exist in
|
47
|
+
# CloudFormation.
|
48
|
+
#
|
49
|
+
# @param template [AwsCftTools::Template]
|
50
|
+
#
|
51
|
+
def delete_stack(template)
|
52
|
+
aws_client.delete_stack(delete_stack_params(template))
|
53
|
+
aws_client.wait_until(:stack_delete_complete, stack_name: template.name)
|
54
|
+
end
|
55
|
+
|
56
|
+
private
|
57
|
+
|
58
|
+
def update_stack_params(template)
|
59
|
+
common_stack_params(template).merge(
|
60
|
+
use_previous_template: false,
|
61
|
+
# on_failure: "ROLLBACK", # for updating
|
62
|
+
)
|
63
|
+
end
|
64
|
+
|
65
|
+
def create_stack_params(template)
|
66
|
+
common_stack_params(template).merge(
|
67
|
+
on_failure: 'DELETE', # for creation
|
68
|
+
)
|
69
|
+
end
|
70
|
+
|
71
|
+
def delete_stack_params(template)
|
72
|
+
{
|
73
|
+
stack_name: template.name
|
74
|
+
}
|
75
|
+
end
|
76
|
+
|
77
|
+
def common_stack_params(template)
|
78
|
+
template.stack_parameters.merge(
|
79
|
+
capabilities: %w[CAPABILITY_IAM CAPABILITY_NAMED_IAM]
|
80
|
+
)
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
@@ -0,0 +1,136 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module AwsCftTools
|
4
|
+
class Client
|
5
|
+
##
|
6
|
+
# = EC2 Instance Client
|
7
|
+
#
|
8
|
+
# All of the business logic behind direct interaction with the AWS API for EC2 instances and related
|
9
|
+
# resources.
|
10
|
+
#
|
11
|
+
class EC2 < Base
|
12
|
+
##
|
13
|
+
#
|
14
|
+
# @param options [Hash] client configuration
|
15
|
+
# @option options [String] :environment the operational environment in which to act
|
16
|
+
# @option options [String] :profile the AWS credential profile to use
|
17
|
+
# @option options [String] :region the AWS region in which to act
|
18
|
+
# @option options [String] :role the operational role of the resources under consideration
|
19
|
+
#
|
20
|
+
def initialize(options)
|
21
|
+
super(options)
|
22
|
+
end
|
23
|
+
|
24
|
+
def self.aws_client_class
|
25
|
+
Aws::EC2::Resource
|
26
|
+
end
|
27
|
+
|
28
|
+
##
|
29
|
+
# Returns a list of running instances filtered by any environment or role specified in the
|
30
|
+
# options passed to the constructor.
|
31
|
+
#
|
32
|
+
# Each instance is represented by a Hash with the following keys:
|
33
|
+
# - private_ip: the private IP address of the instance
|
34
|
+
# - public_ip: the public IP address (if any) of the instance
|
35
|
+
# - instance: the ID of the instance
|
36
|
+
# - role: the value of the `Role` tag if not filtering by role
|
37
|
+
# - environment: the value of the `Environment` tag if not filtering by environment
|
38
|
+
#
|
39
|
+
# @return [Array<OpenStruct>]
|
40
|
+
#
|
41
|
+
def instances
|
42
|
+
@instances ||= aws_client.instances(filters: instance_filters).map do |instance|
|
43
|
+
OpenStruct.new(
|
44
|
+
with_tags(instance, private_ip: instance.private_ip_address,
|
45
|
+
public_ip: instance.public_ip_address,
|
46
|
+
instance: instance.instance_id)
|
47
|
+
)
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
##
|
52
|
+
# Returns a list of available AMI images filtered by any environment or role specified in the
|
53
|
+
# options passed to the constructor.
|
54
|
+
#
|
55
|
+
# Each image is represented by an OpenStruct with the following keys/methods:
|
56
|
+
# - image_id: the ID of the AMI or image
|
57
|
+
# - type: the type of AMI or image
|
58
|
+
# - public: a flag indicating if the image is public
|
59
|
+
# - created_at: the date/time at which the image was created
|
60
|
+
# - role: the value of the `Role` tag if not filtering by role
|
61
|
+
# - environment: the value of the `Environment` tag if not filtering by environment
|
62
|
+
#
|
63
|
+
# @return [Array<OpenStruct>]
|
64
|
+
#
|
65
|
+
def images
|
66
|
+
@images ||= aws_client.images(owners: ['self'], filters: image_filters).map do |image|
|
67
|
+
OpenStruct.new(
|
68
|
+
with_tags(image, image_id: image.image_id,
|
69
|
+
type: image.image_type,
|
70
|
+
public: image.public,
|
71
|
+
created_at: image.creation_date)
|
72
|
+
)
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
private
|
77
|
+
|
78
|
+
def instance_filters
|
79
|
+
@instance_filters ||= begin
|
80
|
+
[
|
81
|
+
{ name: 'instance-state-name', values: ['running'] }
|
82
|
+
] + tag_filters
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
def image_filters
|
87
|
+
@image_filters ||= begin
|
88
|
+
[
|
89
|
+
{ name: 'state', values: ['available'] }
|
90
|
+
] + tag_filters
|
91
|
+
end
|
92
|
+
end
|
93
|
+
|
94
|
+
def tag_filters
|
95
|
+
@tag_filters ||= begin
|
96
|
+
environment_filter +
|
97
|
+
role_filter +
|
98
|
+
arbitrary_tag_filters
|
99
|
+
end
|
100
|
+
end
|
101
|
+
|
102
|
+
def environment_filter
|
103
|
+
tag_filter('Environment', options[:environment])
|
104
|
+
end
|
105
|
+
|
106
|
+
def role_filter
|
107
|
+
tag_filter('Role', options[:role])
|
108
|
+
end
|
109
|
+
|
110
|
+
def arbitrary_tag_filters
|
111
|
+
tags = options[:tags] || []
|
112
|
+
tags.inject([]) do |filter_set, tag_value|
|
113
|
+
tag, value = tag_value
|
114
|
+
filter_set << { name: "tag:#{tag}", values: [value] }
|
115
|
+
end
|
116
|
+
end
|
117
|
+
|
118
|
+
def tag_filter(tag, value)
|
119
|
+
if value
|
120
|
+
[{ name: "tag:#{tag}", values: [value] }]
|
121
|
+
else
|
122
|
+
[]
|
123
|
+
end
|
124
|
+
end
|
125
|
+
|
126
|
+
def with_tags(resource, info = {})
|
127
|
+
tags = resource.tags.each_with_object({}) { |tag, collection| collection[tag.key] = tag.value }
|
128
|
+
info.merge(
|
129
|
+
role: tags.delete('Role'),
|
130
|
+
environment: tags.delete('Environment'),
|
131
|
+
tags: tags
|
132
|
+
)
|
133
|
+
end
|
134
|
+
end
|
135
|
+
end
|
136
|
+
end
|