elastic_beans 0.1.0 → 0.2.0

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: 18b8fddb84235084c0ed2656617d432b4b22e6d4
4
- data.tar.gz: c49a0e1f3240697c150aab493b9373913dc25c61
3
+ metadata.gz: cb92ef373018a6f507082e86909372817ddd7042
4
+ data.tar.gz: 49eb178cbdad8e93930afa6998577588093d7994
5
5
  SHA512:
6
- metadata.gz: 480c7aeed8133114eafa17306f832a0a1e21487c9b0069424567283122b166fb3051eaee5f7d7b3829f62030fc91c2904af0bd65430087603884593dda108858
7
- data.tar.gz: 35cc62acbf16cdbe0b26b2a600b9f84332bb6c5ac7e151053b56072aa40c4692f37245293285a0d671287ede3791f8cdce678ef53e662f9bae43693253371e5c
6
+ metadata.gz: e8b5a831464781d60d79e18c66c723a361d02c597b1e5cea19ed059e62ab70ce8be4f1a80fa4154d37455b7d4e455e409b833d50ccf198e77c600ca2a7148db9
7
+ data.tar.gz: eb62d218f9ab435ff4d90beb9e8f209ddedcf289e981cd09302bc4390763a2a7b1c90d9d0acab694f3deeeefc64c6ad15fa98f1eab0f33fc7ea7c305f8d8baed
data/README.md CHANGED
@@ -26,6 +26,9 @@ As the SDK documentation suggests, using environment variables is recommended.
26
26
  # Create a webserver environment with a pretty DNS name at myapp.TLD (managed by Route53)
27
27
  beans create -a myapp [-d myapp.TLD] [--tags=Environment:production Team:Unicorn] webserver
28
28
 
29
+ # Add additional webserver instances to ensure no downtime
30
+ beans scale -a myapp --min 2 --max 4 webserver
31
+
29
32
  # Create a worker environment using an SQS queue created by the CloudFormation template
30
33
  beans create -a myapp [-q QUEUE] worker
31
34
 
@@ -54,6 +57,7 @@ As the SDK documentation suggests, using environment variables is recommended.
54
57
  name: "myapp",
55
58
  cloudformation: Aws::CloudFormation::Client.new,
56
59
  elastic_beanstalk: Aws::ElasticBeanstalk::Client.new,
60
+ s3: Aws::S3::Client.new,
57
61
  )
58
62
  app.exec_command("rake db:migrate", sqs: Aws::SQS::Client.new)
59
63
 
@@ -105,6 +109,42 @@ Then create a periodic scheduler environment using `cron.yaml`:
105
109
 
106
110
  This environment will enqueue the commands from `cron.yaml` for the `exec` environment to run.
107
111
 
112
+ ## What elastic_beans does differently than awsebcli
113
+
114
+ ### End-to-end encryption
115
+
116
+ Elastic Beans sets up [end-to-end encryption][e2e] by default.
117
+ The ELB is configured with an SSL certificate on an HTTPS listener, as well as a backend key policy.
118
+
119
+ [e2e]: http://docs.aws.amazon.com/elasticbeanstalk/latest/dg/configuring-https-endtoend.html
120
+
121
+ ### Environment variables
122
+
123
+ [Elastic Beanstalk sets a 4096-byte limit on the environment variable string][envlimit], which is passed to CloudFormation.
124
+ The string is of the form `KEY=VALUE,KEY2=VALUE2...`.
125
+ If your application has a lot of environment variables, there is no easy way around this.
126
+ Elastic Beans avoids this by managing your environment variables in S3 from the start.
127
+ Before loading your application, the configuration is fetched and loaded.
128
+
129
+ [envlimit]: http://docs.aws.amazon.com/elasticbeanstalk/latest/dg/command-options-general.html#command-options-docker-elasticbeanstalkapplicationenvironment
130
+
131
+ ### One-off and periodic commands
132
+
133
+ Elastic Beans supports the execution of one-off and periodic commands in an isolated environment.
134
+ This way they can be computationally expensive without affecting application performance.
135
+ Additionally, they are not limited to the visibility timeout of an application background job queue.
136
+ Logs are persisted to an HTTPS endpoint upon command completion.
137
+
138
+ ### Shared code between environments
139
+
140
+ A Rails app supports multiple contexts with the same codebase.
141
+ Elastic Beans supports webserver, worker, one-off command, and scheduled command environments.
142
+
143
+ ### VPC support
144
+
145
+ Elastic Beans enforces the use of a VPC and supports running your application in a private network.
146
+ Commands that need to connect to an instance can use a bastion server as an SSH gateway.
147
+
108
148
  ## Requirements
109
149
 
110
150
  Elastic Beans assumes that you use CloudFormation to manage your infrastructure.
