elastic_beans 0.1.0 → 0.2.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.
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