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 +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
|