elbas 0.27.0 → 3.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (39) hide show
  1. checksums.yaml +5 -5
  2. data/README.md +25 -35
  3. data/elbas.gemspec +15 -12
  4. data/lib/elbas.rb +15 -9
  5. data/lib/elbas/aws/ami.rb +71 -0
  6. data/lib/elbas/aws/autoscale_group.rb +43 -0
  7. data/lib/elbas/aws/base.rb +39 -0
  8. data/lib/elbas/aws/instance.rb +24 -0
  9. data/lib/elbas/aws/instance_collection.rb +36 -0
  10. data/lib/elbas/aws/launch_template.rb +32 -0
  11. data/lib/elbas/aws/snapshot.rb +21 -0
  12. data/lib/elbas/aws/taggable.rb +18 -0
  13. data/lib/elbas/capistrano.rb +14 -12
  14. data/lib/elbas/errors/no_launch_template.rb +6 -0
  15. data/lib/elbas/logger.rb +10 -2
  16. data/lib/elbas/retryable.rb +34 -9
  17. data/lib/elbas/tasks/elbas.rake +19 -10
  18. data/lib/elbas/version.rb +1 -1
  19. data/spec/aws/ami_spec.rb +140 -0
  20. data/spec/aws/autoscale_group_spec.rb +53 -0
  21. data/spec/aws/instance_collection_spec.rb +28 -0
  22. data/spec/aws/instance_spec.rb +30 -0
  23. data/spec/aws/launch_template_spec.rb +52 -0
  24. data/spec/aws/taggable_spec.rb +53 -0
  25. data/spec/spec_helper.rb +14 -24
  26. data/spec/support/stubs/CreateLaunchTemplateVersion.200.xml +16 -0
  27. data/spec/support/stubs/DescribeAutoScalingGroups.200.xml +60 -0
  28. data/spec/support/stubs/DescribeImages.200.xml +36 -1
  29. data/spec/support/stubs/DescribeInstances.200.xml +109 -2
  30. metadata +60 -21
  31. data/lib/elbas/ami.rb +0 -56
  32. data/lib/elbas/aws/autoscaling.rb +0 -21
  33. data/lib/elbas/aws/credentials.rb +0 -20
  34. data/lib/elbas/aws/ec2.rb +0 -13
  35. data/lib/elbas/aws_resource.rb +0 -36
  36. data/lib/elbas/launch_configuration.rb +0 -64
  37. data/lib/elbas/taggable.rb +0 -9
  38. data/spec/ami_spec.rb +0 -10
  39. 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
@@ -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::AWS::AutoScaling
7
+ include Elbas::Logger
9
8
 
10
- autoscale_group = autoscaling.groups[groupname]
11
- running_instances = autoscale_group.ec2_instances.filter('instance-state-name', 'running')
9
+ set :aws_autoscale_group_name, groupname
12
10
 
13
- set :aws_autoscale_group, groupname
11
+ asg = Elbas::AWS::AutoscaleGroup.new groupname
12
+ instances = asg.instances.running
14
13
 
15
- running_instances.each do |instance|
16
- hostname = instance.dns_name || instance.private_ip_address
17
- p "ELBAS: Adding server: #{hostname}"
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 running_instances.count > 0
22
- after('deploy', 'elbas:scale')
19
+ if instances.any?
20
+ after 'deploy', 'elbas:deploy'
23
21
  else
24
- p "ELBAS: AMI could not be created because no running instances were found. Is your autoscale group name correct?"
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
@@ -0,0 +1,6 @@
1
+ module Elbas
2
+ module Errors
3
+ class NoLaunchTemplate < StandardError
4
+ end
5
+ end
6
+ end
@@ -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
- p "ELBAS: #{message}"
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
@@ -1,16 +1,41 @@
1
1
  module Elbas
2
- module Retryable
3
- def with_retry(max: fetch(:elbas_retry_max, 3), delay: fetch(:elbas_retry_delay, 5))
4
- tries ||= 0
5
- tries += 1
6
- yield
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
- p "Rescued #{e.message}"
9
- if tries < max
10
- p "Retrying in #{delay} seconds..."
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
@@ -1,17 +1,26 @@
1
1
  require 'elbas'
2
+ include Elbas::Logger
2
3
 
3
4
  namespace :elbas do
4
- task :scale do
5
- set :aws_access_key_id, fetch(:aws_access_key_id, ENV['AWS_ACCESS_KEY_ID'])
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
- Elbas::AMI.create do |ami|
9
- p "ELBAS: Created AMI: #{ami.aws_counterpart.id}"
10
- Elbas::LaunchConfiguration.create(ami) do |lc|
11
- p "ELBAS: Created Launch Configuration: #{lc.aws_counterpart.name}"
12
- lc.attach_to_autoscale_group!
13
- end
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
@@ -1,3 +1,3 @@
1
1
  module Elbas
2
- VERSION = '0.27.0'.freeze
2
+ VERSION = '3.0.0'.freeze
3
3
  end
@@ -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