@@ -179,6 +219,18 @@ After checking out the repo, run `bin/setup` to install dependencies. Then, run
179
219
 
180
220
  To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).
181
221
 
222
+ ### Philosophy
223
+
224
+ Elastic Beanstalk is an evolving platform, albeit slowly.
225
+ It has many gaps and surprises in functionality that beans addresses.
226
+ Whenever possible, beans adds behavior using publicized and approved methods, such as `commands` in an ebextension.
227
+ But sometimes, beans must reach under the hood and tweak things in a less-safe manner.
228
+
229
+ Beans uses Ruby objects to represent Elastic Beanstalk concepts, like `Application` or `Environment`.
230
+ Those objects should be constructed with enough information to locate an existing entity.
231
+ However, it is expected that a Ruby object can represent an Elastic Beanstalk entity that does not exist.
232
+ This is most common when creating them.
233
+
182
234
  ## Contributing
183
235
 
184
236
  Bug reports and pull requests are welcome on GitHub at https://github.com/onemedical/elastic_beans.
data/exe/beans CHANGED
@@ -45,10 +45,10 @@ class ElasticBeans::CLI < Thor
45
45
  option :dns, aliases: %w(-d)
46
46
  option :queue, aliases: %w(-q)
47
47
  option :tags, type: :hash, default: {}
48
- def create(environment_name)
48
+ def create(environment_type)
49
49
  @verbose = options[:verbose]
