elastic_beans 0.9.1 → 0.10.0.alpha1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/Gemfile +0 -1
- data/README.md +4 -0
- data/elastic_beans.gemspec +1 -0
- data/lib/elastic_beans/cli.rb +35 -0
- data/lib/elastic_beans/command/create.rb +1 -3
- data/lib/elastic_beans/command/scale.rb +1 -1
- data/lib/elastic_beans/command/ssh.rb +185 -0
- data/lib/elastic_beans/command.rb +1 -0
- data/lib/elastic_beans/environment/worker.rb +2 -2
- data/lib/elastic_beans/environment.rb +27 -6
- data/lib/elastic_beans/version.rb +1 -1
- metadata +20 -5
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 44f1c4f70c8a932819315f3d02623b75423ff299
|
4
|
+
data.tar.gz: 270d9d55029d816b5f45f91b13d2ff3e89e6b456
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: c9808cfe16431ef5bd7ac8df11ac31e85331d36c0fefb89d24c6aa95bd91ca593762e09db378c3913a29bd0dd4d732cc4e0b831aaf39645684cbd123cc720acc
|
7
|
+
data.tar.gz: e223d9b76c7b922dd3271958458e86a100eaf45842c47012b33de0d4b647fc405f68d009f56daa7cb9d37365dec7230181b93631240fb0bbf0de8a26daad345c
|
data/Gemfile
CHANGED
data/README.md
CHANGED
@@ -53,6 +53,10 @@ As the SDK documentation suggests, using environment variables is recommended.
|
|
53
53
|
# Run one-off tasks
|
54
54
|
beans exec -a myapp rake db:migrate
|
55
55
|
|
56
|
+
# SSH to an instance for debugging, tunneling through a bastion instance to reach the private network
|
57
|
+
beans ssh -a myapp webserver [-n INDEX] [-i IDENTITY_FILE] [-u USERNAME] \
|
58
|
+
[-b BASTION_HOST] [--bastion-identity-file BASTION_IDENTITY_FILE] [--bastion-username BASTION_USERNAME]
|
59
|
+
|
56
60
|
# Update all existing environments and configuration
|
57
61
|
beans configure -n myapp-networking -a myapp \
|
58
62
|
[-b SECRET_KEY_BASE] [-d DATABASE_URL] [-k KEYPAIR] \
|
data/elastic_beans.gemspec
CHANGED
@@ -20,6 +20,7 @@ Gem::Specification.new do |spec|
|
|
20
20
|
|
21
21
|
spec.add_dependency "aws-sdk", "~> 2.3"
|
22
22
|
spec.add_dependency "nesty", "~> 1.0"
|
23
|
+
spec.add_dependency "net-ssh-gateway"
|
23
24
|
spec.add_dependency "rails"
|
24
25
|
spec.add_dependency "ruby-progressbar", "~> 1.2"
|
25
26
|
spec.add_dependency "rubyzip", "~> 1.2"
|
data/lib/elastic_beans/cli.rb
CHANGED
@@ -200,6 +200,37 @@ class ElasticBeans::CLI < Thor
|
|
200
200
|
|
201
201
|
map ["delvar", "rmvar"] => "unsetenv"
|
202
202
|
|
203
|
+
desc ElasticBeans::Command::Ssh::USAGE, ElasticBeans::Command::Ssh::DESC
|
204
|
+
long_desc ElasticBeans::Command::Ssh::LONG_DESC
|
205
|
+
option :application, aliases: %w(-a), required: true, desc: APPLICATION_DESC
|
206
|
+
option :bastion_host, aliases: %w(-b), desc: "The hostname of the bastion server in the VPC"
|
207
|
+
option :bastion_identity_file, desc: "The SSH private key to use to connect to the bastion server"
|
208
|
+
option :bastion_username, desc: "The username to use to connect to the bastion server"
|
209
|
+
option :identity_file, aliases: %w(-i), desc: "The SSH private key associated with the keypair used when creating the environment"
|
210
|
+
option :index, aliases: %w(-n), desc: "The 0-based index of the instance to connect to"
|
211
|
+
option :queue, aliases: %w(-q), desc: "The name of the queue to identify the worker environment to connect to, e.g. `default`"
|
212
|
+
option :username, aliases: %w(-u), desc: "The username to log into the instance with, e.g. `ec2-user`"
|
213
|
+
def ssh(environment_type)
|
214
|
+
@verbose = options[:verbose]
|
215
|
+
ElasticBeans::Command::Ssh.new(
|
216
|
+
application: application(
|
217
|
+
name: options[:application],
|
218
|
+
),
|
219
|
+
bastion_host: options[:bastion_host],
|
220
|
+
bastion_identity_file: options[:bastion_identity_file],
|
221
|
+
bastion_username: options[:bastion_username],
|
222
|
+
identity_file: options[:identity_file],
|
223
|
+
index: options[:index],
|
224
|
+
queue: options[:queue],
|
225
|
+
username: options[:username],
|
226
|
+
ec2: ec2_client,
|
227
|
+
elastic_beanstalk: elastic_beanstalk_client,
|
228
|
+
ui: ui,
|
229
|
+
).run(environment_type)
|
230
|
+
rescue StandardError => e
|
231
|
+
error(e)
|
232
|
+
end
|
233
|
+
|
203
234
|
desc "talk", ""
|
204
235
|
def talk
|
205
236
|
ElasticBeans::Command::Talk.new(ui: ui).run
|
@@ -233,6 +264,10 @@ class ElasticBeans::CLI < Thor
|
|
233
264
|
::Aws::CloudFormation::Client.new
|
234
265
|
end
|
235
266
|
|
267
|
+
def ec2_client
|
268
|
+
::Aws::EC2::Client.new
|
269
|
+
end
|
270
|
+
|
236
271
|
def elastic_beanstalk_client
|
237
272
|
::Aws::ElasticBeanstalk::Client.new
|
238
273
|
end
|
@@ -34,9 +34,7 @@ Requires AWS credentials to be set in the environment, i.e. AWS_ACCESS_KEY_ID an
|
|
34
34
|
)
|
35
35
|
@environment_type = environment_type
|
36
36
|
@dns = dns
|
37
|
-
|
38
|
-
@queue = queue || "default"
|
39
|
-
end
|
37
|
+
@queue = queue
|
40
38
|
@tags = tags
|
41
39
|
@application = application
|
42
40
|
@ui = ui
|
@@ -18,7 +18,7 @@ Requires AWS credentials to be set in the environment, i.e. AWS_ACCESS_KEY_ID an
|
|
18
18
|
@application = application
|
19
19
|
@minimum = minimum
|
20
20
|
@maximum = maximum
|
21
|
-
@queue = queue
|
21
|
+
@queue = queue
|
22
22
|
@elastic_beanstalk = elastic_beanstalk
|
23
23
|
@ui = ui
|
24
24
|
end
|
@@ -0,0 +1,185 @@
|
|
1
|
+
require "net/ssh/gateway"
|
2
|
+
require "elastic_beans/error/environments_not_ready"
|
3
|
+
|
4
|
+
module ElasticBeans
|
5
|
+
module Command
|
6
|
+
# :nodoc: all
|
7
|
+
class Ssh
|
8
|
+
USAGE = "ssh ENVIRONMENT_TYPE"
|
9
|
+
DESC = "Connect to an instance for debugging, tunneling through a bastion server"
|
10
|
+
LONG_DESC = <<-LONG_DESC
|
11
|
+
Connect to an instance for debugging, tunneling through a bastion server.
|
12
|
+
Opens a login shell.
|
13
|
+
|
14
|
+
Requires AWS credentials to be set in the environment, i.e. AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY.
|
15
|
+
LONG_DESC
|
16
|
+
|
17
|
+
def initialize(
|
18
|
+
application:,
|
19
|
+
bastion_host:,
|
20
|
+
bastion_identity_file:,
|
21
|
+
bastion_username:,
|
22
|
+
identity_file:,
|
23
|
+
index:,
|
24
|
+
queue:,
|
25
|
+
username:,
|
26
|
+
ec2:,
|
27
|
+
elastic_beanstalk:,
|
28
|
+
ui:
|
29
|
+
)
|
30
|
+
@application = application
|
31
|
+
@bastion_host = bastion_host
|
32
|
+
@bastion_identity_file = bastion_identity_file
|
33
|
+
@bastion_username = bastion_username
|
34
|
+
@identity_file = identity_file
|
35
|
+
@index = index.to_i
|
36
|
+
@queue = queue
|
37
|
+
@username = username || "ec2-user"
|
38
|
+
@ec2 = ec2
|
39
|
+
@elastic_beanstalk = elastic_beanstalk
|
40
|
+
@ui = ui
|
41
|
+
end
|
42
|
+
|
43
|
+
def run(environment_type)
|
44
|
+
environment = ElasticBeans::Environment.new_by_type(
|
45
|
+
environment_type,
|
46
|
+
queue: queue,
|
47
|
+
application: application,
|
48
|
+
elastic_beanstalk: elastic_beanstalk,
|
49
|
+
)
|
50
|
+
instance_id = environment.instance_ids[index]
|
51
|
+
ui.debug { "In environment '#{environment.name}' found instance '#{instance_id}' at index '#{index}' of #{environment.instance_ids.inspect}" }
|
52
|
+
if instance_id.nil?
|
53
|
+
raise NoInstanceError.new(environment: environment, index: index)
|
54
|
+
end
|
55
|
+
|
56
|
+
begin
|
57
|
+
instance_ip = ec2.describe_instances(instance_ids: [instance_id]).reservations[0].instances[0].private_ip_address
|
58
|
+
rescue ::Aws::EC2::Errors::InvalidInstanceIDMalformed
|
59
|
+
raise NoInstanceError.new(environment: environment, index: index)
|
60
|
+
end
|
61
|
+
# It's possible a retry would just work, but I'd like to see this happen in reality before I assume that.
|
62
|
+
if instance_ip.nil?
|
63
|
+
raise TerminatedInstanceError.new(instance_id: instance_id, environment: environment)
|
64
|
+
end
|
65
|
+
|
66
|
+
ssh_args = ["ssh"]
|
67
|
+
|
68
|
+
if identity_file
|
69
|
+
ssh_args << "-i"
|
70
|
+
ssh_args << identity_file
|
71
|
+
end
|
72
|
+
|
73
|
+
gateway = nil
|
74
|
+
if bastion_host
|
75
|
+
ssh_args << "#{username}@localhost"
|
76
|
+
ssh_args += ["-o", "UserKnownHostsFile=/dev/null", "-o", "StrictHostKeyChecking=no"]
|
77
|
+
begin
|
78
|
+
bastion_options = {auth_methods: ["publickey"]}
|
79
|
+
if bastion_identity_file
|
80
|
+
bastion_options[:key_data] = File.read(bastion_identity_file)
|
81
|
+
end
|
82
|
+
|
83
|
+
ui.info("Connecting to '#{bastion_username}@#{bastion_host}'...")
|
84
|
+
gateway = Net::SSH::Gateway.new(
|
85
|
+
bastion_host,
|
86
|
+
bastion_username,
|
87
|
+
bastion_options
|
88
|
+
)
|
89
|
+
port = gateway.open(instance_ip, 22)
|
90
|
+
ui.debug { "Opened gateway to '#{instance_ip}' on port '#{port}'" }
|
91
|
+
ssh_args += ["-p", port.to_s]
|
92
|
+
rescue Net::SSH::AuthenticationFailed => e
|
93
|
+
raise BastionAuthenticationError.new(cause: e)
|
94
|
+
end
|
95
|
+
else
|
96
|
+
ssh_args << "#{username}@#{instance_ip}"
|
97
|
+
end
|
98
|
+
|
99
|
+
ui.info("Connecting to '#{username}@#{instance_ip}'...")
|
100
|
+
pid = Process.spawn(*ssh_args)
|
101
|
+
_, status = Process.waitpid2(pid)
|
102
|
+
unless status.success?
|
103
|
+
raise SshFailedError
|
104
|
+
end
|
105
|
+
ensure
|
106
|
+
gateway.close(port) if gateway
|
107
|
+
end
|
108
|
+
|
109
|
+
private
|
110
|
+
|
111
|
+
attr_reader(
|
112
|
+
:application,
|
113
|
+
:bastion_host,
|
114
|
+
:bastion_identity_file,
|
115
|
+
:bastion_username,
|
116
|
+
:identity_file,
|
117
|
+
:index,
|
118
|
+
:queue,
|
119
|
+
:username,
|
120
|
+
:ec2,
|
121
|
+
:elastic_beanstalk,
|
122
|
+
:ui,
|
123
|
+
)
|
124
|
+
|
125
|
+
class BastionAuthenticationError < ElasticBeans::Error
|
126
|
+
def initialize(cause:)
|
127
|
+
@cause = cause
|
128
|
+
end
|
129
|
+
|
130
|
+
def message
|
131
|
+
<<-MESSAGE
|
132
|
+
#{@cause.message}
|
133
|
+
|
134
|
+
Please check the --bastion-identity-file or --bastion-username options and try again.
|
135
|
+
|
136
|
+
#{command_help "ssh"}
|
137
|
+
MESSAGE
|
138
|
+
end
|
139
|
+
end
|
140
|
+
|
141
|
+
class NoInstanceError < ElasticBeans::Error
|
142
|
+
def initialize(environment:, index:)
|
143
|
+
@environment = environment
|
144
|
+
@index = index
|
145
|
+
end
|
146
|
+
|
147
|
+
def message
|
148
|
+
msg = "The '#{@environment.name}' environment has no instance at index #{@index}.\n"
|
149
|
+
if @index > 0
|
150
|
+
msg << "Try again with a lower index.\n"
|
151
|
+
else
|
152
|
+
msg << "Wait for instances to be created and try again.\n"
|
153
|
+
end
|
154
|
+
msg << <<-MESSAGE
|
155
|
+
|
156
|
+
#{command_help "ssh"}
|
157
|
+
MESSAGE
|
158
|
+
msg
|
159
|
+
end
|
160
|
+
end
|
161
|
+
|
162
|
+
class SshFailedError < ElasticBeans::Error
|
163
|
+
def message
|
164
|
+
""
|
165
|
+
end
|
166
|
+
end
|
167
|
+
|
168
|
+
class TerminatedInstanceError < ElasticBeans::Error
|
169
|
+
def initialize(instance_id:, environment:)
|
170
|
+
@instance_id = instance_id
|
171
|
+
@environment = environment
|
172
|
+
end
|
173
|
+
|
174
|
+
def message
|
175
|
+
<<-MESSAGE
|
176
|
+
The instance '#{@instance_id}' in the environment '#{@environment.name}' has been terminated.
|
177
|
+
Please try again in a moment.
|
178
|
+
|
179
|
+
#{command_help "ssh"}
|
180
|
+
MESSAGE
|
181
|
+
end
|
182
|
+
end
|
183
|
+
end
|
184
|
+
end
|
185
|
+
end
|
@@ -8,6 +8,7 @@ require "elastic_beans/command/scale"
|
|
8
8
|
require "elastic_beans/command/get_env"
|
9
9
|
require "elastic_beans/command/set_env"
|
10
10
|
require "elastic_beans/command/unset_env"
|
11
|
+
require "elastic_beans/command/ssh"
|
11
12
|
require "elastic_beans/command/talk"
|
12
13
|
require "elastic_beans/command/version"
|
13
14
|
|
@@ -40,7 +40,8 @@ module ElasticBeans
|
|
40
40
|
when "webserver"
|
41
41
|
ElasticBeans::Environment::Webserver.new(name, application: application, **args)
|
42
42
|
when "worker"
|
43
|
-
|
43
|
+
queue_name = args[:queue] || "default"
|
44
|
+
name = "#{name}-#{queue_name}"
|
44
45
|
ElasticBeans::Environment::Worker.new(name, application: application, **args)
|
45
46
|
else
|
46
47
|
raise UnknownEnvironmentType.new(environment_type: type)
|
@@ -78,7 +79,6 @@ module ElasticBeans
|
|
78
79
|
ElasticBeans::Environment::Worker.new(
|
79
80
|
environment.environment_name,
|
80
81
|
application: application,
|
81
|
-
queue: "default",
|
82
82
|
**args,
|
83
83
|
)
|
84
84
|
else
|
@@ -211,6 +211,15 @@ module ElasticBeans
|
|
211
211
|
environment.cname
|
212
212
|
end
|
213
213
|
|
214
|
+
# Returns the instance IDs that are part of this environment.
|
215
|
+
#
|
216
|
+
# Raises an error if the environment does not exist.
|
217
|
+
def instance_ids
|
218
|
+
environment_resources.instances.map(&:id)
|
219
|
+
rescue ::Aws::ElasticBeanstalk::Errors::InvalidParameterValue
|
220
|
+
raise MissingEnvironmentError.new(environment: self, application: application)
|
221
|
+
end
|
222
|
+
|
214
223
|
# Returns the status of the environment in Elastic Beanstalk.
|
215
224
|
#
|
216
225
|
# Raises an error if the environment does not exist.
|
@@ -242,6 +251,15 @@ module ElasticBeans
|
|
242
251
|
retry
|
243
252
|
end
|
244
253
|
|
254
|
+
def environment_resources
|
255
|
+
elastic_beanstalk.describe_environment_resources(
|
256
|
+
environment_name: name,
|
257
|
+
).environment_resources
|
258
|
+
rescue ::Aws::ElasticBeanstalk::Errors::Throttling
|
259
|
+
sleep 5
|
260
|
+
retry
|
261
|
+
end
|
262
|
+
|
245
263
|
def template_name
|
246
264
|
TEMPLATE_NAME
|
247
265
|
end
|
@@ -324,10 +342,13 @@ Please re-run `#{command_as_string "configure"}`.
|
|
324
342
|
|
325
343
|
def message
|
326
344
|
require "elastic_beans/command/create"
|
327
|
-
|
328
|
-
|
329
|
-
|
330
|
-
|
345
|
+
msg = "Environment `#{@environment.name}' does not exist.\n"
|
346
|
+
msg << "Please run `#{command_as_string "create #{@environment.type}"}"
|
347
|
+
if @environment.respond_to?(:queue)
|
348
|
+
msg << " -q #{@environment.queue}"
|
349
|
+
end
|
350
|
+
msg << " -a #{@application.name}`.\n"
|
351
|
+
msg
|
331
352
|
end
|
332
353
|
end
|
333
354
|
|
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.10.0.alpha1
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Adam Stegman
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date: 2017-
|
11
|
+
date: 2017-04-20 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: aws-sdk
|
@@ -38,6 +38,20 @@ dependencies:
|
|
38
38
|
- - "~>"
|
39
39
|
- !ruby/object:Gem::Version
|
40
40
|
version: '1.0'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: net-ssh-gateway
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - ">="
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '0'
|
48
|
+
type: :runtime
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - ">="
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '0'
|
41
55
|
- !ruby/object:Gem::Dependency
|
42
56
|
name: rails
|
43
57
|
requirement: !ruby/object:Gem::Requirement
|
@@ -186,6 +200,7 @@ files:
|
|
186
200
|
- lib/elastic_beans/command/restart.rb
|
187
201
|
- lib/elastic_beans/command/scale.rb
|
188
202
|
- lib/elastic_beans/command/set_env.rb
|
203
|
+
- lib/elastic_beans/command/ssh.rb
|
189
204
|
- lib/elastic_beans/command/talk.rb
|
190
205
|
- lib/elastic_beans/command/unset_env.rb
|
191
206
|
- lib/elastic_beans/command/version.rb
|
@@ -232,12 +247,12 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
232
247
|
version: '0'
|
233
248
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
234
249
|
requirements:
|
235
|
-
- - "
|
250
|
+
- - ">"
|
236
251
|
- !ruby/object:Gem::Version
|
237
|
-
version:
|
252
|
+
version: 1.3.1
|
238
253
|
requirements: []
|
239
254
|
rubyforge_project:
|
240
|
-
rubygems_version: 2.
|
255
|
+
rubygems_version: 2.6.11
|
241
256
|
signing_key:
|
242
257
|
specification_version: 4
|
243
258
|
summary: Elastic Beanstalk environment orchestration for a Rails app
|