elastic_beans 0.13.0.alpha3 → 0.13.0.alpha4

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: f706a4e17c133611b418f5ac6b7f57a7859d7ef2
4
- data.tar.gz: dd94a36a7bf6cf844972158a84dc30ecb57330f0
3
+ metadata.gz: a69e6d19ef9b23f9db1ccddb65d7f49644e72233
4
+ data.tar.gz: 1b2b65654a6abcc05e7033946f13464d5f51a85b
5
5
  SHA512:
6
- metadata.gz: 8d0efdaff74f60b3c64bac5950fa57aa2faa35f679e8c82dc6436539e66b748f902aa6ad1382d07a9d605f8d1c7d1ff409124edf6ac0c14a2fa7e59792d608be
7
- data.tar.gz: 32b43316bb1848aa77638d6777c3e80daa547ee10ab10b5221edb963bb27d6afaf9dabdd10773047c003611c6e7d28f46c3d60b96a8eef30f6f1f19d6a405629
6
+ metadata.gz: ced50156c632ae34c5219d7d8f4384cf7ccf1243611dd23e06c57eb552707907305d6bbd03a9bfda390126c220a96ff35a7b363649d72ea94bebc41da91dd5a7
7
+ data.tar.gz: 87f4ec5eb18640de5d43327b24a29a0539c4d22fbffb08c3fb8968054de09d0d2bff152f38467722ea625e70b68b932a362694846ca00c4872cc2c016ff07bef
data/README.md CHANGED
@@ -24,8 +24,9 @@ As the SDK documentation suggests, using environment variables is recommended.
24
24
 
25
25
  # Pre-configure the application before creating environments
26
26
  beans configure -n myapp-networking -a myapp \
27
- -p INTERNAL_PUBLIC_KEY -s SSL_CERTIFICATE_ARN \
28
- -k KEYPAIR [-i IMAGE_ID] [-t INSTANCE_TYPE]
27
+ -p INTERNAL_PUBLIC_KEY -s SSL_CERTIFICATE_ARN -k KEYPAIR \
28
+ [--solution-stack '64bit Amazon Linux 2017.03 v2.4.2 running Ruby 2.3 (Puma)'] \
29
+ [-o 'OPTION_SETTING_NAMESPACE/OPTION_NAME=VALUE'...]
29
30
  beans setenv -a myapp \
30
31
  DATABASE_URL=mysql2://db.example.com:3306/myapp \
31
32
  SECRET_KEY_BASE=abc123
@@ -65,8 +66,9 @@ As the SDK documentation suggests, using environment variables is recommended.
65
66
 
66
67
  # Update all existing environments and configuration
67
68
  beans configure -n myapp-networking -a myapp \
68
- [-p INTERNAL_PUBLIC_KEY] [-s SSL_CERTIFICATE_ARN] \
69
- [-k KEYPAIR] [-i IMAGE_ID] [-t INSTANCE_TYPE]
69
+ [-p INTERNAL_PUBLIC_KEY] [-s SSL_CERTIFICATE_ARN] [-k KEYPAIR] \
70
+ [--solution-stack '64bit Amazon Linux 2017.03 v2.4.2 running Ruby 2.3 (Puma)'] \
71
+ [-o 'OPTION_SETTING_NAMESPACE/OPTION_NAME=VALUE'...]
70
72
 
71
73
  ### API
72
74
 
@@ -29,7 +29,7 @@ module ElasticBeans
29
29
  live_environments = response.environments.select { |environment| environment.status !~ /Terminat/ }
30
30
  environment = live_environments.max_by(&:date_updated)
31
31
  if environment
32
- ElasticBeans::ApplicationVersion.new(environment.version_label)
32
+ ElasticBeans::ApplicationVersion.new(environment.version_label, application: self, elastic_beanstalk: elastic_beanstalk)
33
33
  end
34
34
  rescue ::Aws::ElasticBeanstalk::Errors::Throttling
35
35
  sleep 5
@@ -226,11 +226,22 @@ module ElasticBeans
226
226
 
227
227
  # Returns an ElasticBeans::ApplicationVersion for each version of the Elastic Beanstalk application.
228
228
  def versions
229
- response = elastic_beanstalk.describe_application_versions(application_name: name)
230
- response.application_versions.map { |version| ElasticBeans::ApplicationVersion.new(version.version_label) }
231
- rescue ::Aws::ElasticBeanstalk::Errors::Throttling
232
- sleep 5
233
- retry
229
+ application_versions = []
230
+ next_token = nil
231
+ loop do
232
+ begin
233
+ response = elastic_beanstalk.describe_application_versions(application_name: name, next_token: next_token)
234
+ next_token = response.next_token
235
+ application_versions += response.application_versions.map { |version|
236
+ ElasticBeans::ApplicationVersion.new_from_existing(version, application: self, elastic_beanstalk: elastic_beanstalk)
237
+ }
238
+ break unless next_token
239
+ rescue ::Aws::ElasticBeanstalk::Errors::Throttling
240
+ sleep 5
241
+ retry
242
+ end
243
+ end
244
+ application_versions
234
245
  end
235
246
 
236
247
  # Returns the +Worker{QUEUE}QueueUrl+ from the application CloudFormation stack.
@@ -7,11 +7,23 @@ require "elastic_beans/error"
7
7
 
8
8
  module ElasticBeans
9
9
  class ApplicationVersion
10
- attr_reader :version_label
10
+ attr_reader :version_label, :application
11
+ attr_writer :version_description
11
12
 
12
13
  # Create a representation of an existing application version in Elastic Beanstalk.
13
- def initialize(version_label)
14
+ # Details will be fetched from the Elastic Beanstalk API.
15
+ def initialize(version_label, application:, elastic_beanstalk:)
14
16
  @version_label = version_label
17
+ @application = application
18
+ @elastic_beanstalk = elastic_beanstalk
19
+ end
20
+
21
+ # Create a representation of an existing application version in Elastic Beanstalk.
22
+ # +version_description+ should be the +Aws::ElasticBeanstalk::Types::ApplicationVersionDescription+ from the AWS API.
23
+ def self.new_from_existing(version_description, application:, elastic_beanstalk:)
24
+ version = new(version_description.version_label, application: application, elastic_beanstalk: elastic_beanstalk)
25
+ version.version_description = version_description
26
+ version
15
27
  end
