MovableInkAWS 0.1.1

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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 669c0c6bef77e8bfb4b4eda8971acb83794abd1b
4
+ data.tar.gz: b9b7805f3aeb75e9e6162522b5f94d5e2e57dcb8
5
+ SHA512:
6
+ metadata.gz: eb0abc4ce9cbff973ca3bf729f29fba02ed2ea91507bef521e2c7df147184da8aec5ffdb61462ce98133d01708adef6fcaed5898c700b1e0c9c68bd892cad6f6
7
+ data.tar.gz: 9b24941a0844d01f41d30b926b6c79e74c44a7bb367d572ee36146d9da20366f7ff49ae6ef81b1f94ceeacaec27c980ce0bbf0ae2b15a8adb343224e7167090a
data/.travis.yml ADDED
@@ -0,0 +1,8 @@
1
+ language: ruby
2
+ rvm:
3
+ - 2.2
4
+
5
+ bundler_args: "--retry=3"
6
+
7
+ script:
8
+ - rspec spec/
data/Gemfile ADDED
@@ -0,0 +1,6 @@
1
+ source 'https://rubygems.org'
2
+ gemspec
3
+
4
+ group :test do
5
+ gem 'rspec', '~> 3.6'
6
+ end
data/Gemfile.lock ADDED
@@ -0,0 +1,42 @@
1
+ PATH
2
+ remote: .
3
+ specs:
4
+ MovableInkAWS (0.1.1)
5
+ aws-sdk (~> 2.10, >= 2.10.0)
6
+
7
+ GEM
8
+ remote: https://rubygems.org/
9
+ specs:
10
+ aws-sdk (2.10.125)
11
+ aws-sdk-resources (= 2.10.125)
12
+ aws-sdk-core (2.10.125)
13
+ aws-sigv4 (~> 1.0)
14
+ jmespath (~> 1.0)
15
+ aws-sdk-resources (2.10.125)
16
+ aws-sdk-core (= 2.10.125)
17
+ aws-sigv4 (1.0.2)
18
+ diff-lcs (1.3)
19
+ jmespath (1.3.1)
20
+ rspec (3.6.0)
21
+ rspec-core (~> 3.6.0)
22
+ rspec-expectations (~> 3.6.0)
23
+ rspec-mocks (~> 3.6.0)
24
+ rspec-core (3.6.0)
25
+ rspec-support (~> 3.6.0)
26
+ rspec-expectations (3.6.0)
27
+ diff-lcs (>= 1.2.0, < 2.0)
28
+ rspec-support (~> 3.6.0)
29
+ rspec-mocks (3.6.0)
30
+ diff-lcs (>= 1.2.0, < 2.0)
31
+ rspec-support (~> 3.6.0)
32
+ rspec-support (3.6.0)
33
+
34
+ PLATFORMS
35
+ ruby
36
+
37
+ DEPENDENCIES
38
+ MovableInkAWS!
39
+ rspec (~> 3.6)
40
+
41
+ BUNDLED WITH
42
+ 1.16.0
@@ -0,0 +1,20 @@
1
+ $LOAD_PATH.push File.expand_path("../lib", __FILE__)
2
+ require "movable_ink/version"
3
+
4
+ Gem::Specification.new do |s|
5
+ s.name = 'MovableInkAWS'
6
+ s.version = MovableInk::AWS::VERSION
7
+ s.summary = 'AWS Utility methods for MovableInk'
8
+ s.description = 'AWS Utility methods for MovableInk'
9
+ s.authors = ['Matt Chesler']
10
+ s.email = 'mchesler@movableink.com'
11
+
12
+ s.add_runtime_dependency 'aws-sdk', '~> 2.10', '>= 2.10.0'
13
+
14
+ all_files = `git ls-files`.split("\n")
15
+ test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
16
+
17
+ s.files = all_files - test_files
18
+ s.test_files = test_files
19
+ s.require_paths = ["lib"]
20
+ end
data/README.md ADDED
@@ -0,0 +1,27 @@
1
+ # MovableInk::AWS gem
2
+
3
+ ## Building
4
+
5
+ `gem build MovableInkAWS.gemspec`
6
+
7
+ ## "Publishing"
8
+
9
+ `s3cmd put -P MovableInkAWS-<VERSION>.gem s3://movableink-chef/MovableInkAWS/`
10
+
11
+ ## Installing
12
+
13
+ `gem install ./MovableInkAWS-<VERSION>.gem`
14
+
15
+ ## Using
16
+
17
+ ```ruby
18
+ require 'movable_ink/aws'
19
+
20
+ miaws = MovableInk::AWS.new
21
+
22
+ miaws.datacenter
23
+
24
+ miaws.instance_ip_addresses_by_role(role: 'varnish').map { |m| "http://#{m}:8080" }
25
+
26
+ ...
27
+ ```
@@ -0,0 +1,68 @@
1
+ module MovableInk
2
+ class AWS
3
+ module Autoscaling
4
+ def autoscaling(region: my_region)
5
+ @autoscaling_client ||= {}
6
+ @autoscaling_client[region] ||= Aws::AutoScaling::Client.new(region: region)
7
+ end
8
+
9
+ def mark_me_as_unhealthy
10
+ run_with_backoff do
11
+ autoscaling.set_instance_health({
12
+ health_status: "Unhealthy",
13
+ instance_id: instance_id,
14
+ should_respect_grace_period: false
15
+ })
16
+ end
17
+ end
18
+
19
+ def mark_me_as_healthy(role:)
20
+ run_with_backoff do
21
+ ec2.create_tags({
22
+ resources: [instance_id],
23
+ tags: [
24
+ {
25
+ key: "mi:roles",
26
+ value: role
27
+ }
28
+ ]
29
+ })
30
+ end
31
+ end
32
+
33
+ def delete_role_tag(role:)
34
+ run_with_backoff do
35
+ ec2.delete_tags({
36
+ resources: [instance_id],
37
+ tags: [
38
+ {
39
+ key: "mi:roles",
40
+ value: role
41
+ }
42
+ ]
43
+ })
44
+ end
45
+ end
46
+
47
+ def complete_lifecycle_action(hook_name:, group_name:, token:)
48
+ run_with_backoff do
49
+ autoscaling.complete_lifecycle_action({
50
+ lifecycle_hook_name: hook_name,
51
+ auto_scaling_group_name: group_name,
52
+ lifecycle_action_token: token,
53
+ lifecycle_action_result: 'CONTINUE'
54
+ })
55
+ end
56
+ end
57
+
58
+ def record_lifecycle_action_heartbeat(hook_name:, group_name:)
59
+ run_with_backoff do
60
+ autoscaling.record_lifecycle_action_heartbeat({
61
+ lifecycle_hook_name: hook_name,
62
+ auto_scaling_group_name: group_name
63
+ })
64
+ end
65
+ end
66
+ end
67
+ end
68
+ end
@@ -0,0 +1,128 @@
1
+ module MovableInk
2
+ class AWS
3
+ module EC2
4
+ def ec2(region: my_region)
5
+ @ec2_client ||= {}
6
+ @ec2_client[region] ||= Aws::EC2::Client.new(region: region)
7
+ end
8
+
9
+ def mi_env
10
+ @mi_env ||= load_mi_env
11
+ end
12
+
13
+ def load_mi_env
14
+ run_with_backoff do
15
+ ec2.describe_tags(filters: [
16
+ {
17
+ name: 'resource-id',
18
+ values: [instance_id]
19
+ }
20
+ ])
21
+ .tags
22
+ .detect { |tag| tag.key == 'mi:env' }
23
+ .value
24
+ end
25
+ end
26
+
27
+ def thopter_instance
28
+ @thopter_instance ||= load_thopter_instance
29
+ end
30
+
31
+ def load_thopter_instance
32
+ run_with_backoff do
33
+ ec2(region: 'us-east-1').describe_instances(filters: [
34
+ {
35
+ name: 'tag:mi:roles',
36
+ values: ['*thopter*']
37
+ },
38
+ {
39
+ name: 'tag:mi:env',
40
+ values: [mi_env]
41
+ },
42
+ {
43
+ name: 'instance-state-name',
44
+ values: ['running']
45
+ }
46
+ ])
47
+ .reservations
48
+ .flat_map(&:instances)
49
+ end
50
+ end
51
+
52
+ def all_instances(region: my_region, no_filter: false)
53
+ @all_instances ||= {}
54
+ @all_instances[region] ||= load_all_instances(region, no_filter: no_filter)
55
+ end
56
+
57
+ def load_all_instances(region, no_filter: false)
58
+ filters = if no_filter
59
+ nil
60
+ else
61
+ [{
62
+ name: 'instance-state-name',
63
+ values: ['running']
64
+ },
65
+ {
66
+ name: 'tag:mi:env',
67
+ values: [mi_env]
68
+ }]
69
+ end
70
+ run_with_backoff do
71
+ ec2(region: region).describe_instances(filters: filters).flat_map do |resp|
72
+ resp.reservations.flat_map(&:instances)
73
+ end
74
+ end
75
+ end
76
+
77
+ def instances(role:, region: my_region, availability_zone: nil)
78
+ role_pattern = mi_env == 'production' ? "^#{role}$" : "^*#{role}*$"
79
+ role_pattern = role_pattern.gsub('**','*').gsub('*','.*')
80
+ instances = all_instances(region: region).select { |instance|
81
+ instance.tags.detect { |tag|
82
+ tag.key == 'mi:roles'&&
83
+ Regexp.new(role_pattern).match(tag.value) &&
84
+ !tag.value.include?('decommissioned')
85
+ }
86
+ }
87
+ if availability_zone
88
+ instances.select { |instance|
89
+ instance.placement.availability_zone == availability_zone
90
+ }
91
+ else
92
+ instances
93
+ end
94
+ end
95
+
96
+ def thopter
97
+ private_ip_addresses(thopter_instance).first
98
+ end
99
+
100
+ def statsd_host
101
+ instance_ip_addresses_by_role(role: 'statsd', availability_zone: availability_zone).sample
102
+ end
103
+
104
+ def private_ip_addresses(instances)
105
+ instances.map(&:private_ip_address)
106
+ end
107
+
108
+ def instance_ip_addresses_by_role(role:, availability_zone: nil)
109
+ private_ip_addresses(instances(role: role, availability_zone: availability_zone))
110
+ end
111
+
112
+ def instance_ip_addresses_by_role_ordered(role:)
113
+ instances = instances(role: role)
114
+ instances_in_my_az = instances.select { |instance| instance.placement.availability_zone == availability_zone }
115
+ ordered_instances = instances_in_my_az.shuffle + (instances - instances_in_my_az).shuffle
116
+ private_ip_addresses(ordered_instances)
117
+ end
118
+
119
+ def redis_by_role(role, port)
120
+ instance_ip_addresses_by_role(role: role)
121
+ .shuffle
122
+ .inject([]) { |redii, instance|
123
+ redii.push({"host" => instance, "port" => port})
124
+ }
125
+ end
126
+ end
127
+ end
128
+ end
@@ -0,0 +1,8 @@
1
+ module MovableInk
2
+ class AWS
3
+ module Errors
4
+ class FailedWithBackoff < StandardError; end
5
+ class EC2Required < StandardError; end
6
+ end
7
+ end
8
+ end
@@ -0,0 +1,58 @@
1
+ module MovableInk
2
+ class AWS
3
+ module Route53
4
+ def route53
5
+ @route53_client ||= Aws::Route53::Client.new(region: 'us-east-1')
6
+ end
7
+
8
+ def elastic_ips
9
+ @all_elastic_ips ||= load_all_elastic_ips
10
+ end
11
+
12
+ def load_all_elastic_ips
13
+ run_with_backoff do
14
+ ec2.describe_addresses.flat_map(&:addresses)
15
+ end
16
+ end
17
+
18
+ def unassigned_elastic_ips
19
+ @unassigned_elastic_ips ||= elastic_ips.select { |address| address.association_id.nil? }
20
+ end
21
+
22
+ def reserved_elastic_ips
23
+ @reserved_elastic_ips ||= s3.get_object({bucket: 'movableink-chef', key: 'reserved_ips.json'}).body
24
+ .map { |line| JSON.parse(line) }
25
+ end
26
+
27
+ def available_elastic_ips(role:)
28
+ reserved_elastic_ips.select do |ip|
29
+ ip["datacenter"] == datacenter &&
30
+ ip["role"] == role &&
31
+ unassigned_elastic_ips.map(&:allocation_id).include?(ip["allocation_id"])
32
+ end
33
+ end
34
+
35
+ def assign_ip_address(role:)
36
+ run_with_backoff do
37
+ ec2.associate_address({
38
+ instance_id: instance_id,
39
+ allocation_id: available_elastic_ips(role: role).sample["allocation_id"]
40
+ })
41
+ end
42
+ end
43
+
44
+ def resource_record_sets(hosted_zone_id)
45
+ @resource_record_sets ||= {}
46
+ @resource_record_sets[hosted_zone_id] ||= list_all_r53_resource_record_sets(hosted_zone_id)
47
+ end
48
+
49
+ def list_all_r53_resource_record_sets(hosted_zone_id)
50
+ run_with_backoff do
51
+ route53.list_resource_record_sets({
52
+ hosted_zone_id: hosted_zone_id
53
+ }).flat_map(&:resource_record_sets)
54
+ end
55
+ end
56
+ end
57
+ end
58
+ end
@@ -0,0 +1,41 @@
1
+ module MovableInk
2
+ class AWS
3
+ module SNS
4
+ def sns(region: my_region)
5
+ @sns_client ||= {}
6
+ @sns_client[region] ||= Aws::SNS::Client.new(region: region)
7
+ end
8
+
9
+ def sns_slack_topic_arn
10
+ run_with_backoff do
11
+ sns.list_topics.each do |resp|
12
+ resp.topics.each do |topic|
13
+ return topic.topic_arn if topic.topic_arn.include? "slack-aws-alerts"
14
+ end
15
+ end
16
+ end
17
+ end
18
+
19
+ def notify_and_sleep(seconds, error_class)
20
+ message = "Throttled by AWS. Sleeping #{seconds} seconds, (#{error_class})"
21
+ notify_slack(subject: 'API Throttled',
22
+ message: message)
23
+ puts message
24
+ sleep seconds
25
+ end
26
+
27
+ def notify_nsq_can_not_be_drained
28
+ notify_slack(subject: 'NSQ not drained',
29
+ message: 'Unable to drain NSQ')
30
+ end
31
+
32
+ def notify_slack(subject:, message:)
33
+ run_with_backoff do
34
+ sns.publish(topic_arn: sns_slack_topic_arn,
35
+ subject: "#{subject} (#{instance_id}, #{my_region})",
36
+ message: message)
37
+ end
38
+ end
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,41 @@
1
+ module MovableInk
2
+ class AWS
3
+ module SSM
4
+ def ssm
5
+ @ssm_client ||= Aws::SSM::Client.new(region: 'us-east-1')
6
+ end
7
+
8
+ def get_secret(environment: mi_env, role:, attribute:)
9
+ run_with_backoff do
10
+ begin
11
+ resp = ssm.get_parameter(
12
+ name: "/#{environment}/#{role}/#{attribute}",
13
+ with_decryption: true
14
+ )
15
+ resp.parameter.value
16
+ rescue Aws::SSM::Errors::ParameterNotFound => e
17
+ nil
18
+ end
19
+ end
20
+ end
21
+
22
+ def get_role_secrets(environment: mi_env, role:)
23
+ path = "/#{environment}/#{role}"
24
+ run_with_backoff do
25
+ ssm.get_parameters_by_path(
26
+ path: path,
27
+ with_decryption: true
28
+ ).inject({}) do |secrets, resp|
29
+ extract_parameters(resp.parameters, path)
30
+ end
31
+ end
32
+ end
33
+
34
+ def extract_parameters(parameters, path)
35
+ parameters.map do |param|
36
+ [ param.name.gsub("#{path}/", ''), param.value ]
37
+ end.to_h
38
+ end
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,73 @@
1
+ require 'aws-sdk'
2
+
3
+ require_relative 'aws/errors'
4
+ require_relative 'aws/ec2'
5
+ require_relative 'aws/sns'
6
+ require_relative 'aws/autoscaling'
7
+ require_relative 'aws/route53'
8
+ require_relative 'aws/ssm'
9
+
10
+ module MovableInk
11
+ class AWS
12
+ include EC2
13
+ include SNS
14
+ include Autoscaling
15
+ include Route53
16
+ include SSM
17
+
18
+ class << self
19
+ def regions
20
+ {
21
+ 'iad' => 'us-east-1',
22
+ 'rld' => 'us-west-2',
23
+ 'dub' => 'eu-west-1',
24
+ 'ord' => 'us-east-2'
25
+ }
26
+ end
27
+ end
28
+
29
+ def initialize(environment: nil)
30
+ @mi_env = environment
31
+ end
32
+
33
+ def run_with_backoff
34
+ 9.times do |num|
35
+ begin
36
+ return yield
37
+ rescue Aws::EC2::Errors::RequestLimitExceeded,
38
+ Aws::SNS::Errors::ThrottledException,
39
+ Aws::AutoScaling::Errors::ThrottledException,
40
+ Aws::S3::Errors::SlowDown,
41
+ Aws::Route53::Errors::ThrottlingException,
42
+ Aws::SSM::Errors::TooManyUpdates
43
+ notify_and_sleep((num+1)**2 + rand(10), $!.class)
44
+ end
45
+ end
46
+ raise MovableInk::AWS::Errors::FailedWithBackoff
47
+ end
48
+
49
+ def regions
50
+ self.class.regions
51
+ end
52
+
53
+ def availability_zone
54
+ @availability_zone ||= `ec2metadata --availability-zone`.chomp rescue raise(MovableInk::AWS::Errors::EC2Required)
55
+ end
56
+
57
+ def my_region
58
+ @my_region ||= availability_zone.chop
59
+ end
60
+
61
+ def instance_id
62
+ @instance_id ||= `ec2metadata --instance-id`.chomp rescue raise(MovableInk::AWS::Errors::EC2Required)
63
+ end
64
+
65
+ def datacenter(region: my_region)
66
+ regions.key(region)
67
+ end
68
+
69
+ def s3
70
+ @s3_client ||= Aws::S3::Client.new(region: 'us-east-1')
71
+ end
72
+ end
73
+ end
@@ -0,0 +1,5 @@
1
+ module MovableInk
2
+ class AWS
3
+ VERSION = '0.1.1'
4
+ end
5
+ end
@@ -0,0 +1,56 @@
1
+ require 'aws-sdk'
2
+ require_relative '../lib/movable_ink/aws'
3
+
4
+ describe MovableInk::AWS::Autoscaling do
5
+ let(:aws) { MovableInk::AWS.new }
6
+ let(:autoscaling) { Aws::AutoScaling::Client.new(stub_responses: true) }
7
+ let(:ec2) { Aws::EC2::Client.new(stub_responses: true) }
8
+ let(:set_instance_health_data) { autoscaling.stub_data(:set_instance_health, {}) }
9
+ let(:complete_lifecycle_action_data) { autoscaling.stub_data(:complete_lifecycle_action, {}) }
10
+ let(:record_lifecycle_action_heartbeat_data) { autoscaling.stub_data(:record_lifecycle_action_heartbeat, {}) }
11
+ let(:create_tags_data) { ec2.stub_data(:create_tags, {}) }
12
+ let(:delete_tags_data) { ec2.stub_data(:delete_tags, {}) }
13
+
14
+ it "should mark an instance as unhealthy" do
15
+ autoscaling.stub_responses(:set_instance_health, set_instance_health_data)
16
+ allow(aws).to receive(:my_region).and_return('us-east-1')
17
+ allow(aws).to receive(:instance_id).and_return('i-12345')
18
+ allow(aws).to receive(:autoscaling).and_return(autoscaling)
19
+
20
+ expect(aws.mark_me_as_unhealthy).to eq(Aws::EmptyStructure.new)
21
+ end
22
+
23
+ it "should mark an instance as healthy" do
24
+ ec2.stub_responses(:create_tags, create_tags_data)
25
+ allow(aws).to receive(:my_region).and_return('us-east-1')
26
+ allow(aws).to receive(:instance_id).and_return('i-12345')
27
+ allow(aws).to receive(:ec2).and_return(ec2)
28
+
29
+ expect(aws.mark_me_as_healthy(role: 'some_role')).to eq(Aws::EmptyStructure.new)
30
+ end
31
+
32
+ it "should remove role tags" do
33
+ ec2.stub_responses(:delete_tags, delete_tags_data)
34
+ allow(aws).to receive(:my_region).and_return('us-east-1')
35
+ allow(aws).to receive(:instance_id).and_return('i-12345')
36
+ allow(aws).to receive(:ec2).and_return(ec2)
37
+
38
+ expect(aws.delete_role_tag(role: 'some_role')).to eq(Aws::EmptyStructure.new)
39
+ end
40
+
41
+ it "should complete lifecycle actions" do
42
+ autoscaling.stub_responses(:complete_lifecycle_action, complete_lifecycle_action_data)
43
+ allow(aws).to receive(:my_region).and_return('us-east-1')
44
+ allow(aws).to receive(:autoscaling).and_return(autoscaling)
45
+
46
+ expect(aws.complete_lifecycle_action(hook_name: 'hook', group_name: 'group', token: 'token')).to eq(Aws::EmptyStructure.new)
47
+ end
48
+
49
+ it "should record lifecycle action heartbeats" do
50
+ autoscaling.stub_responses(:record_lifecycle_action_heartbeat, record_lifecycle_action_heartbeat_data)
51
+ allow(aws).to receive(:my_region).and_return('us-east-1')
52
+ allow(aws).to receive(:autoscaling).and_return(autoscaling)
53
+
54
+ expect(aws.record_lifecycle_action_heartbeat(hook_name: 'hook', group_name: 'group')).to eq(Aws::EmptyStructure.new)
55
+ end
56
+ end
data/spec/aws_spec.rb ADDED
@@ -0,0 +1,54 @@
1
+ require 'aws-sdk'
2
+ require_relative '../lib/movable_ink/aws'
3
+
4
+ describe MovableInk::AWS do
5
+ context "outside EC2" do
6
+ it "should raise an error if EC2 is required" do
7
+ aws = MovableInk::AWS.new
8
+ expect{ aws.instance_id }.to raise_error(MovableInk::AWS::Errors::EC2Required)
9
+ expect{ aws.availability_zone }.to raise_error(MovableInk::AWS::Errors::EC2Required)
10
+ end
11
+ end
12
+
13
+ context "inside EC2" do
14
+ it "should call ec2metadata to get the instance ID" do
15
+ aws = MovableInk::AWS.new
16
+ expect(aws).to receive(:`).with('ec2metadata --instance-id').and_return('i-12345')
17
+ expect(aws.instance_id).to eq('i-12345')
18
+ end
19
+
20
+ it "should call ec2metadata to get the availability zone" do
21
+ aws = MovableInk::AWS.new
22
+ expect(aws).to receive(:`).with('ec2metadata --availability-zone').and_return("us-east-1a\n")
23
+ expect(aws.availability_zone).to eq('us-east-1a')
24
+ end
25
+
26
+ it "should find the datacenter by region" do
27
+ aws = MovableInk::AWS.new
28
+ expect(aws).to receive(:`).with('ec2metadata --availability-zone').and_return("us-east-1a\n")
29
+ expect(aws.datacenter).to eq('iad')
30
+ end
31
+
32
+ context "MovableInk::AWS#run_with_backoff" do
33
+ it "should retry when throttled with increasing timeouts" do
34
+ aws = MovableInk::AWS.new(environment: 'test')
35
+ ec2 = Aws::EC2::Client.new(stub_responses: true)
36
+ ec2.stub_responses(:describe_instances, 'RequestLimitExceeded')
37
+
38
+ expect(aws).to receive(:notify_slack).exactly(9).times
39
+ expect(aws).to receive(:sleep).exactly(9).times.and_return(true)
40
+
41
+ aws.run_with_backoff { ec2.describe_instances } rescue nil
42
+ end
43
+
44
+ it "should raise an error after too many timeouts" do
45
+ aws = MovableInk::AWS.new(environment: 'test')
46
+ ec2 = Aws::EC2::Client.new(stub_responses: true)
47
+ ec2.stub_responses(:describe_instances, 'RequestLimitExceeded')
48
+
49
+ expect(aws).to receive(:notify_and_sleep).exactly(9).times
50
+ expect{ aws.run_with_backoff { ec2.describe_instances } }.to raise_error(MovableInk::AWS::Errors::FailedWithBackoff)
51
+ end
52
+ end
53
+ end
54
+ end
data/spec/ec2_spec.rb ADDED
@@ -0,0 +1,223 @@
1
+ require 'aws-sdk'
2
+ require_relative '../lib/movable_ink/aws'
3
+
4
+ describe MovableInk::AWS::EC2 do
5
+ context "outside EC2" do
6
+ it "should raise an error if trying to load mi_env outside of EC2" do
7
+ aws = MovableInk::AWS.new
8
+ expect{ aws.mi_env }.to raise_error(MovableInk::AWS::Errors::EC2Required)
9
+ end
10
+
11
+ it "should use the provided environment" do
12
+ aws = MovableInk::AWS.new(environment: 'test')
13
+ expect(aws.mi_env).to eq('test')
14
+ end
15
+ end
16
+
17
+ context "inside EC2" do
18
+ let(:aws) { MovableInk::AWS.new }
19
+ let(:ec2) { Aws::EC2::Client.new(stub_responses: true) }
20
+ let(:tag_data) { ec2.stub_data(:describe_tags, tags: [
21
+ {
22
+ key: 'mi:env',
23
+ value: 'test'
24
+ }
25
+ ])
26
+ }
27
+
28
+ it "should find the environment from the current instance's tags" do
29
+ ec2.stub_responses(:describe_tags, tag_data)
30
+ allow(aws).to receive(:my_region).and_return('us-east-1')
31
+ allow(aws).to receive(:instance_id).and_return('i-12345')
32
+ allow(aws).to receive(:ec2).and_return(ec2)
33
+
34
+ expect(aws.mi_env).to eq('test')
35
+ end
36
+
37
+ context "thopter" do
38
+ let(:thopter_data) { ec2.stub_data(:describe_instances, reservations: [
39
+ instances: [
40
+ {
41
+ tags: [
42
+ {
43
+ key: 'mi:env',
44
+ value: 'test'
45
+ },
46
+ {
47
+ key: 'mi:roles',
48
+ value: 'thopter'
49
+ },
50
+ {
51
+ key: 'Name',
52
+ value: 'thopter'
53
+ }
54
+ ],
55
+ private_ip_address: '1.2.3.4'
56
+ }
57
+ ]])
58
+ }
59
+
60
+ it "should find the thopter instance" do
61
+ ec2.stub_responses(:describe_instances, thopter_data)
62
+ allow(aws).to receive(:mi_env).and_return('test')
63
+ allow(aws).to receive(:my_region).and_return('us-east-1')
64
+ allow(aws).to receive(:ec2).and_return(ec2)
65
+
66
+ expect(aws.thopter_instance.count).to eq(1)
67
+ end
68
+
69
+ it "should find the thopter instance's private IP" do
70
+ ec2.stub_responses(:describe_instances, thopter_data)
71
+ allow(aws).to receive(:mi_env).and_return('test')
72
+ allow(aws).to receive(:my_region).and_return('us-east-1')
73
+ allow(aws).to receive(:ec2).and_return(ec2)
74
+
75
+ expect(aws.thopter).to eq('1.2.3.4')
76
+ end
77
+ end
78
+
79
+ context "statsd" do
80
+ let(:availability_zone) { 'us-east-1a' }
81
+ let(:statsd_data) { ec2.stub_data(:describe_instances, reservations: [
82
+ instances: [
83
+ {
84
+ tags: [
85
+ {
86
+ key: 'mi:roles',
87
+ value: 'statsd'
88
+ }
89
+ ],
90
+ private_ip_address: '10.0.0.1',
91
+ placement: {
92
+ availability_zone: availability_zone
93
+ }
94
+ },
95
+ {
96
+ tags: [
97
+ {
98
+ key: 'mi:roles',
99
+ value: 'statsd'
100
+ }
101
+ ],
102
+ private_ip_address: '10.0.0.2',
103
+ placement: {
104
+ availability_zone: availability_zone
105
+ }
106
+ },
107
+ {
108
+ tags: [
109
+ {
110
+ key: 'mi:roles',
111
+ value: 'something_else'
112
+ }
113
+ ],
114
+ private_ip_address: '10.0.0.3',
115
+ placement: {
116
+ availability_zone: availability_zone
117
+ }
118
+ }
119
+ ]])
120
+ }
121
+
122
+ it "should find one of the statsd hosts" do
123
+ ec2.stub_responses(:describe_instances, statsd_data)
124
+ allow(aws).to receive(:mi_env).and_return('test')
125
+ allow(aws).to receive(:availability_zone).and_return(availability_zone)
126
+ allow(aws).to receive(:my_region).and_return('us-east-1')
127
+ allow(aws).to receive(:ec2).and_return(ec2)
128
+
129
+ expect(['10.0.0.1', '10.0.0.2']).to include(aws.statsd_host)
130
+ expect(['10.0.0.1', '10.0.0.2']).to include(aws.statsd_host)
131
+ end
132
+ end
133
+
134
+ context "ordered roles" do
135
+ let(:my_availability_zone) { 'us-east-1a' }
136
+ let(:other_availability_zone) { 'us-east-1b' }
137
+ let(:instance_data) { ec2.stub_data(:describe_instances, reservations: [
138
+ instances: [
139
+ {
140
+ tags: [
141
+ {
142
+ key: 'mi:roles',
143
+ value: 'app_db_replica'
144
+ }
145
+ ],
146
+ private_ip_address: '10.0.0.1',
147
+ placement: {
148
+ availability_zone: my_availability_zone
149
+ }
150
+ },
151
+ {
152
+ tags: [
153
+ {
154
+ key: 'mi:roles',
155
+ value: 'app_db_replica'
156
+ }
157
+ ],
158
+ private_ip_address: '10.0.0.2',
159
+ placement: {
160
+ availability_zone: other_availability_zone
161
+ }
162
+ }
163
+ ]])
164
+ }
165
+
166
+ it "should find hosts and order them with my AZ first" do
167
+ ec2.stub_responses(:describe_instances, instance_data)
168
+ allow(aws).to receive(:mi_env).and_return('test')
169
+ allow(aws).to receive(:availability_zone).and_return(my_availability_zone)
170
+ allow(aws).to receive(:my_region).and_return('us-east-1')
171
+ allow(aws).to receive(:ec2).and_return(ec2)
172
+
173
+ expect(aws.instance_ip_addresses_by_role_ordered(role: 'app_db_replica').count).to eq(2)
174
+ expect(aws.instance_ip_addresses_by_role_ordered(role: 'app_db_replica').first).to eq('10.0.0.1')
175
+ expect(aws.instance_ip_addresses_by_role_ordered(role: 'app_db_replica').last).to eq('10.0.0.2')
176
+ end
177
+ end
178
+
179
+ context "redii" do
180
+ let(:port) { 6379 }
181
+ let(:availability_zone) { 'us-east-1a' }
182
+ let(:redis_data) { ec2.stub_data(:describe_instances, reservations: [
183
+ instances: [
184
+ {
185
+ tags: [
186
+ {
187
+ key: 'mi:roles',
188
+ value: 'visitor_redis'
189
+ }
190
+ ],
191
+ private_ip_address: '10.0.0.1',
192
+ placement: {
193
+ availability_zone: availability_zone
194
+ }
195
+ },
196
+ {
197
+ tags: [
198
+ {
199
+ key: 'mi:roles',
200
+ value: 'visitor_redis'
201
+ }
202
+ ],
203
+ private_ip_address: '10.0.0.2',
204
+ placement: {
205
+ availability_zone: availability_zone
206
+ }
207
+ }
208
+ ]])
209
+ }
210
+ let(:redii) { [{"host" => '10.0.0.1', "port" => 6379},{"host" => '10.0.0.2', "port" => 6379}] }
211
+
212
+ it "should return redis IPs and ports" do
213
+ ec2.stub_responses(:describe_instances, redis_data)
214
+ allow(aws).to receive(:mi_env).and_return('test')
215
+ allow(aws).to receive(:availability_zone).and_return(availability_zone)
216
+ allow(aws).to receive(:my_region).and_return('us-east-1')
217
+ allow(aws).to receive(:ec2).and_return(ec2)
218
+
219
+ expect(aws.redis_by_role('visitor_redis', port)).to match_array(redii)
220
+ end
221
+ end
222
+ end
223
+ end
@@ -0,0 +1,115 @@
1
+ require 'aws-sdk'
2
+ require_relative '../lib/movable_ink/aws'
3
+
4
+ describe MovableInk::AWS::Route53 do
5
+ let(:aws) { MovableInk::AWS.new }
6
+
7
+ context "elastic IPs" do
8
+ let(:ec2) { Aws::EC2::Client.new(stub_responses: true) }
9
+ let(:s3) { Aws::S3::Client.new(stub_responses: true) }
10
+ let(:elastic_ip_data) { ec2.stub_data(:describe_addresses, addresses: [
11
+ {
12
+ allocation_id: "eipalloc-1",
13
+ association_id: "eipassoc-1",
14
+ domain: "vpc",
15
+ public_ip: "1.1.1.1"
16
+ },
17
+ {
18
+ allocation_id: "eipalloc-2",
19
+ association_id: "eipassoc-2",
20
+ domain: "vpc",
21
+ public_ip: "1.1.1.2"
22
+ },
23
+ {
24
+ allocation_id: "eipalloc-3",
25
+ association_id: nil,
26
+ domain: "vpc",
27
+ public_ip: "1.1.1.3"
28
+ }
29
+ ])
30
+ }
31
+ let(:associate_address_data) { ec2.stub_data(:associate_address, association_id: 'eipassoc-3') }
32
+ let(:reserved_ips) { StringIO.new(
33
+ "{\"datacenter\":\"iad\",\"allocation_id\":\"eipalloc-1\",\"public_ip\":\"1.1.1.1\",\"role\":\"some_role\"}
34
+ {\"datacenter\":\"iad\",\"allocation_id\":\"eipalloc-2\",\"public_ip\":\"1.1.1.2\",\"role\":\"some_role\"}
35
+ {\"datacenter\":\"iad\",\"allocation_id\":\"eipalloc-3\",\"public_ip\":\"1.1.1.3\",\"role\":\"some_role\"}"
36
+ )}
37
+ let(:s3_reserved_ips_data) { s3.stub_data(:get_object, body: reserved_ips) }
38
+
39
+ it "should find all elastic IPs" do
40
+ ec2.stub_responses(:describe_addresses, elastic_ip_data)
41
+ allow(aws).to receive(:my_region).and_return('us-east-1')
42
+ allow(aws).to receive(:instance_id).and_return('i-12345')
43
+ allow(aws).to receive(:ec2).and_return(ec2)
44
+
45
+ expect(aws.elastic_ips.count).to eq(3)
46
+ expect(aws.elastic_ips.first.public_ip).to eq('1.1.1.1')
47
+ expect(aws.elastic_ips.last.public_ip).to eq('1.1.1.3')
48
+ end
49
+
50
+ it "should find unassigned elastic IPs" do
51
+ ec2.stub_responses(:describe_addresses, elastic_ip_data)
52
+ allow(aws).to receive(:my_region).and_return('us-east-1')
53
+ allow(aws).to receive(:instance_id).and_return('i-12345')
54
+ allow(aws).to receive(:ec2).and_return(ec2)
55
+
56
+ expect(aws.unassigned_elastic_ips.count).to eq(1)
57
+ expect(aws.unassigned_elastic_ips.first.public_ip).to eq('1.1.1.3')
58
+ end
59
+
60
+ it "should load reserved elastic IPs from S3" do
61
+ s3.stub_responses(:get_object, s3_reserved_ips_data)
62
+ allow(aws).to receive(:my_region).and_return('us-east-1')
63
+ allow(aws).to receive(:instance_id).and_return('i-12345')
64
+ allow(aws).to receive(:s3).and_return(s3)
65
+
66
+ expect(aws.reserved_elastic_ips.count).to eq(3)
67
+ end
68
+
69
+ it "should filter reserved IPs to get available IPs" do
70
+ ec2.stub_responses(:describe_addresses, elastic_ip_data)
71
+ s3.stub_responses(:get_object, s3_reserved_ips_data)
72
+ allow(aws).to receive(:my_region).and_return('us-east-1')
73
+ allow(aws).to receive(:instance_id).and_return('i-12345')
74
+ allow(aws).to receive(:ec2).and_return(ec2)
75
+ allow(aws).to receive(:s3).and_return(s3)
76
+
77
+ expect(aws.available_elastic_ips(role: 'some_role').count).to eq(1)
78
+ expect(aws.available_elastic_ips(role: 'some_role').first['public_ip']).to eq('1.1.1.3')
79
+ end
80
+
81
+ it "should assign an elastic IP" do
82
+ ec2.stub_responses(:describe_addresses, elastic_ip_data)
83
+ ec2.stub_responses(:associate_address, associate_address_data)
84
+ s3.stub_responses(:get_object, s3_reserved_ips_data)
85
+ allow(aws).to receive(:my_region).and_return('us-east-1')
86
+ allow(aws).to receive(:instance_id).and_return('i-12345')
87
+ allow(aws).to receive(:ec2).and_return(ec2)
88
+ allow(aws).to receive(:s3).and_return(s3)
89
+
90
+ expect(aws.assign_ip_address(role: 'some_role').association_id).to eq('eipassoc-3')
91
+ end
92
+ end
93
+
94
+ context "resource record sets" do
95
+ let(:route53) { Aws::Route53::Client.new(stub_responses: true) }
96
+ let(:rrset_data) { route53.stub_data(:list_resource_record_sets, resource_record_sets: [
97
+ {
98
+ name: 'host1.domain.tld.'
99
+ },
100
+ {
101
+ name: 'host2.domain.tld.'
102
+ }
103
+ ])
104
+ }
105
+
106
+ it "should retrieve all rrsets for zone" do
107
+ route53.stub_responses(:list_resource_record_sets, rrset_data)
108
+ allow(aws).to receive(:route53).and_return(route53)
109
+
110
+ expect(aws.resource_record_sets('Z123').count).to eq(2)
111
+ expect(aws.resource_record_sets('Z123').first.name).to eq('host1.domain.tld.')
112
+ expect(aws.resource_record_sets('Z123').last.name).to eq('host2.domain.tld.')
113
+ end
114
+ end
115
+ end
data/spec/sns_spec.rb ADDED
@@ -0,0 +1,31 @@
1
+ require 'aws-sdk'
2
+ require_relative '../lib/movable_ink/aws'
3
+
4
+ describe MovableInk::AWS::SNS do
5
+ let(:aws) { MovableInk::AWS.new }
6
+ let(:sns) { Aws::SNS::Client.new(stub_responses: true) }
7
+ let(:topic_data) { sns.stub_data(:list_topics, topics: [
8
+ {
9
+ topic_arn: 'slack-aws-alerts'
10
+ }
11
+ ])
12
+ }
13
+ let(:publish_response) { sns.stub_data(:publish, message_id: 'messageId')}
14
+
15
+ it "should find the slack sns topic" do
16
+ sns.stub_responses(:list_topics, topic_data)
17
+ allow(aws).to receive(:my_region).and_return('us-east-1')
18
+ allow(aws).to receive(:sns).and_return(sns)
19
+
20
+ expect(aws.sns_slack_topic_arn).to eq('slack-aws-alerts')
21
+ end
22
+
23
+ it "should notify with the specified subject and message" do
24
+ sns.stub_responses(:list_topics, topic_data)
25
+ allow(aws).to receive(:my_region).and_return('us-east-1')
26
+ allow(aws).to receive(:instance_id).and_return('test instance')
27
+ allow(aws).to receive(:sns).and_return(sns)
28
+
29
+ expect(aws.notify_slack(subject: 'Test subject', message: 'Test message').message_id).to eq('messageId')
30
+ end
31
+ end
data/spec/ssm_spec.rb ADDED
@@ -0,0 +1,66 @@
1
+ require 'aws-sdk'
2
+ require_relative '../lib/movable_ink/aws'
3
+
4
+ describe MovableInk::AWS::SSM do
5
+ let(:aws) { MovableInk::AWS.new }
6
+ let(:ssm) { Aws::SSM::Client.new(stub_responses: true) }
7
+ let(:parameter) { ssm.stub_data(:get_parameter, parameter: {
8
+ name: '/test/sneakers/setec-astronomy',
9
+ type: 'SecureString',
10
+ value: 'too-many-secrets'
11
+ })
12
+ }
13
+ let(:parameters) { ssm.stub_data(:get_parameters_by_path, parameters: [
14
+ {
15
+ name: '/test/zelda/Its',
16
+ type: 'SecureString',
17
+ value: "It's"
18
+ },
19
+ {
20
+ name: '/test/zelda/a',
21
+ type: 'SecureString',
22
+ value: "dangerous"
23
+ },
24
+ {
25
+ name: '/test/zelda/secret',
26
+ type: 'SecureString',
27
+ value: "to"
28
+ },
29
+ {
30
+ name: '/test/zelda/to',
31
+ type: 'SecureString',
32
+ value: "go"
33
+ },
34
+ {
35
+ name: '/test/zelda/everyone',
36
+ type: 'SecureString',
37
+ value: "alone"
38
+ }
39
+ ])
40
+ }
41
+ let(:zelda_secrets) {
42
+ {
43
+ "Its" => "It's",
44
+ "a" => "dangerous",
45
+ "secret" => "to",
46
+ "to" => "go",
47
+ "everyone" => "alone"
48
+ }
49
+ }
50
+
51
+ it "should retrieve a decrypted secret" do
52
+ ssm.stub_responses(:get_parameter, parameter)
53
+ allow(aws).to receive(:mi_env).and_return('test')
54
+ allow(aws).to receive(:ssm).and_return(ssm)
55
+
56
+ expect(aws.get_secret(role: 'sneakers', attribute: 'setec-astronomy')).to eq('too-many-secrets')
57
+ end
58
+
59
+ it "should retrieve all secrets for a role" do
60
+ ssm.stub_responses(:get_parameters_by_path, parameters)
61
+ allow(aws).to receive(:mi_env).and_return('test')
62
+ allow(aws).to receive(:ssm).and_return(ssm)
63
+
64
+ expect(aws.get_role_secrets(role: 'zelda')).to eq(zelda_secrets)
65
+ end
66
+ end
metadata ADDED
@@ -0,0 +1,87 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: MovableInkAWS
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.1
5
+ platform: ruby
6
+ authors:
7
+ - Matt Chesler
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2018-02-05 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: aws-sdk
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '2.10'
20
+ - - ">="
21
+ - !ruby/object:Gem::Version
22
+ version: 2.10.0
23
+ type: :runtime
24
+ prerelease: false
25
+ version_requirements: !ruby/object:Gem::Requirement
26
+ requirements:
27
+ - - "~>"
28
+ - !ruby/object:Gem::Version
29
+ version: '2.10'
30
+ - - ">="
31
+ - !ruby/object:Gem::Version
32
+ version: 2.10.0
33
+ description: AWS Utility methods for MovableInk
34
+ email: mchesler@movableink.com
35
+ executables: []
36
+ extensions: []
37
+ extra_rdoc_files: []
38
+ files:
39
+ - ".travis.yml"
40
+ - Gemfile
41
+ - Gemfile.lock
42
+ - MovableInkAWS.gemspec
43
+ - README.md
44
+ - lib/movable_ink/aws.rb
45
+ - lib/movable_ink/aws/autoscaling.rb
46
+ - lib/movable_ink/aws/ec2.rb
47
+ - lib/movable_ink/aws/errors.rb
48
+ - lib/movable_ink/aws/route53.rb
49
+ - lib/movable_ink/aws/sns.rb
50
+ - lib/movable_ink/aws/ssm.rb
51
+ - lib/movable_ink/version.rb
52
+ - spec/autoscaling_spec.rb
53
+ - spec/aws_spec.rb
54
+ - spec/ec2_spec.rb
55
+ - spec/route53_spec.rb
56
+ - spec/sns_spec.rb
57
+ - spec/ssm_spec.rb
58
+ homepage:
59
+ licenses: []
60
+ metadata: {}
61
+ post_install_message:
62
+ rdoc_options: []
63
+ require_paths:
64
+ - lib
65
+ required_ruby_version: !ruby/object:Gem::Requirement
66
+ requirements:
67
+ - - ">="
68
+ - !ruby/object:Gem::Version
69
+ version: '0'
70
+ required_rubygems_version: !ruby/object:Gem::Requirement
71
+ requirements:
72
+ - - ">="
73
+ - !ruby/object:Gem::Version
74
+ version: '0'
75
+ requirements: []
76
+ rubyforge_project:
77
+ rubygems_version: 2.6.14
78
+ signing_key:
79
+ specification_version: 4
80
+ summary: AWS Utility methods for MovableInk
81
+ test_files:
82
+ - spec/autoscaling_spec.rb
83
+ - spec/aws_spec.rb
84
+ - spec/ec2_spec.rb
85
+ - spec/route53_spec.rb
86
+ - spec/sns_spec.rb
87
+ - spec/ssm_spec.rb