capistrano-elb-autoscaling 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +26 -0
- data/.rspec +1 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +20 -0
- data/README.md +97 -0
- data/Rakefile +5 -0
- data/elbas.gemspec +31 -0
- data/lib/elbas/aws/ami.rb +71 -0
- data/lib/elbas/aws/autoscale_group.rb +52 -0
- data/lib/elbas/aws/base.rb +39 -0
- data/lib/elbas/aws/instance.rb +29 -0
- data/lib/elbas/aws/instance_collection.rb +35 -0
- data/lib/elbas/aws/launch_template.rb +39 -0
- data/lib/elbas/aws/snapshot.rb +21 -0
- data/lib/elbas/aws/taggable.rb +18 -0
- data/lib/elbas/capistrano.rb +34 -0
- data/lib/elbas/errors/no_launch_template.rb +6 -0
- data/lib/elbas/logger.rb +38 -0
- data/lib/elbas/retryable.rb +41 -0
- data/lib/elbas/tasks/elbas.rake +38 -0
- data/lib/elbas/version.rb +3 -0
- data/lib/elbas.rb +21 -0
- data/spec/aws/ami_spec.rb +140 -0
- data/spec/aws/autoscale_group_spec.rb +67 -0
- data/spec/aws/instance_collection_spec.rb +38 -0
- data/spec/aws/instance_spec.rb +30 -0
- data/spec/aws/launch_template_spec.rb +52 -0
- data/spec/aws/taggable_spec.rb +53 -0
- data/spec/capistrano_spec.rb +77 -0
- data/spec/spec_helper.rb +27 -0
- data/spec/support/stubs/CreateImage.200.xml +4 -0
- data/spec/support/stubs/CreateLaunchConfiguration.200.xml +5 -0
- data/spec/support/stubs/CreateLaunchTemplateVersion.200.xml +16 -0
- data/spec/support/stubs/CreateTags.200.xml +5 -0
- data/spec/support/stubs/DeleteLaunchConfiguration.200.xml +5 -0
- data/spec/support/stubs/DeleteSnapshot.200.xml +4 -0
- data/spec/support/stubs/DeregisterImage.200.xml +4 -0
- data/spec/support/stubs/DescribeAutoScalingGroups.200.xml +60 -0
- data/spec/support/stubs/DescribeImages.200.xml +70 -0
- data/spec/support/stubs/DescribeInstances.200.xml +231 -0
- data/spec/support/stubs/DescribeInstances_Empty.200.xml +17 -0
- data/spec/support/stubs/DescribeInstances_MultipleReservations.200.xml +243 -0
- data/spec/support/stubs/DescribeInstances_MultipleRunning.200.xml +338 -0
- data/spec/support/stubs/DescribeLaunchConfigurations.200.xml +29 -0
- data/spec/support/stubs/DescribeSnapshots.200.xml +18 -0
- data/spec/support/stubs/DescribeTags.200.xml +17 -0
- data/spec/support/stubs/UpdateAutoScalingGroup.200.xml +5 -0
- data/spec/support/stubs/security-credentials.200.json +9 -0
- metadata +243 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 9414a155b6afd51ac1d0a5357cab5599baf5c51bc4efeaf601d16acd54c310c0
|
4
|
+
data.tar.gz: c68a44e88ca7dedbf8072d932687a4385e966d5f0a58af39620a53254b3ad826
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 3ccc33d9417dcefd95575116cadac35ca400cc9ab849894d81286419f7fafa77986dc3bc86d47d9b3e5b9bff4e7f89e9e311b34ad88c0ed65f7322b936178ef0
|
7
|
+
data.tar.gz: f77880fe0f5974c425646278a031f00075d02826e5dcb7e521bdcd1522eb378288a222993e6e269e8347050fb532ff22b2b646badbdb7d181ed5c826868c8d46
|
data/.gitignore
ADDED
@@ -0,0 +1,26 @@
|
|
1
|
+
bin/
|
2
|
+
*.gem
|
3
|
+
*.rbc
|
4
|
+
/Gemfile.lock
|
5
|
+
.bundle
|
6
|
+
.config
|
7
|
+
.yardoc
|
8
|
+
InstalledFiles
|
9
|
+
_yardoc
|
10
|
+
coverage
|
11
|
+
doc/
|
12
|
+
lib/bundler/man
|
13
|
+
pkg
|
14
|
+
rdoc
|
15
|
+
test/tmp
|
16
|
+
test/version_tmp
|
17
|
+
tmp
|
18
|
+
*.bundle
|
19
|
+
*.so
|
20
|
+
*.o
|
21
|
+
*.a
|
22
|
+
mkmf.log
|
23
|
+
|
24
|
+
.ruby-version
|
25
|
+
|
26
|
+
.idea
|
data/.rspec
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
--require spec_helper
|
data/Gemfile
ADDED
data/LICENSE.txt
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
MIT License
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
4
|
+
a copy of this software and associated documentation files (the
|
5
|
+
"Software"), to deal in the Software without restriction, including
|
6
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
7
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
8
|
+
permit persons to whom the Software is furnished to do so, subject to
|
9
|
+
the following conditions:
|
10
|
+
|
11
|
+
The above copyright notice and this permission notice shall be
|
12
|
+
included in all copies or substantial portions of the Software.
|
13
|
+
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
15
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
16
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
17
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
18
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
19
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
20
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,97 @@
|
|
1
|
+
*Versions < 3 of ELBAS are no longer being maintained. I will only be maintaining the current feature-set which relies on Launch Templates and AWS SDK v3.*
|
2
|
+
|
3
|
+
# Capistrano ELBAS (Elastic Load Balancer & AutoScaling)
|
4
|
+
|
5
|
+
[![Gem Version](https://badge.fury.io/rb/elbas.svg)](https://badge.fury.io/rb/elbas)
|
6
|
+
|
7
|
+
ELBAS was written to ease the deployment of Rails applications to AWS AutoScale
|
8
|
+
groups. During your Capistrano deployment, ELBAS will:
|
9
|
+
|
10
|
+
- Deploy your code to each running instance connected to a given AutoScale group
|
11
|
+
- After deployment, create an AMI from one of the running instances
|
12
|
+
- Update the AutoScale group's launch template with the AMI ID
|
13
|
+
- Delete any outdated AMIs created by previous ELBAS deployments
|
14
|
+
|
15
|
+
## Installation
|
16
|
+
|
17
|
+
Add to Gemfile, then `bundle`:
|
18
|
+
|
19
|
+
`gem 'elbas'`
|
20
|
+
|
21
|
+
Add to Capfile:
|
22
|
+
|
23
|
+
`require 'elbas/capistrano'`
|
24
|
+
|
25
|
+
## Configuration
|
26
|
+
|
27
|
+
Setup AWS credentials:
|
28
|
+
|
29
|
+
```ruby
|
30
|
+
set :aws_access_key, ENV['AWS_ACCESS_KEY_ID']
|
31
|
+
set :aws_secret_key, ENV['AWS_SECRET_ACCESS_KEY']
|
32
|
+
set :aws_region, ENV['AWS_REGION']
|
33
|
+
```
|
34
|
+
|
35
|
+
## Usage
|
36
|
+
|
37
|
+
Instead of using Capistrano's `server` method, use `autoscale` instead in
|
38
|
+
`deploy/<environment>.rb` (replace <environment> with your environment). Provide
|
39
|
+
the name of your AutoScale group instead of a hostname:
|
40
|
+
|
41
|
+
```ruby
|
42
|
+
autoscale 'my-autoscale-group', user: 'apps', roles: [:app, :web, :db]
|
43
|
+
```
|
44
|
+
|
45
|
+
If you have multiple autoscaling groups to deploy to, specify each of them:
|
46
|
+
|
47
|
+
```ruby
|
48
|
+
autoscale 'app-autoscale-group', user: 'apps', roles: [:app, :web]
|
49
|
+
autoscale 'worker-autoscale-group', user: 'apps', roles: [:worker]
|
50
|
+
```
|
51
|
+
|
52
|
+
Run `cap production deploy`.
|
53
|
+
|
54
|
+
**As of version 3, your AWS setup must use launch templates as opposed to launch
|
55
|
+
configurations.** This allows ELBAS to simply create a new launch template version
|
56
|
+
with the new AMI ID after a deployment. It no longer needs to update your
|
57
|
+
AutoScale group or mess around with network settings, instance sizes, etc., as
|
58
|
+
that information is all contained within the launch template. Failure to use a
|
59
|
+
launch template will result in a `Elbas::Errors::NoLaunchTemplate` error.
|
60
|
+
|
61
|
+
### Customizing Server Properties
|
62
|
+
|
63
|
+
You can pass a block to `autoscale` and return properties for any specific server.
|
64
|
+
The block accepts the server and the server's index as arguments.
|
65
|
+
|
66
|
+
For example, if you want to apply the `:db` role to only the first server:
|
67
|
+
|
68
|
+
```ruby
|
69
|
+
autoscale 'my-autoscale-group', roles: [:app, :web] do |server, i|
|
70
|
+
{ roles: [:app, :web, :db] } if i == 0
|
71
|
+
end
|
72
|
+
```
|
73
|
+
|
74
|
+
Returning `nil` from this block will cause the server to use the properties
|
75
|
+
passed to `autoscale`.
|
76
|
+
|
77
|
+
Returning anything but `nil` will override the entire properties hash (as
|
78
|
+
opposed to merging the two hashes together).
|
79
|
+
|
80
|
+
### Listing Servers
|
81
|
+
|
82
|
+
You may need to SSH into your servers while debugging deployed code, and
|
83
|
+
not everyone has a jumpbox on a basic AutoScaling setup. ELBAS provides a command
|
84
|
+
that will list the `ssh` command necessary to connect to each server in any given
|
85
|
+
environment:
|
86
|
+
|
87
|
+
```
|
88
|
+
cap production elbas:ssh
|
89
|
+
```
|
90
|
+
|
91
|
+
Output will be something like:
|
92
|
+
|
93
|
+
```
|
94
|
+
[ELBAS] Adding server: ec2-12-34-567-890.compute-1.amazonaws.com
|
95
|
+
[ELBAS] SSH commands:
|
96
|
+
[ELBAS] 1) ssh deploy@ec2-12-34-567-890.compute-1.amazonaws.com
|
97
|
+
```
|
data/Rakefile
ADDED
data/elbas.gemspec
ADDED
@@ -0,0 +1,31 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require 'elbas/version'
|
5
|
+
|
6
|
+
Gem::Specification.new do |spec|
|
7
|
+
spec.name = 'capistrano-elb-autoscaling'
|
8
|
+
spec.version = Elbas::VERSION
|
9
|
+
spec.authors = ['Heng Gek']
|
10
|
+
spec.email = ['tanhengegk@gmail.com']
|
11
|
+
spec.summary = 'Capistrano plugin for deploying to AWS Auto Scaling Group.'
|
12
|
+
spec.description = spec.summary
|
13
|
+
spec.homepage = 'https://github.com/tanhenggek/capistrano-elb-autoscaling'
|
14
|
+
spec.license = 'MIT'
|
15
|
+
|
16
|
+
spec.files = `git ls-files -z`.split("\x0")
|
17
|
+
spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
|
18
|
+
spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
|
19
|
+
spec.require_paths = ['lib']
|
20
|
+
|
21
|
+
spec.add_development_dependency 'bundler'
|
22
|
+
spec.add_development_dependency 'byebug'
|
23
|
+
spec.add_development_dependency 'rake'
|
24
|
+
spec.add_development_dependency 'rspec'
|
25
|
+
spec.add_development_dependency 'webmock'
|
26
|
+
spec.add_development_dependency 'webmock-rspec-helper'
|
27
|
+
|
28
|
+
spec.add_dependency 'aws-sdk-autoscaling', '~> 1'
|
29
|
+
spec.add_dependency 'aws-sdk-ec2', '~> 1'
|
30
|
+
spec.add_dependency 'capistrano', '> 3'
|
31
|
+
end
|
@@ -0,0 +1,71 @@
|
|
1
|
+
module Elbas
|
2
|
+
module AWS
|
3
|
+
class AMI < Base
|
4
|
+
include Taggable
|
5
|
+
|
6
|
+
DEPLOY_ID_TAG = 'ELBAS-Deploy-id'.freeze
|
7
|
+
DEPLOY_GROUP_TAG = 'ELBAS-Deploy-group'.freeze
|
8
|
+
|
9
|
+
attr_reader :id, :snapshots
|
10
|
+
|
11
|
+
def initialize(id, snapshots = [])
|
12
|
+
@id = id
|
13
|
+
@aws_counterpart = ::Aws::EC2::Image.new id, client: aws_client
|
14
|
+
|
15
|
+
@snapshots = snapshots.map do |snapshot|
|
16
|
+
Elbas::AWS::Snapshot.new snapshot&.ebs&.snapshot_id
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
def deploy_id
|
21
|
+
tags[DEPLOY_ID_TAG]
|
22
|
+
end
|
23
|
+
|
24
|
+
def deploy_group
|
25
|
+
tags[DEPLOY_GROUP_TAG]
|
26
|
+
end
|
27
|
+
|
28
|
+
def ancestors
|
29
|
+
aws_amis_in_deploy_group.select { |aws_ami|
|
30
|
+
deploy_id_from_aws_tags(aws_ami.tags) != deploy_id
|
31
|
+
}.map { |aws_ami|
|
32
|
+
self.class.new aws_ami.image_id, aws_ami.block_device_mappings
|
33
|
+
}
|
34
|
+
end
|
35
|
+
|
36
|
+
def delete
|
37
|
+
aws_client.deregister_image image_id: id
|
38
|
+
snapshots.each(&:delete)
|
39
|
+
end
|
40
|
+
|
41
|
+
def self.create(instance, no_reboot: true)
|
42
|
+
ami = instance.aws_counterpart.create_image({
|
43
|
+
name: "ELBAS-ami-#{Time.now.to_i}",
|
44
|
+
instance_id: instance.id,
|
45
|
+
no_reboot: no_reboot
|
46
|
+
})
|
47
|
+
|
48
|
+
new ami.id
|
49
|
+
end
|
50
|
+
|
51
|
+
private
|
52
|
+
def aws_namespace
|
53
|
+
::Aws::EC2
|
54
|
+
end
|
55
|
+
|
56
|
+
def aws_amis_in_deploy_group
|
57
|
+
aws_client.describe_images({
|
58
|
+
owners: ['self'],
|
59
|
+
filters: [{
|
60
|
+
name: "tag:#{DEPLOY_GROUP_TAG}",
|
61
|
+
values: [deploy_group],
|
62
|
+
}]
|
63
|
+
}).images
|
64
|
+
end
|
65
|
+
|
66
|
+
def deploy_id_from_aws_tags(tags)
|
67
|
+
tags.detect { |tag| tag.key == DEPLOY_ID_TAG }&.value
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
@@ -0,0 +1,52 @@
|
|
1
|
+
module Elbas
|
2
|
+
module AWS
|
3
|
+
class AutoscaleGroup < Base
|
4
|
+
attr_reader :name
|
5
|
+
|
6
|
+
def initialize(name)
|
7
|
+
@name = name
|
8
|
+
@aws_counterpart = query_autoscale_group_by_name(name)
|
9
|
+
end
|
10
|
+
|
11
|
+
def instance_ids
|
12
|
+
aws_counterpart.instances.map(&:instance_id)
|
13
|
+
end
|
14
|
+
|
15
|
+
def instances
|
16
|
+
InstanceCollection.new instance_ids
|
17
|
+
end
|
18
|
+
|
19
|
+
def launch_template
|
20
|
+
lts = aws_launch_template || aws_launch_template_specification
|
21
|
+
raise Elbas::Errors::NoLaunchTemplate unless lts
|
22
|
+
|
23
|
+
LaunchTemplate.new(
|
24
|
+
lts.launch_template_id,
|
25
|
+
lts.launch_template_name,
|
26
|
+
lts.version
|
27
|
+
)
|
28
|
+
end
|
29
|
+
|
30
|
+
private
|
31
|
+
def aws_namespace
|
32
|
+
::Aws::AutoScaling
|
33
|
+
end
|
34
|
+
|
35
|
+
def query_autoscale_group_by_name(name)
|
36
|
+
aws_client
|
37
|
+
.describe_auto_scaling_groups(auto_scaling_group_names: [name])
|
38
|
+
.auto_scaling_groups
|
39
|
+
.first
|
40
|
+
end
|
41
|
+
|
42
|
+
def aws_launch_template
|
43
|
+
aws_counterpart.launch_template
|
44
|
+
end
|
45
|
+
|
46
|
+
def aws_launch_template_specification
|
47
|
+
aws_counterpart.mixed_instances_policy&.launch_template
|
48
|
+
&.launch_template_specification
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
@@ -0,0 +1,39 @@
|
|
1
|
+
module Elbas
|
2
|
+
module AWS
|
3
|
+
class Base
|
4
|
+
include Capistrano::DSL
|
5
|
+
|
6
|
+
attr_reader :aws_counterpart
|
7
|
+
|
8
|
+
def aws_client(namespace = aws_namespace)
|
9
|
+
@aws_client ||= begin
|
10
|
+
options = {}
|
11
|
+
options[:region] = aws_region if aws_region
|
12
|
+
options[:credentials] = aws_credentials if aws_credentials.set?
|
13
|
+
|
14
|
+
namespace::Client.new options
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
def aws_credentials
|
19
|
+
fetch :aws_credentials, ::Aws::Credentials.new(aws_access_key, aws_secret_key)
|
20
|
+
end
|
21
|
+
|
22
|
+
def aws_access_key
|
23
|
+
fetch :aws_access_key
|
24
|
+
end
|
25
|
+
|
26
|
+
def aws_secret_key
|
27
|
+
fetch :aws_secret_key
|
28
|
+
end
|
29
|
+
|
30
|
+
def aws_region
|
31
|
+
fetch :aws_region
|
32
|
+
end
|
33
|
+
|
34
|
+
def self.aws_client(namespace = aws_namespace)
|
35
|
+
Base.new.aws_client namespace
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
module Elbas
|
2
|
+
module AWS
|
3
|
+
class Instance < Base
|
4
|
+
STATE_RUNNING = 16.freeze
|
5
|
+
|
6
|
+
attr_reader :aws_counterpart, :id, :state
|
7
|
+
|
8
|
+
def initialize(id, public_dns, state)
|
9
|
+
@id = id
|
10
|
+
@public_dns = public_dns
|
11
|
+
@state = state
|
12
|
+
@aws_counterpart = aws_namespace::Instance.new id, client: aws_client
|
13
|
+
end
|
14
|
+
|
15
|
+
def hostname
|
16
|
+
@public_dns
|
17
|
+
end
|
18
|
+
|
19
|
+
def running?
|
20
|
+
state == STATE_RUNNING
|
21
|
+
end
|
22
|
+
|
23
|
+
private
|
24
|
+
def aws_namespace
|
25
|
+
::Aws::EC2
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,35 @@
|
|
1
|
+
module Elbas
|
2
|
+
module AWS
|
3
|
+
class InstanceCollection < Base
|
4
|
+
include Enumerable
|
5
|
+
|
6
|
+
attr_reader :instances
|
7
|
+
|
8
|
+
def initialize(ids)
|
9
|
+
@ids = ids
|
10
|
+
@instances = query_instances_by_ids(ids).map do |i|
|
11
|
+
Instance.new(i.instance_id, i.public_dns_name, i.state.code)
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
def running
|
16
|
+
select(&:running?)
|
17
|
+
end
|
18
|
+
|
19
|
+
def each(&block)
|
20
|
+
instances.each(&block)
|
21
|
+
end
|
22
|
+
|
23
|
+
private
|
24
|
+
def aws_namespace
|
25
|
+
::Aws::EC2
|
26
|
+
end
|
27
|
+
|
28
|
+
def query_instances_by_ids(ids)
|
29
|
+
aws_client
|
30
|
+
.describe_instances(instance_ids: @ids)
|
31
|
+
.reservations.flat_map(&:instances)
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
@@ -0,0 +1,39 @@
|
|
1
|
+
module Elbas
|
2
|
+
module AWS
|
3
|
+
class LaunchTemplate < Base
|
4
|
+
attr_reader :id, :name, :version
|
5
|
+
|
6
|
+
def initialize(id, name, version)
|
7
|
+
@id = id
|
8
|
+
@name = name
|
9
|
+
@version = version
|
10
|
+
end
|
11
|
+
|
12
|
+
def update(ami)
|
13
|
+
latest = aws_client.create_launch_template_version({
|
14
|
+
launch_template_data: { image_id: ami.id },
|
15
|
+
launch_template_id: self.id,
|
16
|
+
source_version: self.version
|
17
|
+
}).launch_template_version
|
18
|
+
|
19
|
+
response = aws_client.modify_launch_template({
|
20
|
+
default_version: latest.version_number.to_s,
|
21
|
+
launch_template_id: latest.launch_template_id
|
22
|
+
})
|
23
|
+
return unless response.successful?
|
24
|
+
|
25
|
+
self.class.new(
|
26
|
+
latest&.launch_template_id,
|
27
|
+
latest&.launch_template_name,
|
28
|
+
latest&.version_number
|
29
|
+
)
|
30
|
+
end
|
31
|
+
|
32
|
+
private
|
33
|
+
|
34
|
+
def aws_namespace
|
35
|
+
::Aws::EC2
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
module Elbas
|
2
|
+
module AWS
|
3
|
+
class Snapshot < Base
|
4
|
+
attr_reader :id
|
5
|
+
|
6
|
+
def initialize(id)
|
7
|
+
@id = id
|
8
|
+
end
|
9
|
+
|
10
|
+
def delete
|
11
|
+
return unless id
|
12
|
+
aws_client.delete_snapshot snapshot_id: id
|
13
|
+
end
|
14
|
+
|
15
|
+
private
|
16
|
+
def aws_namespace
|
17
|
+
::Aws::EC2
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
module Elbas
|
2
|
+
module AWS
|
3
|
+
module Taggable
|
4
|
+
def tag(key, value)
|
5
|
+
@tags ||= {}
|
6
|
+
|
7
|
+
Elbas::Retryable.times(3).delay(5) do
|
8
|
+
aws_counterpart.create_tags tags: [{ key: key, value: value }]
|
9
|
+
@tags[key] = value
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
def tags
|
14
|
+
@tags || {}
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
require 'capistrano/dsl'
|
2
|
+
|
3
|
+
load File.expand_path("../tasks/elbas.rake", __FILE__)
|
4
|
+
|
5
|
+
def autoscale(groupname, properties = {})
|
6
|
+
include Capistrano::DSL
|
7
|
+
include Elbas::Logger
|
8
|
+
|
9
|
+
set :aws_autoscale_group_names, Array(fetch(:aws_autoscale_group_names)).push(groupname)
|
10
|
+
|
11
|
+
asg = Elbas::AWS::AutoscaleGroup.new groupname
|
12
|
+
instances = asg.instances.running
|
13
|
+
|
14
|
+
info "Auto Scaling Group: #{groupname}"
|
15
|
+
instances.each.with_index do |instance, i|
|
16
|
+
info "Adding server: #{instance.hostname}"
|
17
|
+
|
18
|
+
props = nil
|
19
|
+
props = yield(instance, i) if block_given?
|
20
|
+
props ||= properties
|
21
|
+
|
22
|
+
server instance.hostname, props
|
23
|
+
end
|
24
|
+
|
25
|
+
if instances.any?
|
26
|
+
after 'deploy', 'elbas:deploy'
|
27
|
+
else
|
28
|
+
error <<~MESSAGE
|
29
|
+
Could not create AMI because no running instances were found in the specified
|
30
|
+
AutoScale group. Ensure that the AutoScale group name is correct and that
|
31
|
+
there is at least one running instance attached to it.
|
32
|
+
MESSAGE
|
33
|
+
end
|
34
|
+
end
|
data/lib/elbas/logger.rb
ADDED
@@ -0,0 +1,38 @@
|
|
1
|
+
require 'capistrano/doctor/output_helpers'
|
2
|
+
|
3
|
+
module Elbas
|
4
|
+
module Logger
|
5
|
+
include Capistrano::Doctor::OutputHelpers
|
6
|
+
|
7
|
+
PREFIX_TEXT = '[ELBAS] '.freeze
|
8
|
+
|
9
|
+
def info(message)
|
10
|
+
$stdout.puts [prefix, message, "\n"].join
|
11
|
+
end
|
12
|
+
|
13
|
+
def error(message)
|
14
|
+
$stderr.puts [error_prefix, message, "\n"].join
|
15
|
+
end
|
16
|
+
|
17
|
+
private
|
18
|
+
def prefix
|
19
|
+
@prefix ||= cyan(PREFIX_TEXT)
|
20
|
+
end
|
21
|
+
|
22
|
+
def error_prefix
|
23
|
+
@error_prefix ||= red(PREFIX_TEXT)
|
24
|
+
end
|
25
|
+
|
26
|
+
def cyan(text)
|
27
|
+
color_text text, :cyan
|
28
|
+
end
|
29
|
+
|
30
|
+
def red(text)
|
31
|
+
color_text text, :red
|
32
|
+
end
|
33
|
+
|
34
|
+
def color_text(text, coloring)
|
35
|
+
color.colorize text, coloring
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
module Elbas
|
2
|
+
class Retryable
|
3
|
+
include Elbas::Logger
|
4
|
+
|
5
|
+
def initialize
|
6
|
+
@max = 0
|
7
|
+
@delay = 0
|
8
|
+
end
|
9
|
+
|
10
|
+
def times(max, &block)
|
11
|
+
@max = max
|
12
|
+
run block if block_given?
|
13
|
+
end
|
14
|
+
|
15
|
+
def delay(seconds, &block)
|
16
|
+
@delay = seconds
|
17
|
+
run block if block_given?
|
18
|
+
end
|
19
|
+
|
20
|
+
def run(proc)
|
21
|
+
attempts ||= 0
|
22
|
+
attempts += 1
|
23
|
+
proc.call
|
24
|
+
rescue => e
|
25
|
+
info "Rescued error in retryable action: #{e.message}"
|
26
|
+
if attempts < @max
|
27
|
+
info "Retrying in #{@delay} seconds..."
|
28
|
+
sleep @delay
|
29
|
+
retry
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
def self.times(max, &block)
|
34
|
+
new.tap { |r| r.times max }
|
35
|
+
end
|
36
|
+
|
37
|
+
def self.delay(seconds, &block)
|
38
|
+
new.tap { |r| r.delay seconds }
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
@@ -0,0 +1,38 @@
|
|
1
|
+
require 'elbas'
|
2
|
+
include Elbas::Logger
|
3
|
+
|
4
|
+
namespace :elbas do
|
5
|
+
task :ssh do
|
6
|
+
include Capistrano::DSL
|
7
|
+
|
8
|
+
info "SSH commands:"
|
9
|
+
env.servers.to_a.each.with_index do |server, i|
|
10
|
+
info " #{i + 1}) ssh #{fetch(:user)}@#{server.hostname}"
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
task :deploy do
|
15
|
+
fetch(:aws_autoscale_group_names).each do |aws_autoscale_group_name|
|
16
|
+
info "Auto Scaling Group: #{aws_autoscale_group_name}"
|
17
|
+
asg = Elbas::AWS::AutoscaleGroup.new aws_autoscale_group_name
|
18
|
+
|
19
|
+
info "Creating AMI from a running instance..."
|
20
|
+
ami = Elbas::AWS::AMI.create asg.instances.running.sample
|
21
|
+
ami.tag 'ELBAS-Deploy-group', asg.name
|
22
|
+
ami.tag 'ELBAS-Deploy-id', env.timestamp.to_i.to_s
|
23
|
+
info "Created AMI: #{ami.id}"
|
24
|
+
|
25
|
+
info "Updating launch template with the new AMI..."
|
26
|
+
launch_template = asg.launch_template.update ami
|
27
|
+
info "Updated launch template, new default version = #{launch_template.version}"
|
28
|
+
|
29
|
+
info "Cleaning up old AMIs..."
|
30
|
+
ami.ancestors.each do |ancestor|
|
31
|
+
info "Deleting old AMI: #{ancestor.id}"
|
32
|
+
ancestor.delete
|
33
|
+
end
|
34
|
+
|
35
|
+
info "Deployment complete!"
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|