elastic_beans 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
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