50
50
  ElasticBeans::Command::Create.new(
51
- environment_name: environment_name,
51
+ environment_type: environment_type,
52
52
  dns: options[:dns],
53
53
  queue: options[:queue],
54
54
  tags: options[:tags],
@@ -95,6 +95,28 @@ class ElasticBeans::CLI < Thor
95
95
  error(e)
96
96
  end
97
97
 
98
+ desc ElasticBeans::Command::Scale::USAGE, ElasticBeans::Command::Scale::DESC
99
+ long_desc ElasticBeans::Command::Scale::LONG_DESC
100
+ option :application, aliases: %w(-a), required: true
101
+ option :minimum, aliases: %w(-i --min), required: true
102
+ option :maximum, aliases: %w(-m --max), required: true
103
+ option :queue, aliases: %w(-q)
104
+ def scale(environment_type)
105
+ @verbose = options[:verbose]
106
+ ElasticBeans::Command::Scale.new(
107
+ application: application(
108
+ name: options[:application],
109
+ ),
110
+ minimum: options[:minimum],
111
+ maximum: options[:maximum],
112
+ queue: options[:queue],
113
+ elastic_beanstalk: elastic_beanstalk_client,
114
+ ui: ui,
115
+ ).run(environment_type)
116
+ rescue StandardError => e
117
+ error(e)
118
+ end
119
+
98
120
  desc ElasticBeans::Command::SetEnv::USAGE, ElasticBeans::Command::SetEnv::DESC
99
121
  long_desc ElasticBeans::Command::SetEnv::LONG_DESC
100
122
  option :application, aliases: %w(-a), required: true
@@ -126,12 +148,14 @@ class ElasticBeans::CLI < Thor
126
148
  def application(
127
149
  name:,
128
150
  cloudformation: cloudformation_client,
129
- elastic_beanstalk: elastic_beanstalk_client
151
+ elastic_beanstalk: elastic_beanstalk_client,
152
+ s3: s3_client
130
153
  )
131
154
  @application ||= ElasticBeans::Application.new(
132
155
  name: name,
133
156
  cloudformation: cloudformation,
134
157
  elastic_beanstalk: elastic_beanstalk,
158
+ s3: s3,
135
159
  )
136
160
  end
137
161
 
data/lib/elastic_beans.rb CHANGED
@@ -4,6 +4,7 @@ end
4
4
  require "elastic_beans/application"
5
5
  require "elastic_beans/application_version"
6
6
  require "elastic_beans/configuration_template"
7
+ require "elastic_beans/env_vars"
7
8
  require "elastic_beans/environment"
8
9
  require "elastic_beans/network"
9
10
  require "elastic_beans/version"
@@ -7,9 +7,10 @@ module ElasticBeans
7
7
  class Application
8
8
  attr_reader :name
9
9
 
10
- def initialize(name:, cloudformation:, elastic_beanstalk:)
10
+ def initialize(name:, cloudformation:, elastic_beanstalk:, s3:)
11
11
  @name = name
12
12
  @elastic_beanstalk = elastic_beanstalk
13
+ @s3 = s3
13
14
  @stack = ElasticBeans::Aws::CloudformationStack.new(name, cloudformation: cloudformation)
14
15
  end
15
16
 
@@ -45,6 +46,14 @@ module ElasticBeans
45
46
  retry
46
47
  end
47
48
 
49
+ def env_vars
50
+ unless exists?
51
+ raise MissingApplicationError
52
+ end
53
+
54
+ EnvVars.new(application: self, s3: s3)
55
+ end
56
+
48
57
  def environments
49
58
  response = elastic_beanstalk.describe_environments(application_name: name)
50
59
  live_environments = response.environments.select { |environment| environment.status !~ /Terminat/ }
@@ -60,6 +69,15 @@ module ElasticBeans
60
69
  retry
61
70
  end
62
71
 
72
+ def bucket_name
73
+ return @bucket_name if @bucket_name
74
+ bucket = s3.list_buckets.buckets.find { |bucket| bucket.name.start_with?("elasticbeanstalk-") }
75
+ unless bucket
76
+ raise MissingBucketError
77
+ end
78
+ @bucket_name = bucket.name
79
+ end
80
+
63
81
  def enqueue_command(command, sqs:)
64
82
  if environments.none? { |environment| environment.is_a?(Environment::Exec) }
65
83
  raise MissingExecEnvironmentError
@@ -100,7 +118,7 @@ module ElasticBeans
100
118
 
101
119
  private
102
120
 
103
- attr_reader :elastic_beanstalk, :stack
121
+ attr_reader :elastic_beanstalk, :s3, :stack
104
122
 
105
123
  def exec_message(command)
106
124
  {
@@ -108,12 +126,26 @@ module ElasticBeans
108
126
  }.to_json
109
127
  end
110
128
 
129
+ def exists?
130
+ elastic_beanstalk.describe_applications(application_names: [name]).applications.any?
131
+ rescue ::Aws::ElasticBeanstalk::Errors::Throttling
132
+ sleep 5
133
+ retry
134
+ end
135
+
111
136
  class MissingApplicationError < ElasticBeans::Error
112
137
  def message
113
138
  "Application `#{@application_name}' does not exist. Please create the Elastic Beanstalk application using a CloudFormation stack."
114
139
  end
115
140
  end
116
141
 
142
+ class MissingBucketError < ElasticBeans::Error
143
+ def message
144
+ "Cannot find the Elastic Beanstalk S3 bucket." \
145
+ " Create an S3 bucket with a name starting with \"elasticbeanstalk-\"."
146
+ end
147
+ end
148
+
117
149
  class MissingExecEnvironmentError < ElasticBeans::Error
118
150
  def message
119
151
  <<-MESSAGE
@@ -37,11 +37,10 @@ module ElasticBeans
37
37
  def create_application_version(path:, version_label:, application:, elastic_beanstalk:, s3:)
38
38
  archive_filename = File.basename(path)
39
39
  archive = File.new(path)
40
- eb_bucket_name = find_eb_bucket_name(s3)
41
- s3.put_object(body: archive, bucket: eb_bucket_name, key: archive_filename)
40
+ s3.put_object(body: archive, bucket: application.bucket_name, key: archive_filename)
42
41
  elastic_beanstalk.create_application_version(
43
42
  application_name: application.name,
44
- source_bundle: {s3_bucket: eb_bucket_name, s3_key: archive_filename},
43
+ source_bundle: {s3_bucket: application.bucket_name, s3_key: archive_filename},
45
44
  version_label: version_label,
46
45
  process: true,
47
46
  )
@@ -77,19 +76,12 @@ module ElasticBeans
77
76
  end
78
77
 
79
78
  Zip::File.open(path) do |zip_file|
79
+ inject_env_vars_into_zip(zip_file)
80
80
  inject_exec_into_zip(zip_file)
81
81
  inject_scheduler_into_zip(zip_file)
82
82
  end
83
83
  end
84
84
 
85
- def find_eb_bucket_name(s3)
86
- bucket = s3.list_buckets.buckets.find { |bucket| bucket.name.start_with?("elasticbeanstalk-") }
87
- unless bucket
88
- raise MissingBucketError
89
- end
90
- bucket.name
91
- end
92
-
93
85
  def fetch_version_label(working_directory)
94
86
  if Dir.glob(File.join(working_directory, ".git")).empty?
95
87
  raise InvalidVersionWorkingDirectoryError.new(wd: working_directory)
@@ -100,6 +92,17 @@ module ElasticBeans
100
92
  end
101
93
  end
102
94
 
95
+ def inject_env_vars_into_zip(zip_file)
96
+ begin
97
+ zip_file.mkdir(".ebextensions")
98
+ rescue Errno::EEXIST
99
+ end
100
+ zip_file.add(
101
+ ".ebextensions/elastic_beans_env_vars.config",
102
+ File.expand_path('../env_vars/ebextension.yml', __FILE__),
103
+ )
104
+ end
105
+
103
106
  def inject_exec_into_zip(zip_file)
104
107
  begin
105
108
  zip_file.mkdir(".elastic_beans")
@@ -191,12 +194,5 @@ module ElasticBeans
191
194
  "Cannot create a version from '#{@wd}`, please change to a Rails project root directory."
192
195
  end
193
196
  end
194
-
195
- class MissingBucketError < ElasticBeans::Error
196
- def message
197
- "Cannot find the Elastic Beanstalk S3 bucket." \
198
- " Create an S3 bucket with a name starting with \"elasticbeanstalk-\"."
199
- end
200
- end
201
197
  end
202
198
  end
@@ -2,6 +2,7 @@ require "elastic_beans/command/configure"
2
2
  require "elastic_beans/command/create"
3
3
  require "elastic_beans/command/deploy"
4
4
  require "elastic_beans/command/exec"
5
+ require "elastic_beans/command/scale"
5
6
  require "elastic_beans/command/set_env"
6
7
  require "elastic_beans/command/talk"
7
8
  require "elastic_beans/command/version"
@@ -1,3 +1,4 @@
1
+ require "ruby-progressbar"
1
2
  require "elastic_beans/error/environments_not_ready"
2
3
 
3
4
  module ElasticBeans
@@ -7,7 +7,7 @@ require "elastic_beans/environment/webserver"
7
7
  module ElasticBeans
8
8
  module Command
9
9
  class Create
10
- USAGE = "create -a APPLICATION [-t TYPE] [-q QUEUE] [-d PRETTY_DNS] ENVIRONMENT"
10
+ USAGE = "create -a APPLICATION [-q QUEUE] [-d PRETTY_DNS] ENVIRONMENT_TYPE"
11
11
  DESC = "Create a new environment in Elastic Beanstalk and attach it to relevant resources"
12
12
  LONG_DESC = <<-LONG_DESC
13
13
  Create a new environment in Elastic Beanstalk and attach it to relevant resources
@@ -18,7 +18,7 @@ Requires AWS credentials to be set in the environment, i.e. AWS_ACCESS_KEY_ID an
18
18
  LONG_DESC
19
19
 
20
20
  def initialize(
21
- environment_name:,
21
+ environment_type:,
22
22
  dns: nil,
23
23
  queue: nil,
24
24
  tags:,
@@ -28,12 +28,10 @@ Requires AWS credentials to be set in the environment, i.e. AWS_ACCESS_KEY_ID an
28
28
  route53:,
29
29
  s3:
30
30
  )
31
- @environment_name = environment_name
32
- @environment_type = environment_name
31
+ @environment_type = environment_type
33
32
  @dns = dns
34
33
  if @environment_type == "worker"
35
34
  @queue = queue || "default"
36
- @environment_name = "#{@environment_name}-#{@queue}"
37
35
  end
38
36
  @tags = tags
39
37
  @application = application
@@ -53,7 +51,6 @@ Requires AWS credentials to be set in the environment, i.e. AWS_ACCESS_KEY_ID an
53
51
  )
