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,114 @@
1
+ require "elastic_beans/error"
2
+
3
+ module ElasticBeans
4
+ class ConfigurationTemplate
5
+ class Base < ElasticBeans::ConfigurationTemplate
6
+ SOLUTION_STACK_NAME = "64bit Amazon Linux 2016.09 v2.2.0 running Ruby 2.3 (Puma)"
7
+
8
+ def initialize(**args)
9
+ super(name: "base", **args)
10
+ end
11
+
12
+ def upsert(**args)
13
+ @option_settings = build_option_settings(**args)
14
+ if configuration_settings_description
15
+ elastic_beanstalk.update_configuration_template(
16
+ application_name: application.name,
17
+ template_name: name,
18
+ option_settings: option_settings,
19
+ )
20
+ else
21
+ elastic_beanstalk.create_configuration_template(
22
+ application_name: application.name,
23
+ template_name: name,
24
+ solution_stack_name: SOLUTION_STACK_NAME,
25
+ option_settings: option_settings,
26
+ )
27
+ end
28
+ rescue ::Aws::ElasticBeanstalk::Errors::Throttling
29
+ sleep 5
30
+ retry
31
+ end
32
+
33
+ protected
34
+
35
+ def build_option_settings(
36
+ network:,
37
+ database_url: nil,
38
+ secret_key_base: nil,
39
+ image_id: nil,
40
+ instance_type: nil,
41
+ keypair: nil,
42
+ iam:,
43
+ **_
44
+ )
45
+ instance_profile_setting = template_option_setting(namespace: "aws:autoscaling:launchconfiguration", option_name: "IamInstanceProfile", override: instance_profile(iam))
46
+ if instance_profile_setting[:value].nil?
47
+ raise MissingInstanceProfileError
48
+ end
49
+
50
+ keypair_setting = template_option_setting(namespace: "aws:autoscaling:launchconfiguration", option_name: "EC2KeyName", override: keypair)
51
+ database_url_setting = template_option_setting(namespace: "aws:elasticbeanstalk:application:environment", option_name: "DATABASE_URL", override: database_url)
52
+ secret_key_base_setting = template_option_setting(namespace: "aws:elasticbeanstalk:application:environment", option_name: "SECRET_KEY_BASE", override: secret_key_base)
53
+ if database_url_setting[:value].nil? || secret_key_base_setting[:value].nil? || keypair_setting[:value].nil?
54
+ raise MissingConfigurationError
55
+ end
56
+
57
+ security_groups = [network.ssh_security_group] + network.application_security_groups
58
+ settings = [
59
+ template_option_setting(namespace: "aws:elasticbeanstalk:command", option_name: "BatchSize", default: "1"),
60
+ template_option_setting(namespace: "aws:elasticbeanstalk:command", option_name: "BatchSizeType", default: "Fixed"),
61
+ template_option_setting(namespace: "aws:elasticbeanstalk:command", option_name: "DeploymentPolicy", default: "Rolling"),
62
+ template_option_setting(namespace: "aws:elasticbeanstalk:application:environment", option_name: "DISABLE_SQS_CONSUMER", default: "true"),
63
+ template_option_setting(namespace: "aws:elasticbeanstalk:environment", option_name: "ServiceRole", default: "aws-elasticbeanstalk-service-role"),
64
+ template_option_setting(namespace: "aws:elasticbeanstalk:healthreporting:system", option_name: "SystemType", default: "enhanced"),
65
+ template_option_setting(namespace: "aws:ec2:vpc", option_name: "AssociatePublicIpAddress", default: "false"),
66
+ template_option_setting(namespace: "aws:autoscaling:launchconfiguration", option_name: "InstanceType", default: "c4.large", override: instance_type),
67
+ template_option_setting(namespace: "aws:autoscaling:launchconfiguration", option_name: "SSHSourceRestriction", default: "tcp, 22, 22, 0.0.0.0/32"),
68
+ template_option_setting(namespace: "aws:autoscaling:updatepolicy:rollingupdate", option_name: "RollingUpdateType", default: "Health"),
69
+ template_option_setting(namespace: "aws:autoscaling:updatepolicy:rollingupdate", option_name: "RollingUpdateEnabled", default: "true"),
70
+ template_option_setting(namespace: "aws:autoscaling:launchconfiguration", option_name: "SecurityGroups", override: security_groups.join(",")),
71
+ template_option_setting(namespace: "aws:ec2:vpc", option_name: "ELBSubnets", override: network.elb_subnets.join(",")),
72
+ template_option_setting(namespace: "aws:ec2:vpc", option_name: "Subnets", override: network.application_subnets.join(",")),
73
+ template_option_setting(namespace: "aws:ec2:vpc", option_name: "VPCId", override: network.vpc),
74
+ instance_profile_setting,
75
+ keypair_setting,
76
+ database_url_setting,
77
+ secret_key_base_setting,
78
+ ]
79
+ if image_id
80
+ settings << template_option_setting(namespace: "aws:autoscaling:launchconfiguration", option_name: "ImageId", override: image_id)
81
+ end
82
+ settings
83
+ end
84
+
85
+ private
86
+
87
+ def instance_profile(iam)
88
+ return @instance_profile if @instance_profile
89
+ marker = nil
90
+ loop do
91
+ response = iam.list_instance_profiles(marker: marker)
92
+ profile = response.instance_profiles.find { |profile|
93
+ profile.instance_profile_name == "aws-elasticbeanstalk-ec2-role"
94
+ }
95
+ if profile
96
+ @instance_profile = profile.arn
97
+ return @instance_profile
98
+ end
99
+ break unless response.is_truncated
100
+ marker = response.marker
101
+ end
102
+ nil
103
+ end
104
+
105
+ class MissingInstanceProfileError < ElasticBeans::Error
106
+ def message
107
+ "Could not find Elastic Beanstalk instance profile." \
108
+ " Please create the default instance profile named \"aws-elasticbeanstalk-ec2-role\":" \
109
+ " http://docs.aws.amazon.com/elasticbeanstalk/latest/dg/AWSHowTo.iam.html"
110
+ end
111
+ end
112
+ end
113
+ end
114
+ end
@@ -0,0 +1,50 @@
1
+ require "uri"
2
+ require "elastic_beans/error"
3
+
4
+ module ElasticBeans
5
+ class ConfigurationTemplate
6
+ class Exec < ElasticBeans::ConfigurationTemplate::Base
7
+ def initialize(**args)
8
+ super(name: "exec", **args)
9
+ end
10
+
11
+ protected
12
+
13
+ def build_option_settings(logging_endpoint: nil, **_)
14
+ if logging_endpoint
15
+ begin
16
+ if URI(logging_endpoint).scheme != "https"
17
+ raise InvalidLoggingEndpointError.new(logging_endpoint: logging_endpoint)
18
+ end
19
+ rescue URI::InvalidURIError
20
+ raise InvalidLoggingEndpointError.new(logging_endpoint: logging_endpoint)
21
+ end
22
+
23
+ logging_endpoint_setting = template_option_setting(namespace: "aws:elasticbeanstalk:application:environment", option_name: "ELASTIC_BEANS_EXEC_LOGGING_ENDPOINT", override: logging_endpoint)
24
+ end
25
+
26
+ settings = [
27
+ template_option_setting(namespace: "aws:elasticbeanstalk:application", option_name: "Application Healthcheck URL", default: "HTTP:80/"),
28
+ template_option_setting(namespace: "aws:elasticbeanstalk:application:environment", option_name: "DISABLE_SQS_CONSUMER", override: "false"),
29
+ template_option_setting(namespace: "aws:elasticbeanstalk:application:environment", option_name: "ELASTIC_BEANS_EXEC_QUEUE_URL", override: application.exec_queue_url),
30
+ template_option_setting(namespace: "aws:elasticbeanstalk:application:environment", option_name: "RAILS_SKIP_ASSET_COMPILATION", default: "true"),
31
+ template_option_setting(namespace: "aws:elasticbeanstalk:application:environment", option_name: "RAILS_SKIP_MIGRATIONS", default: "true"),
32
+ ]
33
+ if logging_endpoint
34
+ settings << logging_endpoint_setting
35
+ end
36
+ super + settings
37
+ end
38
+
39
+ class InvalidLoggingEndpointError < ElasticBeans::Error
40
+ def initialize(logging_endpoint:)
41
+ @logging_endpoint = logging_endpoint
42
+ end
43
+
44
+ def message
45
+ "Logging endpoint `#{@logging_endpoint}' must be a valid HTTPS URL."
46
+ end
47
+ end
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,20 @@
1
+ module ElasticBeans
2
+ class ConfigurationTemplate
3
+ class Scheduler < ElasticBeans::ConfigurationTemplate::Base
4
+ def initialize(**args)
5
+ super(name: "scheduler", **args)
6
+ end
7
+
8
+ protected
9
+
10
+ def build_option_settings(**_)
11
+ super + [
12
+ template_option_setting(namespace: "aws:elasticbeanstalk:application", option_name: "Application Healthcheck URL", default: "HTTP:80/"),
13
+ template_option_setting(namespace: "aws:elasticbeanstalk:application:environment", option_name: "DISABLE_SQS_CONSUMER", override: "false"),
14
+ template_option_setting(namespace: "aws:elasticbeanstalk:application:environment", option_name: "RAILS_SKIP_ASSET_COMPILATION", default: "true"),
15
+ template_option_setting(namespace: "aws:elasticbeanstalk:application:environment", option_name: "RAILS_SKIP_MIGRATIONS", default: "true"),
16
+ ]
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,49 @@
1
+ require "elastic_beans/error"
2
+
3
+ module ElasticBeans
4
+ class ConfigurationTemplate
5
+ class Webserver < ElasticBeans::ConfigurationTemplate::Base
6
+ def initialize(**args)
7
+ super(name: "webserver", **args)
8
+ end
9
+
10
+ protected
11
+
12
+ def build_option_settings(network:, public_key:, ssl_certificate_id:, **_)
13
+ public_key_policy_names_setting = template_option_setting(namespace: "aws:elb:policies:backendencryption", option_name: "PublicKeyPolicyNames", default: "backendkey")
14
+ public_key_setting = template_option_setting(namespace: "aws:elb:policies:#{public_key_policy_names_setting[:value]}", option_name: "PublicKey", override: public_key)
15
+ ssl_certificate_setting = template_option_setting(namespace: "aws:elb:listener:443", option_name: "SSLCertificateId", override: ssl_certificate_id)
16
+ if public_key_setting[:value].nil? || ssl_certificate_setting[:value].nil?
17
+ raise NoEncryptionSettingsError
18
+ end
19
+
20
+ super + [
21
+ template_option_setting(namespace: "aws:elasticbeanstalk:application", option_name: "Application Healthcheck URL", default: "HTTPS:443/"),
22
+ template_option_setting(namespace: "aws:elasticbeanstalk:application:environment", option_name: "RAILS_SKIP_ASSET_COMPILATION", default: "false"),
23
+ template_option_setting(namespace: "aws:elasticbeanstalk:application:environment", option_name: "RAILS_SKIP_MIGRATIONS", default: "false"),
24
+ template_option_setting(namespace: "aws:elb:listener:443", option_name: "InstancePort", default: "443"),
25
+ template_option_setting(namespace: "aws:elb:listener:443", option_name: "InstanceProtocol", default: "HTTPS"),
26
+ template_option_setting(namespace: "aws:elb:listener:443", option_name: "ListenerProtocol", default: "HTTPS"),
27
+ template_option_setting(namespace: "aws:elb:loadbalancer", option_name: "ManagedSecurityGroup", override: network.elb_security_groups[0]),
28
+ template_option_setting(namespace: "aws:elb:loadbalancer", option_name: "SecurityGroups", override: network.elb_security_groups.join(",")),
29
+ template_option_setting(namespace: "aws:elb:policies", option_name: "ConnectionDrainingEnabled", default: "true"),
30
+ template_option_setting(namespace: "aws:elb:policies:backendencryption", option_name: "InstancePorts", default: "443"),
31
+ public_key_policy_names_setting,
32
+ public_key_setting,
33
+ ssl_certificate_setting,
34
+ ]
35
+ end
36
+
37
+ class NoEncryptionSettingsError < ElasticBeans::Error
38
+ def message
39
+ require "elastic_beans/command/configure"
40
+ <<-MESSAGE
41
+ Missing required end-to-end encryption settings. Make sure to specify SSL certificate ID and public key.
42
+
43
+ #{$0} #{ElasticBeans::Command::Configure::USAGE}
44
+ MESSAGE
45
+ end
46
+ end
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,27 @@
1
+ module ElasticBeans
2
+ class ConfigurationTemplate
3
+ class Worker < ElasticBeans::ConfigurationTemplate::Base
4
+ attr_reader :queue
5
+
6
+ def initialize(queue:, **args)
7
+ @queue = queue
8
+ super(name: "worker-#{queue}", **args)
9
+ end
10
+
11
+ protected
12
+
13
+ def build_option_settings(**_)
14
+ super + [
15
+ template_option_setting(namespace: "aws:elasticbeanstalk:application", option_name: "Application Healthcheck URL", default: "HTTP:80/"),
16
+ template_option_setting(namespace: "aws:elasticbeanstalk:application:environment", option_name: "DISABLE_SQS_CONSUMER", override: "false"),
17
+ template_option_setting(namespace: "aws:elasticbeanstalk:application:environment", option_name: "RAILS_SKIP_ASSET_COMPILATION", default: "true"),
18
+ template_option_setting(namespace: "aws:elasticbeanstalk:application:environment", option_name: "RAILS_SKIP_MIGRATIONS", default: "true"),
19
+ template_option_setting(namespace: "aws:elasticbeanstalk:sqsd", option_name: "InactivityTimeout", default: "1800"),
20
+ template_option_setting(namespace: "aws:elasticbeanstalk:sqsd", option_name: "MaxRetries", default: "10"),
21
+ template_option_setting(namespace: "aws:elasticbeanstalk:sqsd", option_name: "VisibilityTimeout", default: "1800"),
22
+ template_option_setting(namespace: "aws:elasticbeanstalk:sqsd", option_name: "WorkerQueueURL", override: application.worker_queue_url(queue)),
23
+ ]
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,197 @@
1
+ require "elastic_beans/configuration_template/base"
2
+ require "elastic_beans/configuration_template/exec"
3
+ require "elastic_beans/configuration_template/scheduler"
4
+ require "elastic_beans/configuration_template/webserver"
5
+ require "elastic_beans/configuration_template/worker"
6
+
7
+ module ElasticBeans
8
+ class ConfigurationTemplate
9
+ WORKER_TEMPLATE_NAME_PATTERN = /\Aworker-(?<queue>\w+)\z/
10
+
11
+ attr_reader :name
12
+
13
+ def self.new_by_type(type, **args)
14
+ case type
15
+ when "base"
16
+ ElasticBeans::ConfigurationTemplate::Base.new(**args)
17
+ when "exec"
18
+ ElasticBeans::ConfigurationTemplate::Exec.new(**args)
19
+ when "scheduler"
20
+ ElasticBeans::ConfigurationTemplate::Scheduler.new(**args)
21
+ when "webserver"
22
+ ElasticBeans::ConfigurationTemplate::Webserver.new(**args)
23
+ when "worker"
24
+ ElasticBeans::ConfigurationTemplate::Worker.new(**args)
25
+ else
26
+ raise UnknownConfigurationType.new(type: type)
27
+ end
28
+ end
29
+
30
+ def self.new_from_existing(template_name, **args)
31
+ case template_name
32
+ when "base"
33
+ ElasticBeans::ConfigurationTemplate::Base.new(**args)
34
+ when "exec"
35
+ ElasticBeans::ConfigurationTemplate::Exec.new(**args)
36
+ when "scheduler"
37
+ ElasticBeans::ConfigurationTemplate::Scheduler.new(**args)
38
+ when "webserver"
39
+ ElasticBeans::ConfigurationTemplate::Webserver.new(**args)
40
+ when WORKER_TEMPLATE_NAME_PATTERN
41
+ match = WORKER_TEMPLATE_NAME_PATTERN.match(template_name)
42
+ ElasticBeans::ConfigurationTemplate::Worker.new(queue: match[:queue], **args)
43
+ else
44
+ raise UnknownConfigurationType.new(type: template_name)
45
+ end
46
+ end
47
+
48
+ def initialize(
49
+ name:,
50
+ application:,
51
+ elastic_beanstalk:,
52
+ **_
53
+ )
54
+ @name = name
55
+ @application = application
56
+ @elastic_beanstalk = elastic_beanstalk
57
+ end
58
+
59
+ def option_settings
60
+ # do not fetch option settings from Elastic Beanstalk, because those cannot be naively used.
61
+ # the set built in #build_option_settings is what we care about.
62
+ return @option_settings if @option_settings
63
+ raise MissingConfigurationError
64
+ end
65
+
66
+ def update_environment(env_vars)
67
+ new_env_settings = env_vars.map { |k, v|
68
+ {namespace: "aws:elasticbeanstalk:application:environment", option_name: k, value: v}
69
+ }
70
+ elastic_beanstalk.update_configuration_template(
71
+ application_name: application.name,
72
+ template_name: name,
73
+ option_settings: new_env_settings,
74
+ )
75
+ rescue ::Aws::ElasticBeanstalk::Errors::ConfigurationValidationException => e
76
+ raise InvalidConfigurationError.new(cause: e)
77
+ rescue ::Aws::ElasticBeanstalk::Errors::InvalidParameterValue => e
78
+ raise MissingConfigurationError.new(cause: e)
79
+ rescue ::Aws::ElasticBeanstalk::Errors::Throttling
80
+ sleep 5
81
+ retry
82
+ end
83
+
84
+ def upsert(**args)
85
+ @option_settings = build_option_settings(**args)
86
+ if configuration_settings_description
87
+ elastic_beanstalk.update_configuration_template(
88
+ application_name: application.name,
89
+ template_name: name,
90
+ option_settings: option_settings,
91
+ )
92
+ else
93
+ elastic_beanstalk.create_configuration_template(
94
+ application_name: application.name,
95
+ template_name: name,
96
+ source_configuration: {application_name: application.name, template_name: "base"},
97
+ option_settings: option_settings,
98
+ )
99
+ end
100
+ rescue ::Aws::ElasticBeanstalk::Errors::Throttling
101
+ sleep 5
102
+ retry
103
+ end
104
+
105
+ protected
106
+
107
+ attr_reader :application, :elastic_beanstalk
108
+
109
+ def build_option_settings(**_)
110
+ []
111
+ end
112
+
113
+ def configuration_settings_description
114
+ @configuration_template ||= elastic_beanstalk.describe_configuration_settings(
115
+ application_name: application.name,
116
+ template_name: name,
117
+ ).configuration_settings[0]
118
+ rescue ::Aws::ElasticBeanstalk::Errors::InvalidParameterValue => e
119
+ if e.message =~ /\bconfiguration template\b/i
120
+ return nil
121
+ end
122
+ raise MissingConfigurationError.new(cause: e)
123
+ end
124
+
125
+ def template_option_setting(template: configuration_settings_description, namespace:, option_name:, default: nil, override: nil)
126
+ option_setting = {namespace: namespace, option_name: option_name, value: default}
127
+ if override
128
+ return option_setting.merge!(value: override)
129
+ end
130
+
131
+ if template.nil?
132
+ return option_setting
133
+ end
134
+ setting = template.to_h[:option_settings].find { |setting|
135
+ setting[:namespace] == namespace && setting[:option_name] == option_name
136
+ }
137
+ if setting.nil?
138
+ return option_setting
139
+ end
140
+
141
+ option_setting.merge!(value: setting[:value])
142
+ end
143
+
144
+ class InvalidConfigurationError < ElasticBeans::Error
145
+ include Nesty::NestedError
146
+
147
+ def initialize(cause: nil)
148
+ @nested = cause
149
+ end
150
+
151
+ def message
152
+ msg = "Configuration was rejected by Elastic Beanstalk. Check for too much configuration or the use of reserved words."
153
+ if nested
154
+ msg << "The error from AWS was \"#{nested.message}\""
155
+ end
156
+ msg
157
+ end
158
+ end
159
+
160
+ class MissingConfigurationError < ElasticBeans::Error
161
+ include Nesty::NestedError
162
+
163
+ def initialize(cause: nil)
164
+ @nested = cause
165
+ end
166
+
167
+ def message
168
+ require "elastic_beans/command/configure"
169
+ msg = <<-MESSAGE
170
+ Some configuration must be set before creating an environment:
171
+
172
+ * keypair
173
+ * SSL configuration
174
+ * environment variables that Rails always requires
175
+ * DATABASE_URL
176
+ * SECRET_KEY_BASE
177
+
178
+ #{$0} #{ElasticBeans::Command::Configure::USAGE}
179
+ MESSAGE
180
+ if nested
181
+ msg = "The error from AWS was \"#{nested.message}\"\n#{msg}"
182
+ end
183
+ msg
184
+ end
185
+ end
186
+
187
+ class UnknownConfigurationType < ElasticBeans::Error
188
+ def initialize(type:)
189
+ @type = type
190
+ end
191
+
192
+ def message
193
+ "Unknown configuration type `#{@type}'"
194
+ end
195
+ end
196
+ end
197
+ end