Merge branch 'master' into feat/calico

This commit is contained in:
Karim Naufal 2022-04-13 14:33:33 +02:00 committed by GitHub
commit 8b25815de8
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 252 additions and 109 deletions

View File

@ -21,6 +21,8 @@
## About The Project ## About The Project
_For the moment this README is not synced with the current release, as we currently use the master branch for active development, please find the one that aligns with the latest stable release [here](https://github.com/kube-hetzner/terraform-hcloud-kube-hetzner/tree/v0.1.3)._
[Hetzner Cloud](https://hetzner.com) is a good cloud provider that offers very affordable prices for cloud instances, with data center locations in both Europe and the US. [Hetzner Cloud](https://hetzner.com) is a good cloud provider that offers very affordable prices for cloud instances, with data center locations in both Europe and the US.
The goal of this project is to create an optimal and highly optimized Kubernetes installation that is easily maintained, secure, and automatically upgrades. We aimed for functionality as close as possible to GKE's auto-pilot. The goal of this project is to create an optimal and highly optimized Kubernetes installation that is easily maintained, secure, and automatically upgrades. We aimed for functionality as close as possible to GKE's auto-pilot.
@ -33,9 +35,11 @@ _Please note that we are not affiliated to Hetzner, this is just an open source
- Maintenance free with auto-upgrade to the latest version of MicroOS and k3s. - Maintenance free with auto-upgrade to the latest version of MicroOS and k3s.
- Proper use of the Hetzner private network to minimize latency and remove the need for encryption. - Proper use of the Hetzner private network to minimize latency and remove the need for encryption.
- Automatic HA with the default setting of three control-plane nodes and two agent nodepools. - Automatic HA with the default setting of three control-plane nodes and two agent nodes.
- Ability to add or remove as many nodes as you want while the cluster stays running. - Super-HA: Nodepools for both control-plane and agent nodes can be in different locations.
- Automatic Traefik ingress controller attached to a Hetzner load balancer with proxy protocol turned on. - Possibility to have a single node cluster with a proper ingress controller (Traefik).
- Ability to add nodes and nodepools when the cluster running.
- Traefik ingress controller attached to a Hetzner load balancer with proxy protocol turned on.
- Tons of flexible configuration options to suits all needs. - Tons of flexible configuration options to suits all needs.
_It uses Terraform to deploy as it's easy to use, and Hetzner provides a great [Hetzner Terraform Provider](https://registry.terraform.io/providers/hetznercloud/hcloud/latest/docs)._ _It uses Terraform to deploy as it's easy to use, and Hetzner provides a great [Hetzner Terraform Provider](https://registry.terraform.io/providers/hetznercloud/hcloud/latest/docs)._
@ -52,18 +56,18 @@ Follow those simple steps, and your world's cheapest Kube cluster will be up and
First and foremost, you need to have a Hetzner Cloud account. You can sign up for free [here](https://hetzner.com/cloud/). First and foremost, you need to have a Hetzner Cloud account. You can sign up for free [here](https://hetzner.com/cloud/).
Then you'll need to have [terraform](https://learn.hashicorp.com/tutorials/terraform/install-cli), [kubectl](https://kubernetes.io/docs/tasks/tools/) cli, and [hcloud](<https://github.com/hetznercloud/cli>) the Hetzner cli. The easiest way is to use the [gofish](https://gofi.sh/#install) package manager to install them. Then you'll need to have [terraform](https://learn.hashicorp.com/tutorials/terraform/install-cli), [kubectl](https://kubernetes.io/docs/tasks/tools/) cli, and [hcloud](<https://github.com/hetznercloud/cli>) the Hetzner cli. The easiest way is to use the [homebrew](https://brew.sh/) package manager to install them (available on Linux, Mac, and Windows Linux Subsystem).
```sh ```sh
gofish install terraform brew install terraform
gofish install kubectl brew install kubectl
gofish install hcloud brew install hcloud
``` ```
### 💡 [Do not skip] Creating the terraform.tfvars file ### 💡 [Do not skip] Creating the terraform.tfvars file
1. Create a project in your [Hetzner Cloud Console](https://console.hetzner.cloud/), and go to **Security > API Tokens** of that project to grab the API key. Take note of the key! ✅ 1. Create a project in your [Hetzner Cloud Console](https://console.hetzner.cloud/), and go to **Security > API Tokens** of that project to grab the API key. Take note of the key! ✅
2. Either, generate a passphrase-less ed25519 SSH key-pair for your cluster, unless you already have one that you'd like to use. Take note of the respective paths of your private and public keys. Or, for a key-pair with passphrase or a device like a Yubikey, make sure you have have an SSH agent running and your key is loaded (`ssh-add -L` to verify) and set `private_key = null` in the following step. ✅ 2. Generate a passphrase-less ed25519 SSH key-pair for your cluster, take note of the respective paths of your private and public keys. Or, see our detailed [SSH options](https://github.com/kube-hetzner/kube-hetzner/blob/master/docs/ssh.md). ✅
3. Copy `terraform.tfvars.example` to `terraform.tfvars`, and replace the values from steps 1 and 2. ✅ 3. Copy `terraform.tfvars.example` to `terraform.tfvars`, and replace the values from steps 1 and 2. ✅
4. Make sure you have the latest Terraform version, ideally at least 1.1.0. You can check with `terraform -v`. ✅ 4. Make sure you have the latest Terraform version, ideally at least 1.1.0. You can check with `terraform -v`. ✅
5. (Optional) There are other variables in `terraform.tfvars` that could be customized, like Hetzner region, and the node counts and sizes. 5. (Optional) There are other variables in `terraform.tfvars` that could be customized, like Hetzner region, and the node counts and sizes.
@ -93,9 +97,13 @@ _Once you start with Terraform, it's best not to change the state manually in He
### Scaling Nodes ### Scaling Nodes
To scale the number of nodes up or down, just make sure to properly `kubectl drain` the nodes in question first if scaling down. Then just edit your `terraform.tfvars` and re-apply terraform with `terraform apply -auto-approve`. Two things can be scaled, the number of nodepools or the count of nodes in these nodepools. You have two list of nodepools you can add to in terraform.tfvars, the control plane nodepool list and the agent nodepool list. Both combined cannot exceed 255 nodepools (you extremely unlikely to reach this limit). As for the count of nodes per nodepools, if you raise your limits in Hetzner, you can have up to 64,670 nodes per nodepool (also very unlikely to need that much).
About nodepools, `terraform.tfvars.example` has clear example how to configure them. There are some limitations (to scaling down mainly) that you need to be aware of:
_Once the cluster is created, you can change nodepool count, and even set it to 0 (in the case of the first control-plane nodepool, the minimum is 1), you can also rename a nodepool (if the count is taken to 0), but should not remove a nodepool from the list after the cluster is created. This is due to how subnets and IPs are allocated. The only nodepools you can remove are the ones at the end of each list of nodepools._
_However you can freely add others nodepools the end of the list if you want, and of course increase the node count. You can also decrease the node count, but make sure you drain the node in question before, otherwise it will leave your cluster in a bad state. The only nodepool that needs at least to have a count of 1 always, is the first control-plane nodepool, for obvious reasons._
## High Availability ## High Availability
@ -169,14 +177,9 @@ spec:
<summary>Single-node cluster</summary> <summary>Single-node cluster</summary>
Running a development cluster on a single node, without any high-availability is possible as well. Running a development cluster on a single node, without any high-availability is possible as well. You need one control plane nodepool with a count of 1, and one agent nodepool with a count of 0.
In this case, we don't deploy an external load-balancer, but use [k3s service load balancer](https://rancher.com/docs/k3s/latest/en/networking/#service-load-balancer) on the host itself and open up port 80 & 443 in the firewall.
``` terraform In this case, we don't deploy an external load-balancer, but use the default [k3s service load balancer](https://rancher.com/docs/k3s/latest/en/networking/#service-load-balancer) on the host itself and open up port 80 & 443 in the firewall (done automatically).
control_plane_count = 1
allow_scheduling_on_control_plane = true
agent_nodepools = []
```
</details> </details>

View File

@ -9,14 +9,12 @@ module "agents" {
private_key = var.private_key private_key = var.private_key
additional_public_keys = var.additional_public_keys additional_public_keys = var.additional_public_keys
firewall_ids = [hcloud_firewall.k3s.id] firewall_ids = [hcloud_firewall.k3s.id]
placement_group_id = hcloud_placement_group.k3s.id placement_group_id = var.placement_group_disable ? 0 : element(hcloud_placement_group.agent.*.id, ceil(each.value.index / 10))
location = var.location location = each.value.location
server_type = each.value.server_type server_type = each.value.server_type
ipv4_subnet_id = hcloud_network_subnet.subnet[[for i, v in var.agent_nodepools : i if v.name == each.value.nodepool_name][0] + 2].id ipv4_subnet_id = hcloud_network_subnet.agent[[for i, v in var.agent_nodepools : i if v.name == each.value.nodepool_name][0]].id
# We leave some room so 100 eventual Hetzner LBs that can be created perfectly safely private_ipv4 = cidrhost(hcloud_network_subnet.agent[[for i, v in var.agent_nodepools : i if v.name == each.value.nodepool_name][0]].ip_range, each.value.index + 101)
# It leaves the subnet with 254 x 254 - 100 = 64416 IPs to use, so probably enough.
private_ipv4 = cidrhost(local.network_ipv4_subnets[[for i, v in var.agent_nodepools : i if v.name == each.value.nodepool_name][0] + 2], each.value.index + 101)
labels = { labels = {
"provisioner" = "terraform", "provisioner" = "terraform",
@ -24,7 +22,7 @@ module "agents" {
} }
depends_on = [ depends_on = [
hcloud_network_subnet.subnet hcloud_network_subnet.agent
] ]
} }
@ -46,12 +44,13 @@ resource "null_resource" "agents" {
provisioner "file" { provisioner "file" {
content = yamlencode({ content = yamlencode({
node-name = module.agents[each.key].name node-name = module.agents[each.key].name
server = "https://${module.control_planes[0].private_ipv4_address}:6443" server = "https://${module.control_planes[keys(module.control_planes)[0]].private_ipv4_address}:6443"
token = random_password.k3s_token.result token = random_password.k3s_token.result
kubelet-arg = ["cloud-provider=external", "volume-plugin-dir=/var/lib/kubelet/volumeplugins"] kubelet-arg = ["cloud-provider=external", "volume-plugin-dir=/var/lib/kubelet/volumeplugins"]
flannel-iface = "eth1" flannel-iface = "eth1"
node-ip = module.agents[each.key].private_ipv4_address node-ip = module.agents[each.key].private_ipv4_address
node-label = var.automatically_upgrade_k3s ? ["k3s_upgrade=true"] : [] node-label = each.value.labels
node-taint = each.value.taints
}) })
destination = "/tmp/config.yaml" destination = "/tmp/config.yaml"
} }
@ -79,6 +78,6 @@ resource "null_resource" "agents" {
depends_on = [ depends_on = [
null_resource.first_control_plane, null_resource.first_control_plane,
hcloud_network_subnet.subnet hcloud_network_subnet.agent
] ]
} }

View File

@ -1,21 +1,22 @@
module "control_planes" { module "control_planes" {
source = "./modules/host" source = "./modules/host"
count = var.control_plane_count for_each = local.control_plane_nodepools
name = "${var.use_cluster_name_in_node_name ? "${var.cluster_name}-" : ""}control-plane"
name = "${var.use_cluster_name_in_node_name ? "${var.cluster_name}-" : ""}${each.value.nodepool_name}"
ssh_keys = [hcloud_ssh_key.k3s.id] ssh_keys = [hcloud_ssh_key.k3s.id]
public_key = var.public_key public_key = var.public_key
private_key = var.private_key private_key = var.private_key
additional_public_keys = var.additional_public_keys additional_public_keys = var.additional_public_keys
firewall_ids = [hcloud_firewall.k3s.id] firewall_ids = [hcloud_firewall.k3s.id]
placement_group_id = hcloud_placement_group.k3s.id placement_group_id = var.placement_group_disable ? 0 : element(hcloud_placement_group.control_plane.*.id, ceil(each.value.index / 10))
location = var.location location = each.value.location
server_type = var.control_plane_server_type server_type = each.value.server_type
ipv4_subnet_id = hcloud_network_subnet.subnet[1].id ipv4_subnet_id = hcloud_network_subnet.control_plane[[for i, v in var.control_plane_nodepools : i if v.name == each.value.nodepool_name][0]].id
# We leave some room so 100 eventual Hetzner LBs that can be created perfectly safely # We leave some room so 100 eventual Hetzner LBs that can be created perfectly safely
# It leaves the subnet with 254 x 254 - 100 = 64416 IPs to use, so probably enough. # It leaves the subnet with 254 x 254 - 100 = 64416 IPs to use, so probably enough.
private_ipv4 = cidrhost(local.network_ipv4_subnets[1], count.index + 101) private_ipv4 = cidrhost(hcloud_network_subnet.control_plane[[for i, v in var.control_plane_nodepools : i if v.name == each.value.nodepool_name][0]].ip_range, each.value.index + 101)
labels = { labels = {
"provisioner" = "terraform", "provisioner" = "terraform",
@ -23,44 +24,45 @@ module "control_planes" {
} }
depends_on = [ depends_on = [
hcloud_network_subnet.subnet hcloud_network_subnet.control_plane
] ]
} }
resource "null_resource" "control_planes" { resource "null_resource" "control_planes" {
count = var.control_plane_count for_each = local.control_plane_nodepools
triggers = { triggers = {
control_plane_id = module.control_planes[count.index].id control_plane_id = module.control_planes[each.key].id
} }
connection { connection {
user = "root" user = "root"
private_key = local.ssh_private_key private_key = local.ssh_private_key
agent_identity = local.ssh_identity agent_identity = local.ssh_identity
host = module.control_planes[count.index].ipv4_address host = module.control_planes[each.key].ipv4_address
} }
# Generating k3s server config file # Generating k3s server config file
provisioner "file" { provisioner "file" {
content = yamlencode(merge({ content = yamlencode(merge({
node-name = module.control_planes[count.index].name node-name = module.control_planes[each.key].name
server = "https://${element(module.control_planes.*.private_ipv4_address, count.index > 0 ? 0 : 1)}:6443" server = length(module.control_planes) == 1 ? null : "https://${module.control_planes[each.key].private_ipv4_address == module.control_planes[keys(module.control_planes)[0]].private_ipv4_address ? module.control_planes[keys(module.control_planes)[1]].private_ipv4_address : module.control_planes[keys(module.control_planes)[0]].private_ipv4_address}:6443"
token = random_password.k3s_token.result token = random_password.k3s_token.result
disable-cloud-controller = true disable-cloud-controller = true
disable = local.disable_extras disable = local.disable_extras
flannel-iface = "eth1" flannel-iface = "eth1"
kubelet-arg = ["cloud-provider=external", "volume-plugin-dir=/var/lib/kubelet/volumeplugins"] kubelet-arg = ["cloud-provider=external", "volume-plugin-dir=/var/lib/kubelet/volumeplugins"]
kube-controller-manager-arg = "flex-volume-plugin-dir=/var/lib/kubelet/volumeplugins" kube-controller-manager-arg = "flex-volume-plugin-dir=/var/lib/kubelet/volumeplugins"
node-ip = module.control_planes[count.index].private_ipv4_address node-ip = module.control_planes[each.key].private_ipv4_address
advertise-address = module.control_planes[count.index].private_ipv4_address advertise-address = module.control_planes[each.key].private_ipv4_address
node-taint = var.allow_scheduling_on_control_plane ? [] : ["node-role.kubernetes.io/master:NoSchedule"] node-label = each.value.labels
node-label = var.automatically_upgrade_k3s ? ["k3s_upgrade=true"] : [] node-taint = each.value.taints
disable-network-policy = var.cni_plugin == "calico" ? true : var.disable_network_policy disable-network-policy = var.cni_plugin == "calico" ? true : var.disable_network_policy
}, },
var.cni_plugin == "calico" ? { var.cni_plugin == "calico" ? {
flannel-backend = "none" flannel-backend = "none"
} : {})) } : {}))
destination = "/tmp/config.yaml" destination = "/tmp/config.yaml"
} }
@ -87,6 +89,6 @@ resource "null_resource" "control_planes" {
depends_on = [ depends_on = [
null_resource.first_control_plane, null_resource.first_control_plane,
hcloud_network_subnet.subnet hcloud_network_subnet.control_plane
] ]
} }

24
docs/ssh.md Normal file
View File

@ -0,0 +1,24 @@
Kube-Hetzner requires you to have a recent version of OpenSSH (>=6.5) installed on your client, and the use of a key-pair generated with either of the following algorithms:
- ssh-ed25519 (preferred, and most simple to use without passphrase)
- rsa-sha2-512
- rsa-sha2-256
If your key-pair is of the `ssh-ed25519` sort, and without of passphrase, you do not need to do anything else. Just set `public_key` and `private_key` to their respective path values in your terraform.tfvars.
---
Otherwise, for a key-pair with passphrase or a device like a Yubikey, make sure you have have an SSH agent running and your key is loaded with:
```bash
eval ssh-agent $SHELL
ssh-add ~/.ssh/my_private-key_id
```
Verify it is loaded with:
```bash
ssh-add -l
```
Then set `private_key = null` in your terraform.tfvars, as it will be read from the ssh-agent automatically.

19
init.tf
View File

@ -3,13 +3,13 @@ resource "null_resource" "first_control_plane" {
user = "root" user = "root"
private_key = local.ssh_private_key private_key = local.ssh_private_key
agent_identity = local.ssh_identity agent_identity = local.ssh_identity
host = module.control_planes[0].ipv4_address host = module.control_planes[keys(module.control_planes)[0]].ipv4_address
} }
# Generating k3s master config file # Generating k3s master config file
provisioner "file" { provisioner "file" {
content = yamlencode(merge({ content = yamlencode(merge({
node-name = module.control_planes[0].name node-name = module.control_planes[keys(module.control_planes)[0]].name
token = random_password.k3s_token.result token = random_password.k3s_token.result
cluster-init = true cluster-init = true
disable-cloud-controller = true disable-cloud-controller = true
@ -17,15 +17,16 @@ resource "null_resource" "first_control_plane" {
flannel-iface = "eth1" flannel-iface = "eth1"
kubelet-arg = ["cloud-provider=external", "volume-plugin-dir=/var/lib/kubelet/volumeplugins"] kubelet-arg = ["cloud-provider=external", "volume-plugin-dir=/var/lib/kubelet/volumeplugins"]
kube-controller-manager-arg = "flex-volume-plugin-dir=/var/lib/kubelet/volumeplugins" kube-controller-manager-arg = "flex-volume-plugin-dir=/var/lib/kubelet/volumeplugins"
node-ip = module.control_planes[0].private_ipv4_address node-ip = module.control_planes[keys(module.control_planes)[0]].private_ipv4_address
advertise-address = module.control_planes[0].private_ipv4_address advertise-address = module.control_planes[keys(module.control_planes)[0]].private_ipv4_address
node-taint = var.allow_scheduling_on_control_plane ? [] : ["node-role.kubernetes.io/master:NoSchedule"] node-taint = local.control_plane_nodepools[keys(module.control_planes)[0]].taints
node-label = var.automatically_upgrade_k3s ? ["k3s_upgrade=true"] : [] node-label = local.control_plane_nodepools[keys(module.control_planes)[0]].labels
disable-network-policy = var.cni_plugin == "calico" ? true : var.disable_network_policy disable-network-policy = var.cni_plugin == "calico" ? true : var.disable_network_policy
}, },
var.cni_plugin == "calico" ? { var.cni_plugin == "calico" ? {
flannel-backend = "none" flannel-backend = "none"
} : {})) } : {}))
destination = "/tmp/config.yaml" destination = "/tmp/config.yaml"
} }
@ -62,7 +63,7 @@ resource "null_resource" "first_control_plane" {
} }
depends_on = [ depends_on = [
hcloud_network_subnet.subnet["control_plane"] hcloud_network_subnet.control_plane
] ]
} }
@ -71,7 +72,7 @@ resource "null_resource" "kustomization" {
user = "root" user = "root"
private_key = local.ssh_private_key private_key = local.ssh_private_key
agent_identity = local.ssh_identity agent_identity = local.ssh_identity
host = module.control_planes[0].ipv4_address host = module.control_planes[keys(module.control_planes)[0]].ipv4_address
} }
# Upload kustomization.yaml, containing Hetzner CSI & CSM, as well as kured. # Upload kustomization.yaml, containing Hetzner CSI & CSM, as well as kured.
@ -103,7 +104,7 @@ resource "null_resource" "kustomization" {
name = "${var.cluster_name}-traefik" name = "${var.cluster_name}-traefik"
load_balancer_disable_ipv6 = var.load_balancer_disable_ipv6 load_balancer_disable_ipv6 = var.load_balancer_disable_ipv6
load_balancer_type = var.load_balancer_type load_balancer_type = var.load_balancer_type
location = var.location location = var.load_balancer_location
traefik_acme_tls = var.traefik_acme_tls traefik_acme_tls = var.traefik_acme_tls
traefik_acme_email = var.traefik_acme_email traefik_acme_email = var.traefik_acme_email
traefik_additional_options = var.traefik_additional_options traefik_additional_options = var.traefik_additional_options

View File

@ -1,7 +1,7 @@
data "remote_file" "kubeconfig" { data "remote_file" "kubeconfig" {
conn { conn {
host = module.control_planes[0].ipv4_address host = module.control_planes[keys(module.control_planes)[0]].ipv4_address
port = 22 port = 22
user = "root" user = "root"
private_key = local.ssh_private_key private_key = local.ssh_private_key
@ -13,7 +13,7 @@ data "remote_file" "kubeconfig" {
} }
locals { locals {
kubeconfig_external = replace(data.remote_file.kubeconfig.content, "127.0.0.1", module.control_planes[0].ipv4_address) kubeconfig_external = replace(data.remote_file.kubeconfig.content, "127.0.0.1", module.control_planes[keys(module.control_planes)[0]].ipv4_address)
kubeconfig_parsed = yamldecode(local.kubeconfig_external) kubeconfig_parsed = yamldecode(local.kubeconfig_external)
kubeconfig_data = { kubeconfig_data = {
host = local.kubeconfig_parsed["clusters"][0]["cluster"]["server"] host = local.kubeconfig_parsed["clusters"][0]["cluster"]["server"]

View File

@ -1,6 +1,9 @@
locals { locals {
# if we are in a single cluster config, we use the default klipper lb instead of Hetzner LB # if we are in a single cluster config, we use the default klipper lb instead of Hetzner LB
is_single_node_cluster = var.control_plane_count + sum(concat([for v in var.agent_nodepools : v.count], [0])) == 1 total_node_count = sum(concat([for v in var.control_plane_nodepools : v.count], [0])) + sum(concat([for v in var.agent_nodepools : v.count], [0]))
control_plane_count = sum(concat([for v in var.control_plane_nodepools : v.count], [0]))
agent_count = sum(concat([for v in var.agent_nodepools : v.count], [0]))
is_single_node_cluster = local.total_node_count == 1
ssh_public_key = trimspace(file(var.public_key)) ssh_public_key = trimspace(file(var.public_key))
# ssh_private_key is either the contents of var.private_key or null to use a ssh agent. # ssh_private_key is either the contents of var.private_key or null to use a ssh agent.
ssh_private_key = var.private_key == null ? null : trimspace(file(var.private_key)) ssh_private_key = var.private_key == null ? null : trimspace(file(var.private_key))
@ -169,13 +172,30 @@ locals {
install_k3s_server = concat(local.common_commands_install_k3s, ["curl -sfL https://get.k3s.io | INSTALL_K3S_SKIP_START=true INSTALL_K3S_SKIP_SELINUX_RPM=true INSTALL_K3S_CHANNEL=${var.initial_k3s_channel} INSTALL_K3S_EXEC=server sh -"], local.apply_k3s_selinux) install_k3s_server = concat(local.common_commands_install_k3s, ["curl -sfL https://get.k3s.io | INSTALL_K3S_SKIP_START=true INSTALL_K3S_SKIP_SELINUX_RPM=true INSTALL_K3S_CHANNEL=${var.initial_k3s_channel} INSTALL_K3S_EXEC=server sh -"], local.apply_k3s_selinux)
install_k3s_agent = concat(local.common_commands_install_k3s, ["curl -sfL https://get.k3s.io | INSTALL_K3S_SKIP_START=true INSTALL_K3S_SKIP_SELINUX_RPM=true INSTALL_K3S_CHANNEL=${var.initial_k3s_channel} INSTALL_K3S_EXEC=agent sh -"], local.apply_k3s_selinux) install_k3s_agent = concat(local.common_commands_install_k3s, ["curl -sfL https://get.k3s.io | INSTALL_K3S_SKIP_START=true INSTALL_K3S_SKIP_SELINUX_RPM=true INSTALL_K3S_CHANNEL=${var.initial_k3s_channel} INSTALL_K3S_EXEC=agent sh -"], local.apply_k3s_selinux)
agent_nodepools = merge([ control_plane_nodepools = merge([
for nodepool_obj in var.agent_nodepools : { for pool_index, nodepool_obj in var.control_plane_nodepools : {
for index in range(nodepool_obj.count) : for node_index in range(nodepool_obj.count) :
format("%s-%s", nodepool_obj.name, index) => { format("%s-%s-%s", pool_index, node_index, nodepool_obj.name) => {
nodepool_name : nodepool_obj.name, nodepool_name : nodepool_obj.name,
server_type : nodepool_obj.server_type, server_type : nodepool_obj.server_type,
index : index location : nodepool_obj.location,
labels : concat(local.default_control_plane_labels, nodepool_obj.labels),
taints : concat(local.default_control_plane_taints, nodepool_obj.taints),
index : node_index
}
}
]...)
agent_nodepools = merge([
for pool_index, nodepool_obj in var.agent_nodepools : {
for node_index in range(nodepool_obj.count) :
format("%s-%s-%s", pool_index, node_index, nodepool_obj.name) => {
nodepool_name : nodepool_obj.name,
server_type : nodepool_obj.server_type,
location : nodepool_obj.location,
labels : concat(local.default_agent_labels, nodepool_obj.labels),
taints : nodepool_obj.taints,
index : node_index
} }
} }
]...) ]...)
@ -185,9 +205,17 @@ locals {
# The first two subnets are respectively the default subnet 10.0.0.0/16 use for potientially anything and 10.1.0.0/16 used for control plane nodes. # The first two subnets are respectively the default subnet 10.0.0.0/16 use for potientially anything and 10.1.0.0/16 used for control plane nodes.
# the rest of the subnets are for agent nodes in each nodepools. # the rest of the subnets are for agent nodes in each nodepools.
network_ipv4_subnets = [for index in range(length(var.agent_nodepools) + 2) : cidrsubnet(local.network_ipv4_cidr, 8, index)] network_ipv4_subnets = [for index in range(256) : cidrsubnet(local.network_ipv4_cidr, 8, index)]
# disable k3s extras # disable k3s extras
disable_extras = concat(["local-storage"], local.is_single_node_cluster ? [] : ["servicelb"], var.traefik_enabled ? [] : ["traefik"], var.metrics_server_enabled ? [] : ["metrics-server"]) disable_extras = concat(["local-storage"], local.is_single_node_cluster ? [] : ["servicelb"], var.traefik_enabled ? [] : ["traefik"], var.metrics_server_enabled ? [] : ["metrics-server"])
# Default k3s node labels
default_agent_labels = concat([], var.automatically_upgrade_k3s ? ["k3s_upgrade=true"] : [])
default_control_plane_labels = concat([], var.automatically_upgrade_k3s ? ["k3s_upgrade=true"] : [])
allow_scheduling_on_control_plane = local.is_single_node_cluster ? true : var.allow_scheduling_on_control_plane
# Default k3s node taints
default_control_plane_taints = concat([], local.allow_scheduling_on_control_plane ? [] : ["node-role.kubernetes.io/master:NoSchedule"])
} }

34
main.tf
View File

@ -13,8 +13,19 @@ resource "hcloud_network" "k3s" {
ip_range = local.network_ipv4_cidr ip_range = local.network_ipv4_cidr
} }
resource "hcloud_network_subnet" "subnet" { # We start from the end of the subnets cird array,
count = length(local.network_ipv4_subnets) # as we would have fewer control plane nodepools, than angent ones.
resource "hcloud_network_subnet" "control_plane" {
count = length(local.control_plane_nodepools)
network_id = hcloud_network.k3s.id
type = "cloud"
network_zone = var.network_region
ip_range = local.network_ipv4_subnets[255 - count.index]
}
# Here we start at the beginning of the subnets cird array
resource "hcloud_network_subnet" "agent" {
count = length(local.agent_nodepools)
network_id = hcloud_network.k3s.id network_id = hcloud_network.k3s.id
type = "cloud" type = "cloud"
network_zone = var.network_region network_zone = var.network_region
@ -36,13 +47,16 @@ resource "hcloud_firewall" "k3s" {
} }
} }
resource "hcloud_placement_group" "k3s" { resource "hcloud_placement_group" "control_plane" {
name = var.cluster_name count = ceil(local.control_plane_count / 10)
name = "${var.cluster_name}-control-plane-${count.index + 1}"
type = "spread" type = "spread"
labels = {
"provisioner" = "terraform",
"engine" = "k3s"
} }
resource "hcloud_placement_group" "agent" {
count = ceil(local.agent_count / 10)
name = "${var.cluster_name}-agent-${count.index + 1}"
type = "spread"
} }
data "hcloud_load_balancer" "traefik" { data "hcloud_load_balancer" "traefik" {
@ -70,10 +84,12 @@ resource "null_resource" "destroy_traefik_loadbalancer" {
depends_on = [ depends_on = [
local_sensitive_file.kubeconfig, local_sensitive_file.kubeconfig,
null_resource.control_planes[0], null_resource.control_planes[0],
hcloud_network_subnet.subnet, hcloud_network_subnet.control_plane,
hcloud_network_subnet.agent,
hcloud_placement_group.control_plane,
hcloud_placement_group.agent,
hcloud_network.k3s, hcloud_network.k3s,
hcloud_firewall.k3s, hcloud_firewall.k3s,
hcloud_placement_group.k3s,
hcloud_ssh_key.k3s hcloud_ssh_key.k3s
] ]
} }

View File

@ -4,7 +4,9 @@ output "cluster_name" {
} }
output "control_planes_public_ipv4" { output "control_planes_public_ipv4" {
value = module.control_planes.*.ipv4_address value = [
for obj in module.control_planes : obj.ipv4_address
]
description = "The public IPv4 addresses of the controlplane server." description = "The public IPv4 addresses of the controlplane server."
} }
@ -17,7 +19,9 @@ output "agents_public_ipv4" {
output "load_balancer_public_ipv4" { output "load_balancer_public_ipv4" {
description = "The public IPv4 address of the Hetzner load balancer" description = "The public IPv4 address of the Hetzner load balancer"
value = local.is_single_node_cluster ? module.control_planes[0].ipv4_address : var.traefik_enabled == false ? null : data.hcloud_load_balancer.traefik[0].ipv4 value = local.is_single_node_cluster ? [
for obj in module.control_planes : obj.ipv4_address
][0] : var.traefik_enabled == false ? null : data.hcloud_load_balancer.traefik[0].ipv4
} }
output "kubeconfig_file" { output "kubeconfig_file" {

View File

@ -14,38 +14,91 @@ private_key = "/home/username/.ssh/id_ed25519"
# These can be customized, or left with the default values # These can be customized, or left with the default values
# For Hetzner locations see https://docs.hetzner.com/general/others/data-centers-and-connection/ # For Hetzner locations see https://docs.hetzner.com/general/others/data-centers-and-connection/
# For Hetzner server types see https://www.hetzner.com/cloud
location = "fsn1" # change to `ash` for us-east Ashburn, Virginia location
network_region = "eu-central" # change to `us-east` if location is ash network_region = "eu-central" # change to `us-east` if location is ash
# At least 3 server nodes is recommended for HA, otherwise you need to turn off automatic upgrade (see ReadMe). # For the control-planes, at least 3 nodes is recommended for HA, otherwise you need to turn off automatic upgrade (see ReadMe).
# As per rancher docs, it must be always an odd number, never even! See https://rancher.com/docs/k3s/latest/en/installation/ha-embedded/ # As per rancher docs, it must be always an odd number, never even! See https://rancher.com/docs/k3s/latest/en/installation/ha-embedded/
# For instance, 1 is ok (non-HA), 2 not ok, 3 is ok (becomes HA). # For instance, 1 is ok (non-HA), 2 not ok, 3 is ok (becomes HA). It does not matter if they are in the same nodepool or not! So they can be in different locations, and of different types.
control_plane_count = 3
# The type of control plane nodes, see https://www.hetzner.com/cloud, the minimum instance supported is cpx11 (just a few cents more than cx11) # Of course, you can choose any number of nodepools you want, with the location you want. The only contraint on the location is that you need to stay in the same network region, basically Europe or US, see above.
control_plane_server_type = "cpx11" # For the server type, # The type of control plane nodes, the minimum instance supported is cpx11 (just a few cents more than cx11), see https://www.hetzner.com/cloud.
# As for the agent nodepools, below is just an example, if you do not want nodepools, just use one, # IMPORTANT: Before the your cluster is created, you can do anything you want with the nodepools, but you need at least one of each control plane and agent.
# and change the name to what you want, it need not be "agent-big" or "agent-small", also give them the subnet prefer. # Once the cluster is created, you can change nodepool count, and even set it to 0 (in the case of the first control-plane nodepool, the minimum is 1),
# For single node clusters set this equal to [] or just set the counts to 0. # you can also rename it (if the count is taken to 0), but do not remove a nodepool from the list after the cluster is created.
# IMPORTANT: Once the cluster is created, you can change the count, and even set it to 0, but do not remove a nodepool from the list.
# You can add others at the end of the list if you want. # The only nodepools that are safe to remove from the list when you edit it, are the ones at the end of the lists. This is due to how IPs are allocated.
agent_nodepools = [ # You can however freely add others nodepools the end of each list if you want! The maximum number of nodepools you can create, combined for both lists is 255.
# Also, before decreasing the count of any nodepools to 0, it's important to drain and cordon it the nodes in question, otherwise it will leave your cluster in a bad state.
# Before initializing the cluster, you can change all parameters and add or remove any nodepools. You just need at least one nodepool of each kind, control plane and agent.
# The nodepool names are fully arbitrary, you can choose whatever you want, but no special characters or underscore, only alphanumeric characters and dashes are allowed.
# If you want to have a single node cluster, just have 1 control plane nodepools with a count of 1, and one agent nodepool with a count of 0.
# Example below:
control_plane_nodepools = [
{ {
name = "agent-small", name = "control-plane-fsn1",
server_type = "cpx11", server_type = "cpx11",
count = 2 location = "fsn1",
labels = [],
taints = [],
count = 1
}, },
{ {
name = "agent-large", name = "control-plane-nbg1",
server_type = "cpx21", server_type = "cpx11",
location = "nbg1",
labels = [],
taints = [],
count = 1
},
{
name = "control-plane-hel1",
server_type = "cpx11",
location = "hel1",
labels = [],
taints = [],
count = 1 count = 1
} }
] ]
# That will depend on how much load you want it to handle, see https://www.hetzner.com/cloud/load-balancer agent_nodepools = [
{
name = "agent-small",
server_type = "cpx11",
location = "fsn1",
labels = [],
taints = [],
count = 1
},
{
name = "agent-large",
server_type = "cpx21",
location = "nbg1",
labels = [],
taints = [],
count = 1
},
{
name = "storage",
server_type = "cpx21",
location = "fsn1",
labels = [
"node.kubernetes.io/server-usage=storage"
],
taints = [
"server-usage=storage:NoSchedule"
],
count = 1
}
]
# LB location and type, the latter will depend on how much load you want it to handle, see https://www.hetzner.com/cloud/load-balancer
load_balancer_type = "lb11" load_balancer_type = "lb11"
load_balancer_location = "fsn1"
### The following values are fully optional ### The following values are fully optional
@ -65,7 +118,7 @@ load_balancer_type = "lb11"
# metrics_server_enabled = false # metrics_server_enabled = false
# If you want to allow non-control-plane workloads to run on the control-plane nodes set "true" below. The default is "false". # If you want to allow non-control-plane workloads to run on the control-plane nodes set "true" below. The default is "false".
# Also good for single node clusters. # True by default for single node clusters.
# allow_scheduling_on_control_plane = true # allow_scheduling_on_control_plane = true
# If you want to disable automatic upgrade of k3s, you can set this to false, default is "true". # If you want to disable automatic upgrade of k3s, you can set this to false, default is "true".
@ -81,7 +134,7 @@ load_balancer_type = "lb11"
# use_cluster_name_in_node_name = false # use_cluster_name_in_node_name = false
# Adding extra firewall rules, like opening a port # Adding extra firewall rules, like opening a port
# In this example with allow port TCP 5432 for a Postgres service we will open via a nodeport # In this example with allow port TCP 5432 for a Postgres service we will open via a nodeport and allow outgoing SMTP traffic on port TCP 465
# More info on the format here https://registry.terraform.io/providers/hetznercloud/hcloud/latest/docs/resources/firewall # More info on the format here https://registry.terraform.io/providers/hetznercloud/hcloud/latest/docs/resources/firewall
# extra_firewall_rules = [ # extra_firewall_rules = [
# { # {
@ -92,6 +145,14 @@ load_balancer_type = "lb11"
# "0.0.0.0/0" # "0.0.0.0/0"
# ] # ]
# }, # },
# {
# direction = "out"
# protocol = "tcp"
# port = "465"
# destination_ips = [
# "0.0.0.0/0"
# ]
# },
# ] # ]
# If you want to configure additional Arguments for traefik, enter them here as a list and in the form of traefik CLI arguments; see https://doc.traefik.io/traefik/reference/static-configuration/cli/ # If you want to configure additional Arguments for traefik, enter them here as a list and in the form of traefik CLI arguments; see https://doc.traefik.io/traefik/reference/static-configuration/cli/
@ -105,3 +166,7 @@ load_balancer_type = "lb11"
# If you want to disable the k3s default network policy controller, use this flag # If you want to disable the k3s default network policy controller, use this flag
# Calico overrides this value to true automatically # Calico overrides this value to true automatically
# disable_network_policy = false # disable_network_policy = false
# If you want to disable the automatic use of placement group "spread". See https://docs.hetzner.com/cloud/placement-groups/overview/
# That may be useful if you need to deploy more than 500 nodes! The default is "false".
# placement_group_disable = true

View File

@ -20,26 +20,16 @@ variable "additional_public_keys" {
default = [] default = []
} }
variable "location" {
description = "Default server location"
type = string
}
variable "network_region" { variable "network_region" {
description = "Default region for network" description = "Default region for network"
type = string type = string
} }
variable "control_plane_server_type" { variable "load_balancer_location" {
description = "Default control plane server type" description = "Default load balancer location"
type = string type = string
} }
variable "control_plane_count" {
description = "Number of control plane nodes."
type = number
}
variable "load_balancer_type" { variable "load_balancer_type" {
description = "Default load balancer server type" description = "Default load balancer server type"
type = string type = string
@ -51,6 +41,12 @@ variable "load_balancer_disable_ipv6" {
default = false default = false
} }
variable "control_plane_nodepools" {
description = "Number of control plane nodes."
type = list(any)
default = []
}
variable "agent_nodepools" { variable "agent_nodepools" {
description = "Number of agent nodes." description = "Number of agent nodes."
type = list(any) type = list(any)
@ -142,7 +138,12 @@ variable "cluster_name" {
variable "traefik_additional_options" { variable "traefik_additional_options" {
type = list(string) type = list(string)
default = [] default = []
}
variable "placement_group_disable" {
type = bool
default = false
description = "Whether to disable placement groups"
} }
variable "disable_network_policy" { variable "disable_network_policy" {