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