hetzner-k3s 0.1.0 → 0.3.2
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/Gemfile.lock +1 -1
- data/README.md +22 -2
- data/cluster_config.yaml.example +1 -0
- data/lib/hetzner/infra/client.rb +17 -3
- data/lib/hetzner/infra/load_balancer.rb +4 -4
- data/lib/hetzner/infra/server.rb +6 -1
- data/lib/hetzner/infra/ssh_key.rb +30 -6
- data/lib/hetzner/k3s/cli.rb +7 -6
- data/lib/hetzner/k3s/cluster.rb +51 -33
- data/lib/hetzner/k3s/version.rb +1 -1
- metadata +2 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 109610e9f4d807bac091471880141069488865da1024cee8bb0479846c772165
|
4
|
+
data.tar.gz: d4f882d488ecc94d6f7234fc122c4dc6241dfc8ce946cbce1855619353296917
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 6cd9649eef2f75f616f9dc5bc2bb86d2b10e0e227137adb6683c502b0ed42a0888f59f361e088edece98eb91c0ee11c596c8734c00865081564cb1539cb537ea
|
7
|
+
data.tar.gz: e1569ef139160e15daa78566cb48e71bf99999d5e6f19a98f30fc73b984bc7fb5fe0a208a4594c7d8c4606ffd6ea509409cc31cd48818739be1ed83be914c9b0
|
data/Gemfile.lock
CHANGED
data/README.md
CHANGED
@@ -44,6 +44,7 @@ cluster_name: test
|
|
44
44
|
kubeconfig_path: "./kubeconfig"
|
45
45
|
k3s_version: v1.21.3+k3s1
|
46
46
|
ssh_key_path: "~/.ssh/id_rsa.pub"
|
47
|
+
verify_host_key: false
|
47
48
|
location: nbg1
|
48
49
|
masters:
|
49
50
|
instance_type: cpx21
|
@@ -53,7 +54,7 @@ worker_node_pools:
|
|
53
54
|
instance_type: cpx21
|
54
55
|
instance_count: 4
|
55
56
|
- name: big
|
56
|
-
instance_type:
|
57
|
+
instance_type: cpx31
|
57
58
|
instance_count: 2
|
58
59
|
```
|
59
60
|
|
@@ -74,6 +75,8 @@ curl \
|
|
74
75
|
```
|
75
76
|
|
76
77
|
|
78
|
+
Note: the option `verify_host_key` is by default set to `false` to disable host key verification. This is because sometimes when creating new servers, Hetzner may assign IP addresses that were previously used by other servers you owned in the past. Therefore the host key verification would fail. If you set this option to `true` and this happens, the tool won't be able to continue creating the cluster until you resolve the issue with one of the suggestions it will give you.
|
79
|
+
|
77
80
|
Finally, to create the cluster run:
|
78
81
|
|
79
82
|
```bash
|
@@ -208,6 +211,23 @@ The other annotations should be self explanatory. You can find a list of the ava
|
|
208
211
|
|
209
212
|
Once the cluster is ready you can create persistent volumes out of the box with the default storage class `hcloud-volumes`, since the Hetzner CSI driver is installed automatically. This will use Hetzner's block storage (based on Ceph so it's replicated and highly available) for your persistent volumes. Note that the minimum size of a volume is 10Gi. If you specify a smaller size for a volume, the volume will be created with a capacity of 10Gi anyway.
|
210
213
|
|
214
|
+
|
215
|
+
## changelog
|
216
|
+
|
217
|
+
- 0.3.2
|
218
|
+
- Configure DNS to use Cloudflare's resolver instead of Hetzner's, since Hetzner's resolvers are not always reliable
|
219
|
+
|
220
|
+
- 0.3.1
|
221
|
+
- Allow enabling/disabling the host key verification
|
222
|
+
|
223
|
+
- 0.3.0
|
224
|
+
- Handle case when an SSH key with the given fingerprint already exists in the Hetzner project
|
225
|
+
- Handle a timeout of 5 seconds for requests to the Hetzner API
|
226
|
+
- Retry waiting for server to be up when timeouts/host-unreachable errors occur
|
227
|
+
- Ignore known_hosts entry to prevent errors when recreating servers with IPs that have been used previously
|
228
|
+
|
229
|
+
- 0.2.0
|
230
|
+
- Allow mixing servers of different series Intel/AMD
|
211
231
|
## Contributing and support
|
212
232
|
|
213
233
|
Please create a PR if you want to propose any changes, or open an issue if you are having trouble with the tool - I will do my best to help if I can.
|
@@ -218,4 +238,4 @@ The gem is available as open source under the terms of the [MIT License](https:/
|
|
218
238
|
|
219
239
|
## Code of Conduct
|
220
240
|
|
221
|
-
Everyone interacting in the hetzner-k3s project's codebases, issue trackers, chat rooms and mailing lists is expected to follow the [code of conduct](https://github.com/vitobotta/k3s/blob/
|
241
|
+
Everyone interacting in the hetzner-k3s project's codebases, issue trackers, chat rooms and mailing lists is expected to follow the [code of conduct](https://github.com/vitobotta/hetzner-k3s/blob/main/CODE_OF_CONDUCT.md).
|
data/cluster_config.yaml.example
CHANGED
data/lib/hetzner/infra/client.rb
CHANGED
@@ -9,15 +9,21 @@ module Hetzner
|
|
9
9
|
end
|
10
10
|
|
11
11
|
def get(path)
|
12
|
-
|
12
|
+
make_request do
|
13
|
+
JSON.parse HTTP.headers(headers).get(BASE_URI + path).body
|
14
|
+
end
|
13
15
|
end
|
14
16
|
|
15
17
|
def post(path, data)
|
16
|
-
|
18
|
+
make_request do
|
19
|
+
HTTP.headers(headers).post(BASE_URI + path, json: data)
|
20
|
+
end
|
17
21
|
end
|
18
22
|
|
19
23
|
def delete(path, id)
|
20
|
-
|
24
|
+
make_request do
|
25
|
+
HTTP.headers(headers).delete(BASE_URI + path + "/" + id.to_s)
|
26
|
+
end
|
21
27
|
end
|
22
28
|
|
23
29
|
private
|
@@ -28,5 +34,13 @@ module Hetzner
|
|
28
34
|
"Content-Type": "application/json"
|
29
35
|
}
|
30
36
|
end
|
37
|
+
|
38
|
+
def make_request &block
|
39
|
+
Timeout::timeout(5) do
|
40
|
+
block.call
|
41
|
+
end
|
42
|
+
rescue Timeout::Error
|
43
|
+
retry
|
44
|
+
end
|
31
45
|
end
|
32
46
|
end
|
@@ -26,12 +26,12 @@ module Hetzner
|
|
26
26
|
JSON.parse(response)["load_balancer"]["id"]
|
27
27
|
end
|
28
28
|
|
29
|
-
def delete
|
29
|
+
def delete(ha:)
|
30
30
|
if load_balancer = find_load_balancer
|
31
|
-
puts "Deleting API load balancer..."
|
31
|
+
puts "Deleting API load balancer..." unless ha
|
32
32
|
hetzner_client.delete("/load_balancers", load_balancer["id"])
|
33
|
-
puts "...API load balancer deleted."
|
34
|
-
|
33
|
+
puts "...API load balancer deleted." unless ha
|
34
|
+
elsif ha
|
35
35
|
puts "API load balancer no longer exists, skipping."
|
36
36
|
end
|
37
37
|
|
data/lib/hetzner/infra/server.rb
CHANGED
@@ -74,7 +74,12 @@ module Hetzner
|
|
74
74
|
- sed -i 's/[#]*PermitRootLogin yes/PermitRootLogin prohibit-password/g' /etc/ssh/sshd_config
|
75
75
|
- sed -i 's/[#]*PasswordAuthentication yes/PasswordAuthentication no/g' /etc/ssh/sshd_config
|
76
76
|
- systemctl restart sshd
|
77
|
-
|
77
|
+
- systemctl stop systemd-resolved
|
78
|
+
- systemctl disable systemd-resolved
|
79
|
+
- rm /etc/resolv.conf
|
80
|
+
- echo "nameserver 1.1.1.1" > /etc/resolv.conf
|
81
|
+
- echo "nameserver 1.0.0.1" >> /etc/resolv.conf
|
82
|
+
EOS
|
78
83
|
end
|
79
84
|
|
80
85
|
end
|
@@ -26,11 +26,17 @@ module Hetzner
|
|
26
26
|
JSON.parse(response)["ssh_key"]["id"]
|
27
27
|
end
|
28
28
|
|
29
|
-
def delete
|
29
|
+
def delete(ssh_key_path:)
|
30
|
+
@ssh_key_path = ssh_key_path
|
31
|
+
|
30
32
|
if ssh_key = find_ssh_key
|
31
|
-
|
32
|
-
|
33
|
-
|
33
|
+
if ssh_key["name"] == cluster_name
|
34
|
+
puts "Deleting ssh_key..."
|
35
|
+
hetzner_client.delete("/ssh_keys", ssh_key["id"])
|
36
|
+
puts "...ssh_key deleted."
|
37
|
+
else
|
38
|
+
puts "The SSH key existed before creating the cluster, so I won't delete it."
|
39
|
+
end
|
34
40
|
else
|
35
41
|
puts "SSH key no longer exists, skipping."
|
36
42
|
end
|
@@ -42,15 +48,33 @@ module Hetzner
|
|
42
48
|
|
43
49
|
attr_reader :hetzner_client, :cluster_name, :ssh_key_path
|
44
50
|
|
51
|
+
def public_key
|
52
|
+
@public_key ||= File.read(ssh_key_path).chop
|
53
|
+
end
|
54
|
+
|
45
55
|
def ssh_key_config
|
46
56
|
{
|
47
57
|
name: cluster_name,
|
48
|
-
public_key:
|
58
|
+
public_key: public_key
|
49
59
|
}
|
50
60
|
end
|
51
61
|
|
62
|
+
def fingerprint
|
63
|
+
@fingerprint ||= ::SSHKey.fingerprint(public_key)
|
64
|
+
end
|
65
|
+
|
52
66
|
def find_ssh_key
|
53
|
-
hetzner_client.get("/ssh_keys")["ssh_keys"].detect
|
67
|
+
key = hetzner_client.get("/ssh_keys")["ssh_keys"].detect do |ssh_key|
|
68
|
+
ssh_key["fingerprint"] == fingerprint
|
69
|
+
end
|
70
|
+
|
71
|
+
unless key
|
72
|
+
key = hetzner_client.get("/ssh_keys")["ssh_keys"].detect do |ssh_key|
|
73
|
+
ssh_key["name"] == cluster_name
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
key
|
54
78
|
end
|
55
79
|
|
56
80
|
end
|
data/lib/hetzner/k3s/cli.rb
CHANGED
@@ -80,7 +80,7 @@ module Hetzner
|
|
80
80
|
validate_k3s_version
|
81
81
|
validate_masters
|
82
82
|
validate_worker_node_pools
|
83
|
-
|
83
|
+
validate_verify_host_key
|
84
84
|
when :delete
|
85
85
|
validate_kubeconfig_path_must_exist
|
86
86
|
when :upgrade
|
@@ -221,11 +221,6 @@ module Hetzner
|
|
221
221
|
end
|
222
222
|
end
|
223
223
|
|
224
|
-
def validate_all_nodes_must_be_of_same_series
|
225
|
-
series = used_server_types.map{ |used_server_type| used_server_type[0..1]}
|
226
|
-
errors << "Master and worker node pools must all be of the same server series for networking to function properly (available series: cx, cp, ccx)" unless series.uniq.size == 1
|
227
|
-
end
|
228
|
-
|
229
224
|
def validate_new_k3s_version_must_be_more_recent
|
230
225
|
return if options[:force] == "true"
|
231
226
|
return unless kubernetes_client
|
@@ -298,6 +293,12 @@ module Hetzner
|
|
298
293
|
errors << "Cannot connect to the Kubernetes cluster"
|
299
294
|
false
|
300
295
|
end
|
296
|
+
|
297
|
+
|
298
|
+
def validate_verify_host_key
|
299
|
+
return unless [true, false].include?(configuration.fetch("ssh_key_path", false))
|
300
|
+
errors << "Please set the verify_host_key option to either true or false"
|
301
|
+
end
|
301
302
|
end
|
302
303
|
end
|
303
304
|
end
|
data/lib/hetzner/k3s/cluster.rb
CHANGED
@@ -29,7 +29,7 @@ class Cluster
|
|
29
29
|
@masters_config = configuration.dig("masters")
|
30
30
|
@worker_node_pools = configuration.dig("worker_node_pools")
|
31
31
|
@location = configuration.dig("location")
|
32
|
-
@
|
32
|
+
@verify_host_key = configuration.fetch("verify_host_key", false)
|
33
33
|
@servers = []
|
34
34
|
|
35
35
|
create_resources
|
@@ -46,6 +46,7 @@ class Cluster
|
|
46
46
|
def delete(configuration:)
|
47
47
|
@cluster_name = configuration.dig("cluster_name")
|
48
48
|
@kubeconfig_path = File.expand_path(configuration.dig("kubeconfig_path"))
|
49
|
+
@ssh_key_path = File.expand_path(configuration.dig("ssh_key_path"))
|
49
50
|
|
50
51
|
delete_resources
|
51
52
|
end
|
@@ -66,9 +67,9 @@ class Cluster
|
|
66
67
|
|
67
68
|
attr_reader :hetzner_client, :cluster_name, :kubeconfig_path, :k3s_version,
|
68
69
|
:masters_config, :worker_node_pools,
|
69
|
-
:location, :
|
70
|
+
:location, :ssh_key_path, :kubernetes_client,
|
70
71
|
:hetzner_token, :tls_sans, :new_k3s_version, :configuration,
|
71
|
-
:config_file
|
72
|
+
:config_file, :verify_host_key
|
72
73
|
|
73
74
|
|
74
75
|
def latest_k3s_version
|
@@ -138,17 +139,18 @@ class Cluster
|
|
138
139
|
end
|
139
140
|
end
|
140
141
|
|
141
|
-
threads.each(&:join)
|
142
|
+
threads.each(&:join) unless threads.empty?
|
142
143
|
|
143
144
|
puts
|
144
145
|
threads = servers.map do |server|
|
145
146
|
Thread.new { wait_for_ssh server }
|
146
147
|
end
|
147
148
|
|
148
|
-
threads.each(&:join)
|
149
|
+
threads.each(&:join) unless threads.empty?
|
149
150
|
end
|
150
151
|
|
151
152
|
def delete_resources
|
153
|
+
# Deleting nodes defined according to Kubernetes first
|
152
154
|
begin
|
153
155
|
Timeout::timeout(5) do
|
154
156
|
servers = kubernetes_client.api("v1").resource("nodes").list
|
@@ -159,12 +161,23 @@ class Cluster
|
|
159
161
|
end
|
160
162
|
end
|
161
163
|
|
162
|
-
threads.each(&:join)
|
164
|
+
threads.each(&:join) unless threads.empty?
|
163
165
|
end
|
164
|
-
rescue Timeout::Error
|
166
|
+
rescue Timeout::Error, Excon::Error::Socket
|
165
167
|
puts "Unable to fetch nodes from Kubernetes API. Is the cluster online?"
|
166
168
|
end
|
167
169
|
|
170
|
+
# Deleting nodes defined in the config file just in case there are leftovers i.e. nodes that
|
171
|
+
# were not part of the cluster for some reason
|
172
|
+
|
173
|
+
threads = all_servers.map do |server|
|
174
|
+
Thread.new do
|
175
|
+
Hetzner::Server.new(hetzner_client: hetzner_client, cluster_name: cluster_name).delete(server_name: server["name"])
|
176
|
+
end
|
177
|
+
end
|
178
|
+
|
179
|
+
threads.each(&:join) unless threads.empty?
|
180
|
+
|
168
181
|
puts
|
169
182
|
|
170
183
|
sleep 5 # give time for the servers to actually be deleted
|
@@ -182,12 +195,12 @@ class Cluster
|
|
182
195
|
Hetzner::SSHKey.new(
|
183
196
|
hetzner_client: hetzner_client,
|
184
197
|
cluster_name: cluster_name
|
185
|
-
).delete
|
198
|
+
).delete(ssh_key_path: ssh_key_path)
|
186
199
|
|
187
200
|
Hetzner::LoadBalancer.new(
|
188
201
|
hetzner_client: hetzner_client,
|
189
202
|
cluster_name: cluster_name
|
190
|
-
).delete
|
203
|
+
).delete(ha: (masters.size > 1))
|
191
204
|
|
192
205
|
end
|
193
206
|
|
@@ -216,6 +229,7 @@ class Cluster
|
|
216
229
|
|
217
230
|
def master_script(master)
|
218
231
|
server = master == first_master ? " --cluster-init " : " --server https://#{first_master_private_ip}:6443 "
|
232
|
+
flannel_interface = find_flannel_interface(master)
|
219
233
|
|
220
234
|
<<~EOF
|
221
235
|
curl -sfL https://get.k3s.io | INSTALL_K3S_VERSION="#{k3s_version}" K3S_TOKEN="#{k3s_token}" INSTALL_K3S_EXEC="server \
|
@@ -242,7 +256,9 @@ class Cluster
|
|
242
256
|
EOF
|
243
257
|
end
|
244
258
|
|
245
|
-
def worker_script
|
259
|
+
def worker_script(worker)
|
260
|
+
flannel_interface = find_flannel_interface(worker)
|
261
|
+
|
246
262
|
<<~EOF
|
247
263
|
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 \
|
248
264
|
--node-name="$(hostname -f)" \
|
@@ -277,7 +293,7 @@ class Cluster
|
|
277
293
|
end
|
278
294
|
end
|
279
295
|
|
280
|
-
threads.each(&:join)
|
296
|
+
threads.each(&:join) unless threads.empty?
|
281
297
|
end
|
282
298
|
|
283
299
|
threads = workers.map do |worker|
|
@@ -285,14 +301,14 @@ class Cluster
|
|
285
301
|
puts
|
286
302
|
puts "Deploying k3s to worker (#{worker["name"]})..."
|
287
303
|
|
288
|
-
ssh worker, worker_script, print_output: true
|
304
|
+
ssh worker, worker_script(worker), print_output: true
|
289
305
|
|
290
306
|
puts
|
291
307
|
puts "...k3s has been deployed to worker (#{worker["name"]})."
|
292
308
|
end
|
293
309
|
end
|
294
310
|
|
295
|
-
threads.each(&:join)
|
311
|
+
threads.each(&:join) unless threads.empty?
|
296
312
|
end
|
297
313
|
|
298
314
|
def deploy_cloud_controller_manager
|
@@ -429,17 +445,19 @@ class Cluster
|
|
429
445
|
end
|
430
446
|
|
431
447
|
def wait_for_ssh(server)
|
432
|
-
|
448
|
+
Timeout::timeout(5) do
|
449
|
+
server_name = server["name"]
|
433
450
|
|
434
|
-
|
451
|
+
puts "Waiting for server #{server_name} to be up..."
|
435
452
|
|
436
|
-
|
437
|
-
|
438
|
-
|
439
|
-
|
453
|
+
loop do
|
454
|
+
result = ssh(server, "echo UP")
|
455
|
+
break if result == "UP"
|
456
|
+
end
|
440
457
|
|
441
|
-
|
442
|
-
|
458
|
+
puts "...server #{server_name} is now up."
|
459
|
+
end
|
460
|
+
rescue Errno::ENETUNREACH, Errno::EHOSTUNREACH, Timeout::Error
|
443
461
|
retry
|
444
462
|
end
|
445
463
|
|
@@ -447,20 +465,23 @@ class Cluster
|
|
447
465
|
public_ip = server.dig("public_net", "ipv4", "ip")
|
448
466
|
output = ""
|
449
467
|
|
450
|
-
Net::SSH.start(public_ip, "root") do |session|
|
468
|
+
Net::SSH.start(public_ip, "root", verify_host_key: (verify_host_key ? :always : :never)) do |session|
|
451
469
|
session.exec!(command) do |channel, stream, data|
|
452
470
|
output << data
|
453
471
|
puts data if print_output
|
454
472
|
end
|
455
473
|
end
|
456
|
-
|
457
474
|
output.chop
|
458
|
-
rescue Net::SSH::ConnectionTimeout
|
459
|
-
retry
|
460
475
|
rescue Net::SSH::Disconnect => e
|
461
476
|
retry unless e.message =~ /Too many authentication failures/
|
462
|
-
rescue Errno::ECONNREFUSED
|
477
|
+
rescue Net::SSH::ConnectionTimeout, Errno::ECONNREFUSED, Errno::ENETUNREACH, Errno::EHOSTUNREACH
|
463
478
|
retry
|
479
|
+
rescue Net::SSH::HostKeyMismatch
|
480
|
+
puts
|
481
|
+
puts "Cannot continue: Unable to SSH into server with IP #{public_ip} because the existing fingerprint in the known_hosts file does not match that of the actual host key."
|
482
|
+
puts "This is due to a security check but can also happen when creating a new server that gets assigned the same IP address as another server you've owned in the past."
|
483
|
+
puts "If are sure no security is being violated here and you're just creating new servers, you can eiher remove the relevant lines from your known_hosts (see IPs from the cloud console) or disable host key verification by setting the option 'verify_host_key' to false in the configuration file for the cluster."
|
484
|
+
exit 1
|
464
485
|
end
|
465
486
|
|
466
487
|
def kubernetes_client
|
@@ -471,14 +492,11 @@ class Cluster
|
|
471
492
|
@kubernetes_client = K8s::Client.config(K8s::Config.new(config_hash))
|
472
493
|
end
|
473
494
|
|
474
|
-
def find_flannel_interface(
|
475
|
-
|
476
|
-
when "cp"
|
477
|
-
"enp7s0"
|
478
|
-
when "cc"
|
479
|
-
"enp7s0"
|
480
|
-
when "cx"
|
495
|
+
def find_flannel_interface(server)
|
496
|
+
if ssh(server, "lscpu | grep Vendor") =~ /Intel/
|
481
497
|
"ens10"
|
498
|
+
else
|
499
|
+
"enp7s0"
|
482
500
|
end
|
483
501
|
end
|
484
502
|
|
data/lib/hetzner/k3s/version.rb
CHANGED
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: hetzner-k3s
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.3.2
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Vito Botta
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date: 2021-08-
|
11
|
+
date: 2021-08-15 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: thor
|