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.
- checksums.yaml +15 -0
- data/.gitignore +9 -0
- data/.gitmodules +0 -0
- data/.rspec +2 -0
- data/.travis.yml +16 -0
- data/Gemfile +8 -0
- data/LICENSE.txt +191 -0
- data/README.md +16 -0
- data/kontena-plugin-aws.gemspec +24 -0
- data/lib/kontena/machine/aws.rb +13 -0
- data/lib/kontena/machine/aws/cloudinit.yml +73 -0
- data/lib/kontena/machine/aws/cloudinit_master.yml +120 -0
- data/lib/kontena/machine/aws/common.rb +54 -0
- data/lib/kontena/machine/aws/master_provisioner.rb +183 -0
- data/lib/kontena/machine/aws/node_destroyer.rb +51 -0
- data/lib/kontena/machine/aws/node_provisioner.rb +200 -0
- data/lib/kontena/machine/aws/node_restarter.rb +37 -0
- data/lib/kontena/plugin/aws.rb +7 -0
- data/lib/kontena/plugin/aws/master/create_command.rb +52 -0
- data/lib/kontena/plugin/aws/master_command.rb +9 -0
- data/lib/kontena/plugin/aws/node_command.rb +13 -0
- data/lib/kontena/plugin/aws/nodes/create_command.rb +59 -0
- data/lib/kontena/plugin/aws/nodes/restart_command.rb +25 -0
- data/lib/kontena/plugin/aws/nodes/terminate_command.rb +25 -0
- data/lib/kontena/plugin/aws_command.rb +11 -0
- data/lib/kontena_cli_plugin.rb +5 -0
- metadata +131 -0
@@ -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
|