54
52
  environment = ElasticBeans::Environment.new_by_type(
55
53
  environment_type,
56
- suffix: environment_name,
57
54
  queue: queue,
58
55
  application: application,
59
56
  elastic_beanstalk: elastic_beanstalk,
@@ -1,3 +1,4 @@
1
+ require "ruby-progressbar"
1
2
  require "elastic_beans/error/environments_not_ready"
2
3
 
3
4
  module ElasticBeans
@@ -0,0 +1,77 @@
1
+ require "ruby-progressbar"
2
+ require "elastic_beans/error/environments_not_ready"
3
+
4
+ module ElasticBeans
5
+ module Command
6
+ class Scale
7
+ USAGE = "scale -a APPLICATION -i MINIMUM -m MAXIMUM ENVIRONMENT"
8
+ DESC = "Change the autoscaling minimum and maximum for the given environment"
9
+ LONG_DESC = <<-LONG_DESC
10
+ Change the autoscaling minimum and maximum for the given environment.
11
+
12
+ Requires the application name.
13
+ Requires AWS credentials to be set in the environment, i.e. AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY.
14
+ LONG_DESC
15
+
16
+ def initialize(application:, minimum:, maximum:, queue:, elastic_beanstalk:, ui:)
17
+ @application = application
18
+ @minimum = minimum
19
+ @maximum = maximum
20
+ @queue = queue || "default"
21
+ @elastic_beanstalk = elastic_beanstalk
22
+ @ui = ui
23
+ end
24
+
25
+ def run(environment_type)
26
+ environment = ElasticBeans::Environment.new_by_type(
27
+ environment_type,
28
+ queue: queue,
29
+ application: application,
30
+ elastic_beanstalk: elastic_beanstalk,
31
+ )
32
+ if environment.status != "Ready"
33
+ raise EnvironmentsNotReady.new(environments: [environment])
34
+ end
35
+
36
+ progressbar = ProgressBar.create(title: "Configuring", total: nil, output: ui.stdout)
37
+
38
+ thread = Thread.new do
39
+ config = ElasticBeans::ConfigurationTemplate.new_by_type(
40
+ environment_type,
41
+ queue: queue,
42
+ application: application,
43
+ elastic_beanstalk: elastic_beanstalk,
44
+ )
45
+ progressbar.log("Updating `#{config.name}' configuration template in #{application.name}...")
46
+ config.upsert(min_size: minimum, max_size: maximum)
47
+ end
48
+ loop do
49
+ sleep 0.5
50
+ progressbar.increment
51
+ if !thread.alive?
52
+ break
53
+ end
54
+ end
55
+ thread.join
56
+
57
+ thread = Thread.new do
58
+ progressbar.log("Updating `#{environment.name}'...")
59
+ environment.scale(min_size: minimum, max_size: maximum)
60
+ end
61
+ loop do
62
+ sleep 0.5
63
+ progressbar.increment
64
+ if !thread.alive?
65
+ progressbar.total = progressbar.progress
66
+ break
67
+ end
68
+ end
69
+ thread.join
70
+ end
71
+
72
+ private
73
+
74
+ attr_reader :application, :minimum, :maximum, :queue, :elastic_beanstalk, :ui
75
+ end
76
+ end
77
+ end
@@ -31,19 +31,16 @@ Requires AWS credentials to be set in the environment, i.e. AWS_ACCESS_KEY_ID an
31
31
 
32
32
  progressbar = ProgressBar.create(title: "Updating", total: nil, output: ui.stdout)
33
33
 
34
- progressbar.log("Updating configuration templates in #{application.name}...")
35
- threads += application.configuration_templates.map { |config|
36
- thread = Thread.new do
37
- config.update_environment(env_vars)
38
- end
39
- progressbar.increment
40
- thread
41
- }
34
+ progressbar.log("Updating configuration in #{application.name}...")
35
+ threads << Thread.new do
36
+ application.env_vars.update(env_vars)
37
+ end
38
+ progressbar.increment
42
39
 
43
40
  threads += environments.map { |environment|
44
41
  progressbar.log("Updating `#{environment.name}'...")
45
42
  thread = Thread.new do
46
- environment.update_environment(env_vars)
43
+ environment.restart
47
44
  end
48
45
  progressbar.increment
49
46
  thread
@@ -63,24 +63,6 @@ module ElasticBeans
63
63
  raise MissingConfigurationError
64
64
  end
65
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
66
  def upsert(**args)
85
67
  @option_settings = build_option_settings(**args)
86
68
  if configuration_settings_description
@@ -33,13 +33,15 @@ module ElasticBeans
33
33
  protected
34
34
 
35
35
  def build_option_settings(
36
- network:,
36
+ network: nil,
37
37
  database_url: nil,
38
38
  secret_key_base: nil,
39
39
  image_id: nil,
40
40
  instance_type: nil,
41
41
  keypair: nil,
42
- iam:,
42
+ min_size: nil,
43
+ max_size: nil,
44
+ iam: nil,
43
45
  **_
44
46
  )
45
47
  instance_profile_setting = template_option_setting(namespace: "aws:autoscaling:launchconfiguration", option_name: "IamInstanceProfile", override: instance_profile(iam))
@@ -54,23 +56,26 @@ module ElasticBeans
54
56
  raise MissingConfigurationError
55
57
  end
56
58
 
57
- security_groups = [network.ssh_security_group] + network.application_security_groups
59
+ config_path = "#{application.bucket_name}/#{application.env_vars.s3_key}"
58
60
  settings = [
59
61
  template_option_setting(namespace: "aws:elasticbeanstalk:command", option_name: "BatchSize", default: "1"),
60
62
  template_option_setting(namespace: "aws:elasticbeanstalk:command", option_name: "BatchSizeType", default: "Fixed"),
61
63
  template_option_setting(namespace: "aws:elasticbeanstalk:command", option_name: "DeploymentPolicy", default: "Rolling"),
62
64
  template_option_setting(namespace: "aws:elasticbeanstalk:application:environment", option_name: "DISABLE_SQS_CONSUMER", default: "true"),
65
+ template_option_setting(namespace: "aws:elasticbeanstalk:application:environment", option_name: "ELASTIC_BEANS_ENV_VARS", default: config_path),
63
66
  template_option_setting(namespace: "aws:elasticbeanstalk:environment", option_name: "ServiceRole", default: "aws-elasticbeanstalk-service-role"),
64
67
  template_option_setting(namespace: "aws:elasticbeanstalk:healthreporting:system", option_name: "SystemType", default: "enhanced"),
65
68
  template_option_setting(namespace: "aws:ec2:vpc", option_name: "AssociatePublicIpAddress", default: "false"),
69
+ template_option_setting(namespace: "aws:autoscaling:asg", option_name: "MinSize", default: "1", override: min_size),
70
+ template_option_setting(namespace: "aws:autoscaling:asg", option_name: "MaxSize", default: "4", override: max_size),
66
71
  template_option_setting(namespace: "aws:autoscaling:launchconfiguration", option_name: "InstanceType", default: "c4.large", override: instance_type),
67
72
  template_option_setting(namespace: "aws:autoscaling:launchconfiguration", option_name: "SSHSourceRestriction", default: "tcp, 22, 22, 0.0.0.0/32"),
68
73
  template_option_setting(namespace: "aws:autoscaling:updatepolicy:rollingupdate", option_name: "RollingUpdateType", default: "Health"),
69
74
  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),
75
+ template_option_setting(namespace: "aws:autoscaling:launchconfiguration", option_name: "SecurityGroups", override: security_groups(network)),
76
+ template_option_setting(namespace: "aws:ec2:vpc", option_name: "ELBSubnets", override: elb_subnets(network)),
77
+ template_option_setting(namespace: "aws:ec2:vpc", option_name: "Subnets", override: subnets(network)),
78
+ template_option_setting(namespace: "aws:ec2:vpc", option_name: "VPCId", override: vpc_id(network)),
74
79
  instance_profile_setting,
75
80
  keypair_setting,
76
81
  database_url_setting,
@@ -84,8 +89,15 @@ module ElasticBeans
84
89
 
85
90
  private
86
91
 
92
+ def elb_subnets(network)
93
+ if network
94
+ network.elb_subnets.join(",")
95
+ end
96
+ end
97
+
87
98
  def instance_profile(iam)
88
99
  return @instance_profile if @instance_profile
100
+ return nil unless iam
89
101
  marker = nil
90
102
  loop do
91
103
  response = iam.list_instance_profiles(marker: marker)
@@ -102,6 +114,18 @@ module ElasticBeans
102
114
  nil
103
115
  end
104
116
 
117
+ def security_groups(network)
118
+ ([network.ssh_security_group] + network.application_security_groups).join(",") if network
119
+ end
120
+
121
+ def subnets(network)
122
+ network.application_subnets.join(",") if network
123
+ end
124
+
125
+ def vpc_id(network)
126
+ network.vpc if network
127
+ end
128
+
105
129
  class MissingInstanceProfileError < ElasticBeans::Error
106
130
  def message
107
131
  "Could not find Elastic Beanstalk instance profile." \
@@ -9,7 +9,7 @@ module ElasticBeans
9
9
 
10
10
  protected
11
11
 
12
- def build_option_settings(network:, public_key:, ssl_certificate_id:, **_)
12
+ def build_option_settings(network: nil, public_key: nil, ssl_certificate_id: nil, **_)
13
13
  public_key_policy_names_setting = template_option_setting(namespace: "aws:elb:policies:backendencryption", option_name: "PublicKeyPolicyNames", default: "backendkey")
14
14
  public_key_setting = template_option_setting(namespace: "aws:elb:policies:#{public_key_policy_names_setting[:value]}", option_name: "PublicKey", override: public_key)
15
15
  ssl_certificate_setting = template_option_setting(namespace: "aws:elb:listener:443", option_name: "SSLCertificateId", override: ssl_certificate_id)
@@ -24,8 +24,8 @@ module ElasticBeans
24
24
  template_option_setting(namespace: "aws:elb:listener:443", option_name: "InstancePort", default: "443"),
25
25
  template_option_setting(namespace: "aws:elb:listener:443", option_name: "InstanceProtocol", default: "HTTPS"),
26
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(",")),
27
+ template_option_setting(namespace: "aws:elb:loadbalancer", option_name: "ManagedSecurityGroup", override: managed_security_group(network)),
28
+ template_option_setting(namespace: "aws:elb:loadbalancer", option_name: "SecurityGroups", override: elb_security_groups(network)),
29
29
  template_option_setting(namespace: "aws:elb:policies", option_name: "ConnectionDrainingEnabled", default: "true"),
30
30
  template_option_setting(namespace: "aws:elb:policies:backendencryption", option_name: "InstancePorts", default: "443"),
31
31
  public_key_policy_names_setting,
@@ -34,6 +34,14 @@ module ElasticBeans
34
34
  ]