16
28
 
17
29
  class << self
@@ -24,7 +36,7 @@ module ElasticBeans
24
36
  def create(working_directory:, application:, elastic_beanstalk:, s3:)
25
37
  version_label = fetch_version_label(working_directory)
26
38
  if application.versions.any? { |version| version.version_label == version_label }
27
- return new(version_label)
39
+ return new(version_label, application: application, elastic_beanstalk: elastic_beanstalk)
28
40
  end
29
41
 
30
42
  archive_path = File.join(working_directory, ".elasticbeanstalk", "#{version_label}.zip")
@@ -36,7 +48,7 @@ module ElasticBeans
36
48
  elastic_beanstalk: elastic_beanstalk,
37
49
  s3: s3,
38
50
  )
39
- new(version_label)
51
+ new(version_label, application: application, elastic_beanstalk: elastic_beanstalk)
40
52
  end
41
53
 
42
54
  private
@@ -164,12 +176,40 @@ module ElasticBeans
164
176
  end
165
177
  end
166
178
 
179
+ def delete
180
+ elastic_beanstalk.delete_application_version(
181
+ application_name: application.name,
182
+ version_label: version_label,
183
+ delete_source_bundle: true,
184
+ )
185
+ rescue ::Aws::ElasticBeanstalk::Errors::Throttling
186
+ sleep 5
187
+ retry
188
+ end
189
+
190
+ def date_updated
191
+ version_description.date_updated
192
+ end
193
+
167
194
  def ==(other)
168
- version_label == other.version_label
195
+ version_label == other.version_label &&
196
+ application.name == other.application.name
169
197
  end
170
198
 
171
199
  private
172
200
 
201
+ attr_reader :elastic_beanstalk
202
+
203
+ def version_description
204
+ @version_description ||= elastic_beanstalk.describe_application_versions(
205
+ application_name: application.name,
206
+ version_labels: [version_label],
207
+ ).application_versions[0]
208
+ rescue ::Aws::ElasticBeanstalk::Errors::Throttling
209
+ sleep 5
210
+ retry
211
+ end
212
+
173
213
  # :nodoc: all
174
214
  # @!visibility private
175
215
  class FailedApplicationVersion < ElasticBeans::Error
@@ -55,8 +55,8 @@ module ElasticBeans
55
55
  end
56
56
 
57
57
  def message
58
- "CloudFormation stack `#{@stack_name}' not found." \
59
- " Make sure the stack exists and matches the outputs required."
58
+ "CloudFormation stack `#{@stack_name}' is in progress or does not exist." \
59
+ " Make sure the stack exists, is stable, and matches the outputs required."
60
60
  end
61
61
  end
62
62
 
@@ -16,21 +16,23 @@ class ElasticBeans::CLI < Thor
16
16
  long_desc ElasticBeans::Command::Configure::LONG_DESC
17
17
  option :application, aliases: %w(-a), required: true, desc: APPLICATION_DESC
18
18
  option :network, aliases: %w(-n), required: true, desc: "The name of the CloudFormation stack that contains networking settings"
19
- option :image_id, aliases: %w(-i), desc: "A custom AMI to use instead of the default Ruby Elastic Beanstalk AMI"
20
- option :instance_type, aliases: %w(-t), desc: "A default instance type to use for all environments instead of c4.large"
21
19
  option :internal, type: :boolean, desc: "Configure the webserver to only be available for internal VPC access"
22
20
  option :keypair, aliases: %w(-k), desc: "Required on first run. The EC2 keypair to use for Elastic Beanstalk instances"
21
+ option :option_settings, aliases: %w(-o), type: :array, default: [], desc: "Option settings to configure, format is NAMESPACE/OPTION_NAME=VALUE, e.g. aws:ec2:vpc/ELBScheme=internal"
22
+ option :option_settings_to_remove, aliases: %w(-r), type: :array, default: [], desc: "Option settings to remove, format is NAMESPACE/OPTION_NAME, e.g. aws:ec2:vpc/ELBScheme"
23
23
  option :public_key, aliases: %w(-p), desc: "For end-to-end encryption. The public key of the SSL certificate the ELB will verify to communicate with your Rails app"
24
24
  option :ssl_certificate_arn, aliases: %w(-s), desc: "The ARN of the SSL server certificate stored in IAM to attach to the ELB"
25
+ option :solution_stack, desc: "Solution stack name to deploy the application on. Defaults to the latest Ruby Puma solution stack."
25
26
  def configure
26
27
  @verbose = options[:verbose]
27
28
  ElasticBeans::Command::Configure.new(
28
- image_id: options[:image_id],
29
- instance_type: options[:instance_type],
30
29
  internal: options[:internal],
31
30
  keypair: options[:keypair],
32
31
  public_key: options[:public_key],
33
32
  ssl_certificate_arn: options[:ssl_certificate_arn],
33
+ solution_stack: options[:solution_stack],
34
+ option_settings: options[:option_settings],
35
+ option_settings_to_remove: options[:option_settings_to_remove],
34
36
  application: application(name: options[:application]),
35
37
  network: network(stack_name: options[:network]),
36
38
  elastic_beanstalk: elastic_beanstalk_client,
@@ -16,24 +16,26 @@ Requires AWS credentials to be set in the environment, i.e. AWS_ACCESS_KEY_ID an
16
16
  LONG_DESC
17
17
 
18
18
  def initialize(
19
- image_id:,
20
- instance_type:,
21
19
  internal:,
22
20
  keypair:,
23
21
  public_key:,
24
22
  ssl_certificate_arn:,
23
+ solution_stack:,
24
+ option_settings:,
25
+ option_settings_to_remove:,
25
26
  application:,
26
27
  network:,
27
28
  elastic_beanstalk:,
28
29
  iam:,
29
30
  ui:
30
31
  )
31
- @image_id = image_id
32
- @instance_type = instance_type
33
32
  @internal = internal
34
33
  @keypair = keypair
35
34
  @public_key = public_key
36
35
  @ssl_certificate_arn = ssl_certificate_arn
36
+ @solution_stack = solution_stack
37
+ @option_setting_strings = option_settings
38
+ @option_strings_to_remove = option_settings_to_remove
37
39
  @application = application
38
40
  @network = network
39
41
  @elastic_beanstalk = elastic_beanstalk
@@ -71,9 +73,10 @@ Requires AWS credentials to be set in the environment, i.e. AWS_ACCESS_KEY_ID an
71
73
  )
