elastic_beans 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.
Files changed (49) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +9 -0
  3. data/.rspec +3 -0
  4. data/Gemfile +9 -0
  5. data/LICENSE.md +21 -0
  6. data/README.md +184 -0
  7. data/Rakefile +6 -0
  8. data/bin/console +14 -0
  9. data/bin/setup +8 -0
  10. data/circle.yml +20 -0
  11. data/elastic_beans.gemspec +31 -0
  12. data/exe/beans +198 -0
  13. data/lib/elastic_beans/application.rb +127 -0
  14. data/lib/elastic_beans/application_version.rb +202 -0
  15. data/lib/elastic_beans/aws/cloudformation_stack.rb +66 -0
  16. data/lib/elastic_beans/command/configure.rb +184 -0
  17. data/lib/elastic_beans/command/create.rb +150 -0
  18. data/lib/elastic_beans/command/deploy.rb +77 -0
  19. data/lib/elastic_beans/command/exec.rb +37 -0
  20. data/lib/elastic_beans/command/set_env.rb +77 -0
  21. data/lib/elastic_beans/command/talk.rb +74 -0
  22. data/lib/elastic_beans/command/version.rb +17 -0
  23. data/lib/elastic_beans/command.rb +12 -0
  24. data/lib/elastic_beans/configuration_template/base.rb +114 -0
  25. data/lib/elastic_beans/configuration_template/exec.rb +50 -0
  26. data/lib/elastic_beans/configuration_template/scheduler.rb +20 -0
  27. data/lib/elastic_beans/configuration_template/webserver.rb +49 -0
  28. data/lib/elastic_beans/configuration_template/worker.rb +27 -0
  29. data/lib/elastic_beans/configuration_template.rb +197 -0
  30. data/lib/elastic_beans/dns_entry.rb +127 -0
  31. data/lib/elastic_beans/environment/exec.rb +23 -0
  32. data/lib/elastic_beans/environment/scheduler.rb +23 -0
  33. data/lib/elastic_beans/environment/webserver.rb +23 -0
  34. data/lib/elastic_beans/environment/worker.rb +29 -0
  35. data/lib/elastic_beans/environment.rb +300 -0
  36. data/lib/elastic_beans/error/environments_not_ready.rb +15 -0
  37. data/lib/elastic_beans/error.rb +15 -0
  38. data/lib/elastic_beans/exec/ebextension.yml +10 -0
  39. data/lib/elastic_beans/exec/elastic_beans_exec.conf +15 -0
  40. data/lib/elastic_beans/exec/init.rb +50 -0
  41. data/lib/elastic_beans/exec/run_command.sh +13 -0
  42. data/lib/elastic_beans/exec/sqs_consumer.rb +75 -0
  43. data/lib/elastic_beans/network.rb +44 -0
  44. data/lib/elastic_beans/rack/exec.rb +63 -0
  45. data/lib/elastic_beans/scheduler/ebextension.yml +4 -0
  46. data/lib/elastic_beans/ui.rb +31 -0
  47. data/lib/elastic_beans/version.rb +3 -0
  48. data/lib/elastic_beans.rb +9 -0
  49. metadata +218 -0
