hetzner-k3s 0.4.3 → 0.4.7
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/Gemfile.lock +6 -6
- data/README.md +30 -4
- data/bin/build.sh +3 -3
- data/lib/hetzner/infra/network.rb +4 -3
- data/lib/hetzner/infra/placement_group.rb +55 -0
- data/lib/hetzner/infra/server.rb +16 -5
- data/lib/hetzner/k3s/cli.rb +1 -1
- data/lib/hetzner/k3s/cluster.rb +54 -31
- data/lib/hetzner/k3s/version.rb +1 -1
- metadata +3 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 0e4c46ae74be52152443a9fab3b7b7dd00fa5e7cae5dffb99457b522646103ba
|
4
|
+
data.tar.gz: e2495e717211c2351c2e203bd2741d3079e84729c305e8c6ad7434c24cf3d989
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: c82cd916cdde9cec408d0681dc7d46902309bd1246424c8267ed2164345aef54e8e6eca8730c668b146245129450e1daabe97a698e2aca165e7f2a63cb9e5b0d
|
7
|
+
data.tar.gz: 857abb763f92c64a65eeb2d484528708060db50aa121d7ccad847a5f6cbc2fec3c8d9a3873e8568ba2f03ea76ab53b529bc50267271d235b369d69e67d6c4f5a
|
data/Gemfile.lock
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
PATH
|
2
2
|
remote: .
|
3
3
|
specs:
|
4
|
-
hetzner-k3s (0.4.
|
4
|
+
hetzner-k3s (0.4.5)
|
5
5
|
bcrypt_pbkdf
|
6
6
|
ed25519
|
7
7
|
http
|
@@ -20,12 +20,12 @@ GEM
|
|
20
20
|
diff-lcs (1.4.4)
|
21
21
|
domain_name (0.5.20190701)
|
22
22
|
unf (>= 0.0.5, < 1.0.0)
|
23
|
-
dry-configurable (0.
|
23
|
+
dry-configurable (0.13.0)
|
24
24
|
concurrent-ruby (~> 1.0)
|
25
|
-
dry-core (~> 0.
|
26
|
-
dry-container (0.
|
25
|
+
dry-core (~> 0.6)
|
26
|
+
dry-container (0.9.0)
|
27
27
|
concurrent-ruby (~> 1.0)
|
28
|
-
dry-configurable (~> 0.
|
28
|
+
dry-configurable (~> 0.13, >= 0.13.0)
|
29
29
|
dry-core (0.7.1)
|
30
30
|
concurrent-ruby (~> 1.0)
|
31
31
|
dry-equalizer (0.3.0)
|
@@ -99,7 +99,7 @@ GEM
|
|
99
99
|
to_regexp (0.2.1)
|
100
100
|
unf (0.1.4)
|
101
101
|
unf_ext
|
102
|
-
unf_ext (0.0.
|
102
|
+
unf_ext (0.0.8)
|
103
103
|
yajl-ruby (1.4.1)
|
104
104
|
yaml-safe_load_stream (0.1.1)
|
105
105
|
|
data/README.md
CHANGED
@@ -2,7 +2,7 @@
|
|
2
2
|
|
3
3
|
This is a CLI tool - based on a Ruby gem - to quickly create and manage Kubernetes clusters in [Hetzner Cloud](https://www.hetzner.com/cloud) using the lightweight Kubernetes distribution [k3s](https://k3s.io/) from [Rancher](https://rancher.com/).
|
4
4
|
|
5
|
-
Hetzner Cloud is an awesome cloud provider which offers a truly great service with the best performance/cost ratio in the market.
|
5
|
+
Hetzner Cloud is an awesome cloud provider which offers a truly great service with the best performance/cost ratio in the market. With Hetzner's Cloud Controller Manager and CSI driver you can provision load balancers and persistent volumes very easily.
|
6
6
|
|
7
7
|
k3s is my favorite Kubernetes distribution now because it uses much less memory and CPU, leaving more resources to workloads. It is also super quick to deploy because it's a single binary.
|
8
8
|
|
@@ -38,7 +38,7 @@ This will install the `hetzner-k3s` executable in your PATH.
|
|
38
38
|
Alternatively, if you don't want to set up a Ruby runtime but have Docker installed, you can use a container. Run the following from inside the directory where you have the config file for the cluster (described in the next section):
|
39
39
|
|
40
40
|
```bash
|
41
|
-
docker run --rm -it -v ${PWD}:/cluster -v ${HOME}/.ssh:/tmp/.ssh vitobotta/hetzner-k3s:v0.4.
|
41
|
+
docker run --rm -it -v ${PWD}:/cluster -v ${HOME}/.ssh:/tmp/.ssh vitobotta/hetzner-k3s:v0.4.7 create-cluster --config-file /cluster/test.yaml
|
42
42
|
```
|
43
43
|
|
44
44
|
Replace `test.yaml` with the name of your config file.
|
@@ -86,7 +86,8 @@ If you set `masters.instance_count` to 1 then the tool will create a non highly
|
|
86
86
|
|
87
87
|
You can specify any number of worker node pools for example to have mixed nodes with different specs for different workloads.
|
88
88
|
|
89
|
-
At the moment Hetzner Cloud has
|
89
|
+
At the moment Hetzner Cloud has four locations: two in Germany (`nbg1`, Nuremberg and `fsn1`, Falkensteing), one in Finland (`hel1`, Helsinki) and one in the USA (`ash`, Ashburn, Virginia). Please note that the Ashburn, Virginia location has just
|
90
|
+
been announced and it's limited to AMD instances for now.
|
90
91
|
|
91
92
|
For the available instance types and their specs, either check from inside a project when adding a server manually or run the following with your Hetzner token:
|
92
93
|
|
@@ -96,8 +97,20 @@ curl \
|
|
96
97
|
'https://api.hetzner.cloud/v1/server_types'
|
97
98
|
```
|
98
99
|
|
100
|
+
By default, the image in use is Ubuntu 20.04, but you can specify an image to use with the `image` config option. This makes it also possible
|
101
|
+
to use a snapshot that you have already created from and existing server (for example to preinstall some tools). If you want to use a custom
|
102
|
+
snapshot you'll need to specify the **ID** of the snapshot/image, not the description you gave when you created the template server. To find
|
103
|
+
the ID of your custom image/snapshot, run:
|
99
104
|
|
100
|
-
|
105
|
+
```bash
|
106
|
+
curl \
|
107
|
+
-H "Authorization: Bearer $API_TOKEN" \
|
108
|
+
'https://api.hetzner.cloud/v1/images'
|
109
|
+
```
|
110
|
+
|
111
|
+
Note that if you use a custom image, the creation of the servers may take longer than when using the default image.
|
112
|
+
|
113
|
+
Also 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.
|
101
114
|
|
102
115
|
Finally, to create the cluster run:
|
103
116
|
|
@@ -241,6 +254,19 @@ I recommend that you create a separate Hetzner project for each cluster, because
|
|
241
254
|
|
242
255
|
## changelog
|
243
256
|
|
257
|
+
- 0.4.7
|
258
|
+
- Made it possible to specify a custom image/snapshot for the servers
|
259
|
+
|
260
|
+
- 0.4.6
|
261
|
+
- Added a check to abort gracefully when for some reason one or more servers are not created, for example due to temporary problems with the Hetzner API.
|
262
|
+
|
263
|
+
- 0.4.5
|
264
|
+
- Fix network creation (bug introduced in the previous version)
|
265
|
+
|
266
|
+
- 0.4.4
|
267
|
+
- Add support for the new Ashburn, Virginia (USA) location
|
268
|
+
- Automatically use a placement group so that the instances are all created on different physical hosts for high availability
|
269
|
+
|
244
270
|
- 0.4.3
|
245
271
|
- Fix an issue with SSH key creation
|
246
272
|
|
data/bin/build.sh
CHANGED
@@ -6,9 +6,9 @@ set -e
|
|
6
6
|
|
7
7
|
IMAGE="vitobotta/hetzner-k3s"
|
8
8
|
|
9
|
-
docker build -t ${IMAGE}:v0.4.
|
9
|
+
docker build -t ${IMAGE}:v0.4.7 \
|
10
10
|
--platform=linux/amd64 \
|
11
|
-
--cache-from ${IMAGE}:v0.4.
|
11
|
+
--cache-from ${IMAGE}:v0.4.6 \
|
12
12
|
--build-arg BUILDKIT_INLINE_CACHE=1 .
|
13
13
|
|
14
|
-
docker push vitobotta/hetzner-k3s:v0.4.
|
14
|
+
docker push vitobotta/hetzner-k3s:v0.4.7
|
@@ -5,7 +5,8 @@ module Hetzner
|
|
5
5
|
@cluster_name = cluster_name
|
6
6
|
end
|
7
7
|
|
8
|
-
def create
|
8
|
+
def create(location:)
|
9
|
+
@location = location
|
9
10
|
puts
|
10
11
|
|
11
12
|
if network = find_network
|
@@ -38,7 +39,7 @@ module Hetzner
|
|
38
39
|
|
39
40
|
private
|
40
41
|
|
41
|
-
attr_reader :hetzner_client, :cluster_name
|
42
|
+
attr_reader :hetzner_client, :cluster_name, :location
|
42
43
|
|
43
44
|
def network_config
|
44
45
|
{
|
@@ -47,7 +48,7 @@ module Hetzner
|
|
47
48
|
subnets: [
|
48
49
|
{
|
49
50
|
ip_range: "10.0.0.0/16",
|
50
|
-
network_zone: "eu-central",
|
51
|
+
network_zone: (location == "ash" ? "us-east" : "eu-central"),
|
51
52
|
type: "cloud"
|
52
53
|
}
|
53
54
|
]
|
@@ -0,0 +1,55 @@
|
|
1
|
+
module Hetzner
|
2
|
+
class PlacementGroup
|
3
|
+
def initialize(hetzner_client:, cluster_name:)
|
4
|
+
@hetzner_client = hetzner_client
|
5
|
+
@cluster_name = cluster_name
|
6
|
+
end
|
7
|
+
|
8
|
+
def create
|
9
|
+
puts
|
10
|
+
|
11
|
+
if (placement_group = find_placement_group)
|
12
|
+
puts "Placement group already exists, skipping."
|
13
|
+
puts
|
14
|
+
return placement_group["id"]
|
15
|
+
end
|
16
|
+
|
17
|
+
puts "Creating placement group..."
|
18
|
+
|
19
|
+
response = hetzner_client.post("/placement_groups", placement_group_config).body
|
20
|
+
|
21
|
+
puts "...placement group created."
|
22
|
+
puts
|
23
|
+
|
24
|
+
JSON.parse(response)["placement_group"]["id"]
|
25
|
+
end
|
26
|
+
|
27
|
+
def delete
|
28
|
+
if (placement_group = find_placement_group)
|
29
|
+
puts "Deleting placement group..."
|
30
|
+
hetzner_client.delete("/placement_groups", placement_group["id"])
|
31
|
+
puts "...placement group deleted."
|
32
|
+
else
|
33
|
+
puts "Placement group no longer exists, skipping."
|
34
|
+
end
|
35
|
+
|
36
|
+
puts
|
37
|
+
end
|
38
|
+
|
39
|
+
private
|
40
|
+
|
41
|
+
attr_reader :hetzner_client, :cluster_name
|
42
|
+
|
43
|
+
def placement_group_config
|
44
|
+
{
|
45
|
+
name: cluster_name,
|
46
|
+
type: "spread"
|
47
|
+
}
|
48
|
+
end
|
49
|
+
|
50
|
+
def find_placement_group
|
51
|
+
hetzner_client.get("/placement_groups")["placement_groups"].detect{ |placement_group| placement_group["name"] == cluster_name }
|
52
|
+
end
|
53
|
+
|
54
|
+
end
|
55
|
+
end
|
data/lib/hetzner/infra/server.rb
CHANGED
@@ -5,7 +5,7 @@ module Hetzner
|
|
5
5
|
@cluster_name = cluster_name
|
6
6
|
end
|
7
7
|
|
8
|
-
def create(location:, instance_type:, instance_id:, firewall_id:, network_id:, ssh_key_id:)
|
8
|
+
def create(location:, instance_type:, instance_id:, firewall_id:, network_id:, ssh_key_id:, placement_group_id:, image:)
|
9
9
|
puts
|
10
10
|
|
11
11
|
server_name = "#{cluster_name}-#{instance_type}-#{instance_id}"
|
@@ -21,7 +21,7 @@ module Hetzner
|
|
21
21
|
server_config = {
|
22
22
|
name: server_name,
|
23
23
|
location: location,
|
24
|
-
image:
|
24
|
+
image: image,
|
25
25
|
firewalls: [
|
26
26
|
{ firewall: firewall_id }
|
27
27
|
],
|
@@ -36,15 +36,26 @@ module Hetzner
|
|
36
36
|
labels: {
|
37
37
|
cluster: cluster_name,
|
38
38
|
role: (server_name =~ /master/ ? "master" : "worker")
|
39
|
-
}
|
39
|
+
},
|
40
|
+
placement_group: placement_group_id
|
40
41
|
}
|
41
42
|
|
42
|
-
response = hetzner_client.post("/servers", server_config)
|
43
|
+
response = hetzner_client.post("/servers", server_config)
|
44
|
+
response_body = response.body
|
45
|
+
|
46
|
+
server = JSON.parse(response_body)["server"]
|
47
|
+
|
48
|
+
unless server
|
49
|
+
puts "Error creating server #{server_name}. Response details below:"
|
50
|
+
puts
|
51
|
+
p response
|
52
|
+
return
|
53
|
+
end
|
43
54
|
|
44
55
|
puts "...server #{server_name} created."
|
45
56
|
puts
|
46
57
|
|
47
|
-
|
58
|
+
server
|
48
59
|
end
|
49
60
|
|
50
61
|
def delete(server_name:)
|
data/lib/hetzner/k3s/cli.rb
CHANGED
@@ -193,7 +193,7 @@ module Hetzner
|
|
193
193
|
|
194
194
|
def validate_location
|
195
195
|
return if locations.empty? && !valid_token?
|
196
|
-
errors << "Invalid location - available locations: nbg1 (Nuremberg, Germany), fsn1 (Falkenstein, Germany), hel1 (Helsinki, Finland)" unless locations.include? configuration.dig("location")
|
196
|
+
errors << "Invalid location - available locations: nbg1 (Nuremberg, Germany), fsn1 (Falkenstein, Germany), hel1 (Helsinki, Finland) or ash (Ashburn, Virginia, USA)" unless locations.include? configuration.dig("location")
|
197
197
|
end
|
198
198
|
|
199
199
|
def find_available_releases
|
data/lib/hetzner/k3s/cluster.rb
CHANGED
@@ -11,6 +11,7 @@ require_relative "../infra/network"
|
|
11
11
|
require_relative "../infra/ssh_key"
|
12
12
|
require_relative "../infra/server"
|
13
13
|
require_relative "../infra/load_balancer"
|
14
|
+
require_relative "../infra/placement_group"
|
14
15
|
|
15
16
|
require_relative "../k3s/client_patch"
|
16
17
|
|
@@ -89,6 +90,11 @@ class Cluster
|
|
89
90
|
master_instance_type = masters_config["instance_type"]
|
90
91
|
masters_count = masters_config["instance_count"]
|
91
92
|
|
93
|
+
placement_group_id = Hetzner::PlacementGroup.new(
|
94
|
+
hetzner_client: hetzner_client,
|
95
|
+
cluster_name: cluster_name
|
96
|
+
).create
|
97
|
+
|
92
98
|
firewall_id = Hetzner::Firewall.new(
|
93
99
|
hetzner_client: hetzner_client,
|
94
100
|
cluster_name: cluster_name
|
@@ -97,7 +103,7 @@ class Cluster
|
|
97
103
|
network_id = Hetzner::Network.new(
|
98
104
|
hetzner_client: hetzner_client,
|
99
105
|
cluster_name: cluster_name
|
100
|
-
).create
|
106
|
+
).create(location: location)
|
101
107
|
|
102
108
|
ssh_key_id = Hetzner::SSHKey.new(
|
103
109
|
hetzner_client: hetzner_client,
|
@@ -113,7 +119,9 @@ class Cluster
|
|
113
119
|
instance_id: "master#{i+1}",
|
114
120
|
firewall_id: firewall_id,
|
115
121
|
network_id: network_id,
|
116
|
-
ssh_key_id: ssh_key_id
|
122
|
+
ssh_key_id: ssh_key_id,
|
123
|
+
placement_group_id: placement_group_id,
|
124
|
+
image: image
|
117
125
|
}
|
118
126
|
end
|
119
127
|
|
@@ -136,7 +144,9 @@ class Cluster
|
|
136
144
|
instance_id: "pool-#{worker_node_pool_name}-worker#{i+1}",
|
137
145
|
firewall_id: firewall_id,
|
138
146
|
network_id: network_id,
|
139
|
-
ssh_key_id: ssh_key_id
|
147
|
+
ssh_key_id: ssh_key_id,
|
148
|
+
placement_group_id: placement_group_id,
|
149
|
+
image: image
|
140
150
|
}
|
141
151
|
end
|
142
152
|
end
|
@@ -149,6 +159,10 @@ class Cluster
|
|
149
159
|
|
150
160
|
threads.each(&:join) unless threads.empty?
|
151
161
|
|
162
|
+
while servers.size != server_configs.size
|
163
|
+
sleep 1
|
164
|
+
end
|
165
|
+
|
152
166
|
puts
|
153
167
|
threads = servers.map do |server|
|
154
168
|
Thread.new { wait_for_ssh server }
|
@@ -158,6 +172,11 @@ class Cluster
|
|
158
172
|
end
|
159
173
|
|
160
174
|
def delete_resources
|
175
|
+
Hetzner::PlacementGroup.new(
|
176
|
+
hetzner_client: hetzner_client,
|
177
|
+
cluster_name: cluster_name
|
178
|
+
).delete
|
179
|
+
|
161
180
|
Hetzner::LoadBalancer.new(
|
162
181
|
hetzner_client: hetzner_client,
|
163
182
|
cluster_name: cluster_name
|
@@ -217,28 +236,28 @@ class Cluster
|
|
217
236
|
taint = schedule_workloads_on_masters? ? " " : " --node-taint CriticalAddonsOnly=true:NoExecute "
|
218
237
|
|
219
238
|
<<~EOF
|
220
|
-
|
221
|
-
|
222
|
-
|
223
|
-
|
224
|
-
|
225
|
-
|
226
|
-
|
227
|
-
|
228
|
-
|
229
|
-
|
230
|
-
|
231
|
-
|
232
|
-
|
233
|
-
|
234
|
-
|
235
|
-
|
236
|
-
|
237
|
-
|
238
|
-
|
239
|
-
|
240
|
-
|
241
|
-
|
239
|
+
curl -sfL https://get.k3s.io | INSTALL_K3S_VERSION="#{k3s_version}" K3S_TOKEN="#{k3s_token}" INSTALL_K3S_EXEC="server \
|
240
|
+
--disable-cloud-controller \
|
241
|
+
--disable servicelb \
|
242
|
+
--disable traefik \
|
243
|
+
--disable local-storage \
|
244
|
+
--disable metrics-server \
|
245
|
+
--write-kubeconfig-mode=644 \
|
246
|
+
--node-name="$(hostname -f)" \
|
247
|
+
--cluster-cidr=10.244.0.0/16 \
|
248
|
+
--etcd-expose-metrics=true \
|
249
|
+
--kube-controller-manager-arg="address=0.0.0.0" \
|
250
|
+
--kube-controller-manager-arg="bind-address=0.0.0.0" \
|
251
|
+
--kube-proxy-arg="metrics-bind-address=0.0.0.0" \
|
252
|
+
--kube-scheduler-arg="address=0.0.0.0" \
|
253
|
+
--kube-scheduler-arg="bind-address=0.0.0.0" \
|
254
|
+
#{taint} \
|
255
|
+
--kubelet-arg="cloud-provider=external" \
|
256
|
+
--advertise-address=$(hostname -I | awk '{print $2}') \
|
257
|
+
--node-ip=$(hostname -I | awk '{print $2}') \
|
258
|
+
--node-external-ip=$(hostname -I | awk '{print $1}') \
|
259
|
+
--flannel-iface=#{flannel_interface} \
|
260
|
+
#{server} #{tls_sans}" sh -
|
242
261
|
EOF
|
243
262
|
end
|
244
263
|
|
@@ -246,12 +265,12 @@ class Cluster
|
|
246
265
|
flannel_interface = find_flannel_interface(worker)
|
247
266
|
|
248
267
|
<<~EOF
|
249
|
-
|
250
|
-
|
251
|
-
|
252
|
-
|
253
|
-
|
254
|
-
|
268
|
+
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 \
|
269
|
+
--node-name="$(hostname -f)" \
|
270
|
+
--kubelet-arg="cloud-provider=external" \
|
271
|
+
--node-ip=$(hostname -I | awk '{print $2}') \
|
272
|
+
--node-external-ip=$(hostname -I | awk '{print $1}') \
|
273
|
+
--flannel-iface=#{flannel_interface}" sh -
|
255
274
|
EOF
|
256
275
|
end
|
257
276
|
|
@@ -638,4 +657,8 @@ class Cluster
|
|
638
657
|
schedule_workloads_on_masters ? !!schedule_workloads_on_masters : false
|
639
658
|
end
|
640
659
|
|
660
|
+
def image
|
661
|
+
configuration.dig("image") || "ubuntu-20.04"
|
662
|
+
end
|
663
|
+
|
641
664
|
end
|
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.
|
4
|
+
version: 0.4.7
|
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-
|
11
|
+
date: 2021-11-13 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: thor
|
@@ -140,6 +140,7 @@ files:
|
|
140
140
|
- lib/hetzner/infra/firewall.rb
|
141
141
|
- lib/hetzner/infra/load_balancer.rb
|
142
142
|
- lib/hetzner/infra/network.rb
|
143
|
+
- lib/hetzner/infra/placement_group.rb
|
143
144
|
- lib/hetzner/infra/server.rb
|
144
145
|
- lib/hetzner/infra/ssh_key.rb
|
145
146
|
- lib/hetzner/k3s/cli.rb
|