72
74
  base_config.upsert(
73
75
  network: network,
74
- image_id: image_id,
75
- instance_type: instance_type,
76
76
  keypair: keypair,
77
+ solution_stack: solution_stack,
78
+ option_settings: option_settings,
79
+ options_to_remove: options_to_remove,
77
80
  iam: iam,
78
81
  )
79
82
  progressbar.increment
@@ -85,13 +88,14 @@ Requires AWS credentials to be set in the environment, i.e. AWS_ACCESS_KEY_ID an
85
88
  )
86
89
  webserver_config.upsert(
87
90
  network: network,
88
- image_id: image_id,
89
- instance_type: instance_type,
90
91
  internal: internal,
91
92
  keypair: keypair,
93
+ option_settings: option_settings,
94
+ options_to_remove: options_to_remove,
92
95
  iam: iam,
93
96
  public_key: public_key,
94
97
  ssl_certificate_arn: ssl_certificate_arn,
98
+ solution_stack: solution_stack,
95
99
  )
96
100
  webserver_environment = webserver_config.environment
97
101
  if webserver_environment
@@ -109,9 +113,10 @@ Requires AWS credentials to be set in the environment, i.e. AWS_ACCESS_KEY_ID an
109
113
  )
110
114
  exec_config.upsert(
111
115
  network: network,
112
- image_id: image_id,
113
- instance_type: instance_type,
114
116
  keypair: keypair,
117
+ solution_stack: solution_stack,
118
+ option_settings: option_settings,
119
+ options_to_remove: options_to_remove,
115
120
  iam: iam,
116
121
  )
117
122
  exec_environment = exec_config.environment
@@ -130,9 +135,10 @@ Requires AWS credentials to be set in the environment, i.e. AWS_ACCESS_KEY_ID an
130
135
  )
131
136
  scheduler_config.upsert(
132
137
  network: network,
133
- image_id: image_id,
134
- instance_type: instance_type,
135
138
  keypair: keypair,
139
+ solution_stack: solution_stack,
140
+ option_settings: option_settings,
141
+ options_to_remove: options_to_remove,
136
142
  iam: iam,
137
143
  )
138
144
  scheduler_environment = scheduler_config.environment
@@ -153,9 +159,10 @@ Requires AWS credentials to be set in the environment, i.e. AWS_ACCESS_KEY_ID an
153
159
  )
154
160
  worker_config.upsert(
155
161
  network: network,
156
- image_id: image_id,
157
- instance_type: instance_type,
158
162
  keypair: keypair,
163
+ solution_stack: solution_stack,
164
+ option_settings: option_settings,
165
+ options_to_remove: options_to_remove,
159
166
  iam: iam,
160
167
  )
161
168
  worker_environment = worker_config.environment
@@ -175,17 +182,76 @@ Requires AWS credentials to be set in the environment, i.e. AWS_ACCESS_KEY_ID an
175
182
 
176
183
  attr_reader(
177
184
  :application,
178
- :image_id,
179
- :instance_type,
180
185
  :internal,
181
186
  :keypair,
182
187
  :network,
183
188
  :public_key,
184
189
  :ssl_certificate_arn,
190
+ :solution_stack,
185
191
  :elastic_beanstalk,
186
192
  :iam,
187
193
  :ui,
188
194
  )
195
+
196
+ # Coerces +@option_setting_strings+ into object format.
197
+ def option_settings
198
+ @option_settings ||= @option_setting_strings.map { |option_setting_string|
199
+ setting, value = option_setting_string.split('=', 2)
200
+ if !setting || setting.empty? || !value || value.empty?
201
+ raise InvalidOptionSettingFormatError
202
+ end
203
+ namespace, option_name = setting.split('/', 2)
204
+ if !namespace || namespace.empty? || !option_name || option_name.empty?
205
+ raise InvalidOptionSettingFormatError
206
+ end
207
+ {namespace: namespace, option_name: option_name, value: value}
208
+ }
209
+ end
210
+
211
+ # Coerces +@option_strings_to_remove+ into object format.
212
+ def options_to_remove
213
+ @options_to_remove ||= @option_strings_to_remove.map { |option_setting_string|
214
+ namespace, option_name = option_setting_string.split('/', 2)
215
+ if !namespace || namespace.empty? || !option_name || option_name.empty? || option_name.include?('=')
216
+ raise InvalidOptionSettingFormatError
217
+ end
218
+ {namespace: namespace, option_name: option_name}
219
+ }
220
+ end
221
+
222
+ # :nodoc: all
223
+ # @!visibility private
224
+ class InvalidOptionSettingFormatError < ElasticBeans::Error
225
+ def message
226
+ require "elastic_beans/cli"
227
+ require "elastic_beans/cli/string_shell"
228
+ <<-MESSAGE
229
+ Invalid format for --option-settings.
230
+ Option settings should in the format 'NAMESPACE/OPTION_NAME=VALUE', e.g. 'aws:autoscaling:asg/Cooldown=180'.
231
+ If you'd like to *remove* option settings, use --option-settings-to-remove 'NAMESPACE/OPTION_NAME'.
232
+ Please re-run `#{command_as_string "configure"}` with updated syntax.
233
+
234
+ #{command_help "configure"}
235
+ MESSAGE
236
+ end
237
+ end
238
+
239
+ # :nodoc: all
240
+ # @!visibility private
241
+ class InvalidOptionSettingsToRemoveFormatError < ElasticBeans::Error
242
+ def message
243
+ require "elastic_beans/cli"
244
+ require "elastic_beans/cli/string_shell"
245
+ <<-MESSAGE
246
+ Invalid format for --option-settings-to-remove.
247
+ Option settings to remove should in the format 'NAMESPACE/OPTION_NAME', e.g. 'aws:autoscaling:asg/Cooldown'.
248
+ If you'd like to *set* option settings, use --option-settings 'NAMESPACE/OPTION_NAME=VALUE'.
249
+ Please re-run `#{command_as_string "configure"}` with updated syntax.
250
+
251
+ #{command_help "configure"}
252
+ MESSAGE
253
+ end
254
+ end
189
255
  end
190
256
  end
191
257
  end
