hetzner-k3s 0.4.9 → 0.5.3
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 +4 -4
- data/.rubocop.yml +121 -0
- data/Dockerfile +5 -2
- data/Gemfile +5 -3
- data/Gemfile.lock +24 -3
- data/README.md +7 -85
- data/Rakefile +5 -3
- data/bin/build.sh +3 -3
- data/bin/{console → console.sh} +3 -3
- data/bin/{setup → setup.sh} +0 -0
- data/hetzner-k3s.gemspec +25 -21
- data/lib/hetzner/infra/client.rb +17 -15
- data/lib/hetzner/infra/firewall.rb +92 -90
- data/lib/hetzner/infra/load_balancer.rb +62 -59
- data/lib/hetzner/infra/network.rb +31 -30
- data/lib/hetzner/infra/placement_group.rb +25 -21
- data/lib/hetzner/infra/server.rb +37 -31
- data/lib/hetzner/infra/ssh_key.rb +34 -35
- data/lib/hetzner/infra.rb +5 -1
- data/lib/hetzner/k3s/cli.rb +279 -262
- data/lib/hetzner/k3s/cluster.rb +448 -397
- data/lib/hetzner/k3s/version.rb +3 -1
- data/lib/hetzner/utils.rb +42 -38
- data/lib/hetzner.rb +2 -0
- metadata +26 -10
data/lib/hetzner/k3s/cluster.rb
CHANGED
|
@@ -1,20 +1,20 @@
|
|
|
1
|
-
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
2
3
|
require 'net/ssh'
|
|
3
|
-
require
|
|
4
|
-
require
|
|
4
|
+
require 'securerandom'
|
|
5
|
+
require 'base64'
|
|
5
6
|
require 'timeout'
|
|
6
|
-
require
|
|
7
|
-
|
|
8
|
-
require_relative "../infra/client"
|
|
9
|
-
require_relative "../infra/firewall"
|
|
10
|
-
require_relative "../infra/network"
|
|
11
|
-
require_relative "../infra/ssh_key"
|
|
12
|
-
require_relative "../infra/server"
|
|
13
|
-
require_relative "../infra/load_balancer"
|
|
14
|
-
require_relative "../infra/placement_group"
|
|
7
|
+
require 'subprocess'
|
|
15
8
|
|
|
16
|
-
require_relative
|
|
9
|
+
require_relative '../infra/client'
|
|
10
|
+
require_relative '../infra/firewall'
|
|
11
|
+
require_relative '../infra/network'
|
|
12
|
+
require_relative '../infra/ssh_key'
|
|
13
|
+
require_relative '../infra/server'
|
|
14
|
+
require_relative '../infra/load_balancer'
|
|
15
|
+
require_relative '../infra/placement_group'
|
|
17
16
|
|
|
17
|
+
require_relative '../utils'
|
|
18
18
|
|
|
19
19
|
class Cluster
|
|
20
20
|
include Utils
|
|
@@ -26,18 +26,19 @@ class Cluster
|
|
|
26
26
|
|
|
27
27
|
def create(configuration:)
|
|
28
28
|
@configuration = configuration
|
|
29
|
-
@cluster_name = configuration
|
|
30
|
-
@kubeconfig_path = File.expand_path(configuration
|
|
31
|
-
@public_ssh_key_path = File.expand_path(configuration
|
|
32
|
-
private_ssh_key_path = configuration
|
|
33
|
-
@private_ssh_key_path = File.expand_path(private_ssh_key_path)
|
|
34
|
-
@k3s_version = configuration
|
|
35
|
-
@masters_config = configuration
|
|
29
|
+
@cluster_name = configuration['cluster_name']
|
|
30
|
+
@kubeconfig_path = File.expand_path(configuration['kubeconfig_path'])
|
|
31
|
+
@public_ssh_key_path = File.expand_path(configuration['public_ssh_key_path'])
|
|
32
|
+
private_ssh_key_path = configuration['private_ssh_key_path']
|
|
33
|
+
@private_ssh_key_path = private_ssh_key_path && File.expand_path(private_ssh_key_path)
|
|
34
|
+
@k3s_version = configuration['k3s_version']
|
|
35
|
+
@masters_config = configuration['masters']
|
|
36
36
|
@worker_node_pools = find_worker_node_pools(configuration)
|
|
37
|
-
@location = configuration
|
|
38
|
-
@verify_host_key = configuration.fetch(
|
|
37
|
+
@location = configuration['location']
|
|
38
|
+
@verify_host_key = configuration.fetch('verify_host_key', false)
|
|
39
39
|
@servers = []
|
|
40
|
-
@networks = configuration
|
|
40
|
+
@networks = configuration['ssh_allowed_networks']
|
|
41
|
+
@enable_encryption = configuration.fetch('enable_encryption', false)
|
|
41
42
|
|
|
42
43
|
create_resources
|
|
43
44
|
|
|
@@ -51,17 +52,20 @@ class Cluster
|
|
|
51
52
|
end
|
|
52
53
|
|
|
53
54
|
def delete(configuration:)
|
|
54
|
-
@
|
|
55
|
-
@
|
|
56
|
-
@
|
|
55
|
+
@configuration = configuration
|
|
56
|
+
@cluster_name = configuration['cluster_name']
|
|
57
|
+
@kubeconfig_path = File.expand_path(configuration['kubeconfig_path'])
|
|
58
|
+
@public_ssh_key_path = File.expand_path(configuration['public_ssh_key_path'])
|
|
59
|
+
@masters_config = configuration['masters']
|
|
60
|
+
@worker_node_pools = find_worker_node_pools(configuration)
|
|
57
61
|
|
|
58
62
|
delete_resources
|
|
59
63
|
end
|
|
60
64
|
|
|
61
65
|
def upgrade(configuration:, new_k3s_version:, config_file:)
|
|
62
66
|
@configuration = configuration
|
|
63
|
-
@cluster_name = configuration
|
|
64
|
-
@kubeconfig_path = File.expand_path(configuration
|
|
67
|
+
@cluster_name = configuration['cluster_name']
|
|
68
|
+
@kubeconfig_path = File.expand_path(configuration['kubeconfig_path'])
|
|
65
69
|
@new_k3s_version = new_k3s_version
|
|
66
70
|
@config_file = config_file
|
|
67
71
|
|
|
@@ -70,463 +74,510 @@ class Cluster
|
|
|
70
74
|
|
|
71
75
|
private
|
|
72
76
|
|
|
73
|
-
|
|
74
|
-
configuration.fetch("worker_node_pools", [])
|
|
75
|
-
end
|
|
77
|
+
attr_accessor :servers
|
|
76
78
|
|
|
77
|
-
|
|
79
|
+
attr_reader :hetzner_client, :cluster_name, :kubeconfig_path, :k3s_version,
|
|
80
|
+
:masters_config, :worker_node_pools,
|
|
81
|
+
:location, :public_ssh_key_path,
|
|
82
|
+
:hetzner_token, :new_k3s_version, :configuration,
|
|
83
|
+
:config_file, :verify_host_key, :networks, :private_ssh_key_path,
|
|
84
|
+
:enable_encryption
|
|
78
85
|
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
:hetzner_token, :tls_sans, :new_k3s_version, :configuration,
|
|
83
|
-
:config_file, :verify_host_key, :networks, :private_ssh_key_path, :configuration
|
|
86
|
+
def find_worker_node_pools(configuration)
|
|
87
|
+
configuration.fetch('worker_node_pools', [])
|
|
88
|
+
end
|
|
84
89
|
|
|
90
|
+
def latest_k3s_version
|
|
91
|
+
response = HTTP.get('https://api.github.com/repos/k3s-io/k3s/tags').body
|
|
92
|
+
JSON.parse(response).first['name']
|
|
93
|
+
end
|
|
85
94
|
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
95
|
+
def create_resources
|
|
96
|
+
create_servers
|
|
97
|
+
create_load_balancer if masters.size > 1
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def delete_placement_groups
|
|
101
|
+
Hetzner::PlacementGroup.new(hetzner_client:, cluster_name:).delete
|
|
102
|
+
|
|
103
|
+
worker_node_pools.each do |pool|
|
|
104
|
+
pool_name = pool['name']
|
|
105
|
+
Hetzner::PlacementGroup.new(hetzner_client:, cluster_name:, pool_name:).delete
|
|
89
106
|
end
|
|
107
|
+
end
|
|
90
108
|
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
masters_count = masters_config["instance_count"]
|
|
94
|
-
|
|
95
|
-
placement_group_id = Hetzner::PlacementGroup.new(
|
|
96
|
-
hetzner_client: hetzner_client,
|
|
97
|
-
cluster_name: cluster_name
|
|
98
|
-
).create
|
|
99
|
-
|
|
100
|
-
firewall_id = Hetzner::Firewall.new(
|
|
101
|
-
hetzner_client: hetzner_client,
|
|
102
|
-
cluster_name: cluster_name
|
|
103
|
-
).create(ha: (masters_count > 1), networks: networks)
|
|
104
|
-
|
|
105
|
-
network_id = Hetzner::Network.new(
|
|
106
|
-
hetzner_client: hetzner_client,
|
|
107
|
-
cluster_name: cluster_name
|
|
108
|
-
).create(location: location)
|
|
109
|
-
|
|
110
|
-
ssh_key_id = Hetzner::SSHKey.new(
|
|
111
|
-
hetzner_client: hetzner_client,
|
|
112
|
-
cluster_name: cluster_name
|
|
113
|
-
).create(public_ssh_key_path: public_ssh_key_path)
|
|
114
|
-
|
|
115
|
-
server_configs = []
|
|
116
|
-
|
|
117
|
-
masters_count.times do |i|
|
|
118
|
-
server_configs << {
|
|
119
|
-
location: location,
|
|
120
|
-
instance_type: master_instance_type,
|
|
121
|
-
instance_id: "master#{i+1}",
|
|
122
|
-
firewall_id: firewall_id,
|
|
123
|
-
network_id: network_id,
|
|
124
|
-
ssh_key_id: ssh_key_id,
|
|
125
|
-
placement_group_id: placement_group_id,
|
|
126
|
-
image: image
|
|
127
|
-
}
|
|
128
|
-
end
|
|
109
|
+
def delete_resources
|
|
110
|
+
Hetzner::LoadBalancer.new(hetzner_client:, cluster_name:).delete(high_availability: (masters.size > 1))
|
|
129
111
|
|
|
130
|
-
|
|
131
|
-
Hetzner::LoadBalancer.new(
|
|
132
|
-
hetzner_client: hetzner_client,
|
|
133
|
-
cluster_name: cluster_name
|
|
134
|
-
).create(location: location, network_id: network_id)
|
|
135
|
-
end
|
|
112
|
+
Hetzner::Firewall.new(hetzner_client:, cluster_name:).delete(all_servers)
|
|
136
113
|
|
|
137
|
-
|
|
138
|
-
worker_node_pool_name = worker_node_pool["name"]
|
|
139
|
-
worker_instance_type = worker_node_pool["instance_type"]
|
|
140
|
-
worker_count = worker_node_pool["instance_count"]
|
|
141
|
-
|
|
142
|
-
worker_count.times do |i|
|
|
143
|
-
server_configs << {
|
|
144
|
-
location: location,
|
|
145
|
-
instance_type: worker_instance_type,
|
|
146
|
-
instance_id: "pool-#{worker_node_pool_name}-worker#{i+1}",
|
|
147
|
-
firewall_id: firewall_id,
|
|
148
|
-
network_id: network_id,
|
|
149
|
-
ssh_key_id: ssh_key_id,
|
|
150
|
-
placement_group_id: placement_group_id,
|
|
151
|
-
image: image
|
|
152
|
-
}
|
|
153
|
-
end
|
|
154
|
-
end
|
|
114
|
+
Hetzner::Network.new(hetzner_client:, cluster_name:).delete
|
|
155
115
|
|
|
156
|
-
|
|
157
|
-
Thread.new do
|
|
158
|
-
servers << Hetzner::Server.new(hetzner_client: hetzner_client, cluster_name: cluster_name).create(**server_config)
|
|
159
|
-
end
|
|
160
|
-
end
|
|
116
|
+
Hetzner::SSHKey.new(hetzner_client:, cluster_name:).delete(public_ssh_key_path:)
|
|
161
117
|
|
|
162
|
-
|
|
118
|
+
delete_placement_groups
|
|
119
|
+
delete_servers
|
|
120
|
+
end
|
|
163
121
|
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
122
|
+
def upgrade_cluster
|
|
123
|
+
worker_upgrade_concurrency = workers.size - 1
|
|
124
|
+
worker_upgrade_concurrency = 1 if worker_upgrade_concurrency.zero?
|
|
125
|
+
|
|
126
|
+
cmd = <<~BASH
|
|
127
|
+
kubectl apply -f - <<-EOF
|
|
128
|
+
apiVersion: upgrade.cattle.io/v1
|
|
129
|
+
kind: Plan
|
|
130
|
+
metadata:
|
|
131
|
+
name: k3s-server
|
|
132
|
+
namespace: system-upgrade
|
|
133
|
+
labels:
|
|
134
|
+
k3s-upgrade: server
|
|
135
|
+
spec:
|
|
136
|
+
concurrency: 1
|
|
137
|
+
version: #{new_k3s_version}
|
|
138
|
+
nodeSelector:
|
|
139
|
+
matchExpressions:
|
|
140
|
+
- {key: node-role.kubernetes.io/master, operator: In, values: ["true"]}
|
|
141
|
+
serviceAccountName: system-upgrade
|
|
142
|
+
tolerations:
|
|
143
|
+
- key: "CriticalAddonsOnly"
|
|
144
|
+
operator: "Equal"
|
|
145
|
+
value: "true"
|
|
146
|
+
effect: "NoExecute"
|
|
147
|
+
cordon: true
|
|
148
|
+
upgrade:
|
|
149
|
+
image: rancher/k3s-upgrade
|
|
150
|
+
EOF
|
|
151
|
+
BASH
|
|
152
|
+
|
|
153
|
+
run cmd, kubeconfig_path: kubeconfig_path
|
|
154
|
+
|
|
155
|
+
cmd = <<~BASH
|
|
156
|
+
kubectl apply -f - <<-EOF
|
|
157
|
+
apiVersion: upgrade.cattle.io/v1
|
|
158
|
+
kind: Plan
|
|
159
|
+
metadata:
|
|
160
|
+
name: k3s-agent
|
|
161
|
+
namespace: system-upgrade
|
|
162
|
+
labels:
|
|
163
|
+
k3s-upgrade: agent
|
|
164
|
+
spec:
|
|
165
|
+
concurrency: #{worker_upgrade_concurrency}
|
|
166
|
+
version: #{new_k3s_version}
|
|
167
|
+
nodeSelector:
|
|
168
|
+
matchExpressions:
|
|
169
|
+
- {key: node-role.kubernetes.io/master, operator: NotIn, values: ["true"]}
|
|
170
|
+
serviceAccountName: system-upgrade
|
|
171
|
+
prepare:
|
|
172
|
+
image: rancher/k3s-upgrade
|
|
173
|
+
args: ["prepare", "k3s-server"]
|
|
174
|
+
cordon: true
|
|
175
|
+
upgrade:
|
|
176
|
+
image: rancher/k3s-upgrade
|
|
177
|
+
EOF
|
|
178
|
+
BASH
|
|
167
179
|
|
|
168
|
-
|
|
169
|
-
threads = servers.map do |server|
|
|
170
|
-
Thread.new { wait_for_ssh server }
|
|
171
|
-
end
|
|
180
|
+
run cmd, kubeconfig_path: kubeconfig_path
|
|
172
181
|
|
|
173
|
-
|
|
174
|
-
|
|
182
|
+
puts 'Upgrade will now start. Run `watch kubectl get nodes` to see the nodes being upgraded. This should take a few minutes for a small cluster.'
|
|
183
|
+
puts 'The API server may be briefly unavailable during the upgrade of the controlplane.'
|
|
184
|
+
|
|
185
|
+
configuration['k3s_version'] = new_k3s_version
|
|
186
|
+
|
|
187
|
+
File.write(config_file, configuration.to_yaml)
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
def master_script(master)
|
|
191
|
+
server = master == first_master ? ' --cluster-init ' : " --server https://#{api_server_ip}:6443 "
|
|
192
|
+
flannel_interface = find_flannel_interface(master)
|
|
193
|
+
flannel_wireguard = enable_encryption ? ' --flannel-backend=wireguard ' : ' '
|
|
194
|
+
|
|
195
|
+
taint = schedule_workloads_on_masters? ? ' ' : ' --node-taint CriticalAddonsOnly=true:NoExecute '
|
|
196
|
+
|
|
197
|
+
<<~SCRIPT
|
|
198
|
+
curl -sfL https://get.k3s.io | INSTALL_K3S_VERSION="#{k3s_version}" K3S_TOKEN="#{k3s_token}" INSTALL_K3S_EXEC="server \
|
|
199
|
+
--disable-cloud-controller \
|
|
200
|
+
--disable servicelb \
|
|
201
|
+
--disable traefik \
|
|
202
|
+
--disable local-storage \
|
|
203
|
+
--disable metrics-server \
|
|
204
|
+
--write-kubeconfig-mode=644 \
|
|
205
|
+
--node-name="$(hostname -f)" \
|
|
206
|
+
--cluster-cidr=10.244.0.0/16 \
|
|
207
|
+
--etcd-expose-metrics=true \
|
|
208
|
+
#{flannel_wireguard} \
|
|
209
|
+
--kube-controller-manager-arg="address=0.0.0.0" \
|
|
210
|
+
--kube-controller-manager-arg="bind-address=0.0.0.0" \
|
|
211
|
+
--kube-proxy-arg="metrics-bind-address=0.0.0.0" \
|
|
212
|
+
--kube-scheduler-arg="address=0.0.0.0" \
|
|
213
|
+
--kube-scheduler-arg="bind-address=0.0.0.0" \
|
|
214
|
+
#{taint} \
|
|
215
|
+
--kubelet-arg="cloud-provider=external" \
|
|
216
|
+
--advertise-address=$(hostname -I | awk '{print $2}') \
|
|
217
|
+
--node-ip=$(hostname -I | awk '{print $2}') \
|
|
218
|
+
--node-external-ip=$(hostname -I | awk '{print $1}') \
|
|
219
|
+
--flannel-iface=#{flannel_interface} \
|
|
220
|
+
#{server} #{tls_sans}" sh -
|
|
221
|
+
SCRIPT
|
|
222
|
+
end
|
|
223
|
+
|
|
224
|
+
def worker_script(worker)
|
|
225
|
+
flannel_interface = find_flannel_interface(worker)
|
|
226
|
+
|
|
227
|
+
<<~BASH
|
|
228
|
+
curl -sfL https://get.k3s.io | K3S_TOKEN="#{k3s_token}" INSTALL_K3S_VERSION="#{k3s_version}" K3S_URL=https://#{first_master_private_ip}:6443 INSTALL_K3S_EXEC="agent \
|
|
229
|
+
--node-name="$(hostname -f)" \
|
|
230
|
+
--kubelet-arg="cloud-provider=external" \
|
|
231
|
+
--node-ip=$(hostname -I | awk '{print $2}') \
|
|
232
|
+
--node-external-ip=$(hostname -I | awk '{print $1}') \
|
|
233
|
+
--flannel-iface=#{flannel_interface}" sh -
|
|
234
|
+
BASH
|
|
235
|
+
end
|
|
236
|
+
|
|
237
|
+
def deploy_kubernetes
|
|
238
|
+
puts
|
|
239
|
+
puts "Deploying k3s to first master (#{first_master['name']})..."
|
|
175
240
|
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
).delete(ha: (masters.size > 1))
|
|
186
|
-
|
|
187
|
-
Hetzner::Firewall.new(
|
|
188
|
-
hetzner_client: hetzner_client,
|
|
189
|
-
cluster_name: cluster_name
|
|
190
|
-
).delete(all_servers)
|
|
191
|
-
|
|
192
|
-
Hetzner::Network.new(
|
|
193
|
-
hetzner_client: hetzner_client,
|
|
194
|
-
cluster_name: cluster_name
|
|
195
|
-
).delete
|
|
196
|
-
|
|
197
|
-
Hetzner::SSHKey.new(
|
|
198
|
-
hetzner_client: hetzner_client,
|
|
199
|
-
cluster_name: cluster_name
|
|
200
|
-
).delete(public_ssh_key_path: public_ssh_key_path)
|
|
201
|
-
|
|
202
|
-
threads = all_servers.map do |server|
|
|
241
|
+
ssh first_master, master_script(first_master), print_output: true
|
|
242
|
+
|
|
243
|
+
puts
|
|
244
|
+
puts '...k3s has been deployed to first master.'
|
|
245
|
+
|
|
246
|
+
save_kubeconfig
|
|
247
|
+
|
|
248
|
+
if masters.size > 1
|
|
249
|
+
threads = masters[1..].map do |master|
|
|
203
250
|
Thread.new do
|
|
204
|
-
|
|
251
|
+
puts
|
|
252
|
+
puts "Deploying k3s to master #{master['name']}..."
|
|
253
|
+
|
|
254
|
+
ssh master, master_script(master), print_output: true
|
|
255
|
+
|
|
256
|
+
puts
|
|
257
|
+
puts "...k3s has been deployed to master #{master['name']}."
|
|
205
258
|
end
|
|
206
259
|
end
|
|
207
260
|
|
|
208
261
|
threads.each(&:join) unless threads.empty?
|
|
209
262
|
end
|
|
210
263
|
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
cmd = <<~EOS
|
|
216
|
-
kubectl apply -f - <<-EOF
|
|
217
|
-
apiVersion: upgrade.cattle.io/v1
|
|
218
|
-
kind: Plan
|
|
219
|
-
metadata:
|
|
220
|
-
name: k3s-server
|
|
221
|
-
namespace: system-upgrade
|
|
222
|
-
labels:
|
|
223
|
-
k3s-upgrade: server
|
|
224
|
-
spec:
|
|
225
|
-
concurrency: 1
|
|
226
|
-
version: #{new_k3s_version}
|
|
227
|
-
nodeSelector:
|
|
228
|
-
matchExpressions:
|
|
229
|
-
- {key: node-role.kubernetes.io/master, operator: In, values: ["true"]}
|
|
230
|
-
serviceAccountName: system-upgrade
|
|
231
|
-
tolerations:
|
|
232
|
-
- key: "CriticalAddonsOnly"
|
|
233
|
-
operator: "Equal"
|
|
234
|
-
value: "true"
|
|
235
|
-
effect: "NoExecute"
|
|
236
|
-
cordon: true
|
|
237
|
-
upgrade:
|
|
238
|
-
image: rancher/k3s-upgrade
|
|
239
|
-
EOF
|
|
240
|
-
EOS
|
|
241
|
-
|
|
242
|
-
run cmd, kubeconfig_path: kubeconfig_path
|
|
243
|
-
|
|
244
|
-
cmd = <<~EOS
|
|
245
|
-
kubectl apply -f - <<-EOF
|
|
246
|
-
apiVersion: upgrade.cattle.io/v1
|
|
247
|
-
kind: Plan
|
|
248
|
-
metadata:
|
|
249
|
-
name: k3s-agent
|
|
250
|
-
namespace: system-upgrade
|
|
251
|
-
labels:
|
|
252
|
-
k3s-upgrade: agent
|
|
253
|
-
spec:
|
|
254
|
-
concurrency: #{worker_upgrade_concurrency}
|
|
255
|
-
version: #{new_k3s_version}
|
|
256
|
-
nodeSelector:
|
|
257
|
-
matchExpressions:
|
|
258
|
-
- {key: node-role.kubernetes.io/master, operator: NotIn, values: ["true"]}
|
|
259
|
-
serviceAccountName: system-upgrade
|
|
260
|
-
prepare:
|
|
261
|
-
image: rancher/k3s-upgrade
|
|
262
|
-
args: ["prepare", "k3s-server"]
|
|
263
|
-
cordon: true
|
|
264
|
-
upgrade:
|
|
265
|
-
image: rancher/k3s-upgrade
|
|
266
|
-
EOF
|
|
267
|
-
EOS
|
|
268
|
-
|
|
269
|
-
run cmd, kubeconfig_path: kubeconfig_path
|
|
270
|
-
|
|
271
|
-
puts "Upgrade will now start. Run `watch kubectl get nodes` to see the nodes being upgraded. This should take a few minutes for a small cluster."
|
|
272
|
-
puts "The API server may be briefly unavailable during the upgrade of the controlplane."
|
|
273
|
-
|
|
274
|
-
configuration["k3s_version"] = new_k3s_version
|
|
275
|
-
|
|
276
|
-
File.write(config_file, configuration.to_yaml)
|
|
277
|
-
end
|
|
264
|
+
threads = workers.map do |worker|
|
|
265
|
+
Thread.new do
|
|
266
|
+
puts
|
|
267
|
+
puts "Deploying k3s to worker (#{worker['name']})..."
|
|
278
268
|
|
|
269
|
+
ssh worker, worker_script(worker), print_output: true
|
|
279
270
|
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
taint = schedule_workloads_on_masters? ? " " : " --node-taint CriticalAddonsOnly=true:NoExecute "
|
|
285
|
-
|
|
286
|
-
<<~EOF
|
|
287
|
-
curl -sfL https://get.k3s.io | INSTALL_K3S_VERSION="#{k3s_version}" K3S_TOKEN="#{k3s_token}" INSTALL_K3S_EXEC="server \
|
|
288
|
-
--disable-cloud-controller \
|
|
289
|
-
--disable servicelb \
|
|
290
|
-
--disable traefik \
|
|
291
|
-
--disable local-storage \
|
|
292
|
-
--disable metrics-server \
|
|
293
|
-
--write-kubeconfig-mode=644 \
|
|
294
|
-
--node-name="$(hostname -f)" \
|
|
295
|
-
--cluster-cidr=10.244.0.0/16 \
|
|
296
|
-
--etcd-expose-metrics=true \
|
|
297
|
-
--kube-controller-manager-arg="address=0.0.0.0" \
|
|
298
|
-
--kube-controller-manager-arg="bind-address=0.0.0.0" \
|
|
299
|
-
--kube-proxy-arg="metrics-bind-address=0.0.0.0" \
|
|
300
|
-
--kube-scheduler-arg="address=0.0.0.0" \
|
|
301
|
-
--kube-scheduler-arg="bind-address=0.0.0.0" \
|
|
302
|
-
#{taint} \
|
|
303
|
-
--kubelet-arg="cloud-provider=external" \
|
|
304
|
-
--advertise-address=$(hostname -I | awk '{print $2}') \
|
|
305
|
-
--node-ip=$(hostname -I | awk '{print $2}') \
|
|
306
|
-
--node-external-ip=$(hostname -I | awk '{print $1}') \
|
|
307
|
-
--flannel-iface=#{flannel_interface} \
|
|
308
|
-
#{server} #{tls_sans}" sh -
|
|
309
|
-
EOF
|
|
271
|
+
puts
|
|
272
|
+
puts "...k3s has been deployed to worker (#{worker['name']})."
|
|
273
|
+
end
|
|
310
274
|
end
|
|
311
275
|
|
|
312
|
-
|
|
313
|
-
|
|
276
|
+
threads.each(&:join) unless threads.empty?
|
|
277
|
+
end
|
|
314
278
|
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
279
|
+
def deploy_cloud_controller_manager
|
|
280
|
+
check_kubectl
|
|
281
|
+
|
|
282
|
+
puts
|
|
283
|
+
puts 'Deploying Hetzner Cloud Controller Manager...'
|
|
284
|
+
|
|
285
|
+
cmd = <<~BASH
|
|
286
|
+
kubectl apply -f - <<-EOF
|
|
287
|
+
apiVersion: "v1"
|
|
288
|
+
kind: "Secret"
|
|
289
|
+
metadata:
|
|
290
|
+
namespace: 'kube-system'
|
|
291
|
+
name: 'hcloud'
|
|
292
|
+
stringData:
|
|
293
|
+
network: "#{cluster_name}"
|
|
294
|
+
token: "#{hetzner_token}"
|
|
322
295
|
EOF
|
|
323
|
-
|
|
296
|
+
BASH
|
|
324
297
|
|
|
325
|
-
|
|
326
|
-
puts
|
|
327
|
-
puts "Deploying k3s to first master (#{first_master["name"]})..."
|
|
298
|
+
run cmd, kubeconfig_path: kubeconfig_path
|
|
328
299
|
|
|
329
|
-
|
|
300
|
+
cmd = 'kubectl apply -f https://github.com/hetznercloud/hcloud-cloud-controller-manager/releases/latest/download/ccm-networks.yaml'
|
|
330
301
|
|
|
331
|
-
|
|
332
|
-
puts "...k3s has been deployed to first master."
|
|
302
|
+
run cmd, kubeconfig_path: kubeconfig_path
|
|
333
303
|
|
|
334
|
-
|
|
304
|
+
puts '...Cloud Controller Manager deployed'
|
|
305
|
+
end
|
|
335
306
|
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
Thread.new do
|
|
339
|
-
puts
|
|
340
|
-
puts "Deploying k3s to master #{master["name"]}..."
|
|
307
|
+
def deploy_system_upgrade_controller
|
|
308
|
+
check_kubectl
|
|
341
309
|
|
|
342
|
-
|
|
310
|
+
puts
|
|
311
|
+
puts 'Deploying k3s System Upgrade Controller...'
|
|
343
312
|
|
|
344
|
-
|
|
345
|
-
puts "...k3s has been deployed to master #{master["name"]}."
|
|
346
|
-
end
|
|
347
|
-
end
|
|
313
|
+
cmd = 'kubectl apply -f https://github.com/rancher/system-upgrade-controller/releases/download/v0.8.1/system-upgrade-controller.yaml'
|
|
348
314
|
|
|
349
|
-
|
|
350
|
-
end
|
|
315
|
+
run cmd, kubeconfig_path: kubeconfig_path
|
|
351
316
|
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
puts
|
|
355
|
-
puts "Deploying k3s to worker (#{worker["name"]})..."
|
|
317
|
+
puts '...k3s System Upgrade Controller deployed'
|
|
318
|
+
end
|
|
356
319
|
|
|
357
|
-
|
|
320
|
+
def deploy_csi_driver
|
|
321
|
+
check_kubectl
|
|
322
|
+
|
|
323
|
+
puts
|
|
324
|
+
puts 'Deploying Hetzner CSI Driver...'
|
|
325
|
+
|
|
326
|
+
cmd = <<~BASH
|
|
327
|
+
kubectl apply -f - <<-EOF
|
|
328
|
+
apiVersion: "v1"
|
|
329
|
+
kind: "Secret"
|
|
330
|
+
metadata:
|
|
331
|
+
namespace: 'kube-system'
|
|
332
|
+
name: 'hcloud-csi'
|
|
333
|
+
stringData:
|
|
334
|
+
token: "#{hetzner_token}"
|
|
335
|
+
EOF
|
|
336
|
+
BASH
|
|
358
337
|
|
|
359
|
-
|
|
360
|
-
puts "...k3s has been deployed to worker (#{worker["name"]})."
|
|
361
|
-
end
|
|
362
|
-
end
|
|
338
|
+
run cmd, kubeconfig_path: kubeconfig_path
|
|
363
339
|
|
|
364
|
-
|
|
365
|
-
|
|
340
|
+
cmd = 'kubectl apply -f https://raw.githubusercontent.com/hetznercloud/csi-driver/v1.6.0/deploy/kubernetes/hcloud-csi.yml'
|
|
341
|
+
|
|
342
|
+
run cmd, kubeconfig_path: kubeconfig_path
|
|
366
343
|
|
|
367
|
-
|
|
368
|
-
|
|
344
|
+
puts '...CSI Driver deployed'
|
|
345
|
+
end
|
|
369
346
|
|
|
370
|
-
|
|
371
|
-
|
|
347
|
+
def find_flannel_interface(server)
|
|
348
|
+
if ssh(server, 'lscpu | grep Vendor') =~ /Intel/
|
|
349
|
+
'ens10'
|
|
350
|
+
else
|
|
351
|
+
'enp7s0'
|
|
352
|
+
end
|
|
353
|
+
end
|
|
372
354
|
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
namespace: 'kube-system'
|
|
379
|
-
name: 'hcloud'
|
|
380
|
-
stringData:
|
|
381
|
-
network: "#{cluster_name}"
|
|
382
|
-
token: "#{hetzner_token}"
|
|
383
|
-
EOF
|
|
384
|
-
EOS
|
|
355
|
+
def all_servers
|
|
356
|
+
@all_servers ||= hetzner_client.get('/servers?sort=created:desc')['servers'].select do |server|
|
|
357
|
+
belongs_to_cluster?(server) == true
|
|
358
|
+
end
|
|
359
|
+
end
|
|
385
360
|
|
|
386
|
-
|
|
361
|
+
def masters
|
|
362
|
+
@masters ||= all_servers.select { |server| server['name'] =~ /master\d+\Z/ }.sort { |a, b| a['name'] <=> b['name'] }
|
|
363
|
+
end
|
|
387
364
|
|
|
388
|
-
|
|
365
|
+
def workers
|
|
366
|
+
@workers = all_servers.select { |server| server['name'] =~ /worker\d+\Z/ }.sort { |a, b| a['name'] <=> b['name'] }
|
|
367
|
+
end
|
|
389
368
|
|
|
390
|
-
|
|
369
|
+
def k3s_token
|
|
370
|
+
@k3s_token ||= begin
|
|
371
|
+
token = ssh(first_master, '{ TOKEN=$(< /var/lib/rancher/k3s/server/node-token); } 2> /dev/null; echo $TOKEN')
|
|
391
372
|
|
|
392
|
-
|
|
373
|
+
if token.empty?
|
|
374
|
+
SecureRandom.hex
|
|
375
|
+
else
|
|
376
|
+
token.split(':').last
|
|
377
|
+
end
|
|
393
378
|
end
|
|
379
|
+
end
|
|
394
380
|
|
|
395
|
-
|
|
396
|
-
|
|
381
|
+
def first_master_private_ip
|
|
382
|
+
@first_master_private_ip ||= first_master['private_net'][0]['ip']
|
|
383
|
+
end
|
|
397
384
|
|
|
398
|
-
|
|
399
|
-
|
|
385
|
+
def first_master
|
|
386
|
+
masters.first
|
|
387
|
+
end
|
|
400
388
|
|
|
401
|
-
|
|
389
|
+
def api_server_ip
|
|
390
|
+
return @api_server_ip if @api_server_ip
|
|
391
|
+
|
|
392
|
+
@api_server_ip = if masters.size > 1
|
|
393
|
+
load_balancer_name = "#{cluster_name}-api"
|
|
394
|
+
load_balancer = hetzner_client.get('/load_balancers')['load_balancers'].detect do |lb|
|
|
395
|
+
lb['name'] == load_balancer_name
|
|
396
|
+
end
|
|
397
|
+
load_balancer['public_net']['ipv4']['ip']
|
|
398
|
+
else
|
|
399
|
+
first_master_public_ip
|
|
400
|
+
end
|
|
401
|
+
end
|
|
402
402
|
|
|
403
|
-
|
|
403
|
+
def tls_sans
|
|
404
|
+
sans = " --tls-san=#{api_server_ip} "
|
|
404
405
|
|
|
405
|
-
|
|
406
|
+
masters.each do |master|
|
|
407
|
+
master_private_ip = master['private_net'][0]['ip']
|
|
408
|
+
sans << " --tls-san=#{master_private_ip} "
|
|
406
409
|
end
|
|
407
410
|
|
|
408
|
-
|
|
409
|
-
|
|
411
|
+
sans
|
|
412
|
+
end
|
|
410
413
|
|
|
411
|
-
|
|
412
|
-
|
|
414
|
+
def first_master_public_ip
|
|
415
|
+
@first_master_public_ip ||= first_master.dig('public_net', 'ipv4', 'ip')
|
|
416
|
+
end
|
|
413
417
|
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
metadata:
|
|
419
|
-
namespace: 'kube-system'
|
|
420
|
-
name: 'hcloud-csi'
|
|
421
|
-
stringData:
|
|
422
|
-
token: "#{hetzner_token}"
|
|
423
|
-
EOF
|
|
424
|
-
EOS
|
|
418
|
+
def save_kubeconfig
|
|
419
|
+
kubeconfig = ssh(first_master, 'cat /etc/rancher/k3s/k3s.yaml')
|
|
420
|
+
.gsub('127.0.0.1', api_server_ip)
|
|
421
|
+
.gsub('default', cluster_name)
|
|
425
422
|
|
|
426
|
-
|
|
423
|
+
File.write(kubeconfig_path, kubeconfig)
|
|
427
424
|
|
|
428
|
-
|
|
425
|
+
FileUtils.chmod 'go-r', kubeconfig_path
|
|
426
|
+
end
|
|
429
427
|
|
|
430
|
-
|
|
428
|
+
def belongs_to_cluster?(server)
|
|
429
|
+
server.dig('labels', 'cluster') == cluster_name
|
|
430
|
+
end
|
|
431
431
|
|
|
432
|
-
|
|
433
|
-
|
|
432
|
+
def schedule_workloads_on_masters?
|
|
433
|
+
schedule_workloads_on_masters = configuration['schedule_workloads_on_masters']
|
|
434
|
+
schedule_workloads_on_masters ? !!schedule_workloads_on_masters : false
|
|
435
|
+
end
|
|
434
436
|
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
else
|
|
439
|
-
"enp7s0"
|
|
440
|
-
end
|
|
441
|
-
end
|
|
437
|
+
def image
|
|
438
|
+
configuration['image'] || 'ubuntu-20.04'
|
|
439
|
+
end
|
|
442
440
|
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
441
|
+
def additional_packages
|
|
442
|
+
configuration['additional_packages'] || []
|
|
443
|
+
end
|
|
446
444
|
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
end
|
|
445
|
+
def check_kubectl
|
|
446
|
+
return if which('kubectl')
|
|
450
447
|
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
448
|
+
puts 'Please ensure kubectl is installed and in your PATH.'
|
|
449
|
+
exit 1
|
|
450
|
+
end
|
|
454
451
|
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
452
|
+
def placement_group_id(pool_name = nil)
|
|
453
|
+
@placement_groups ||= {}
|
|
454
|
+
@placement_groups[pool_name || '__masters__'] ||= Hetzner::PlacementGroup.new(hetzner_client:, cluster_name:, pool_name:).create
|
|
455
|
+
end
|
|
458
456
|
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
token.split(":").last
|
|
463
|
-
end
|
|
464
|
-
end
|
|
465
|
-
end
|
|
457
|
+
def master_instance_type
|
|
458
|
+
@master_instance_type ||= masters_config['instance_type']
|
|
459
|
+
end
|
|
466
460
|
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
461
|
+
def masters_count
|
|
462
|
+
@masters_count ||= masters_config['instance_count']
|
|
463
|
+
end
|
|
470
464
|
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
465
|
+
def firewall_id
|
|
466
|
+
@firewall_id ||= Hetzner::Firewall.new(hetzner_client:, cluster_name:).create(high_availability: (masters_count > 1), networks:)
|
|
467
|
+
end
|
|
474
468
|
|
|
475
|
-
|
|
476
|
-
|
|
469
|
+
def network_id
|
|
470
|
+
@network_id ||= Hetzner::Network.new(hetzner_client:, cluster_name:).create(location:)
|
|
471
|
+
end
|
|
477
472
|
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
473
|
+
def ssh_key_id
|
|
474
|
+
@ssh_key_id ||= Hetzner::SSHKey.new(hetzner_client:, cluster_name:).create(public_ssh_key_path:)
|
|
475
|
+
end
|
|
476
|
+
|
|
477
|
+
def master_definitions_for_create
|
|
478
|
+
definitions = []
|
|
479
|
+
|
|
480
|
+
masters_count.times do |i|
|
|
481
|
+
definitions << {
|
|
482
|
+
instance_type: master_instance_type,
|
|
483
|
+
instance_id: "master#{i + 1}",
|
|
484
|
+
placement_group_id:,
|
|
485
|
+
location:,
|
|
486
|
+
firewall_id:,
|
|
487
|
+
network_id:,
|
|
488
|
+
ssh_key_id:,
|
|
489
|
+
image:,
|
|
490
|
+
additional_packages:
|
|
491
|
+
}
|
|
485
492
|
end
|
|
486
493
|
|
|
487
|
-
|
|
488
|
-
|
|
494
|
+
definitions
|
|
495
|
+
end
|
|
489
496
|
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
sans << " --tls-san=#{master_private_ip} "
|
|
493
|
-
end
|
|
497
|
+
def master_definitions_for_delete
|
|
498
|
+
definitions = []
|
|
494
499
|
|
|
495
|
-
|
|
500
|
+
masters_count.times do |i|
|
|
501
|
+
definitions << {
|
|
502
|
+
instance_type: master_instance_type,
|
|
503
|
+
instance_id: "master#{i + 1}"
|
|
504
|
+
}
|
|
496
505
|
end
|
|
497
506
|
|
|
498
|
-
|
|
499
|
-
|
|
507
|
+
definitions
|
|
508
|
+
end
|
|
509
|
+
|
|
510
|
+
def worker_node_pool_definitions(worker_node_pool)
|
|
511
|
+
worker_node_pool_name = worker_node_pool['name']
|
|
512
|
+
worker_instance_type = worker_node_pool['instance_type']
|
|
513
|
+
worker_count = worker_node_pool['instance_count']
|
|
514
|
+
|
|
515
|
+
definitions = []
|
|
516
|
+
|
|
517
|
+
worker_count.times do |i|
|
|
518
|
+
definitions << {
|
|
519
|
+
instance_type: worker_instance_type,
|
|
520
|
+
instance_id: "pool-#{worker_node_pool_name}-worker#{i + 1}",
|
|
521
|
+
placement_group_id: placement_group_id(worker_node_pool_name),
|
|
522
|
+
location:,
|
|
523
|
+
firewall_id:,
|
|
524
|
+
network_id:,
|
|
525
|
+
ssh_key_id:,
|
|
526
|
+
image:,
|
|
527
|
+
additional_packages:
|
|
528
|
+
}
|
|
500
529
|
end
|
|
501
530
|
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
531
|
+
definitions
|
|
532
|
+
end
|
|
533
|
+
|
|
534
|
+
def create_load_balancer
|
|
535
|
+
Hetzner::LoadBalancer.new(hetzner_client:, cluster_name:).create(location:, network_id:)
|
|
536
|
+
end
|
|
506
537
|
|
|
507
|
-
|
|
538
|
+
def server_configs
|
|
539
|
+
return @server_configs if @server_configs
|
|
508
540
|
|
|
509
|
-
|
|
510
|
-
end
|
|
541
|
+
@server_configs = master_definitions_for_create
|
|
511
542
|
|
|
512
|
-
|
|
513
|
-
|
|
543
|
+
worker_node_pools.each do |worker_node_pool|
|
|
544
|
+
@server_configs += worker_node_pool_definitions(worker_node_pool)
|
|
514
545
|
end
|
|
515
546
|
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
547
|
+
@server_configs
|
|
548
|
+
end
|
|
549
|
+
|
|
550
|
+
def create_servers
|
|
551
|
+
servers = []
|
|
552
|
+
|
|
553
|
+
threads = server_configs.map do |server_config|
|
|
554
|
+
Thread.new do
|
|
555
|
+
servers << Hetzner::Server.new(hetzner_client:, cluster_name:).create(**server_config)
|
|
556
|
+
end
|
|
519
557
|
end
|
|
520
558
|
|
|
521
|
-
|
|
522
|
-
|
|
559
|
+
threads.each(&:join) unless threads.empty?
|
|
560
|
+
|
|
561
|
+
sleep 1 while servers.size != server_configs.size
|
|
562
|
+
|
|
563
|
+
wait_for_servers(servers)
|
|
564
|
+
end
|
|
565
|
+
|
|
566
|
+
def wait_for_servers(servers)
|
|
567
|
+
threads = servers.map do |server|
|
|
568
|
+
Thread.new { wait_for_ssh server }
|
|
523
569
|
end
|
|
524
570
|
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
571
|
+
threads.each(&:join) unless threads.empty?
|
|
572
|
+
end
|
|
573
|
+
|
|
574
|
+
def delete_servers
|
|
575
|
+
threads = all_servers.map do |server|
|
|
576
|
+
Thread.new do
|
|
577
|
+
Hetzner::Server.new(hetzner_client:, cluster_name:).delete(server_name: server['name'])
|
|
529
578
|
end
|
|
530
579
|
end
|
|
531
580
|
|
|
581
|
+
threads.each(&:join) unless threads.empty?
|
|
582
|
+
end
|
|
532
583
|
end
|