35
35
  end
36
36
 
37
+ def elb_security_groups(network)
38
+ network.elb_security_groups.join(",") if network
39
+ end
40
+
41
+ def managed_security_group(network)
42
+ network.elb_security_groups[0] if network
43
+ end
44
+
37
45
  class NoEncryptionSettingsError < ElasticBeans::Error
38
46
  def message
39
47
  require "elastic_beans/command/configure"
@@ -0,0 +1,52 @@
1
+ require "json"
2
+ require "aws-sdk"
3
+ require "elastic_beans/error"
4
+
5
+ module ElasticBeans
6
+ class EnvVars
7
+ def initialize(application:, s3:)
8
+ @application = application
9
+ @s3 = s3
10
+ end
11
+
12
+ def s3_key
13
+ @s3_key ||= "#{application.name}/env_vars.json"
14
+ end
15
+
16
+ def update(env_hash)
17
+ body = env_script(existing_env_hash.merge(env_hash))
18
+ s3.put_object(bucket: application.bucket_name, key: s3_key, body: body)
19
+ end
20
+
21
+ private
22
+
23
+ attr_reader :application, :s3
24
+
25
+ def env_script(env_hash)
26
+ JSON.dump(env_hash)
27
+ end
28
+
29
+ def existing_env_hash
30
+ s3.head_object(bucket: application.bucket_name, key: s3_key)
31
+ response = s3.get_object(bucket: application.bucket_name, key: s3_key)
32
+ existing_script = response.body.read
33
+ JSON.parse(existing_script)
34
+ rescue ::Aws::S3::Errors::NotFound
35
+ {}
36
+ rescue ::Aws::S3::Errors::Forbidden
37
+ raise CannotAccessConfigError.new(bucket_name: application.bucket_name, key: s3_key)
38
+ end
39
+
40
+ class CannotAccessConfigError < ElasticBeans::Error
41
+ def initialize(bucket_name:, key:)
42
+ @bucket_name = bucket_name
43
+ @key = key
44
+ end
45
+
46
+ def message
47
+ "Cannot access configuration stored in S3 with bucket `#{@bucket_name}' and key `#{@key}'." \
48
+ " Please ask an administrator of your AWS account administrator to give you access."
49
+ end
50
+ end
51
+ end
52
+ end
@@ -0,0 +1,61 @@
1
+ files:
2
+ "/opt/elasticbeanstalk/support/get_envvars.rb":
3
+ mode: "000644"
4
+ owner: root
5
+ group: root
6
+ content: |
7
+ require "json"
8
+
9
+ def get_env_vars
10
+ require "aws-sdk"
11
+ eb_env = JSON.parse(`/opt/elasticbeanstalk/bin/get-config environment`)
12
+
13
+ region = eb_env['AWS_REGION'] || eb_env['AWS_DEFAULT_REGION'] || "us-east-1"
14
+ s3_bucket, s3_key = eb_env['ELASTIC_BEANS_ENV_VARS'].split('/', 2)
15
+ $stderr.puts "Fetching environment variables from s3://#{s3_bucket}/#{s3_key}..."
16
+ response = Aws::S3::Client.new(region: region).get_object(bucket: s3_bucket, key: s3_key)
17
+ beans_env_body = response.body.read
18
+
19
+ eb_env.merge(JSON.parse(beans_env_body))
20
+ rescue LoadError, StandardError => e
21
+ $stderr.puts "#{e.class.name}: #{e.message}"
22
+ JSON.parse(`/opt/elasticbeanstalk/bin/get-config environment`)
23
+ end
24
+ "/opt/elasticbeanstalk/support/export_envvars":
25
+ mode: "000755"
26
+ owner: root
27
+ group: root
28
+ content: |
29
+ #!/bin/env bash
30
+
31
+ # execute using Elastic Beanstalk's built-in Ruby with aws-sdk pre-installed
32
+ export PATH="/opt/elasticbeanstalk/lib/ruby/bin:$PATH"
33
+ export GEM_PATH="/opt/elasticbeanstalk/lib/ruby/lib/ruby/2.2.0"
34
+ exec ruby /opt/elasticbeanstalk/support/export_envvars.rb
35
+ "/opt/elasticbeanstalk/support/export_envvars.rb":
36
+ mode: "000755"
37
+ owner: root
38
+ group: root
39
+ content: |
40
+ #!/bin/env ruby
41
+
42
+ require '/opt/elasticbeanstalk/support/get_envvars'
43
+
44
+ if __FILE__ == $0
45
+ env_file = '/opt/elasticbeanstalk/support/envvars'
46
+ env_vars = get_env_vars
47
+
48
+ str = ''
49
+ env_vars.each do |key, value|
50
+ new_key = key.gsub(/\s/, '_')
51
+ str << "export #{new_key}=\"#{value}\"\n"
52
+ end
53
+
54
+ File.open(env_file, 'w') { |f| f.write(str) }
55
+ end
56
+ commands:
57
+ config_hooks:
58
+ command: |
59
+ cp -v /opt/elasticbeanstalk/support/export_envvars /opt/elasticbeanstalk/hooks/restartappserver/pre/09_update_environment.sh
60
+ update_environment:
61
+ command: "/opt/elasticbeanstalk/support/export_envvars"
@@ -16,8 +16,8 @@ module ElasticBeans
16
16
 
