hetzner-k3s 0.5.6 → 0.5.9

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 59601611401c4cb51de78a2890261cd05a1c026479c3e33fcf351f0b3ffb93d1
4
- data.tar.gz: 46c62ab747e5a43e61b271c8de150ef4a29ed496128725cf417d950cd351bc49
3
+ metadata.gz: 65fe7fa7e6bdeeb49175cfb92184ab320ef8ac40a3aa03f921f1e5fda1b172d7
4
+ data.tar.gz: 1448919374ef90f7614580251b28000c470b0ed7fba39724e8953f29f176c1bb
5
5
  SHA512:
6
- metadata.gz: 5be97d9b3445445a81fd7f5f4387c2503a10acedb3000acb78473ce8a9d8793149a84a2354a24a6de9996a19eb9288c6989a9fa28d81bf5729b753b144c28d07
7
- data.tar.gz: 41e54dc8f26485c5c4ed431301a844a727f31bc6672d9e829f3c95b23d687dcae19eb157017a56f72c3ec3d97303034575c5b353d8a82d17b8c8f56deff17366
6
+ metadata.gz: 15f783adb7408a1df5c3e9a114d9dcbc61830ead33acb7bc162ca935b6cdcfe26507e2c404d48c831cae600fe765adc31e686b511c4f675c561d6eb9acabafd2
7
+ data.tar.gz: df10039025e735d6a73c84fb60516d70bc0aa48220db1bab9d99dd30caad87339ba6be83e2d3f1029f40f95719a6fcde5784956c86ef20d524b67d504f93920a
data/.rubocop.yml CHANGED
@@ -1,4 +1,4 @@
1
- Gemspec/DateAssignment: # new in 1.10
1
+ Gemspec/DeprecatedAttributeAssignment: # new in 1.10
2
2
  Enabled: true
3
3
  Gemspec/RequireMFA: # new in 1.23
4
4
  Enabled: true
@@ -119,3 +119,17 @@ Metrics/ParameterLists:
119
119
  Style/FrozenStringLiteralComment:
120
120
  Exclude:
121
121
  - exe/hetzner-k3s
122
+ Lint/RefinementImportMethods: # new in 1.27
123
+ Enabled: true
124
+ Security/CompoundHash: # new in 1.28
125
+ Enabled: true
126
+ Style/EnvHome: # new in 1.29
127
+ Enabled: true
128
+ Style/FetchEnvVar: # new in 1.28
129
+ Enabled: true
130
+ Style/NestedFileDirname: # new in 1.26
131
+ Enabled: true
132
+ Style/ObjectThen: # new in 1.28
133
+ Enabled: true
134
+ Style/RedundantInitialize: # new in 1.27
135
+ Enabled: true
data/.ruby-version CHANGED
@@ -1 +1 @@
1
- 3.1.0
1
+ ruby-3.1.2
data/Dockerfile CHANGED
@@ -1,4 +1,4 @@
1
- FROM ruby:3.1.0-alpine
1
+ FROM ruby:3.1.2-alpine
2
2
 
3
3
  RUN apk update --no-cache \
4
4
  && apk add build-base git openssh-client curl bash
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- hetzner-k3s (0.5.6)
4
+ hetzner-k3s (0.5.7)
5
5
  bcrypt_pbkdf
6
6
  ed25519
7
7
  http
@@ -30,7 +30,7 @@ GEM
30
30
  http-cookie (~> 1.0)
31
31
  http-form_data (~> 2.2)
32
32
  http-parser (~> 1.2.0)
33
- http-cookie (1.0.4)
33
+ http-cookie (1.0.5)
34
34
  domain_name (~> 0.5)
35
35
  http-form_data (2.3.0)
36
36
  http-parser (1.2.3)
@@ -39,7 +39,7 @@ GEM
39
39
  parallel (1.21.0)
40
40
  parser (3.1.0.0)
41
41
  ast (~> 2.4.1)
