elbas 0.27.0 → 3.0.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 +5 -5
- data/README.md +25 -35
- data/elbas.gemspec +15 -12
- data/lib/elbas.rb +15 -9
- data/lib/elbas/aws/ami.rb +71 -0
- data/lib/elbas/aws/autoscale_group.rb +43 -0
- data/lib/elbas/aws/base.rb +39 -0
- data/lib/elbas/aws/instance.rb +24 -0
- data/lib/elbas/aws/instance_collection.rb +36 -0
- data/lib/elbas/aws/launch_template.rb +32 -0
- data/lib/elbas/aws/snapshot.rb +21 -0
- data/lib/elbas/aws/taggable.rb +18 -0
- data/lib/elbas/capistrano.rb +14 -12
- data/lib/elbas/errors/no_launch_template.rb +6 -0
- data/lib/elbas/logger.rb +10 -2
- data/lib/elbas/retryable.rb +34 -9
- data/lib/elbas/tasks/elbas.rake +19 -10
- data/lib/elbas/version.rb +1 -1
- data/spec/aws/ami_spec.rb +140 -0
- data/spec/aws/autoscale_group_spec.rb +53 -0
- data/spec/aws/instance_collection_spec.rb +28 -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/spec_helper.rb +14 -24
- data/spec/support/stubs/CreateLaunchTemplateVersion.200.xml +16 -0
- data/spec/support/stubs/DescribeAutoScalingGroups.200.xml +60 -0
- data/spec/support/stubs/DescribeImages.200.xml +36 -1
- data/spec/support/stubs/DescribeInstances.200.xml +109 -2
- metadata +60 -21
- data/lib/elbas/ami.rb +0 -56
- data/lib/elbas/aws/autoscaling.rb +0 -21
- data/lib/elbas/aws/credentials.rb +0 -20
- data/lib/elbas/aws/ec2.rb +0 -13
- data/lib/elbas/aws_resource.rb +0 -36
- data/lib/elbas/launch_configuration.rb +0 -64
- data/lib/elbas/taggable.rb +0 -9
- data/spec/ami_spec.rb +0 -10
- data/spec/elbas_spec.rb +0 -69
@@ -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
|
data/lib/elbas/capistrano.rb
CHANGED
@@ -1,26 +1,28 @@
|
|
1
|
-
require 'aws-sdk-v1'
|
2
1
|
require 'capistrano/dsl'
|
3
2
|
|
4
3
|
load File.expand_path("../tasks/elbas.rake", __FILE__)
|
5
4
|
|
6
5
|
def autoscale(groupname, *args)
|
7
6
|
include Capistrano::DSL
|
8
|
-
include Elbas::
|
7
|
+
include Elbas::Logger
|
9
8
|
|
10
|
-
|
11
|
-
running_instances = autoscale_group.ec2_instances.filter('instance-state-name', 'running')
|
9
|
+
set :aws_autoscale_group_name, groupname
|
12
10
|
|
13
|
-
|
11
|
+
asg = Elbas::AWS::AutoscaleGroup.new groupname
|
12
|
+
instances = asg.instances.running
|
14
13
|
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
server(hostname, *args)
|
14
|
+
instances.each do |instance|
|
15
|
+
info "Adding server: #{instance.hostname}"
|
16
|
+
server instance.hostname, *args
|
19
17
|
end
|
20
18
|
|
21
|
-
if
|
22
|
-
after
|
19
|
+
if instances.any?
|
20
|
+
after 'deploy', 'elbas:deploy'
|
23
21
|
else
|
24
|
-
|
22
|
+
error <<~MESSAGE
|
23
|
+
Could not create AMI because no running instances were found in the specified
|
24
|
+
AutoScale group. Ensure that the AutoScale group name is correct and that
|
25
|
+
there is at least one running instance attached to it.
|
26
|
+
MESSAGE
|
25
27
|
end
|
26
28
|
end
|
data/lib/elbas/logger.rb
CHANGED
@@ -1,7 +1,15 @@
|
|
1
|
+
require 'capistrano/doctor/output_helpers'
|
2
|
+
|
1
3
|
module Elbas
|
2
4
|
module Logger
|
5
|
+
include Capistrano::Doctor::OutputHelpers
|
6
|
+
|
3
7
|
def info(message)
|
4
|
-
|
8
|
+
$stdout.puts [prefix, message, "\n"].join
|
9
|
+
end
|
10
|
+
|
11
|
+
def prefix
|
12
|
+
@prefix ||= color.colorize("\n[ELBAS] ", :cyan)
|
5
13
|
end
|
6
14
|
end
|
7
|
-
end
|
15
|
+
end
|
data/lib/elbas/retryable.rb
CHANGED
@@ -1,16 +1,41 @@
|
|
1
1
|
module Elbas
|
2
|
-
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
|
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
|
7
24
|
rescue => e
|
8
|
-
|
9
|
-
if
|
10
|
-
|
11
|
-
sleep delay
|
25
|
+
info "Rescued error in retryable action: #{e.message}"
|
26
|
+
if attempts < @max
|
27
|
+
info "Retrying in #{@delay} seconds..."
|
28
|
+
sleep @delay
|
12
29
|
retry
|
13
30
|
end
|
14
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
|
15
40
|
end
|
16
41
|
end
|
data/lib/elbas/tasks/elbas.rake
CHANGED
@@ -1,17 +1,26 @@
|
|
1
1
|
require 'elbas'
|
2
|
+
include Elbas::Logger
|
2
3
|
|
3
4
|
namespace :elbas do
|
4
|
-
task :
|
5
|
-
|
6
|
-
set :aws_secret_access_key, fetch(:aws_secret_access_key, ENV['AWS_SECRET_ACCESS_KEY'])
|
5
|
+
task :deploy do
|
6
|
+
asg = Elbas::AWS::AutoscaleGroup.new fetch(:aws_autoscale_group_name)
|
7
7
|
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
8
|
+
info "Creating AMI from a running instance..."
|
9
|
+
ami = Elbas::AWS::AMI.create asg.instances.running.sample
|
10
|
+
ami.tag 'ELBAS-Deploy-group', asg.name
|
11
|
+
ami.tag 'ELBAS-Deploy-id', env.timestamp.to_i.to_s
|
12
|
+
info "Created AMI: #{ami.id}"
|
13
|
+
|
14
|
+
info "Updating launch template with the new AMI..."
|
15
|
+
launch_template = asg.launch_template.update ami
|
16
|
+
info "Updated launch template, new default version = #{launch_template.version}"
|
17
|
+
|
18
|
+
info "Cleaning up old AMIs..."
|
19
|
+
ami.ancestors.each do |ancestor|
|
20
|
+
info "Deleting old AMI: #{ancestor.id}"
|
21
|
+
ancestor.delete
|
14
22
|
end
|
15
23
|
|
24
|
+
info "Deployment complete!"
|
16
25
|
end
|
17
|
-
end
|
26
|
+
end
|
data/lib/elbas/version.rb
CHANGED
@@ -0,0 +1,140 @@
|
|
1
|
+
describe Elbas::AWS::AMI do
|
2
|
+
subject { Elbas::AWS::AMI.new 'test' }
|
3
|
+
|
4
|
+
describe '#initialize' do
|
5
|
+
it 'sets the id' do
|
6
|
+
expect(subject.id).to eq 'test'
|
7
|
+
end
|
8
|
+
|
9
|
+
it 'has an aws-sdk counterpart' do
|
10
|
+
expect(subject.aws_counterpart).to be_a_kind_of ::Aws::EC2::Image
|
11
|
+
expect(subject.aws_counterpart.id).to eq 'test'
|
12
|
+
end
|
13
|
+
|
14
|
+
context 'with snapshots' do
|
15
|
+
subject do
|
16
|
+
Elbas::AWS::AMI.new 'test', [
|
17
|
+
double(:bdm, ebs: double(:ebs, snapshot_id: 'snap-1'))
|
18
|
+
]
|
19
|
+
end
|
20
|
+
|
21
|
+
it 'sets snapshots to Snapshot objects' do
|
22
|
+
expect(subject.snapshots.size).to eq 1
|
23
|
+
expect(subject.snapshots[0]).to be_a_kind_of Elbas::AWS::Snapshot
|
24
|
+
end
|
25
|
+
|
26
|
+
it 'sets the ID on the Snapshots' do
|
27
|
+
expect(subject.snapshots[0].id).to eq 'snap-1'
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
describe '#deploy_id' do
|
33
|
+
it 'returns the ELBAS-Deploy-id tag, if set' do
|
34
|
+
webmock :post, /ec2/ => 201, with: Hash[body: /Action=CreateTags/]
|
35
|
+
subject.tag 'ELBAS-Deploy-id', 'test'
|
36
|
+
expect(subject.deploy_id).to eq 'test'
|
37
|
+
end
|
38
|
+
|
39
|
+
it 'returns nil if the tag was never set' do
|
40
|
+
expect(subject.deploy_id).to be_nil
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
describe '#deploy_group' do
|
45
|
+
it 'returns the ELBAS-Deploy-group tag, if set' do
|
46
|
+
webmock :post, /ec2/ => 201, with: Hash[body: /Action=CreateTags/]
|
47
|
+
subject.tag 'ELBAS-Deploy-group', 'test'
|
48
|
+
expect(subject.deploy_group).to eq 'test'
|
49
|
+
end
|
50
|
+
|
51
|
+
it 'returns nil if the tag was never set' do
|
52
|
+
expect(subject.deploy_group).to be_nil
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
describe '#ancestors' do
|
57
|
+
before do
|
58
|
+
webmock :post, /ec2/ => 201, with: Hash[body: /Action=CreateTags/]
|
59
|
+
subject.tag 'ELBAS-Deploy-group', 'test'
|
60
|
+
subject.tag 'ELBAS-Deploy-id', 'test'
|
61
|
+
|
62
|
+
webmock :post, %r{ec2.(.*).amazonaws.com\/\z} => 'DescribeImages.200.xml',
|
63
|
+
with: Hash[body: /Action=DescribeImages/]
|
64
|
+
end
|
65
|
+
|
66
|
+
it 'includes AMIs from the same deploy group, different deploy ID' do
|
67
|
+
expect {
|
68
|
+
subject.tag 'ELBAS-Deploy-id', 'not-test'
|
69
|
+
}.to change {
|
70
|
+
subject.ancestors.size
|
71
|
+
}.by 1
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
describe '#delete' do
|
76
|
+
before do
|
77
|
+
webmock :post, /ec2/ => 201, with: Hash[body: /Action=DeregisterImage/]
|
78
|
+
end
|
79
|
+
|
80
|
+
it 'calls the deregister AMI API' do
|
81
|
+
subject.delete
|
82
|
+
expect(WebMock)
|
83
|
+
.to have_requested(:post, /ec2/)
|
84
|
+
.with body: /Action=DeregisterImage&ImageId=test/
|
85
|
+
end
|
86
|
+
|
87
|
+
context 'with snapshots' do
|
88
|
+
subject do
|
89
|
+
Elbas::AWS::AMI.new 'test', [
|
90
|
+
double(:bdm, ebs: double(:ebs, snapshot_id: 'snap-1'))
|
91
|
+
]
|
92
|
+
end
|
93
|
+
|
94
|
+
it 'deletes the AMIs snapshots too' do
|
95
|
+
webmock :post, /ec2/ => 201, with: Hash[body: /Action=DeleteSnapshot/]
|
96
|
+
|
97
|
+
subject.delete
|
98
|
+
expect(WebMock)
|
99
|
+
.to have_requested(:post, /ec2/)
|
100
|
+
.with body: /Action=DeleteSnapshot&SnapshotId=snap-1/
|
101
|
+
end
|
102
|
+
end
|
103
|
+
end
|
104
|
+
|
105
|
+
describe '.create' do
|
106
|
+
subject { described_class }
|
107
|
+
let(:instance) { Elbas::AWS::Instance.new 'i-1234567890', nil, nil }
|
108
|
+
|
109
|
+
before do
|
110
|
+
webmock :post, %r{ec2.(.*).amazonaws.com\/\z} => 'CreateImage.200.xml',
|
111
|
+
with: Hash[body: /Action=CreateImage/]
|
112
|
+
end
|
113
|
+
|
114
|
+
it 'calls the API with the instance given' do
|
115
|
+
subject.create instance
|
116
|
+
expect(WebMock)
|
117
|
+
.to have_requested(:post, /ec2/)
|
118
|
+
.with body: /Action=CreateImage&InstanceId=i-1234567890&Name=ELBAS-ami-(\d+)/
|
119
|
+
end
|
120
|
+
|
121
|
+
it 'uses the no_reboot option by default' do
|
122
|
+
subject.create instance
|
123
|
+
expect(WebMock)
|
124
|
+
.to have_requested(:post, /ec2/)
|
125
|
+
.with(body: /NoReboot=true/)
|
126
|
+
end
|
127
|
+
|
128
|
+
it 'uses no_reboot as false, if given' do
|
129
|
+
subject.create instance, no_reboot: false
|
130
|
+
expect(WebMock)
|
131
|
+
.to have_requested(:post, /ec2/)
|
132
|
+
.with(body: /NoReboot=false/)
|
133
|
+
end
|
134
|
+
|
135
|
+
it 'returns the an AMI object with the new id' do
|
136
|
+
ami = subject.create instance
|
137
|
+
expect(ami.id).to eq 'ami-4fa54026' # from stub
|
138
|
+
end
|
139
|
+
end
|
140
|
+
end
|
@@ -0,0 +1,53 @@
|
|
1
|
+
describe Elbas::AWS::AutoscaleGroup do
|
2
|
+
subject { Elbas::AWS::AutoscaleGroup.new 'test-asg' }
|
3
|
+
|
4
|
+
before do
|
5
|
+
webmock :post, %r{autoscaling.(.*).amazonaws.com\/\z} => 'DescribeAutoScalingGroups.200.xml',
|
6
|
+
with: Hash[body: /Action=DescribeAutoScalingGroups/]
|
7
|
+
|
8
|
+
webmock :post, %r{ec2.(.*).amazonaws.com\/\z} => 'DescribeInstances.200.xml',
|
9
|
+
with: Hash[body: /Action=DescribeInstances/]
|
10
|
+
end
|
11
|
+
|
12
|
+
describe '#initialize' do
|
13
|
+
it 'sets the name' do
|
14
|
+
expect(subject.name).to eq 'test-asg'
|
15
|
+
end
|
16
|
+
|
17
|
+
it 'has an aws-sdk counterpart' do
|
18
|
+
expect(subject.aws_counterpart).to be_a_kind_of ::Aws::AutoScaling::Types::AutoScalingGroup
|
19
|
+
expect(subject.aws_counterpart.auto_scaling_group_name).to eq 'test-asg'
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
describe '#instance_ids' do
|
24
|
+
it 'returns every instance ID in the ASG' do
|
25
|
+
expect(subject.instance_ids).to eq ['i-1234567890', 'i-500']
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
describe '#instances' do
|
30
|
+
it 'returns an instance collection with all instances' do
|
31
|
+
instances = subject.instances
|
32
|
+
expect(instances.count).to eq 2
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
describe '#launch_template' do
|
37
|
+
it 'throws an error if there is no launch template' do
|
38
|
+
expect { subject.launch_template }.to raise_error(Elbas::Errors::NoLaunchTemplate)
|
39
|
+
end
|
40
|
+
|
41
|
+
it 'returns a LaunchTemplate object with the id/name/version set' do
|
42
|
+
allow(subject.aws_counterpart).to receive(:launch_template) do
|
43
|
+
double(:lt, launch_template_id: 'test-1', launch_template_name: 'Test', version: '$Latest')
|
44
|
+
end
|
45
|
+
|
46
|
+
expect(subject.launch_template.id).to eq 'test-1'
|
47
|
+
expect(subject.launch_template.name).to eq 'Test'
|
48
|
+
expect(subject.launch_template.version).to eq '$Latest'
|
49
|
+
end
|
50
|
+
|
51
|
+
|
52
|
+
end
|
53
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
describe Elbas::AWS::InstanceCollection do
|
2
|
+
subject { Elbas::AWS::InstanceCollection.new ['i-1234567890', 'i-500'] }
|
3
|
+
|
4
|
+
before do
|
5
|
+
webmock :post, %r{ec2.(.*).amazonaws.com\/\z} => 'DescribeInstances.200.xml',
|
6
|
+
with: Hash[body: /Action=DescribeInstances/]
|
7
|
+
end
|
8
|
+
|
9
|
+
describe '#instances' do
|
10
|
+
it 'returns Instance objects with name/hostname/state' do
|
11
|
+
expect(subject.instances[0].id).to eq 'i-1234567890'
|
12
|
+
expect(subject.instances[0].hostname).to eq 'ec2-1234567890.amazonaws.com'
|
13
|
+
expect(subject.instances[0].state).to eq 16
|
14
|
+
|
15
|
+
expect(subject.instances[1].id).to eq 'i-500'
|
16
|
+
expect(subject.instances[1].hostname).to eq 'ec2-500.amazonaws.com'
|
17
|
+
expect(subject.instances[1].state).to eq 32
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
describe '#running' do
|
22
|
+
it 'returns only running instances' do
|
23
|
+
expect(subject.instances.size).to eq 2
|
24
|
+
expect(subject.running.size).to eq 1
|
25
|
+
expect(subject.running[0].id).to eq 'i-1234567890'
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
describe Elbas::AWS::Instance do
|
2
|
+
subject { Elbas::AWS::Instance.new 'i-1234567890', 'ec2-1234567890.amazonaws.com', 16 }
|
3
|
+
|
4
|
+
describe '#initialize' do
|
5
|
+
it 'sets the AWS counterpart' do
|
6
|
+
expect(subject.aws_counterpart).to be_a_kind_of ::Aws::EC2::Instance
|
7
|
+
expect(subject.aws_counterpart.id).to eq 'i-1234567890'
|
8
|
+
end
|
9
|
+
end
|
10
|
+
|
11
|
+
describe '#hostname' do
|
12
|
+
it 'returns the public DNS' do
|
13
|
+
expect(subject.hostname).to eq 'ec2-1234567890.amazonaws.com'
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
describe '#running?' do
|
18
|
+
it 'returns true if the state code is 16 ("running")' do
|
19
|
+
expect(subject).to receive(:state) { 16 }
|
20
|
+
expect(subject).to be_running
|
21
|
+
end
|
22
|
+
|
23
|
+
it 'returns false for every other state code' do
|
24
|
+
[0, 32, 48, 64, 80].each do |code|
|
25
|
+
expect(subject).to receive(:state) { code }
|
26
|
+
expect(subject).to_not be_running
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
@@ -0,0 +1,52 @@
|
|
1
|
+
describe Elbas::AWS::LaunchTemplate do
|
2
|
+
subject { Elbas::AWS::LaunchTemplate.new 'test-lt', 'test', '1' }
|
3
|
+
|
4
|
+
before do
|
5
|
+
webmock :post, %r{ec2.(.*).amazonaws.com\/\z} => 'CreateLaunchTemplateVersion.200.xml',
|
6
|
+
with: Hash[body: /Action=CreateLaunchTemplateVersion/]
|
7
|
+
end
|
8
|
+
|
9
|
+
describe '#initialize' do
|
10
|
+
it 'sets the id' do
|
11
|
+
expect(subject.id).to eq 'test-lt'
|
12
|
+
end
|
13
|
+
|
14
|
+
it 'sets the name' do
|
15
|
+
expect(subject.name).to eq 'test'
|
16
|
+
end
|
17
|
+
|
18
|
+
it 'sets the version' do
|
19
|
+
expect(subject.version).to eq '1'
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
describe '#update' do
|
24
|
+
it 'hits the CreateLaunchTemplateVersion API' do
|
25
|
+
subject.update double(:ami, id: 'ami-123')
|
26
|
+
expect(WebMock)
|
27
|
+
.to have_requested(:post, /ec2/)
|
28
|
+
.with(body: %r{Action=CreateLaunchTemplateVersion})
|
29
|
+
end
|
30
|
+
|
31
|
+
it 'creates a new launch template from the given AMI' do
|
32
|
+
subject.update double(:ami, id: 'ami-123')
|
33
|
+
expect(WebMock)
|
34
|
+
.to have_requested(:post, /ec2/)
|
35
|
+
.with(body: %r{LaunchTemplateData.ImageId=ami-123})
|
36
|
+
end
|
37
|
+
|
38
|
+
it 'uses itself as the source' do
|
39
|
+
subject.update double(:ami, id: 'ami-123')
|
40
|
+
expect(WebMock)
|
41
|
+
.to have_requested(:post, /ec2/)
|
42
|
+
.with(body: %r{LaunchTemplateId=test-lt&SourceVersion=1})
|
43
|
+
end
|
44
|
+
|
45
|
+
it 'returns a new launch template' do
|
46
|
+
launch_template = subject.update double(:ami, id: 'ami-123')
|
47
|
+
expect(launch_template.id).to eq 'lt-1234567890'
|
48
|
+
expect(launch_template.name).to eq 'elbas-test'
|
49
|
+
expect(launch_template.version).to eq 123
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|