17
17
  attr_reader :name
18
18
 
19
- def self.new_by_type(type, suffix: nil, application:, **args)
20
- name = "#{application.name}-#{suffix}"
19
+ def self.new_by_type(type, application:, **args)
20
+ name = "#{application.name}-#{type}"
21
21
  case type
22
22
  when "exec"
23
23
  ElasticBeans::Environment::Exec.new(name, application: application, **args)
@@ -26,6 +26,7 @@ module ElasticBeans
26
26
  when "webserver"
27
27
  ElasticBeans::Environment::Webserver.new(name, application: application, **args)
28
28
  when "worker"
29
+ name = "#{name}-#{args[:queue]}"
29
30
  ElasticBeans::Environment::Worker.new(name, application: application, **args)
30
31
  else
31
32
  raise UnknownEnvironmentType.new(environment_type: type)
@@ -117,11 +118,22 @@ module ElasticBeans
117
118
  raise UnhealthyEnvironmentError.new(environment_name: name, cause: e)
118
119
  end
119
120
 
120
- def update_configuration
121
+ def restart
122
+ elastic_beanstalk.restart_app_server(environment_name: name)
123
+ wait_environment(wait_status: "Updating", wait_health_status: "Info")
124
+ rescue ::Aws::ElasticBeanstalk::Errors::Throttling
125
+ sleep 5
126
+ retry
127
+ end
128
+
129
+ def scale(min_size:, max_size:)
130
+ option_settings = [
131
+ {namespace: "aws:autoscaling:asg", option_name: "MinSize", value: min_size},
132
+ {namespace: "aws:autoscaling:asg", option_name: "MaxSize", value: max_size},
133
+ ]
121
134
  elastic_beanstalk.update_environment(
122
135
  environment_name: name,
123
- template_name: template_name,
124
- tier: {name: tier_name, type: tier_type},
136
+ option_settings: option_settings,
125
137
  )