@@ -9,9 +9,14 @@ module ElasticBeans
9
9
  DESC = "Deploy the HEAD git commit to all environments in Elastic Beanstalk"
10
10
  LONG_DESC = <<-LONG_DESC
11
11
  Deploy the HEAD git commit of the working directory to all environments in Elastic Beanstalk.
12
+ Cleans up old application versions in parallel to avoid reaching the 1,000 application version limit.
12
13
 
13
14
  Requires AWS credentials to be set in the environment, i.e. AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY.
14
15
  LONG_DESC
16
+ # Maximum age of application versions that are left behind and not cleaned up. 1 week.
17
+ APPLICATION_VERSION_MAX_AGE = 604_800
18
+ # Minimum number of application versions to leave around.
19
+ APPLICATION_VERSION_MIN_COUNT = 5
15
20
 
16
21
  def initialize(application:, elastic_beanstalk:, s3:, ui:)
17
22
  @application = application
@@ -38,15 +43,19 @@ Requires AWS credentials to be set in the environment, i.e. AWS_ACCESS_KEY_ID an
38
43
  s3: s3,
39
44
  ).version_label
40
45
  end
46
+ cleanup_thread = Thread.new do
47
+ remove_old_versions
48
+ end
41
49
 
42
50
  loop do
43
51
  sleep 0.5
44
52
  progressbar.increment
45
- if !version_thread.alive?
53
+ if !version_thread.alive? && !cleanup_thread.alive?
46
54
  break
47
55
  end
48
56
  end
49
57
  version_thread.join
58
+ cleanup_thread.join
50
59
 
51
60
  Signal.trap("INT") do
52
61
  puts "\nInterrupting beans"
@@ -85,6 +94,21 @@ Requires AWS credentials to be set in the environment, i.e. AWS_ACCESS_KEY_ID an
85
94
  private
86
95
 
87
96
  attr_reader :application, :elastic_beanstalk, :s3, :ui
97
+
98
+ # Removes application versions that are old and just taking up space.
99
+ # Elastic Beanstalk has a limit of 1000 application versions, so we should keep this tidy.
100
+ def remove_old_versions
101
+ cutoff = Time.now
102
+ outdated_versions, leftover_versions = application.versions.partition { |version|
103
+ version.date_updated + APPLICATION_VERSION_MAX_AGE < cutoff
104
+ }
105
+ if leftover_versions.size < APPLICATION_VERSION_MIN_COUNT
106
+ outdated_versions.pop(APPLICATION_VERSION_MIN_COUNT - leftover_versions.size)
107
+ end
108
+ outdated_versions.each do |version|
109
+ version.delete
110
+ end
111
+ end
88
112
  end
89
113
  end
90
114
  end
@@ -10,6 +10,13 @@ module ElasticBeans
10
10
  super(name: "base", **args)
11
11
  end
12
12
 
13
+ # Returns the specifed +solution_stack+ from #upsert.
14
+ # Finds the previously-configured solution stack when one was not specified.
15
+ # Finds the latest available Ruby Puma solution stack (matching +SOLUTION_STACK_PATTERN+) when one was never specified.
16
+ def solution_stack_name
17
+ @solution_stack_name ||= (configured_solution_stack || super)
18
+ end
19
+
13
20
  protected
14
21
 
15
22
  # Constructs the common configuration for all environments.
@@ -18,20 +25,21 @@ module ElasticBeans
18
25
  network: nil,
19
26
  keypair: nil,
20
27
  iam: nil,
21
- image_id: nil,
22
- instance_type: nil,
23
28
  min_size: nil,
24
29
  max_size: nil,
30
+ solution_stack: nil,
31
+ option_settings: [],
25
32
  **_
26
33
  )
34
+ @solution_stack_name = solution_stack
27
35
  template = configuration_settings_description("base")
28
36
 
29
- instance_profile_setting = template_option_setting(template: template, namespace: "aws:autoscaling:launchconfiguration", option_name: "IamInstanceProfile", override: instance_profile(iam))
37
+ instance_profile_setting = template_option_setting(template: template, namespace: "aws:autoscaling:launchconfiguration", option_name: "IamInstanceProfile", override: instance_profile(iam), new_settings: option_settings)
30
38
  if instance_profile_setting[:value].nil?
31
39
  raise MissingInstanceProfileError
32
40
  end
33
41
 
34
- keypair_setting = template_option_setting(template: template, namespace: "aws:autoscaling:launchconfiguration", option_name: "EC2KeyName", override: keypair)
42
+ keypair_setting = template_option_setting(template: template, namespace: "aws:autoscaling:launchconfiguration", option_name: "EC2KeyName", override: keypair, new_settings: option_settings)
35
43
  if keypair_setting[:value].nil?
