kontena-plugin-aws 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,54 @@
1
+ module Kontena::Machine::Aws
2
+ module Common
3
+
4
+ # @param [String] region
5
+ # @return String
6
+ def resolve_ami(region)
7
+ response = Excon.get("https://coreos.com/dist/aws/aws-stable.json")
8
+ images = JSON.parse(response.body)
9
+ info = images[region]
10
+ if info
11
+ info['hvm']
12
+ else
13
+ nil
14
+ end
15
+ end
16
+
17
+ # @param [String] vpc_id
18
+ # @param [String] zone
19
+ # @return [Aws::EC2::Types::Subnet, NilClass]
20
+ def default_subnet(vpc_id, zone)
21
+ ec2.subnets({
22
+ filters: [
23
+ {name: "vpc-id", values: [vpc_id]},
24
+ {name: "availability-zone", values: [zone]}
25
+ ]
26
+ }).first
27
+ end
28
+
29
+ # @return [Aws::EC2::Types::Vpc, NilClass]
30
+ def default_vpc
31
+ ec2.vpcs({filters: [{name: "is-default", values: ["true"]}]}).first
32
+ end
33
+
34
+
35
+
36
+ ##
37
+ # Resolves givne list of group names into group ids
38
+ # @param [String] comma separated list of group names
39
+ # @return [Array]
40
+ def resolve_security_groups_to_ids(group_list, vpc_id)
41
+ ids = group_list.split(',').map { |group|
42
+ sg = ec2.security_groups({
43
+ filters: [
44
+ {name: 'group-name', values: [group]},
45
+ {name: 'vpc-id', values: [vpc_id]}
46
+ ]
47
+ }).first
48
+
49
+ sg ? sg.group_id : nil
50
+ }
51
+ ids.compact
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,183 @@
1
+ require 'fileutils'
2
+ require 'erb'
3
+ require 'open3'
4
+ require 'shell-spinner'
5
+ require_relative 'common'
6
+
7
+ module Kontena::Machine::Aws
8
+ class MasterProvisioner
9
+ include Kontena::Machine::RandomName
10
+ include Kontena::Machine::CertHelper
11
+ include Common
12
+ attr_reader :ec2, :http_client, :region
13
+
14
+ # @param [String] access_key_id aws_access_key_id
15
+ # @param [String] secret_key aws_secret_access_key
16
+ # @param [String] region
17
+ def initialize(access_key_id, secret_key, region)
18
+ @ec2 = ::Aws::EC2::Resource.new(
19
+ region: region, credentials: ::Aws::Credentials.new(access_key_id, secret_key)
20
+ )
21
+ end
22
+
23
+ # @param [Hash] opts
24
+ def run!(opts)
25
+ ssl_cert = nil
26
+ if opts[:ssl_cert]
27
+ abort('Invalid ssl cert') unless File.exists?(File.expand_path(opts[:ssl_cert]))
28
+ ssl_cert = File.read(File.expand_path(opts[:ssl_cert]))
29
+ else
30
+ ShellSpinner "Generating self-signed SSL certificate" do
31
+ ssl_cert = generate_self_signed_cert
32
+ end
33
+ end
34
+
35
+ ami = resolve_ami(region)
36
+ abort('No valid AMI found for region') unless ami
37
+ opts[:vpc] = default_vpc.vpc_id unless opts[:vpc]
38
+ if opts[:subnet].nil?
39
+ subnet = default_subnet(opts[:vpc], region+opts[:zone])
40
+ else
41
+ subnet = ec2.subnet(opts[:subnet])
42
+ end
43
+ abort('Failed to find subnet!') unless subnet
44
+ userdata_vars = {
45
+ ssl_cert: ssl_cert,
46
+ auth_server: opts[:auth_server],
47
+ version: opts[:version],
48
+ vault_secret: opts[:vault_secret],
49
+ vault_iv: opts[:vault_iv],
50
+ mongodb_uri: opts[:mongodb_uri]
51
+ }
52
+
53
+ security_groups = opts[:security_groups] ?
54
+ resolve_security_groups_to_ids(opts[:security_groups], opts[:vpc]) :
55
+ ensure_security_group(opts[:vpc])
56
+
57
+ name = generate_name
58
+ ec2_instance = ec2.create_instances({
59
+ image_id: ami,
60
+ min_count: 1,
61
+ max_count: 1,
62
+ instance_type: opts[:type],
63
+ key_name: opts[:key_pair],
64
+ user_data: Base64.encode64(user_data(userdata_vars)),
65
+ block_device_mappings: [
66
+ {
67
+ device_name: '/dev/xvda',
68
+ virtual_name: 'Root',
69
+ ebs: {
70
+ volume_size: opts[:storage],
71
+ volume_type: 'gp2'
72
+ }
73
+ }
74
+ ],
75
+ network_interfaces: [
76
+ {
77
+ device_index: 0,
78
+ subnet_id: subnet.subnet_id,
79
+ groups: security_groups,
80
+ associate_public_ip_address: opts[:associate_public_ip],
81
+ delete_on_termination: true
82
+ }
83
+ ]
84
+ }).first
85
+ ec2_instance.create_tags({
86
+ tags: [
87
+ {key: 'Name', value: name}
88
+ ]
89
+ })
90
+
91
+ ShellSpinner "Creating AWS instance #{name.colorize(:cyan)} " do
92
+ sleep 5 until ec2_instance.reload.state.name == 'running'
93
+ end
94
+ public_ip = ec2_instance.reload.public_ip_address
95
+ if public_ip.nil?
96
+ master_url = "https://#{ec2_instance.private_ip_address}"
97
+ puts "Could not get public IP for the created master, private connect url is: #{master_url}"
98
+ else
99
+ master_url = "https://#{ec2_instance.public_ip_address}"
100
+ Excon.defaults[:ssl_verify_peer] = false
101
+ http_client = Excon.new(master_url, :connect_timeout => 10)
102
+ ShellSpinner "Waiting for #{name.colorize(:cyan)} to start " do
103
+ sleep 5 until master_running?(http_client)
104
+ end
105
+ end
106
+
107
+ puts "Kontena Master is now running at #{master_url}"
108
+ puts "Use #{"kontena login --name=#{name.sub('kontena-master-', '')} #{master_url}".colorize(:light_black)} to complete Kontena Master setup"
109
+ end
110
+
111
+ ##
112
+ # @param [String] vpc_id
113
+ # @return [Array] Security group id in array
114
+ def ensure_security_group(vpc_id)
115
+ group_name = "kontena_master"
116
+ group_id = resolve_security_groups_to_ids(group_name, vpc_id)
117
+
118
+ if group_id.empty?
119
+ ShellSpinner "Creating AWS security group" do
120
+ sg = create_security_group(group_name, vpc_id)
121
+ group_id = [sg.group_id]
122
+ end
123
+ end
124
+ group_id
125
+ end
126
+
127
+ ##
128
+ # creates security_group and authorizes default port ranges
129
+ #
130
+ # @param [String] name
131
+ # @param [String, NilClass] vpc_id
132
+ # @return Aws::EC2::SecurityGroup
133
+ def create_security_group(name, vpc_id = nil)
134
+ sg = ec2.create_security_group({
135
+ group_name: name,
136
+ description: "Kontena Master",
137
+ vpc_id: vpc_id
138
+ })
139
+
140
+ sg.authorize_ingress({
141
+ ip_protocol: 'tcp',
142
+ from_port: 443,
143
+ to_port: 443,
144
+ cidr_ip: '0.0.0.0/0'
145
+ })
146
+
147
+ sg.authorize_ingress({
148
+ ip_protocol: 'tcp',
149
+ from_port: 22,
150
+ to_port: 22,
151
+ cidr_ip: '0.0.0.0/0'
152
+ })
153
+
154
+ sg
155
+ end
156
+
157
+ # @return [String]
158
+ def region
159
+ ec2.client.config.region
160
+ end
161
+
162
+ def user_data(vars)
163
+ cloudinit_template = File.join(__dir__ , '/cloudinit_master.yml')
164
+ erb(File.read(cloudinit_template), vars)
165
+ end
166
+
167
+ def generate_name
168
+ "kontena-master-#{super}-#{rand(1..9)}"
169
+ end
170
+
171
+ def master_running?(http_client)
172
+ http_client.get(path: '/').status == 200
173
+ rescue
174
+ false
175
+ end
176
+
177
+ def erb(template, vars)
178
+ ERB.new(template, nil, '%<>-').result(
179
+ OpenStruct.new(vars).instance_eval { binding }
180
+ )
181
+ end
182
+ end
183
+ end
@@ -0,0 +1,51 @@
1
+ require 'shell-spinner'
2
+
3
+ module Kontena
4
+ module Machine
5
+ module Aws
6
+ class NodeDestroyer
7
+
8
+ attr_reader :ec2, :api_client
9
+
10
+ # @param [Kontena::Client] api_client Kontena api client
11
+ # @param [String] access_key_id aws_access_key_id
12
+ # @param [String] secret_key aws_secret_access_key
13
+ # @param [String] region
14
+ def initialize(api_client, access_key_id, secret_key, region = 'eu-west-1')
15
+ @api_client = api_client
16
+ @ec2 = ::Aws::EC2::Resource.new(
17
+ region: region,
18
+ credentials: ::Aws::Credentials.new(access_key_id, secret_key)
19
+ )
20
+ end
21
+
22
+ def run!(grid, name)
23
+ instances = ec2.instances({
24
+ filters: [
25
+ {name: 'tag:Name', values: [name]}
26
+ ]
27
+ })
28
+ abort("Cannot find AWS instance #{name}") if instances.to_a.size == 0
29
+ abort("There are multiple instances with name #{name}") if instances.to_a.size > 1
30
+ instance = instances.first
31
+ if instance
32
+ ShellSpinner "Terminating AWS instance #{name.colorize(:cyan)} " do
33
+ instance.terminate
34
+ until instance.reload.state.name.to_s == 'terminated'
35
+ sleep 2
36
+ end
37
+ end
38
+ else
39
+ abort "Cannot find instance #{name.colorize(:cyan)} in AWS"
40
+ end
41
+ node = api_client.get("grids/#{grid['id']}/nodes")['nodes'].find{|n| n['name'] == name}
42
+ if node
43
+ ShellSpinner "Removing node #{name.colorize(:cyan)} from grid #{grid['name'].colorize(:cyan)} " do
44
+ api_client.delete("grids/#{grid['id']}/nodes/#{name}")
45
+ end
46
+ end
47
+ end
48
+ end
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,200 @@
1
+ require 'fileutils'
2
+ require 'erb'
3
+ require 'open3'
4
+ require 'shell-spinner'
5
+ require_relative 'common'
6
+
7
+ module Kontena::Machine::Aws
8
+ class NodeProvisioner
9
+ include Kontena::Machine::RandomName
10
+ include Common
11
+
12
+ attr_reader :ec2, :api_client
13
+
14
+ # @param [Kontena::Client] api_client Kontena api client
15
+ # @param [String] access_key_id aws_access_key_id
16
+ # @param [String] secret_key aws_secret_access_key
17
+ # @param [String] region
18
+ def initialize(api_client, access_key_id, secret_key, region)
19
+ @api_client = api_client
20
+ @ec2 = ::Aws::EC2::Resource.new(
21
+ region: region, credentials: ::Aws::Credentials.new(access_key_id, secret_key)
22
+ )
23
+ end
24
+
25
+ # @param [Hash] opts
26
+ def run!(opts)
27
+ ami = resolve_ami(region)
28
+ abort('No valid AMI found for region') unless ami
29
+
30
+ opts[:vpc] = default_vpc.vpc_id unless opts[:vpc]
31
+
32
+ security_groups = opts[:security_groups] ?
33
+ resolve_security_groups_to_ids(opts[:security_groups], opts[:vpc]) :
34
+ ensure_security_group(opts[:grid], opts[:vpc])
35
+
36
+ name = opts[:name ] || generate_name
37
+
38
+ if opts[:subnet].nil?
39
+ subnet = default_subnet(opts[:vpc], region+opts[:zone])
40
+ else
41
+ subnet = ec2.subnet(opts[:subnet])
42
+ end
43
+ dns_server = aws_dns_supported?(opts[:vpc]) ? '169.254.169.253' : '8.8.8.8'
44
+ userdata_vars = {
45
+ name: name,
46
+ version: opts[:version],
47
+ master_uri: opts[:master_uri],
48
+ grid_token: opts[:grid_token],
49
+ dns_server: dns_server
50
+ }
51
+
52
+ ec2_instance = ec2.create_instances({
53
+ image_id: ami,
54
+ min_count: 1,
55
+ max_count: 1,
56
+ instance_type: opts[:type],
57
+ key_name: opts[:key_pair],
58
+ user_data: Base64.encode64(user_data(userdata_vars)),
59
+ block_device_mappings: [
60
+ {
61
+ device_name: '/dev/xvda',
62
+ virtual_name: 'Root',
63
+ ebs: {
64
+ volume_size: opts[:storage],
65
+ volume_type: 'gp2'
66
+ }
67
+ }
68
+ ],
69
+ network_interfaces: [
70
+ {
71
+ device_index: 0,
72
+ subnet_id: subnet.subnet_id,
73
+ groups: security_groups,
74
+ associate_public_ip_address: opts[:associate_public_ip],
75
+ delete_on_termination: true
76
+ }
77
+ ]
78
+ }).first
79
+ ec2_instance.create_tags({
80
+ tags: [
81
+ {key: 'Name', value: name},
82
+ {key: 'kontena_grid', value: opts[:grid]}
83
+ ]
84
+ })
85
+
86
+ ShellSpinner "Creating AWS instance #{name.colorize(:cyan)} " do
87
+ sleep 5 until ec2_instance.reload.state.name == 'running'
88
+ end
89
+ node = nil
90
+ ShellSpinner "Waiting for node #{name.colorize(:cyan)} join to grid #{opts[:grid].colorize(:cyan)} " do
91
+ sleep 2 until node = instance_exists_in_grid?(opts[:grid], name)
92
+ end
93
+ labels = [
94
+ "region=#{region}",
95
+ "az=#{opts[:zone]}",
96
+ "provider=aws"
97
+ ]
98
+ set_labels(node, labels)
99
+ end
100
+
101
+ ##
102
+ # @param [String] grid
103
+ # @return [Array] Security group id in array
104
+ def ensure_security_group(grid, vpc_id)
105
+ group_name = "kontena_grid_#{grid}"
106
+ group_id = resolve_security_groups_to_ids(group_name, vpc_id)
107
+
108
+ if group_id.empty?
109
+ ShellSpinner "Creating AWS security group" do
110
+ sg = create_security_group(group_name, vpc_id)
111
+ group_id = [sg.group_id]
112
+ end
113
+ end
114
+ group_id
115
+ end
116
+
117
+ ##
118
+ # creates security_group and authorizes default port ranges
119
+ #
120
+ # @param [String] name
121
+ # @param [String] vpc_id
122
+ # @return [Aws::EC2::SecurityGroup]
123
+ def create_security_group(name, vpc_id)
124
+ sg = ec2.create_security_group({
125
+ group_name: name,
126
+ description: "Kontena Grid",
127
+ vpc_id: vpc_id
128
+ })
129
+
130
+ sg.authorize_ingress({ # SSH
131
+ ip_protocol: 'tcp', from_port: 22, to_port: 22, cidr_ip: '0.0.0.0/0'
132
+ })
133
+ sg.authorize_ingress({ # HTTP
134
+ ip_protocol: 'tcp', from_port: 80, to_port: 80, cidr_ip: '0.0.0.0/0'
135
+ })
136
+ sg.authorize_ingress({ # HTTPS
137
+ ip_protocol: 'tcp', from_port: 443, to_port: 443, cidr_ip: '0.0.0.0/0'
138
+ })
139
+ sg.authorize_ingress({ # OpenVPN
140
+ ip_protocol: 'udp', from_port: 1194, to_port: 1194, cidr_ip: '0.0.0.0/0'
141
+ })
142
+ sg.authorize_ingress({ # Overlay / Weave network
143
+ ip_permissions: [
144
+ {
145
+ from_port: 6783, to_port: 6783, ip_protocol: 'tcp',
146
+ user_id_group_pairs: [
147
+ { group_id: sg.group_id, vpc_id: vpc_id }
148
+ ]
149
+ },
150
+ {
151
+ from_port: 6783, to_port: 6784, ip_protocol: 'udp',
152
+ user_id_group_pairs: [
153
+ { group_id: sg.group_id, vpc_id: vpc_id }
154
+ ]
155
+ }
156
+ ]
157
+ })
158
+
159
+ sg
160
+ end
161
+
162
+ # @return [String]
163
+ def region
164
+ ec2.client.config.region
165
+ end
166
+
167
+ def user_data(vars)
168
+ cloudinit_template = File.join(__dir__ , '/cloudinit.yml')
169
+ erb(File.read(cloudinit_template), vars)
170
+ end
171
+
172
+ def generate_name
173
+ "#{super}-#{rand(1..99)}"
174
+ end
175
+
176
+ def instance_exists_in_grid?(grid, name)
177
+ api_client.get("grids/#{grid}/nodes")['nodes'].find{|n| n['name'] == name}
178
+ end
179
+
180
+ def erb(template, vars)
181
+ ERB.new(template).result(OpenStruct.new(vars).instance_eval { binding })
182
+ end
183
+
184
+ # @param [Hash] node
185
+ # @param [Array<String>] labels
186
+ def set_labels(node, labels)
187
+ data = {}
188
+ data[:labels] = labels
189
+ api_client.put("nodes/#{node['id']}", data, {}, {'Kontena-Grid-Token' => node['grid']['token']})
190
+ end
191
+
192
+ # @param [String] vpc_id
193
+ # @return [Boolean]
194
+ def aws_dns_supported?(vpc_id)
195
+ vpc = ec2.vpc(vpc_id)
196
+ response = vpc.describe_attribute({attribute: 'enableDnsSupport'})
197
+ response.enable_dns_support
198
+ end
199
+ end
200
+ end