MovableInkAWS 0.1.1

Sign up to get free protection for your applications and to get access to all the features.
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