126
138
  wait_environment(wait_status: "Updating", wait_health_status: "Info")
127
139
  rescue ::Aws::ElasticBeanstalk::Errors::InvalidParameterValue
@@ -131,15 +143,12 @@ module ElasticBeans
131
143
  retry
132
144
  end
133
145
 
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
- }
146
+ def update_configuration
138
147
  elastic_beanstalk.update_environment(
139
148
  environment_name: name,
140
- option_settings: new_env_settings,
149
+ template_name: template_name,
150
+ tier: {name: tier_name, type: tier_type},
141
151
  )
142
-
143
152
  wait_environment(wait_status: "Updating", wait_health_status: "Info")
144
153
  rescue ::Aws::ElasticBeanstalk::Errors::InvalidParameterValue
145
154
  raise MissingEnvironmentError.new(environment_name: name)
@@ -1,6 +1,6 @@
1
1
  container_commands:
2
2
  00_copy_files:
3
- command: "mkdir -p /opt/elastic_beans && cp -vR /var/app/ondeck/.elastic_beans/exec /opt/elastic_beans/"
3
+ command: "mkdir -vp /opt/elastic_beans && cp -vR /var/app/ondeck/.elastic_beans/exec /opt/elastic_beans/"
4
4
  01_permissions:
5
5
  command: "chmod 755 /opt/elastic_beans/exec/run_command.sh"
