openstax_aws 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +19 -0
- data/.travis.yml +12 -0
- data/CHANGELOG.md +11 -0
- data/Gemfile +6 -0
- data/Gemfile.lock +120 -0
- data/LICENSE.txt +1 -0
- data/README.md +927 -0
- data/Rakefile +6 -0
- data/TODO.md +1 -0
- data/assets/secrets_sequence_diagram.png +0 -0
- data/bin/console +14 -0
- data/bin/create_development_environment +26 -0
- data/bin/get_latest_ubuntu_ami +31 -0
- data/bin/setup +8 -0
- data/bin/templates/aws_ruby_development.yml +221 -0
- data/examples/deployment.rb +90 -0
- data/ideas.md +15 -0
- data/lib/openstax/aws/auto_scaling_group.rb +28 -0
- data/lib/openstax/aws/auto_scaling_instance.rb +96 -0
- data/lib/openstax/aws/build_image_command_1.rb +53 -0
- data/lib/openstax/aws/change_set.rb +100 -0
- data/lib/openstax/aws/deployment_base.rb +372 -0
- data/lib/openstax/aws/distribution.rb +56 -0
- data/lib/openstax/aws/ec2_instance_data.rb +18 -0
- data/lib/openstax/aws/extensions.rb +19 -0
- data/lib/openstax/aws/git_helper.rb +18 -0
- data/lib/openstax/aws/image.rb +34 -0
- data/lib/openstax/aws/msk_cluster.rb +19 -0
- data/lib/openstax/aws/packer_1_2_5.rb +63 -0
- data/lib/openstax/aws/packer_1_4_1.rb +72 -0
- data/lib/openstax/aws/packer_factory.rb +25 -0
- data/lib/openstax/aws/rds_instance.rb +25 -0
- data/lib/openstax/aws/s3_text_file.rb +50 -0
- data/lib/openstax/aws/sam_stack.rb +85 -0
- data/lib/openstax/aws/secrets.rb +302 -0
- data/lib/openstax/aws/secrets_factory.rb +126 -0
- data/lib/openstax/aws/secrets_set.rb +21 -0
- data/lib/openstax/aws/secrets_specification.rb +68 -0
- data/lib/openstax/aws/stack.rb +465 -0
- data/lib/openstax/aws/stack_event.rb +28 -0
- data/lib/openstax/aws/stack_factory.rb +153 -0
- data/lib/openstax/aws/stack_parameters.rb +19 -0
- data/lib/openstax/aws/stack_status.rb +125 -0
- data/lib/openstax/aws/system.rb +21 -0
- data/lib/openstax/aws/tag.rb +31 -0
- data/lib/openstax/aws/template.rb +129 -0
- data/lib/openstax/aws/version.rb +5 -0
- data/lib/openstax/aws/wait_message.rb +20 -0
- data/lib/openstax_aws.rb +154 -0
- data/openstax_aws.gemspec +58 -0
- metadata +350 -0
@@ -0,0 +1,96 @@
|
|
1
|
+
module OpenStax::Aws
|
2
|
+
class AutoScalingInstance
|
3
|
+
|
4
|
+
attr_reader :raw
|
5
|
+
|
6
|
+
delegate_missing_to :@raw
|
7
|
+
|
8
|
+
def initialize(group_name:, id:, region:)
|
9
|
+
@raw = Aws::AutoScaling::Instance.new(
|
10
|
+
group_name,
|
11
|
+
id,
|
12
|
+
client: Aws::AutoScaling::Client.new(region: region)
|
13
|
+
)
|
14
|
+
end
|
15
|
+
|
16
|
+
def self.me
|
17
|
+
instance_id = Ec2InstanceData.instance_id
|
18
|
+
region = Ec2InstanceData.region
|
19
|
+
|
20
|
+
client = Aws::AutoScaling::Client.new(region: region)
|
21
|
+
instance_info = client.describe_auto_scaling_instances({instance_ids: [instance_id]})
|
22
|
+
.auto_scaling_instances[0]
|
23
|
+
|
24
|
+
new(
|
25
|
+
group_name: instance_info.auto_scaling_group_name,
|
26
|
+
id: instance_id,
|
27
|
+
region: region
|
28
|
+
)
|
29
|
+
end
|
30
|
+
|
31
|
+
def terminate(options = {})
|
32
|
+
hook_name = options.delete(:continue_hook_name)
|
33
|
+
raw.terminate(options)
|
34
|
+
if hook_name
|
35
|
+
sleep(terminate_wait_sleep_seconds) until terminating_wait?
|
36
|
+
continue_to_termination(hook_name: hook_name)
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
def unless_waiting_for_termination(hook_name:)
|
41
|
+
# "Terminating" is a transition state to "Terminating:Wait", but we don't
|
42
|
+
# check for it because if we try to continue from "Terminating", AWS freaks
|
43
|
+
# out because it needs to continue from the wait state
|
44
|
+
|
45
|
+
if terminating_wait?
|
46
|
+
continue_to_termination(hook_name: hook_name)
|
47
|
+
return
|
48
|
+
end
|
49
|
+
|
50
|
+
yield
|
51
|
+
|
52
|
+
# In case the yield takes a long time and this code isn't called
|
53
|
+
# again for a while (e.g. an infrequent cron job), check the terminating
|
54
|
+
# state again. If this method is called in a loop, the check here
|
55
|
+
# and the next check at the start of this method will not cause duplicate
|
56
|
+
# network calls because the lifecycle state is cached for a few seconds.
|
57
|
+
|
58
|
+
if terminating_wait?
|
59
|
+
continue_to_termination(hook_name: hook_name)
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
def recent_lifecycle_state
|
64
|
+
if @recent_lifecycle_state_last_refreshed_at.nil? ||
|
65
|
+
Time.now - @recent_lifecycle_state_last_refreshed_at > lifecycle_state_refresh_seconds
|
66
|
+
reload
|
67
|
+
@recent_lifecycle_state_last_refreshed_at = Time.now
|
68
|
+
@recent_lifecycle_state = lifecycle_state
|
69
|
+
else
|
70
|
+
@recent_lifecycle_state
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
def lifecycle_state_refresh_seconds
|
75
|
+
5
|
76
|
+
end
|
77
|
+
|
78
|
+
def terminate_wait_sleep_seconds
|
79
|
+
6
|
80
|
+
end
|
81
|
+
|
82
|
+
def terminating_wait?
|
83
|
+
"Terminating:Wait" == recent_lifecycle_state
|
84
|
+
end
|
85
|
+
|
86
|
+
def continue_to_termination(hook_name:)
|
87
|
+
raw.client.complete_lifecycle_action({
|
88
|
+
lifecycle_hook_name: hook_name,
|
89
|
+
auto_scaling_group_name: raw.group_name,
|
90
|
+
lifecycle_action_result: "CONTINUE",
|
91
|
+
instance_id: raw.id,
|
92
|
+
})
|
93
|
+
end
|
94
|
+
|
95
|
+
end
|
96
|
+
end
|
@@ -0,0 +1,53 @@
|
|
1
|
+
module OpenStax
|
2
|
+
module Aws
|
3
|
+
class BuildImageCommand1
|
4
|
+
|
5
|
+
# A standardized way to use Packer to build images.
|
6
|
+
|
7
|
+
def initialize(ami_name_base:, region:,
|
8
|
+
verbose: false, debug: false,
|
9
|
+
github_org:, repo:, branch: nil, sha: nil,
|
10
|
+
packer_absolute_file_path: , playbook_absolute_file_path:,
|
11
|
+
dry_run: true)
|
12
|
+
if sha.nil?
|
13
|
+
branch ||= 'master'
|
14
|
+
|
15
|
+
sha = OpenStax::Aws::GitHelper.sha_for_branch_name(
|
16
|
+
org_slash_repo: "#{github_org}/#{repo}",
|
17
|
+
branch: branch
|
18
|
+
)
|
19
|
+
end
|
20
|
+
|
21
|
+
ami_name = "#{ami_name_base}@#{sha[0..6]} #{Time.now.utc.strftime("%y%m%d%H%MZ")}"
|
22
|
+
|
23
|
+
@packer = OpenStax::Aws::PackerFactory.new_packer(
|
24
|
+
absolute_file_path: packer_absolute_file_path,
|
25
|
+
dry_run: dry_run
|
26
|
+
)
|
27
|
+
|
28
|
+
@packer.only("amazon-ebs")
|
29
|
+
|
30
|
+
@packer.var("region", region)
|
31
|
+
@packer.var("ami_name", ami_name)
|
32
|
+
@packer.var("sha", sha)
|
33
|
+
@packer.var("playbook_file", playbook_absolute_file_path)
|
34
|
+
@packer.var("ami_description", {
|
35
|
+
sha: sha,
|
36
|
+
github_org: github_org,
|
37
|
+
repo: repo
|
38
|
+
}.to_json)
|
39
|
+
|
40
|
+
@packer.verbose! if verbose
|
41
|
+
@packer.debug! if debug
|
42
|
+
end
|
43
|
+
|
44
|
+
def run
|
45
|
+
@packer.run
|
46
|
+
end
|
47
|
+
|
48
|
+
def to_s
|
49
|
+
@packer.to_s
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
@@ -0,0 +1,100 @@
|
|
1
|
+
module OpenStax::Aws
|
2
|
+
class ChangeSet
|
3
|
+
|
4
|
+
attr_reader :client
|
5
|
+
|
6
|
+
def initialize(client:)
|
7
|
+
@client = client
|
8
|
+
end
|
9
|
+
|
10
|
+
def create(options:)
|
11
|
+
create_change_set_output = client.create_change_set(options)
|
12
|
+
@id = create_change_set_output.id
|
13
|
+
|
14
|
+
wait_message = OpenStax::Aws::WaitMessage.new(
|
15
|
+
message: "Waiting for change set #{id} to be ready"
|
16
|
+
)
|
17
|
+
|
18
|
+
begin
|
19
|
+
client.wait_until(:change_set_create_complete, change_set_name: id) do |w|
|
20
|
+
w.delay = OpenStax::Aws.configuration.stack_waiter_delay
|
21
|
+
w.before_attempt do |attempts, response|
|
22
|
+
wait_message.say_it
|
23
|
+
end
|
24
|
+
end
|
25
|
+
rescue Aws::Waiters::Errors::FailureStateError => ee
|
26
|
+
if ee.response&.status_reason =~ /didn't contain changes/
|
27
|
+
logger.info("No changes detected, deleting change set")
|
28
|
+
delete
|
29
|
+
return self
|
30
|
+
else
|
31
|
+
logger.error(ee.response.status_reason)
|
32
|
+
raise
|
33
|
+
end
|
34
|
+
rescue Aws::Waiters::Errors::WaiterFailed => ee
|
35
|
+
logger.error("An error occurred: #{ee.message}")
|
36
|
+
raise
|
37
|
+
end
|
38
|
+
|
39
|
+
@description = client.describe_change_set(change_set_name: id)
|
40
|
+
|
41
|
+
self
|
42
|
+
end
|
43
|
+
|
44
|
+
def created?
|
45
|
+
@description.present?
|
46
|
+
end
|
47
|
+
|
48
|
+
def delete
|
49
|
+
client.delete_change_set(change_set_name: id)
|
50
|
+
end
|
51
|
+
|
52
|
+
def execute
|
53
|
+
client.execute_change_set(change_set_name: id)
|
54
|
+
end
|
55
|
+
|
56
|
+
def id
|
57
|
+
@id || raise("ID isn't yet known!")
|
58
|
+
end
|
59
|
+
|
60
|
+
def description
|
61
|
+
@description || raise("Description not set; create failed?")
|
62
|
+
end
|
63
|
+
|
64
|
+
def has_change_caused_by?(entity_name)
|
65
|
+
description.changes.any? do |change|
|
66
|
+
change.resource_change.details.any? do |detail|
|
67
|
+
detail.causing_entity == entity_name
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
def parameter_value(parameter_name)
|
73
|
+
description.parameters.select do |parameter|
|
74
|
+
parameter.parameter_key == parameter_name
|
75
|
+
end.first.parameter_value
|
76
|
+
end
|
77
|
+
|
78
|
+
def resource_change_summaries
|
79
|
+
summaries = description.changes.flat_map(&:resource_change).map do |change|
|
80
|
+
summary = "#{change.action} '#{change.logical_resource_id}' (#{change.resource_type})"
|
81
|
+
|
82
|
+
case change.action
|
83
|
+
when "Modify"
|
84
|
+
causes = change.details.map{|detail| [detail.change_source, detail.causing_entity].compact.join(":")}.join(", ")
|
85
|
+
|
86
|
+
summary = "#{summary}: Replacement=#{change.replacement}; Due to change in #{change.scope}; Causes: #{causes}"
|
87
|
+
end
|
88
|
+
|
89
|
+
summary
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
protected
|
94
|
+
|
95
|
+
def logger
|
96
|
+
OpenStax::Aws.configuration.logger
|
97
|
+
end
|
98
|
+
|
99
|
+
end
|
100
|
+
end
|
@@ -0,0 +1,372 @@
|
|
1
|
+
module OpenStax::Aws
|
2
|
+
class DeploymentBase
|
3
|
+
class Status
|
4
|
+
def initialize(deployment)
|
5
|
+
@deployment = deployment
|
6
|
+
end
|
7
|
+
|
8
|
+
def stack_statuses(reload: false)
|
9
|
+
@deployment.stacks.each_with_object({}) do |stack, hash|
|
10
|
+
hash[stack.name] = stack.status(reload: reload)
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
def failed?(reload: false)
|
15
|
+
stack_statuses(reload: reload).values.any?(&:failed?)
|
16
|
+
end
|
17
|
+
|
18
|
+
def succeeded?(reload: false)
|
19
|
+
stack_statuses(reload: reload).values.all?(&:succeeded?)
|
20
|
+
end
|
21
|
+
|
22
|
+
def to_h
|
23
|
+
{
|
24
|
+
stacks: stack_statuses.each_with_object({}) do |(stack_name, stack_status), new_hash|
|
25
|
+
new_hash[stack_name] = stack_status.to_h # convert the stack status object to a hash
|
26
|
+
end
|
27
|
+
}
|
28
|
+
end
|
29
|
+
|
30
|
+
def to_json
|
31
|
+
to_h.to_json
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
attr_reader :env_name, :region, :name, :dry_run
|
36
|
+
|
37
|
+
RESERVED_ENV_NAMES = [
|
38
|
+
"external", # used to namespace external secrets in the parameter store
|
39
|
+
]
|
40
|
+
|
41
|
+
def initialize(env_name: nil, region:, name:, dry_run: true)
|
42
|
+
if RESERVED_ENV_NAMES.include?(env_name)
|
43
|
+
raise "#{env_name} is a reserved word and cannot be used as an environment name"
|
44
|
+
end
|
45
|
+
|
46
|
+
# Allow a blank env_name but normalize it to `nil`
|
47
|
+
@env_name = env_name.blank? ? nil : env_name
|
48
|
+
|
49
|
+
if @env_name && !@env_name.match(/^[a-zA-Z][a-zA-Z0-9-]*$/)
|
50
|
+
raise "The environment name must consist only of letters, numbers, and hyphens, " \
|
51
|
+
"and must start with a letter."
|
52
|
+
end
|
53
|
+
|
54
|
+
@region = region
|
55
|
+
@name = name
|
56
|
+
@dry_run = dry_run
|
57
|
+
end
|
58
|
+
|
59
|
+
def name!
|
60
|
+
raise "`name` is blank" if name.blank?
|
61
|
+
name
|
62
|
+
end
|
63
|
+
|
64
|
+
def env_name!
|
65
|
+
raise "`env_name` is blank" if env_name.blank?
|
66
|
+
env_name
|
67
|
+
end
|
68
|
+
|
69
|
+
def tags
|
70
|
+
self.class.tags.each_with_object(HashWithIndifferentAccess.new) do |(key, value), hsh|
|
71
|
+
hsh[key] = value.is_a?(Proc) ? instance_eval(&value) : value
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
class << self
|
76
|
+
|
77
|
+
def template_directory(*directory_parts)
|
78
|
+
if method_defined?("template_directory")
|
79
|
+
raise "Can only set template_directory once per class definition"
|
80
|
+
end
|
81
|
+
|
82
|
+
define_method "template_directory" do
|
83
|
+
File.join(*directory_parts)
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
def sam_build_directory(*directory_parts)
|
88
|
+
if method_defined?("sam_build_directory")
|
89
|
+
raise "Can only set buisam_build_directoryld_directory once per class definition"
|
90
|
+
end
|
91
|
+
|
92
|
+
define_method "sam_build_directory" do
|
93
|
+
File.expand_path(File.join(*directory_parts))
|
94
|
+
end
|
95
|
+
end
|
96
|
+
|
97
|
+
def secrets(id, &block)
|
98
|
+
if id.blank?
|
99
|
+
raise "The first argument to `secrets` must be a non-blank ID"
|
100
|
+
end
|
101
|
+
|
102
|
+
if !id.to_s.match(/^[a-zA-Z][a-zA-Z0-9_]*$/)
|
103
|
+
raise "The first argument to `secrets` must consist only of letters, numbers, and underscores, " \
|
104
|
+
"and must start with a letter."
|
105
|
+
end
|
106
|
+
|
107
|
+
if method_defined?("#{id}_secrets")
|
108
|
+
raise "Can only define the `#{id}` secrets once per class definition"
|
109
|
+
end
|
110
|
+
|
111
|
+
if method_defined?("#{id}_stack")
|
112
|
+
raise "Cannot define `#{id}` secrets because there is a stack with that ID"
|
113
|
+
end
|
114
|
+
|
115
|
+
define_method("#{id}_secrets") do |for_create_or_update: false|
|
116
|
+
secrets_factory = SecretsFactory.new(
|
117
|
+
region: region,
|
118
|
+
namespace: [env_name, name],
|
119
|
+
context: self,
|
120
|
+
dry_run: dry_run,
|
121
|
+
for_create_or_update: for_create_or_update,
|
122
|
+
shared_substitutions_block: @shared_secrets_substitutions_block
|
123
|
+
)
|
124
|
+
|
125
|
+
secrets_factory.namespace(id)
|
126
|
+
secrets_factory.instance_exec({}, &block)
|
127
|
+
secrets_factory.instance
|
128
|
+
end
|
129
|
+
end
|
130
|
+
|
131
|
+
def stack_ids
|
132
|
+
@stack_ids ||= []
|
133
|
+
end
|
134
|
+
|
135
|
+
def stack(id, &block)
|
136
|
+
if id.blank?
|
137
|
+
raise "The first argument to `stack` must be a non-blank ID"
|
138
|
+
end
|
139
|
+
|
140
|
+
if !id.to_s.match(/^[a-zA-Z][a-zA-Z0-9_]*$/)
|
141
|
+
raise "The first argument to `stack` must consist only of letters, numbers, and underscores, " \
|
142
|
+
"and must start with a letter."
|
143
|
+
end
|
144
|
+
|
145
|
+
if method_defined?("#{id}_secrets")
|
146
|
+
raise "Cannot define `#{id}` stack because there are secrets with that ID"
|
147
|
+
end
|
148
|
+
|
149
|
+
stack_ids.push(id)
|
150
|
+
|
151
|
+
define_method("#{id}_stack") do
|
152
|
+
instance_variable_get("@#{id}_stack") || begin
|
153
|
+
stack_factory = StackFactory.new(id: id, deployment: self)
|
154
|
+
stack_factory.instance_eval(&block) if block_given?
|
155
|
+
|
156
|
+
# Fill in missing attributes using deployment variables and conventions
|
157
|
+
|
158
|
+
if stack_factory.name.blank?
|
159
|
+
stack_factory.name([env_name,name,id].compact.join("-").gsub("_","-"))
|
160
|
+
end
|
161
|
+
|
162
|
+
if stack_factory.region.blank?
|
163
|
+
stack_factory.region(region)
|
164
|
+
end
|
165
|
+
|
166
|
+
if stack_factory.dry_run.nil?
|
167
|
+
stack_factory.dry_run(dry_run)
|
168
|
+
end
|
169
|
+
|
170
|
+
if stack_factory.enable_termination_protection.nil?
|
171
|
+
stack_factory.enable_termination_protection(is_production?)
|
172
|
+
end
|
173
|
+
|
174
|
+
if stack_factory.absolute_template_path.blank?
|
175
|
+
stack_factory.autoset_absolute_template_path(respond_to?(:template_directory) ? template_directory : "")
|
176
|
+
end
|
177
|
+
|
178
|
+
# Populate parameter defaults that match convention names
|
179
|
+
|
180
|
+
if OpenStax::Aws.configuration.infer_parameter_defaults
|
181
|
+
defaults = parameter_defaults_from_template(stack_factory.absolute_template_path)
|
182
|
+
defaults.each{|key,value| stack_factory.parameter_defaults[key] ||= value}
|
183
|
+
end
|
184
|
+
|
185
|
+
stack_factory.build.tap do |stack|
|
186
|
+
instance_variable_set("@#{id}_stack", stack)
|
187
|
+
end
|
188
|
+
end
|
189
|
+
end
|
190
|
+
end
|
191
|
+
|
192
|
+
def secrets_substitutions(&block)
|
193
|
+
define_method("shared_secrets_substitutions_block") do
|
194
|
+
block
|
195
|
+
end
|
196
|
+
end
|
197
|
+
|
198
|
+
def tag(key, value=nil, &block)
|
199
|
+
raise 'The first argument to `tag` must not be blank' if key.blank?
|
200
|
+
raise '`tag` must be given a value or a block' if value.nil? && !block_given?
|
201
|
+
tags[key] = block_given? ? block : value
|
202
|
+
end
|
203
|
+
|
204
|
+
def tags
|
205
|
+
@tags ||= HashWithIndifferentAccess.new
|
206
|
+
end
|
207
|
+
|
208
|
+
def inherited(child_class)
|
209
|
+
# Copy any tags defined in the parent to the child so that it can access them
|
210
|
+
# while not using class variables that are shared across all classes in the
|
211
|
+
# that inherit here
|
212
|
+
child_class.instance_variable_set("@tags", tags.dup)
|
213
|
+
end
|
214
|
+
|
215
|
+
def logger
|
216
|
+
OpenStax::Aws.configuration.logger
|
217
|
+
end
|
218
|
+
end
|
219
|
+
|
220
|
+
def parameter_defaults_from_template(template_or_absolute_template_path)
|
221
|
+
template = template_or_absolute_template_path.is_a?(String) ?
|
222
|
+
Template.from_absolute_file_path(template_or_absolute_template_path) :
|
223
|
+
template_or_absolute_template_path
|
224
|
+
|
225
|
+
template.parameter_names.each_with_object({}) do |parameter_name, defaults|
|
226
|
+
value = parameter_default(parameter_name) ||
|
227
|
+
built_in_parameter_default(parameter_name)
|
228
|
+
|
229
|
+
if !value.nil?
|
230
|
+
defaults[parameter_name.underscore.to_sym] = value
|
231
|
+
end
|
232
|
+
end
|
233
|
+
end
|
234
|
+
|
235
|
+
def stacks
|
236
|
+
self.class.stack_ids.map{|id| self.send("#{id}_stack")}
|
237
|
+
end
|
238
|
+
|
239
|
+
def status(reload: false)
|
240
|
+
@status = nil if reload
|
241
|
+
@status ||= Status.new(self)
|
242
|
+
end
|
243
|
+
|
244
|
+
def deployed_parameters
|
245
|
+
stacks.each_with_object({}) do |stack, hash|
|
246
|
+
hash[stack.name] = stack.deployed_parameters
|
247
|
+
end
|
248
|
+
end
|
249
|
+
|
250
|
+
def built_in_parameter_default(parameter_name)
|
251
|
+
case parameter_name
|
252
|
+
when "EnvName"
|
253
|
+
env_name
|
254
|
+
when /(.+)StackName$/
|
255
|
+
send("#{$1}Stack".underscore).name rescue nil
|
256
|
+
end
|
257
|
+
end
|
258
|
+
|
259
|
+
def shared_secrets_substitutions_block
|
260
|
+
nil # can be overridden by the DSL
|
261
|
+
end
|
262
|
+
|
263
|
+
protected
|
264
|
+
|
265
|
+
def parameter_default(parameter_name)
|
266
|
+
nil
|
267
|
+
end
|
268
|
+
|
269
|
+
def is_production?
|
270
|
+
env_name == OpenStax::Aws.configuration.production_env_name
|
271
|
+
end
|
272
|
+
|
273
|
+
def subdomain_with_trailing_dot(site_name:)
|
274
|
+
parts = []
|
275
|
+
parts.push(site_name) if !site_name.blank?
|
276
|
+
parts.push(env_name!) unless is_production?
|
277
|
+
|
278
|
+
subdomain = parts.join("-")
|
279
|
+
subdomain.blank? ? "" : subdomain + "."
|
280
|
+
end
|
281
|
+
|
282
|
+
delegate :logger, to: :configuration
|
283
|
+
|
284
|
+
def configuration
|
285
|
+
OpenStax::Aws.configuration
|
286
|
+
end
|
287
|
+
|
288
|
+
def client
|
289
|
+
@client ||= ::Aws::CloudFormation::Client.new(region: region)
|
290
|
+
end
|
291
|
+
|
292
|
+
def auto_scaling_client
|
293
|
+
@auto_scaling_client ||= ::Aws::AutoScaling::Client.new(region: region)
|
294
|
+
end
|
295
|
+
|
296
|
+
def set_desired_capacity(asg_name:, desired_capacity:)
|
297
|
+
auto_scaling_client.set_desired_capacity(auto_scaling_group_name: asg_name, desired_capacity: desired_capacity)
|
298
|
+
end
|
299
|
+
|
300
|
+
def auto_scaling_group(name:)
|
301
|
+
::Aws::AutoScaling::AutoScalingGroup.new(name: name, client: auto_scaling_client)
|
302
|
+
end
|
303
|
+
|
304
|
+
def cloudfront_client
|
305
|
+
@cloudfront_client ||= ::Aws::CloudFront::Client.new(region: region)
|
306
|
+
end
|
307
|
+
|
308
|
+
def s3_client
|
309
|
+
@s3_client ||= Aws::S3::Client.new(region: region)
|
310
|
+
end
|
311
|
+
|
312
|
+
def s3_key_exists?(bucket:, key:)
|
313
|
+
begin
|
314
|
+
s3_client.get_object(bucket: bucket, key: key)
|
315
|
+
true
|
316
|
+
rescue Aws::S3::Errors::NoSuchKey
|
317
|
+
false
|
318
|
+
end
|
319
|
+
end
|
320
|
+
|
321
|
+
def wait_for_tag_change(resource:, key:, polling_seconds: 20, timeout_seconds: nil)
|
322
|
+
keep_polling = true
|
323
|
+
started_at = Time.now
|
324
|
+
original_value = resource_tag_value(resource: resource, key: key)
|
325
|
+
|
326
|
+
wait_message = OpenStax::Aws::WaitMessage.new(
|
327
|
+
message: "Waiting for the #{key} tag on #{resource.name} to change from #{original_value}"
|
328
|
+
)
|
329
|
+
|
330
|
+
while keep_polling
|
331
|
+
wait_message.say_it
|
332
|
+
|
333
|
+
sleep(polling_seconds)
|
334
|
+
|
335
|
+
keep_polling = false if resource_tag_value(resource: resource, key: key) != original_value
|
336
|
+
keep_polling = false if !timeout_seconds.nil? && Time.now - timeout_seconds > started_at
|
337
|
+
end
|
338
|
+
end
|
339
|
+
|
340
|
+
def resource_tag_value(resource:, key:)
|
341
|
+
begin
|
342
|
+
resource.tag(key).value
|
343
|
+
rescue NoMethodError => ee
|
344
|
+
nil
|
345
|
+
end
|
346
|
+
end
|
347
|
+
|
348
|
+
def get_image_tag(image_id:, key:)
|
349
|
+
tag = image(image_id).tags.find{|tag| tag.key == key}
|
350
|
+
raise "No tag with key #{key} on AMI #{image_id}" if tag.nil?
|
351
|
+
tag.value
|
352
|
+
end
|
353
|
+
|
354
|
+
def image(image_id)
|
355
|
+
Aws::EC2::Image.new(image_id, region: region)
|
356
|
+
end
|
357
|
+
|
358
|
+
# Returns the SHA on an AMI
|
359
|
+
def image_sha(image_id)
|
360
|
+
get_image_tag(image_id: image_id, key: "sha")
|
361
|
+
end
|
362
|
+
|
363
|
+
protected
|
364
|
+
|
365
|
+
def secrets_namespace(id: 'default')
|
366
|
+
raise "Override this method in your deployment class and provide a namespace " \
|
367
|
+
"for secrets data in the AWS Parameter Store. The key there will be this namespace " \
|
368
|
+
"prefixed by the environment name and suffixed with the secret name."
|
369
|
+
end
|
370
|
+
|
371
|
+
end
|
372
|
+
end
|