@@ -0,0 +1,127 @@
1
+ require "timeout"
2
+ require "elastic_beans/error"
3
+
4
+ module ElasticBeans
5
+ class DnsEntry
6
+ DNS_NAME_PATTERN = /\A[^.]+\.[^.]+\.[^.]+\z/
7
+
8
+ attr_reader :name
9
+
10
+ def initialize(name, route53:)
11
+ unless valid_name?(name)
12
+ raise InvalidDnsName.new(name: name)
13
+ end
14
+
15
+ @name = name
16
+ @route53 = route53
17
+ end
18
+
19
+ def update(value)
20
+ unless value
21
+ raise InvalidDnsValue.new(name: name, value: value)
22
+ end
23
+
24
+ response = route53.change_resource_record_sets(
25
+ hosted_zone_id: hosted_zone_id,
26
+ change_batch: {
27
+ changes: [
28
+ {
29
+ action: "UPSERT",
30
+ resource_record_set: {
31
+ name: name,
32
+ type: "CNAME",
33
+ ttl: 300,
34
+ resource_records: [{value: value}],
35
+ },
36
+ },
37
+ ],
38
+ },
39
+ )
40
+ begin
41
+ Timeout.timeout(300) do
42
+ status = 'PENDING'
43
+ while status == 'PENDING'
44
+ sleep 1
45
+ status = route53.get_change(id: response.change_info.id).change_info.status
46
+ end
47
+ end
48
+ rescue Timeout::Error
49
+ raise UnhealthyDnsError.new(name: name)
50
+ end
51
+ rescue ::Aws::Route53::Errors::InvalidChangeBatch => e
52
+ raise InvalidDnsValue.new(name: name, value: value, cause: e)
53
+ end
54
+
55
+ private
56
+
57
+ attr_reader :route53
58
+
59
+ def dns_tld
60
+ @dns_tld ||= name.sub(/\A[^.]+\./, '')
61
+ end
62
+
63
+ def hosted_zone_id
64
+ marker = nil
65
+ loop do
66
+ response = route53.list_hosted_zones(marker: marker)
67
+ hosted_zone = response.hosted_zones.find { |hz| hz.name == "#{dns_tld}." }
68
+ return hosted_zone.id if hosted_zone
69
+ break unless response.is_truncated
70
+ marker = response.next_marker
71
+ end
72
+ raise MissingHostedZoneError.new(name: name, tld: dns_tld)
73
+ end
74
+
75
+ def valid_name?(name)
76
+ name && name =~ DNS_NAME_PATTERN
77
+ end
78
+
79
+ class InvalidDnsName < ElasticBeans::Error
80
+ def initialize(name:)
81
+ @name = name
82
+ end
83
+
84
+ def message
85
+ "Invalid DNS name `#{@name}'. Only subdomains are accepted, e.g. 'myapp.example.com'"
86
+ end
87
+ end
88
+
89
+ class InvalidDnsValue < ElasticBeans::Error
90
+ include Nesty::NestedError
91
+
92
+ def initialize(name:, value:, cause: nil)
93
+ @name = name
94
+ @value = value
95
+ @nested = cause
96
+ end
97
+
98
+ def message
99
+ msg = "Attempted updating DNS entry `#{@name}' to value `#{@value}', which is not valid."
100
+ msg << "\nThe error from AWS was \"#{@nested.message}\"" if @nested
101
+ msg << "\nTry re-running this command:\n #{command_as_string}"
102
+ msg
103
+ end
104
+ end
105
+
106
+ class MissingHostedZoneError < ElasticBeans::Error
107
+ def initialize(name:, tld:)
108
+ @name = name
109
+ @tld = tld
110
+ end
111
+
112
+ def message
113
+ "Could not find TLD `#{@tld}.' for DNS entry `#{@name}' in Route53"
114
+ end
115
+ end
116
+
117
+ class UnhealthyDnsError < ElasticBeans::Error
118
+ def initialize(name:)
119
+ @name = name
120
+ end
121
+
122
+ def message
123
+ "DNS entry `#{@name}' is still not updated"
124
+ end
125
+ end
126
+ end
127
+ end
@@ -0,0 +1,23 @@
1
+ module ElasticBeans
2
+ class Environment
3
+ class Exec < ElasticBeans::Environment
4
+ protected
5
+
6
+ TEMPLATE_NAME = "exec"
7
+ TIER_NAME = "Worker"
8
+ TIER_TYPE = "SQS/HTTP"
9
+
10
+ def template_name
11
+ TEMPLATE_NAME
12
+ end
13
+
14
+ def tier_name
15
+ TIER_NAME
16
+ end
17
+
18
+ def tier_type
19
+ TIER_TYPE
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,23 @@
1
+ module ElasticBeans
2
+ class Environment
3
+ class Scheduler < ElasticBeans::Environment
4
+ protected
5
+
6
+ TEMPLATE_NAME = "scheduler"
7
+ TIER_NAME = "Worker"
8
+ TIER_TYPE = "SQS/HTTP"
9
+
10
+ def template_name
11
+ TEMPLATE_NAME
12
+ end
13
+
14
+ def tier_name
15
+ TIER_NAME
16
+ end
17
+
18
+ def tier_type
19
+ TIER_TYPE
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,23 @@
1
+ module ElasticBeans
2
+ class Environment
3
+ class Webserver < ElasticBeans::Environment
4
+ protected
5
+
6
+ TEMPLATE_NAME = "webserver"
7
+ TIER_NAME = "Webserver"
8
+ TIER_TYPE = "Standard"
9
+
10
+ def template_name
11
+ TEMPLATE_NAME
12
+ end
13
+
14
+ def tier_name
15
+ TIER_NAME
16
+ end
17
+
18
+ def tier_type
19
+ TIER_TYPE
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,29 @@
1
+ module ElasticBeans
2
+ class Environment
3
+ class Worker < ElasticBeans::Environment
4
+ attr_reader :queue
5
+
6
+ def initialize(name, queue:, **_)
7
+ super
8
+ @queue = queue
9
+ end
10
+
11
+ protected
12
+
13
+ TIER_NAME = "Worker"
14
+ TIER_TYPE = "SQS/HTTP"
15
+
16
+ def template_name
17
+ @template_name ||= "worker-#{queue}"
18
+ end
19
+
20
+ def tier_name
21
+ TIER_NAME
22
+ end
23
+
24
+ def tier_type
25
+ TIER_TYPE
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,300 @@
1
+ require "timeout"
2
+ require "aws-sdk"
3
+ require "nesty"
4
+ require "elastic_beans/environment/exec"
5
+ require "elastic_beans/environment/scheduler"
6
+ require "elastic_beans/environment/webserver"
7
+ require "elastic_beans/environment/worker"
8
+ require "elastic_beans/error"
9
+
10
+ module ElasticBeans
11
+ class Environment
12
+ TEMPLATE_NAME = "base"
13
+ TIER_NAME = "Webserver"
14
+ TIER_TYPE = "Standard"
15
+ WORKER_TEMPLATE_NAME_PATTERN = /worker-(?<queue>\w+)/
16
+
17
+ attr_reader :name
18
+
19
+ def self.new_by_type(type, suffix: nil, application:, **args)
20
+ name = "#{application.name}-#{suffix}"
21
+ case type
22
+ when "exec"
23
+ ElasticBeans::Environment::Exec.new(name, application: application, **args)
24
+ when "scheduler"
25
+ ElasticBeans::Environment::Scheduler.new(name, application: application, **args)
26
+ when "webserver"
27
+ ElasticBeans::Environment::Webserver.new(name, application: application, **args)
28
+ when "worker"
29
+ ElasticBeans::Environment::Worker.new(name, application: application, **args)
30
+ else
31
+ raise UnknownEnvironmentType.new(environment_type: type)
32
+ end
33
+ end
34
+
35
+ def self.new_from_existing(environment, application:, **args)
36
+ app_name_pattern = /#{Regexp.escape(application.name)}/
37
+ case environment.environment_name
38
+ when /\A#{app_name_pattern}-exec\z/
39
+ ElasticBeans::Environment::Exec.new(environment.environment_name, application: application, **args)
40
+ when /\A#{app_name_pattern}-scheduler\z/
41
+ ElasticBeans::Environment::Scheduler.new(environment.environment_name, application: application, **args)
42
+ when /\A#{app_name_pattern}-webserver\z/
43
+ ElasticBeans::Environment::Webserver.new(environment.environment_name, application: application, **args)
44
+ when /\A#{app_name_pattern}-#{WORKER_TEMPLATE_NAME_PATTERN}\z/
45
+ match = WORKER_TEMPLATE_NAME_PATTERN.match(environment.environment_name)
46
+ ElasticBeans::Environment::Worker.new(
47
+ environment.environment_name,
48
+ application: application,
49
+ queue: match[:queue],
50
+ **args,
51
+ )
52
+ else
53
+ case environment.tier.name
54
+ when "WebServer"
55
+ ElasticBeans::Environment::Webserver.new(environment.environment_name, application: application, **args)
56
+ when "Worker"
57
+ ElasticBeans::Environment::Worker.new(
58
+ environment.environment_name,
59
+ application: application,
60
+ queue: "default",
61
+ **args,
62
+ )
63
+ else
64
+ raise UnknownEnvironmentType.new(environment_type: environment.tier.name)
65
+ end
66
+ end
67
+ end
68
+
69
+ def initialize(name, application:, elastic_beanstalk:, **_)
70
+ @name = name
71
+ @application = application
72
+ @elastic_beanstalk = elastic_beanstalk
73
+ end
74
+
75
+ def create(version:, tags: {})
76
+ begin
77
+ elastic_beanstalk.describe_configuration_settings(
78
+ application_name: application.name,
79
+ template_name: template_name,
80
+ ).configuration_settings.empty?
81
+ rescue ::Aws::ElasticBeanstalk::Errors::InvalidParameterValue
82
+ raise MissingConfigurationError
83
+ end
84
+
85
+ tags_list = tags.map { |k, v| {key: k, value: v} }
86
+ elastic_beanstalk.create_environment(
87
+ application_name: application.name,
88
+ environment_name: name,
89
+ tier: {name: tier_name, type: tier_type},
90
+ template_name: template_name,
91
+ version_label: version,
92
+ tags: tags_list,
93
+ )
94
+
95
+ wait_environment(wait_status: "Launching", wait_health_status: ["Unknown", "Pending"])
96
+ rescue ::Aws::ElasticBeanstalk::Errors::Throttling
97
+ sleep 5
98
+ retry
99
+ rescue ::Aws::Errors::ServiceError => e
100
+ raise UnhealthyEnvironmentError.new(
101
+ environment_name: name,
102
+ cause: e,
103
+ )
104
+ end
105
+
106
+ def deploy_version(version_label)
107
+ elastic_beanstalk.update_environment(
108
+ environment_name: name,
109
+ version_label: version_label,
110
+ )
111
+
112
+ wait_environment(wait_status: "Updating", wait_health_status: "Info")
113
+ rescue ::Aws::ElasticBeanstalk::Errors::Throttling
114
+ sleep 5
115
+ retry
116
+ rescue ::Aws::Errors::ServiceError => e
117
+ raise UnhealthyEnvironmentError.new(environment_name: name, cause: e)
118
+ end
119
+
120
+ def update_configuration
121
+ elastic_beanstalk.update_environment(
122
+ environment_name: name,
123
+ template_name: template_name,
124
+ tier: {name: tier_name, type: tier_type},
125
+ )
126
+ wait_environment(wait_status: "Updating", wait_health_status: "Info")
127
+ rescue ::Aws::ElasticBeanstalk::Errors::InvalidParameterValue
128
+ raise MissingEnvironmentError.new(environment_name: name)
129
+ rescue ::Aws::ElasticBeanstalk::Errors::Throttling
130
+ sleep 5
131
+ retry
132
+ end
133
+
134
+ def update_environment(env_vars)
135
+ new_env_settings = env_vars.map { |k, v|
136
+ {namespace: "aws:elasticbeanstalk:application:environment", option_name: k, value: v}
137
+ }
138
+ elastic_beanstalk.update_environment(
139
+ environment_name: name,
140
+ option_settings: new_env_settings,
141
+ )
142
+
143
+ wait_environment(wait_status: "Updating", wait_health_status: "Info")
144
+ rescue ::Aws::ElasticBeanstalk::Errors::InvalidParameterValue
145
+ raise MissingEnvironmentError.new(environment_name: name)
146
+ rescue ::Aws::ElasticBeanstalk::Errors::Throttling
147
+ sleep 5
148
+ retry
149
+ end
150
+
151
+ def cname
152
+ environment = environment_description
153
+ unless environment
154
+ raise MissingEnvironmentError.new(environment_name: name)
155
+ end
156
+ environment.cname
157
+ end
158
+
159
+ def status
160
+ environment = environment_description
161
+ unless environment
162
+ raise MissingEnvironmentError.new(environment_name: name)
163
+ end
164
+ environment.status
165
+ end
166
+
167
+ protected
168
+
169
+ attr_reader :application, :version, :elastic_beanstalk
170
+
171
+ def environment_description
172
+ elastic_beanstalk.describe_environments(
173
+ environment_names: [name],
174
+ ).environments.find { |environment|
175
+ environment.status !~ /Terminat/
176
+ }
177
+ rescue ::Aws::ElasticBeanstalk::Errors::Throttling
178
+ sleep 5
179
+ retry
180
+ end
181
+
182
+ def template_name
183
+ TEMPLATE_NAME
184
+ end
185
+
186
+ def tier_name
187
+ TIER_NAME
188
+ end
189
+
190
+ def tier_type
191
+ TIER_TYPE
192
+ end
193
+
194
+ def wait_environment(wait_status:, wait_health_status:)
195
+ wait_status = Array(wait_status)
196
+ wait_health_status = Array(wait_health_status)
197
+
198
+ status = wait_status[0]
199
+ health = "Grey"
200
+ health_status = wait_health_status[0]
201
+ Timeout.timeout(1800) do
202
+ while wait_status.include?(status) || wait_health_status.include?(health_status)
203
+ sleep 5
204
+ environment = environment_description
205
+ status = environment.status
206
+ health = environment.health
207
+ health_status = environment.health_status
208
+ end
209
+ end
210
+
211
+ # Wait a little bit longer. Sometimes apps are Ready but still Degraded for a few seconds.
212
+ Timeout.timeout(30) do
213
+ while status == "Ready" && health != "Green"
214
+ sleep 5
215
+ environment = environment_description
216
+ status = environment.status
217
+ health = environment.health
218
+ health_status = environment.health_status
219
+ end
220
+ end
221
+
222
+ unless status == "Ready" && health == "Green" && (health_status.nil? || health_status == "Ok")
223
+ raise UnhealthyEnvironmentError.new(
224
+ environment_name: name,
225
+ status: status,
226
+ health: health,
227
+ health_status: health_status,
228
+ )
229
+ end
230
+ rescue Timeout::Error
231
+ raise UnhealthyEnvironmentError.new(
232
+ environment_name: name,
233
+ status: status,
234
+ health: health,
235
+ health_status: health_status,
236
+ )
237
+ end
238
+
239
+ class MissingConfigurationError < ElasticBeans::Error
240
+ def message
241
+ require "elastic_beans/command/configure"
242
+ <<-MESSAGE
243
+ Some configuration must be set before creating an environment:
244
+
245
+ * keypair
246
+ * SSL configuration
247
+ * environment variables that Rails always requires
248
+ * DATABASE_URL
249
+ * SECRET_KEY_BASE
250
+
251
+ #{$0} #{ElasticBeans::Command::Configure::USAGE}
252
+ MESSAGE
253
+ end
254
+ end
255
+
256
+ class MissingEnvironmentError < ElasticBeans::Error
257
+ def initialize(environment_name:)
258
+ @environment_name = environment_name
259
+ end
260
+
261
+ def message
262
+ require "elastic_beans/command/create"
263
+ <<-MESSAGE
264
+ Environment `#{@environment_name}' does not exist. Try creating it!
265
+
266
+ #{$0} #{ElasticBeans::Command::Create::USAGE}
267
+ MESSAGE
268
+ end
269
+ end
270
+
271
+ class UnhealthyEnvironmentError < ElasticBeans::Error
272
+ include Nesty::NestedError
273
+
274
+ def initialize(environment_name:, status: nil, health: nil, health_status: nil, cause: nil)
275
+ @environment_name = environment_name
276
+ @health_status = health_status
277
+ @health = health
278
+ @status = status
279
+ @nested = cause
280
+ end
281
+
282
+ def message
283
+ msg = "Environment `#{@environment_name}' is not healthy."
284
+ msg << "\nIt is in '#{@health_status}' ('#{@health}') health with a status of '#{@status}'." if @health || @health_status || @status
285
+ msg << "\nThe error from AWS was \"#{@nested.message}\"." if @nested
286
+ msg
287
+ end
288
+ end
289
+
290
+ class UnknownEnvironmentType < ElasticBeans::Error
291
+ def initialize(environment_type:)
292
+ @environment_type = environment_type
293
+ end
294
+
295
+ def message
296
+ "Unknown environment type `#{@environment_type}'"
297
+ end
298
+ end
299
+ end
300
+ end