6
6
  09_upstart:
@@ -1,3 +1,3 @@
1
1
  module ElasticBeans
2
- VERSION = "0.1.0"
2
+ VERSION = "0.2.0"
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.1.0
4
+ version: 0.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Adam Stegman
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2016-11-11 00:00:00.000000000 Z
11
+ date: 2016-11-12 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: aws-sdk
@@ -165,6 +165,7 @@ files:
165
165
  - lib/elastic_beans/command/create.rb
166
166
  - lib/elastic_beans/command/deploy.rb
167
167
  - lib/elastic_beans/command/exec.rb
168
+ - lib/elastic_beans/command/scale.rb
168
169
  - lib/elastic_beans/command/set_env.rb
169
170
  - lib/elastic_beans/command/talk.rb
170
171
  - lib/elastic_beans/command/version.rb
@@ -175,6 +176,8 @@ files:
175
176
  - lib/elastic_beans/configuration_template/webserver.rb
176
177
  - lib/elastic_beans/configuration_template/worker.rb
177
178
  - lib/elastic_beans/dns_entry.rb
179
+ - lib/elastic_beans/env_vars.rb
180
+ - lib/elastic_beans/env_vars/ebextension.yml
178
181
  - lib/elastic_beans/environment.rb
179
182
  - lib/elastic_beans/environment/exec.rb
180
183
  - lib/elastic_beans/environment/scheduler.rb