42
- public_suffix (4.0.6)
42
+ public_suffix (4.0.7)
43
43
  rainbow (3.1.1)
44
44
  rake (12.3.3)
45
45
  regexp_parser (2.2.0)
@@ -74,7 +74,7 @@ GEM
74
74
  thor (1.2.1)
75
75
  unf (0.1.4)
76
76
  unf_ext
77
- unf_ext (0.0.8)
77
+ unf_ext (0.0.8.2)
78
78
  unicode-display_width (2.1.0)
79
79
 
80
80
  PLATFORMS
@@ -87,4 +87,4 @@ DEPENDENCIES
87
87
  rubocop
88
88
 
89
89
  BUNDLED WITH
90
- 2.3.4
90
+ 2.3.14
data/README.md CHANGED
@@ -16,6 +16,8 @@ Using this tool, creating a highly available k3s cluster with 3 masters for the
16
16
 
17
17
  See roadmap [here](https://github.com/vitobotta/hetzner-k3s/projects/1) for the features planned or in progress.
18
18
 
19
+ Also see this [wiki page](https://github.com/vitobotta/hetzner-k3s/wiki/Tutorial:---Setting-up-a-cluster) for a tutorial on how to set up a cluster with the most common setup to get you started.
20
+
19
21
  ## Requirements
20
22
 
21
23
  All that is needed to use this tool is
@@ -26,7 +28,7 @@ All that is needed to use this tool is
26
28
 
27
29
  ## Installation
28
30
 
29
- Once you have the Ruby runtime up and running (3.1.0 or newer), you just need to install the gem:
31
+ Once you have the Ruby runtime up and running (3.1.2 or newer), you just need to install the gem:
30
32
 
31
33
  ```bash
32
34
  gem install hetzner-k3s
@@ -39,7 +41,12 @@ This will install the `hetzner-k3s` executable in your PATH.
39
41
  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):
40
42
 
41
43
  ```bash
42
- docker run --rm -it -v ${PWD}:/cluster -v ${HOME}/.ssh:/tmp/.ssh vitobotta/hetzner-k3s:v0.5.6 create-cluster --config-file /cluster/test.yaml
44
+ docker run --rm -it \
45
+ -v ${PWD}:/cluster \
46
+ -v ${HOME}/.ssh:/tmp/.ssh \
47
+ vitobotta/hetzner-k3s:v0.5.9 \
48
+ create-cluster \
49
+ --config-file /cluster/test.yaml
43
50
  ```
44
51
 
45
52
  Replace `test.yaml` with the name of your config file.
@@ -58,6 +65,8 @@ public_ssh_key_path: "~/.ssh/id_rsa.pub"
58
65
  private_ssh_key_path: "~/.ssh/id_rsa"
59
66
  ssh_allowed_networks:
60
67
  - 0.0.0.0/0
68
+ api_allowed_networks:
69
+ - 0.0.0.0/0
61
70
  verify_host_key: false
62
71
  location: nbg1
63
72
  schedule_workloads_on_masters: false
@@ -73,6 +82,11 @@ worker_node_pools:
73
82
  instance_count: 2
74
83
  additional_packages:
75
84
  - somepackage
85
+ post_create_commands:
86
+ - apt update
87
+ - apt upgrade -y
88
+ - apt autoremove -y
89
+ - shutdown -r now
76
90
  enable_encryption: true
77
91
  # kube_api_server_args:
78
92
  # - arg1
@@ -92,6 +106,7 @@ enable_encryption: true
92
106
  # kube_proxy_args:
93
107
  # - arg1
94
108
  # - ...
109
+ # existing_network: <specify if you want to use an existing network, otherwise one will be created for this cluster>
95
110
 
96
111
  ```
97
112
 
@@ -131,9 +146,11 @@ curl \
131
146
  'https://api.hetzner.cloud/v1/images'
132
147
  ```
133
148
 
134
- Note that if you use a custom image, the creation of the servers may take longer than when using the default image.
149
+ Notes:
135
150
 
136
- 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.
151
+ - if you use a custom image, the creation of the servers may take longer than when using the default image
152
+ - 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
153
+ - the setting `api_allowed_networks` allows specifying which networks can access the Kubernetes API, but this only works with single master clusters currently. Multi-master HA clusters require a load balancer for the API, but load balancers are not yet covered by Hetzner's firewalls.
137
154
 
138
155
  Finally, to create the cluster run:
139
156
 
@@ -159,6 +176,8 @@ The `create-cluster` command can be run any number of times with the same config
159
176
 
160
177
  To add one or more nodes to a node pool, just change the instance count in the configuration file for that node pool and re-run the create command.
161
178
 
179
+ **Important**: if you are increasing the size of a node pool created prior to v0.5.7, please see [this thread](https://github.com/vitobotta/hetzner-k3s/issues/80).
180
+
162
181
  ### Scaling down a node pool
163
182
 
164
183
  To make a node pool smaller:
@@ -194,7 +213,6 @@ Note that the API server will briefly be unavailable during the upgrade of the c
194
213
 
195
214
  To check the upgrade progress, run `watch kubectl get nodes -owide`. You will see the masters being upgraded one per time, followed by the worker nodes.
196
215
 
197
-
198
216
  ### What to do if the upgrade doesn't go smoothly
199
217
 
200
218
  If the upgrade gets stuck for some reason, or it doesn't upgrade all the nodes:
@@ -220,7 +238,8 @@ I have noticed that sometimes I need to re-run the upgrade command a couple of t
220
238
  You can also check the logs of the system upgrade controller's pod:
221
239
 
222
240
  ```bash
223
- kubectl -n system-upgrade logs -f $(kubectl -n system-upgrade get pod -l pod-template-hash -o jsonpath="{.items[0].metadata.name}")
241
+ kubectl -n system-upgrade \
242
+ logs -f $(kubectl -n system-upgrade get pod -l pod-template-hash -o jsonpath="{.items[0].metadata.name}")
224
243
  ```
225
244
 
226
245
  A final note about upgrades is that if for some reason the upgrade gets stuck after upgrading the masters and before upgrading the worker nodes, just cleaning up the resources as described above might not be enough. In that case also try running the following to tell the upgrade job for the workers that the masters have already been upgraded, so the upgrade can continue for the workers:
@@ -229,6 +248,15 @@ A final note about upgrades is that if for some reason the upgrade gets stuck af
229
248
  kubectl label node <master1> <master2> <master2> plan.upgrade.cattle.io/k3s-server=upgraded
230
249
  ```
231
250
 
251
+ ## Upgrading the OS on nodes
252
+
253
+ - consider adding a temporary node during the process if you don't have enough spare capacity in the cluster
254
+ - drain one node
255
+ - update etc
256
+ - reboot
257
+ - uncordon
258
+ - proceed with the next node
259
+
232
260
  ## Deleting a cluster
233
261
 
234
262
  To delete a cluster, running
@@ -263,7 +291,7 @@ I set `load-balancer.hetzner.cloud/hostname` to a valid hostname that I configur
263
291
 
264
292
  The annotation `load-balancer.hetzner.cloud/use-private-ip: "true"` ensures that the communication between the load balancer and the nodes happens through the private network, so we don't have to open any ports on the nodes (other than the port 6443 for the Kubernetes API server).
265
293
 
266
- The other annotations should be self explanatory. You can find a list of the available annotations here.
294
+ The other annotations should be self explanatory. You can find a list of the available annotations [here](https://pkg.go.dev/github.com/hetznercloud/hcloud-cloud-controller-manager/internal/annotation).
267
295
 
268
296
  ## Persistent volumes
269
297
 
@@ -279,6 +307,10 @@ I recommend that you create a separate Hetzner project for each cluster, because
279
307
 
280
308
  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.
281
309
 
310
+ Contributors:
311
+
312
+ - [TitanFighter](https://github.com/TitanFighter) for [this awesome tutorial](https://github.com/vitobotta/hetzner-k3s/wiki/Tutorial:---Setting-up-a-cluster)
313
+
282
314
  ## License
283
315
 
284
316
  The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
data/bin/build.sh CHANGED
@@ -2,13 +2,11 @@
2
2
 
3
3
  set -e
4
4
 
5
-
6
-
7
5
  IMAGE="vitobotta/hetzner-k3s"
8
6
 
9
- docker build -t ${IMAGE}:v0.5.6 \
7
+ docker build -t ${IMAGE}:v0.5.9 \
10
8
  --platform=linux/amd64 \
11
- --cache-from ${IMAGE}:v0.5.5 \
9
+ --cache-from ${IMAGE}:v0.5.8 \
12
10
  --build-arg BUILDKIT_INLINE_CACHE=1 .
13
11
 
14
- docker push vitobotta/hetzner-k3s:v0.5.6
12
+ docker push vitobotta/hetzner-k3s:v0.5.9
data/hetzner-k3s.gemspec CHANGED
@@ -12,7 +12,7 @@ Gem::Specification.new do |spec|
12
12
  spec.description = 'A CLI to create a Kubernetes cluster in Hetzner Cloud very quickly using k3s.'
13
13
  spec.homepage = 'https://github.com/vitobotta/hetzner-k3s'
14
14
  spec.license = 'MIT'
15
- spec.required_ruby_version = Gem::Requirement.new('>= 3.1.0')
15
+ spec.required_ruby_version = Gem::Requirement.new('>= 3.1.2')
16
16
 
17
17
  # spec.metadata["allowed_push_host"] = "TODO: Set to 'http://mygemserver.com'"
18
18
 
@@ -7,9 +7,10 @@ module Hetzner
7
7
  @cluster_name = cluster_name
8
8
  end
9
9
 
10
- def create(high_availability:, networks:)
10
+ def create(high_availability:, ssh_networks:, api_networks:)
11
11
  @high_availability = high_availability
12
- @networks = networks
12
+ @ssh_networks = ssh_networks
13
+ @api_networks = api_networks
13
14
  puts
14
15
 
15
16
  if (firewall = find_firewall)
@@ -47,7 +48,7 @@ module Hetzner
47
48
 
48
49
  private
49
50
 
50
- attr_reader :hetzner_client, :cluster_name, :firewall, :high_availability, :networks
51
+ attr_reader :hetzner_client, :cluster_name, :firewall, :high_availability, :ssh_networks, :api_networks
51
52
 
52
53
  def create_firewall_config
53
54
  rules = [
@@ -56,7 +57,7 @@ module Hetzner
56
57
  direction: 'in',
57
58
  protocol: 'tcp',
58
59
  port: '22',
59
- source_ips: networks,
60
+ source_ips: ssh_networks,
60
61
  destination_ips: []
61
62
  },
62
63
  {
@@ -98,10 +99,7 @@ module Hetzner
98
99
  direction: 'in',
99
100
  protocol: 'tcp',
100
101
  port: '6443',
101
- source_ips: [
102
- '0.0.0.0/0',
103
- '::/0'
104
- ],
102
+ source_ips: api_networks,
105
103
  destination_ips: []
106
104
  }
107
105
  end
@@ -2,9 +2,10 @@
2
2
 
3
3
  module Hetzner
4
4
  class Network
5
- def initialize(hetzner_client:, cluster_name:)
5
+ def initialize(hetzner_client:, cluster_name:, existing_network:)
6
6
  @hetzner_client = hetzner_client
7
7
  @cluster_name = cluster_name
8
+ @existing_network = existing_network
8
9
  end
9
10
 
10
11
  def create(location:)
@@ -29,9 +30,13 @@ module Hetzner
29
30
 
30
31
  def delete
31
32
  if (network = find_network)
32
- puts 'Deleting network...'
33
- hetzner_client.delete('/networks', network['id'])
34
- puts '...network deleted.'
33
+ if network["name"] == existing_network
34
+ puts "Network existed before cluster, skipping."
35
+ else
36
+ puts 'Deleting network...'
37
+ hetzner_client.delete('/networks', network['id'])
38
+ puts '...network deleted.'
39
+ end
35
40
  else
36
41
  puts 'Network no longer exists, skipping.'
37
42
  end
@@ -39,9 +44,18 @@ module Hetzner
39
44
  puts
40
45
  end
41
46
 
47
+ def find_network
48
+ network_name = existing_network || cluster_name
49
+ hetzner_client.get('/networks')['networks'].detect { |network| network['name'] == network_name }
50
+ end
51
+
52
+ def get
53
+ find_network
54
+ end
55
+
42
56
  private
43
57
 
44
- attr_reader :hetzner_client, :cluster_name, :location
58
+ attr_reader :hetzner_client, :cluster_name, :location, :existing_network
45
59
 
46
60
  def network_config
47
61
  {
@@ -56,9 +70,5 @@ module Hetzner
56
70
  ]
57
71
  }
58
72
  end
59
-
60
- def find_network
61
- hetzner_client.get('/networks')['networks'].detect { |network| network['name'] == cluster_name }
62
- end
63
73
  end
64
74
  end
@@ -7,13 +7,20 @@ module Hetzner
7
7
  @cluster_name = cluster_name
8
8
  end
9
9
 
10
- def create(location:, instance_type:, instance_id:, firewall_id:, network_id:, ssh_key_id:, placement_group_id:, image:, additional_packages: [])
10
+ def create(location:, instance_type:, instance_id:, firewall_id:, network_id:, ssh_key_id:, placement_group_id:, image:, additional_packages: [], additional_post_create_commands: [])
11
+ @location = location
12
+ @instance_type = instance_type
13
+ @instance_id = instance_id
14
+ @firewall_id = firewall_id
15
+ @network_id = network_id
16
+ @ssh_key_id = ssh_key_id
17
+ @placement_group_id = placement_group_id
18
+ @image = image
11
19
  @additional_packages = additional_packages
20
+ @additional_post_create_commands = additional_post_create_commands
12
21
 
13
22
  puts
14
23
 
15
- server_name = "#{cluster_name}-#{instance_type}-#{instance_id}"
16
-
17
24
  if (server = find_server(server_name))
18
25
  puts "Server #{server_name} already exists, skipping."
19
26
  puts
@@ -22,44 +29,16 @@ module Hetzner
22
29
 
23
30
  puts "Creating server #{server_name}..."
24
31
 
25
- server_config = {
26
- name: server_name,
27
- location:,
28
- image:,
29
- firewalls: [
30
- { firewall: firewall_id }
31
- ],
32
- networks: [
33
- network_id
34
- ],
35
- server_type: instance_type,
36
- ssh_keys: [
37
- ssh_key_id
38
- ],
39
- user_data:,
40
- labels: {
41
- cluster: cluster_name,
42
- role: (server_name =~ /master/ ? 'master' : 'worker')
43
- },
44
- placement_group: placement_group_id
45
- }
46
-
47
- response = hetzner_client.post('/servers', server_config)
48
- response_body = response.body
49
-
50
- server = JSON.parse(response_body)['server']
32
+ if (server = make_request)
33
+ puts "...server #{server_name} created."
34
+ puts
51
35
 
52
- unless server
36
+ server
37
+ else
53
38
  puts "Error creating server #{server_name}. Response details below:"
54
39
  puts
55
40
  p response
56
- return
57
41
  end
58
-
59
- puts "...server #{server_name} created."
60
- puts
61
-
62
- server
63
42
  end
64
43
 
65
44
  def delete(server_name:)
@@ -74,30 +53,78 @@ module Hetzner
74
53
 
75
54
  private
76
55
 
77
- attr_reader :hetzner_client, :cluster_name, :additional_packages
56
+ attr_reader :hetzner_client, :cluster_name, :location, :instance_type, :instance_id, :firewall_id, :network_id, :ssh_key_id, :placement_group_id, :image, :additional_packages, :additional_post_create_commands
78
57
 
79
58
  def find_server(server_name)
80
59
  hetzner_client.get('/servers?sort=created:desc')['servers'].detect { |network| network['name'] == server_name }
81
60
  end
82
61
 
83
62
  def user_data
84
- packages = ['fail2ban', 'wireguard']
63
+ packages = %w[fail2ban wireguard]
85
64
  packages += additional_packages if additional_packages
86
65
  packages = "'#{packages.join("', '")}'"
87
66
 
67
+ post_create_commands = [
68
+ 'crontab -l > /etc/cron_bkp',
69
+ 'echo "@reboot echo true > /etc/ready" >> /etc/cron_bkp',
70
+ 'crontab /etc/cron_bkp',
71
+ 'sed -i \'s/[#]*PermitRootLogin yes/PermitRootLogin prohibit-password/g\' /etc/ssh/sshd_config',
72
+ 'sed -i \'s/[#]*PasswordAuthentication yes/PasswordAuthentication no/g\' /etc/ssh/sshd_config',
73
+ 'systemctl restart sshd',
74
+ 'systemctl stop systemd-resolved',
75
+ 'systemctl disable systemd-resolved',
76
+ 'rm /etc/resolv.conf',
77
+ 'echo \'nameserver 1.1.1.1\' > /etc/resolv.conf',
78
+ 'echo \'nameserver 1.0.0.1\' >> /etc/resolv.conf'
79
+ ]
80
+
81
+ post_create_commands += additional_post_create_commands if additional_post_create_commands
82
+
83
+ post_create_commands += ['shutdown -r now'] if post_create_commands.grep(/shutdown|reboot/).grep_v(/@reboot/).empty?
84
+
85
+ post_create_commands = " - #{post_create_commands.join("\n - ")}"
86
+
88
87
  <<~YAML
89
88
  #cloud-config
90
89
  packages: [#{packages}]
91
90
  runcmd:
92
- - sed -i 's/[#]*PermitRootLogin yes/PermitRootLogin prohibit-password/g' /etc/ssh/sshd_config
93
- - sed -i 's/[#]*PasswordAuthentication yes/PasswordAuthentication no/g' /etc/ssh/sshd_config
94
- - systemctl restart sshd
95
- - systemctl stop systemd-resolved
96
- - systemctl disable systemd-resolved
97
- - rm /etc/resolv.conf
98
- - echo "nameserver 1.1.1.1" > /etc/resolv.conf
99
- - echo "nameserver 1.0.0.1" >> /etc/resolv.conf
91
+ #{post_create_commands}
100
92
  YAML
101
93
  end
94
+
95
+ def server_name
96
+ @server_name ||= "#{cluster_name}-#{instance_type}-#{instance_id}"
97
+ end
98
+
99
+ def server_config
100
+ @server_config ||= {
101
+ name: server_name,
102
+ location:,
103
+ image:,
104
+ firewalls: [
105
+ { firewall: firewall_id }
106
+ ],
107
+ networks: [
108
+ network_id
109
+ ],
110
+ server_type: instance_type,
111
+ ssh_keys: [
112
+ ssh_key_id
113
+ ],
114
+ user_data:,
115
+ labels: {
116
+ cluster: cluster_name,
117
+ role: (server_name =~ /master/ ? 'master' : 'worker')
118
+ },
119
+ placement_group: placement_group_id
120
+ }
121
+ end
122
+
123
+ def make_request
124
+ response = hetzner_client.post('/servers', server_config)
125
+ response_body = response.body
126
+
127
+ JSON.parse(response_body)['server']
128
+ end
102
129
  end
103
130
  end