kontena-plugin-aws 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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