36
44
  raise MissingOptionsError.new(
37
45
  keypair: keypair_setting[:value],
@@ -40,35 +48,57 @@ module ElasticBeans
40
48
 
41
49
  config_path = "#{application.bucket_name}/#{application.env_vars.s3_key}"
42
50
  settings = [
43
- template_option_setting(template: template, namespace: "aws:elasticbeanstalk:command", option_name: "BatchSize", default: "1"),
44
- template_option_setting(template: template, namespace: "aws:elasticbeanstalk:command", option_name: "BatchSizeType", default: "Fixed"),
45
- template_option_setting(template: template, namespace: "aws:elasticbeanstalk:command", option_name: "DeploymentPolicy", default: "Rolling"),
46
- template_option_setting(template: template, namespace: "aws:elasticbeanstalk:application:environment", option_name: "DISABLE_SQS_CONSUMER", default: "true"),
47
- template_option_setting(template: template, namespace: "aws:elasticbeanstalk:application:environment", option_name: "ELASTIC_BEANS_ENV_VARS", default: config_path),
48
- template_option_setting(template: template, namespace: "aws:elasticbeanstalk:environment", option_name: "ServiceRole", default: "aws-elasticbeanstalk-service-role"),
49
- template_option_setting(template: template, namespace: "aws:elasticbeanstalk:healthreporting:system", option_name: "SystemType", default: "enhanced"),
50
- template_option_setting(template: template, namespace: "aws:ec2:vpc", option_name: "AssociatePublicIpAddress", default: "false"),
51
- template_option_setting(template: template, namespace: "aws:autoscaling:asg", option_name: "MinSize", default: "1", override: min_size),
52
- template_option_setting(template: template, namespace: "aws:autoscaling:asg", option_name: "MaxSize", default: "4", override: max_size),
53
- template_option_setting(template: template, namespace: "aws:autoscaling:launchconfiguration", option_name: "InstanceType", default: "c4.large", override: instance_type),
54
- template_option_setting(template: template, namespace: "aws:autoscaling:launchconfiguration", option_name: "SSHSourceRestriction", default: "tcp, 22, 22, 0.0.0.0/32"),
55
- template_option_setting(template: template, namespace: "aws:autoscaling:updatepolicy:rollingupdate", option_name: "RollingUpdateType", default: "Health"),
56
- template_option_setting(template: template, namespace: "aws:autoscaling:updatepolicy:rollingupdate", option_name: "RollingUpdateEnabled", default: "true"),
57
- template_option_setting(template: template, namespace: "aws:autoscaling:launchconfiguration", option_name: "SecurityGroups", override: security_groups(network)),
58
- template_option_setting(template: template, namespace: "aws:ec2:vpc", option_name: "ELBSubnets", override: elb_subnets(network)),
59
- template_option_setting(template: template, namespace: "aws:ec2:vpc", option_name: "Subnets", override: subnets(network)),
60
- template_option_setting(template: template, namespace: "aws:ec2:vpc", option_name: "VPCId", override: vpc_id(network)),
51
+ template_option_setting(template: template, namespace: "aws:elasticbeanstalk:command", option_name: "BatchSize", default: "1", new_settings: option_settings),
52
+ template_option_setting(template: template, namespace: "aws:elasticbeanstalk:command", option_name: "BatchSizeType", default: "Fixed", new_settings: option_settings),
53
+ template_option_setting(template: template, namespace: "aws:elasticbeanstalk:command", option_name: "DeploymentPolicy", default: "Rolling", new_settings: option_settings),
54
+ template_option_setting(template: template, namespace: "aws:elasticbeanstalk:application:environment", option_name: "DISABLE_SQS_CONSUMER", default: "true", new_settings: option_settings),
55
+ template_option_setting(template: template, namespace: "aws:elasticbeanstalk:application:environment", option_name: "ELASTIC_BEANS_ENV_VARS", default: config_path, new_settings: option_settings),
56
+ template_option_setting(template: template, namespace: "aws:elasticbeanstalk:environment", option_name: "ServiceRole", default: "aws-elasticbeanstalk-service-role", new_settings: option_settings),
57
+ template_option_setting(template: template, namespace: "aws:elasticbeanstalk:healthreporting:system", option_name: "SystemType", default: "enhanced", new_settings: option_settings),
58
+ template_option_setting(template: template, namespace: "aws:ec2:vpc", option_name: "AssociatePublicIpAddress", default: "false", new_settings: option_settings),
59
+ template_option_setting(template: template, namespace: "aws:autoscaling:asg", option_name: "MinSize", default: "1", override: min_size, new_settings: option_settings),
60
+ template_option_setting(template: template, namespace: "aws:autoscaling:asg", option_name: "MaxSize", default: "4", override: max_size, new_settings: option_settings),
61
+ template_option_setting(template: template, namespace: "aws:autoscaling:launchconfiguration", option_name: "SSHSourceRestriction", default: "tcp, 22, 22, 0.0.0.0/32", new_settings: option_settings),
62
+ template_option_setting(template: template, namespace: "aws:autoscaling:updatepolicy:rollingupdate", option_name: "RollingUpdateType", default: "Health", new_settings: option_settings),
63
+ template_option_setting(template: template, namespace: "aws:autoscaling:updatepolicy:rollingupdate", option_name: "RollingUpdateEnabled", default: "true", new_settings: option_settings),
64
+ template_option_setting(template: template, namespace: "aws:autoscaling:launchconfiguration", option_name: "SecurityGroups", override: security_groups(network), new_settings: option_settings),
65
+ template_option_setting(template: template, namespace: "aws:ec2:vpc", option_name: "ELBSubnets", override: elb_subnets(network), new_settings: option_settings),
66
+ template_option_setting(template: template, namespace: "aws:ec2:vpc", option_name: "Subnets", override: subnets(network), new_settings: option_settings),
67
+ template_option_setting(template: template, namespace: "aws:ec2:vpc", option_name: "VPCId", override: vpc_id(network), new_settings: option_settings),
61
68
  instance_profile_setting,
62
69
  keypair_setting,
63
70
  ]
64
- if image_id
65
- settings << template_option_setting(template: template, namespace: "aws:autoscaling:launchconfiguration", option_name: "ImageId", override: image_id)
71
+ if solution_stack
72
+ settings << template_option_setting(template: template, namespace: "aws:elasticbeanstalk:customoption", option_name: "SolutionStack", override: solution_stack, new_settings: option_settings)
66
73
  end
67
- settings
74
+ super + settings
75
+ end
76
+
77
+ def source_configuration
78
+ nil
68
79
  end
69
80
 
70
81
  private
71
82
 
83
+ # Finds the previously-configured solution stack when one was not specified.
84
+ # The solution stack name is stored in a custom option named "SolutionStack",
85
+ # because updating the template's solution stack is not supported by Elastic Beanstalk.
86
+ # See http://docs.aws.amazon.com/elasticbeanstalk/latest/dg/configuration-options-custom.html for more information on custom options.
87
+ # If the custom option is not set because the application was configured by an older version of beans, returns the template's configured solution stack.
88
+ def configured_solution_stack
89
+ template = configuration_settings_description("base")
90
+ if template
91
+ setting = template.option_settings.find { |setting|
92
+ setting.namespace == "aws:elasticbeanstalk:customoption" && setting.option_name == "SolutionStack"
93
+ }
94
+ if setting
95
+ return setting.value
96
+ else
97
+ return template.solution_stack_name
98
+ end
99
+ end
100
+ end
101
+
72
102
  def elb_subnets(network)
73
103
  if network
74
104
  network.elb_subnets.join(",")
@@ -12,14 +12,18 @@ module ElasticBeans
12
12
  protected
13
13
 
14
14
  # Constructs the configuration for the exec environment.
15
- def build_option_settings(**_)
15
+ def build_option_settings(option_settings: [], **_)
16
16
  super + [
17
- template_option_setting(namespace: "aws:elasticbeanstalk:application", option_name: "Application Healthcheck URL", default: "HTTP:80/"),
18
- template_option_setting(namespace: "aws:elasticbeanstalk:application:environment", option_name: "DISABLE_SQS_CONSUMER", override: "false"),
19
- template_option_setting(namespace: "aws:elasticbeanstalk:application:environment", option_name: "ELASTIC_BEANS_EXEC_QUEUE_URL", override: application.exec_queue_url),
20
- template_option_setting(namespace: "aws:elasticbeanstalk:application:environment", option_name: "RAILS_SKIP_MIGRATIONS", default: "true"),
17
+ template_option_setting(namespace: "aws:elasticbeanstalk:application", option_name: "Application Healthcheck URL", default: "HTTP:80/", new_settings: option_settings),
18
+ template_option_setting(namespace: "aws:elasticbeanstalk:application:environment", option_name: "DISABLE_SQS_CONSUMER", override: "false", new_settings: option_settings),
19
+ template_option_setting(namespace: "aws:elasticbeanstalk:application:environment", option_name: "ELASTIC_BEANS_EXEC_QUEUE_URL", override: application.exec_queue_url, new_settings: option_settings),
20
+ template_option_setting(namespace: "aws:elasticbeanstalk:application:environment", option_name: "RAILS_SKIP_MIGRATIONS", default: "true", new_settings: option_settings),
21
21
  ]
22
22
  end
23
+
24
+ def source_configuration
25
+ SOURCE_CONFIGURATION
26
+ end
23
27
  end
24
28
  end
25
29
  end
@@ -11,13 +11,17 @@ module ElasticBeans
11
11
 
12
12
  # Constructs the configuration for the scheduler environment.
13
13
  # No special options here!
14
- def build_option_settings(**_)
14
+ def build_option_settings(option_settings: [], **_)
15
15
  super + [
16
- template_option_setting(namespace: "aws:elasticbeanstalk:application", option_name: "Application Healthcheck URL", default: "HTTP:80/"),
17
- template_option_setting(namespace: "aws:elasticbeanstalk:application:environment", option_name: "DISABLE_SQS_CONSUMER", override: "false"),
18
- template_option_setting(namespace: "aws:elasticbeanstalk:application:environment", option_name: "RAILS_SKIP_MIGRATIONS", default: "true"),
16
+ template_option_setting(namespace: "aws:elasticbeanstalk:application", option_name: "Application Healthcheck URL", default: "HTTP:80/", new_settings: option_settings),
17
+ template_option_setting(namespace: "aws:elasticbeanstalk:application:environment", option_name: "DISABLE_SQS_CONSUMER", override: "false", new_settings: option_settings),
18
+ template_option_setting(namespace: "aws:elasticbeanstalk:application:environment", option_name: "RAILS_SKIP_MIGRATIONS", default: "true", new_settings: option_settings),
19
19
  ]
20
20
  end
21
+
22
+ def source_configuration
23
+ SOURCE_CONFIGURATION
24
+ end
21
25
  end
22
26
  end
23
27
  end
@@ -13,35 +13,35 @@ module ElasticBeans
13
13
 
14
14
  # Constructs the configuration for the webserver environment.
15
15
  # All arguments are required on first run.
16
- def build_option_settings(network: nil, public_key: nil, ssl_certificate_arn: nil, internal: nil, **_)
17
- public_key_policy_names_setting = template_option_setting(namespace: "aws:elb:policies:backendencryption", option_name: "PublicKeyPolicyNames", default: "backendkey")
18
- public_key_setting = template_option_setting(namespace: "aws:elb:policies:#{public_key_policy_names_setting[:value]}", option_name: "PublicKey", override: public_key)
19
- ssl_certificate_setting = template_option_setting(namespace: "aws:elb:listener:443", option_name: "SSLCertificateId", override: ssl_certificate_arn)
16
+ def build_option_settings(network: nil, public_key: nil, ssl_certificate_arn: nil, internal: nil, option_settings: [], **_)
17
+ public_key_policy_names_setting = template_option_setting(namespace: "aws:elb:policies:backendencryption", option_name: "PublicKeyPolicyNames", default: "backendkey", new_settings: option_settings)
18
+ public_key_setting = template_option_setting(namespace: "aws:elb:policies:#{public_key_policy_names_setting[:value]}", option_name: "PublicKey", override: public_key, new_settings: option_settings)
19
+ ssl_certificate_setting = template_option_setting(namespace: "aws:elb:listener:443", option_name: "SSLCertificateId", override: ssl_certificate_arn, new_settings: option_settings)
20
20
  if public_key_setting[:value].nil? || ssl_certificate_setting[:value].nil?
21
21
  raise NoEncryptionSettingsError
22
22
  end
23
23
 
24
- option_settings = [
25
- template_option_setting(namespace: "aws:elasticbeanstalk:application", option_name: "Application Healthcheck URL", default: "HTTPS:443/", allow_blank: false),
26
- template_option_setting(namespace: "aws:elasticbeanstalk:application:environment", option_name: "RAILS_SKIP_ASSET_COMPILATION", default: "false"),
27
- template_option_setting(namespace: "aws:elasticbeanstalk:application:environment", option_name: "RAILS_SKIP_MIGRATIONS", default: "false"),
28
- template_option_setting(namespace: "aws:elb:listener:443", option_name: "InstancePort", default: "443"),
29
- template_option_setting(namespace: "aws:elb:listener:443", option_name: "InstanceProtocol", default: "HTTPS"),
30
- template_option_setting(namespace: "aws:elb:listener:443", option_name: "ListenerProtocol", default: "HTTPS"),
31
- template_option_setting(namespace: "aws:elb:loadbalancer", option_name: "ManagedSecurityGroup", override: managed_security_group(network)),
32
- template_option_setting(namespace: "aws:elb:loadbalancer", option_name: "SecurityGroups", override: elb_security_groups(network)),
33
- template_option_setting(namespace: "aws:elb:policies", option_name: "ConnectionDrainingEnabled", default: "true"),
34
- template_option_setting(namespace: "aws:elb:policies:backendencryption", option_name: "InstancePorts", default: "443"),
24
+ settings = [
25
+ template_option_setting(namespace: "aws:elasticbeanstalk:application", option_name: "Application Healthcheck URL", default: "HTTPS:443/", allow_blank: false, new_settings: option_settings),
26
+ template_option_setting(namespace: "aws:elasticbeanstalk:application:environment", option_name: "RAILS_SKIP_ASSET_COMPILATION", default: "false", new_settings: option_settings),
27
+ template_option_setting(namespace: "aws:elasticbeanstalk:application:environment", option_name: "RAILS_SKIP_MIGRATIONS", default: "false", new_settings: option_settings),
28
+ template_option_setting(namespace: "aws:elb:listener:443", option_name: "InstancePort", default: "443", new_settings: option_settings),
29
+ template_option_setting(namespace: "aws:elb:listener:443", option_name: "InstanceProtocol", default: "HTTPS", new_settings: option_settings),
30
+ template_option_setting(namespace: "aws:elb:listener:443", option_name: "ListenerProtocol", default: "HTTPS", new_settings: option_settings),
31
+ template_option_setting(namespace: "aws:elb:loadbalancer", option_name: "ManagedSecurityGroup", override: managed_security_group(network), new_settings: option_settings),
32
+ template_option_setting(namespace: "aws:elb:loadbalancer", option_name: "SecurityGroups", override: elb_security_groups(network), new_settings: option_settings),
33
+ template_option_setting(namespace: "aws:elb:policies", option_name: "ConnectionDrainingEnabled", default: "true", new_settings: option_settings),
34
+ template_option_setting(namespace: "aws:elb:policies:backendencryption", option_name: "InstancePorts", default: "443", new_settings: option_settings),
35
35
  public_key_policy_names_setting,
36
36
  public_key_setting,
37
37
  ssl_certificate_setting,
38
38
  ]
39
39
  if internal
40
- internal_setting = template_option_setting(namespace: "aws:ec2:vpc", option_name: "ELBScheme", override: "internal")
41
- option_settings << internal_setting
40
+ internal_setting = template_option_setting(namespace: "aws:ec2:vpc", option_name: "ELBScheme", override: "internal", new_settings: option_settings)
41
+ settings << internal_setting
42
42
  end
43
43
 
44
- super + option_settings
44
+ super + settings
45
45
  end
46
46
 
47
47
  # Removes the "internal" ELB scheme if explicitly disabled with --no-internal.
@@ -64,6 +64,10 @@ module ElasticBeans
64
64
  network.elb_security_groups[0] if network
65
65
  end
66
66
 
67
+ def source_configuration
68
+ SOURCE_CONFIGURATION
69
+ end
70
+
67
71
  # :nodoc: all
68
72
  # @!visibility private
69
73
  class NoEncryptionSettingsError < ElasticBeans::Error
@@ -24,17 +24,21 @@ module ElasticBeans
24
24
  # Constructs the configuration for the worker environments.
25
25
  # No special arguments, the +queue+ name is stored from the initializer and is used to look up the appropriate
26
26
  # URL.
27
- def build_option_settings(**_)
27
+ def build_option_settings(option_settings: [], **_)
28
28
  super + [
29
- template_option_setting(namespace: "aws:elasticbeanstalk:application", option_name: "Application Healthcheck URL", default: "HTTP:80/"),
30
- template_option_setting(namespace: "aws:elasticbeanstalk:application:environment", option_name: "DISABLE_SQS_CONSUMER", override: "false"),
31
- template_option_setting(namespace: "aws:elasticbeanstalk:application:environment", option_name: "RAILS_SKIP_MIGRATIONS", default: "true"),
32
- template_option_setting(namespace: "aws:elasticbeanstalk:sqsd", option_name: "InactivityTimeout", default: "1800"),
33
- template_option_setting(namespace: "aws:elasticbeanstalk:sqsd", option_name: "MaxRetries", default: "10"),
34
- template_option_setting(namespace: "aws:elasticbeanstalk:sqsd", option_name: "VisibilityTimeout", default: "1800"),
35
- template_option_setting(namespace: "aws:elasticbeanstalk:sqsd", option_name: "WorkerQueueURL", override: application.worker_queue_url(queue)),
29
+ template_option_setting(namespace: "aws:elasticbeanstalk:application", option_name: "Application Healthcheck URL", default: "HTTP:80/", new_settings: option_settings),
30
+ template_option_setting(namespace: "aws:elasticbeanstalk:application:environment", option_name: "DISABLE_SQS_CONSUMER", override: "false", new_settings: option_settings),
31
+ template_option_setting(namespace: "aws:elasticbeanstalk:application:environment", option_name: "RAILS_SKIP_MIGRATIONS", default: "true", new_settings: option_settings),
32
+ template_option_setting(namespace: "aws:elasticbeanstalk:sqsd", option_name: "InactivityTimeout", default: "1800", new_settings: option_settings),
33
+ template_option_setting(namespace: "aws:elasticbeanstalk:sqsd", option_name: "MaxRetries", default: "10", new_settings: option_settings),
34
+ template_option_setting(namespace: "aws:elasticbeanstalk:sqsd", option_name: "VisibilityTimeout", default: "1800", new_settings: option_settings),
35
+ template_option_setting(namespace: "aws:elasticbeanstalk:sqsd", option_name: "WorkerQueueURL", override: application.worker_queue_url(queue), new_settings: option_settings),
36
36
  ]
37
37
  end
38
+
39
+ def source_configuration
40
+ SOURCE_CONFIGURATION
41
+ end
38
42
  end
39
43
  end
40
44
  end
@@ -10,8 +10,11 @@ module ElasticBeans
10
10
  # This is an abstract class; use the provided factories in this class or use a subclass (contained within this
11
11
  # namespace) directly.
12
12
  class ConfigurationTemplate
13
- # The solution stack used for a new application. Should not be hardcoded, but here we are.
14
- SOLUTION_STACK_NAME = "64bit Amazon Linux 2016.09 v2.2.0 running Ruby 2.3 (Puma)"
13
+ # Matches the latest available Ruby Puma solution stack when one was not specified.
14
+ SOLUTION_STACK_PATTERN = /\A64bit Amazon Linux (?<date>\d+(\.\d+)*) v(?<version>\d+(\.\d+)*) running Ruby (?<ruby_version>\d+(\.\d+)*) \(Puma\)\z/
15
+ # The source configuration for new configuration templates.
16
+ # Set to ElasticBeans::ConfigurationTemplate::Base to include all custom configuration that has already been performed.
17
+ SOURCE_CONFIGURATION = {template_name: "base"}
15
18
  # :category: Internal
16
19
  WORKER_TEMPLATE_NAME_PATTERN = /\Aworker-(?<queue>\w+)\z/
17
20
 
@@ -102,6 +105,11 @@ module ElasticBeans
102
105
  @options_to_remove ||= []
103
106
  end
104
107
 
108
+ # Finds the latest available Ruby Puma solution stack (matching +SOLUTION_STACK_PATTERN+).
109
+ def solution_stack_name
110
+ @solution_stack_name ||= latest_solution_stack
111
+ end
112
+
105
113
  # Create or update the configuration template in Elastic Beanstalk.
106
114
  # Arguments are passed to #build_option_settings and #build_options_to_remove.
107
115
  # See the appropriate subclass for what the arguments should be.
@@ -119,7 +127,8 @@ module ElasticBeans
119
127
  elastic_beanstalk.create_configuration_template(
120
128
  application_name: application.name,
121
129
  template_name: name,
122
- solution_stack_name: SOLUTION_STACK_NAME,
130
+ solution_stack_name: solution_stack_name,
131
+ source_configuration: source_configuration,
123
132
  option_settings: option_settings,
124
133
  )
125
134
  end
@@ -135,12 +144,17 @@ module ElasticBeans
135
144
  # :category: Internal
136
145
  attr_reader :elastic_beanstalk
137
146
 
138
- def build_option_settings(**_)
139
- []
147
+ # Returns option settings that should be set on the template.
148
+ def build_option_settings(option_settings: [], **_)
149
+ option_settings
140
150
  end
141
151
 
142
- def build_options_to_remove(**_)
143
- []
152
+ # Returns option settings that should be removed from the template.
153
+ # Will not remove settings that beans has defaults for, even if they aren't being changed right now.
154
+ def build_options_to_remove(options_to_remove: [], **_)
155
+ options_to_remove.reject { |setting|
156
+ option_settings.any? { |new_setting| new_setting[:namespace] == setting[:namespace] && new_setting[:option_name] == setting[:option_name] }
157
+ }
144
158
  end
145
159
 
146
160
  def configuration_settings_description(template_name = name)
@@ -181,6 +195,36 @@ module ElasticBeans
181
195
  retry
182
196
  end
183
197
 
198
+ # Finds the latest available Ruby Puma solution stack (matching +SOLUTION_STACK_PATTERN+).
199
+ def latest_solution_stack
200
+ latest_stack = nil
201
+ latest_date = Gem::Version.new("0.0")
202
+ latest_version = Gem::Version.new("0.0")
203
+ latest_ruby_version = Gem::Version.new("0.0")
204
+ elastic_beanstalk.list_available_solution_stacks.solution_stacks.each do |stack|
205
+ match = SOLUTION_STACK_PATTERN.match(stack)
206
+ if match
207
+ stack_date = Gem::Version.new(match[:date])
208
+ stack_version = Gem::Version.new(match[:version])
209
+ stack_ruby_version = Gem::Version.new(match[:ruby_version])
210
+ if stack_date >= latest_date && stack_version >= latest_version && stack_ruby_version >= latest_ruby_version
211
+ latest_date = stack_date
212
+ latest_version = stack_version
213
+ latest_ruby_version = stack_ruby_version
214
+ latest_stack = stack
215
+ end
216
+ end
217
+ end
218
+ latest_stack
219
+ rescue ::Aws::ElasticBeanstalk::Errors::Throttling
220
+ sleep 5
221
+ retry
222
+ end
223
+
224
+ def source_configuration
225
+ SOURCE_CONFIGURATION
226
+ end
227
+
184
228
  def template_option_setting(
185
229
  template: configuration_settings_description,
186
230
  environment: environment_configuration_settings_description,
@@ -188,13 +232,21 @@ module ElasticBeans
188
232
  option_name:,
189
233
  default: nil,
190
234
  override: nil,
191
- allow_blank: true
235
+ allow_blank: true,
236
+ new_settings:
192
237
  )
193
238
  option_setting = {namespace: namespace, option_name: option_name, value: default}
194
239
  if override
195
240
  return option_setting.merge!(value: override)
196
241
  end
197
242
 
243
+ new_setting = new_settings.find { |setting|
244
+ setting[:namespace] == namespace && setting[:option_name] == option_name
245
+ }
246
+ if new_setting
247
+ return new_setting
248
+ end
249
+
198
250
  existing_settings = []
199
251
  if environment
200
252
  # Persist changes made directly to the environment from the AWS console UI
@@ -116,6 +116,7 @@ module ElasticBeans
116
116
  elastic_beanstalk.create_environment(
117
117
  application_name: application.name,
118
118
  environment_name: name,
119
+ solution_stack_name: configuration_template.solution_stack_name,
119
120
  tier: {name: tier_name, type: tier_type},
120
121
  template_name: template_name,
121
122
  version_label: version,
@@ -196,6 +197,7 @@ module ElasticBeans
196
197
  environment_name: name,
197
198
  option_settings: configuration_template.option_settings,
198
199
  options_to_remove: configuration_template.options_to_remove,
200
+ solution_stack_name: configuration_template.solution_stack_name,
199
201
  )
200
202
  wait_environment(wait_status: "Updating", wait_health_status: "Info") if blocking
201
203
  rescue ::Aws::ElasticBeanstalk::Errors::InvalidParameterValue
@@ -245,6 +247,10 @@ module ElasticBeans
245
247
  # :category: Internal
246
248
  attr_reader :elastic_beanstalk
247
249
 
250
+ def configuration_template
251
+ ElasticBeans::ConfigurationTemplate.new_from_existing(template_name, application: application, elastic_beanstalk: elastic_beanstalk)
252
+ end
253
+
248
254
  def environment_description
249
255
  elastic_beanstalk.describe_environments(
250
256
  environment_names: [name],
@@ -1,3 +1,3 @@
1
1
  module ElasticBeans
2
- VERSION = "0.13.0.alpha3"
2
+ VERSION = "0.13.0.alpha4"
3
3
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: elastic_beans
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.13.0.alpha3
4
+ version: 0.13.0.alpha4
5
5
  platform: ruby
6
6
  authors:
7
7
  - Adam Stegman
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2017-07-21 00:00:00.000000000 Z
11
+ date: 2017-07-26 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: aws-sdk