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 +4 -4
- data/README.md +52 -0
- data/exe/beans +27 -3
- data/lib/elastic_beans.rb +1 -0
- data/lib/elastic_beans/application.rb +34 -2
- data/lib/elastic_beans/application_version.rb +14 -18
- data/lib/elastic_beans/command.rb +1 -0
- data/lib/elastic_beans/command/configure.rb +1 -0
- data/lib/elastic_beans/command/create.rb +3 -6
- data/lib/elastic_beans/command/deploy.rb +1 -0
- data/lib/elastic_beans/command/scale.rb +77 -0
- data/lib/elastic_beans/command/set_env.rb +6 -9
- data/lib/elastic_beans/configuration_template.rb +0 -18
- data/lib/elastic_beans/configuration_template/base.rb +31 -7
- data/lib/elastic_beans/configuration_template/webserver.rb +11 -3
- data/lib/elastic_beans/env_vars.rb +52 -0
- data/lib/elastic_beans/env_vars/ebextension.yml +61 -0
- data/lib/elastic_beans/environment.rb +20 -11
- data/lib/elastic_beans/exec/ebextension.yml +1 -1
- data/lib/elastic_beans/version.rb +1 -1
- metadata +5 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: cb92ef373018a6f507082e86909372817ddd7042
|
4
|
+
data.tar.gz: 49eb178cbdad8e93930afa6998577588093d7994
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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(
|
48
|
+
def create(environment_type)
|
49
49
|
@verbose = options[:verbose]
|
50
50
|
ElasticBeans::Command::Create.new(
|
51
|
-
|
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
|
-
|
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:
|
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"
|
@@ -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 [-
|
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
|
-
|
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
|
-
@
|
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,
|
@@ -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
|
35
|
-
threads
|
36
|
-
|
37
|
-
|
38
|
-
|
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.
|
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
|
-
|
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
|
-
|
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
|
71
|
-
template_option_setting(namespace: "aws:ec2:vpc", option_name: "ELBSubnets", override:
|
72
|
-
template_option_setting(namespace: "aws:ec2:vpc", option_name: "Subnets", override: network
|
73
|
-
template_option_setting(namespace: "aws:ec2:vpc", option_name: "VPCId", override: network
|
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
|
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
|
28
|
-
template_option_setting(namespace: "aws:elb:loadbalancer", option_name: "SecurityGroups", override:
|
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,
|
20
|
-
name = "#{application.name}-#{
|
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
|
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
|
-
|
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
|
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
|
-
|
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 -
|
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:
|
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.
|
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
|
+
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
|