diff --git a/.gitignore b/.gitignore
index b480394c58e2c2a2cc55b93fa861b70f01707049..9d03ef02a98f215b7daebc3f083075b7866735f3 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,2 +1,81 @@
-*.yamld
 *_old
+
+
+# Created by https://www.toptal.com/developers/gitignore/api/terraform,ansible,vim,git
+# Edit at https://www.toptal.com/developers/gitignore?templates=terraform,ansible,vim,git
+
+### Ansible ###
+*.retry
+
+### Git ###
+# Created by git for backups. To disable backups in Git:
+# $ git config --global mergetool.keepBackup false
+*.orig
+
+# Created by git when using merge tools for conflicts
+*.BACKUP.*
+*.BASE.*
+*.LOCAL.*
+*.REMOTE.*
+*_BACKUP_*.txt
+*_BASE_*.txt
+*_LOCAL_*.txt
+*_REMOTE_*.txt
+
+### Terraform ###
+# Local .terraform directories
+**/.terraform/*
+
+# .tfstate files
+*.tfstate
+*.tfstate.*
+
+# Crash log files
+crash.log
+
+# Exclude all .tfvars files, which are likely to contain sentitive data, such as
+# password, private keys, and other secrets. These should not be part of version
+# control as they are data points which are potentially sensitive and subject
+# to change depending on the environment.
+#
+*.tfvars
+
+# Ignore override files as they are usually used to override resources locally and so
+# are not checked in
+override.tf
+override.tf.json
+*_override.tf
+*_override.tf.json
+
+# Include override files you do wish to add to version control using negated pattern
+# !example_override.tf
+
+# Include tfplan files to ignore the plan output of command: terraform plan -out=tfplan
+# example: *tfplan*
+
+# Ignore CLI configuration files
+.terraformrc
+terraform.rc
+
+### Vim ###
+# Swap
+[._]*.s[a-v][a-z]
+!*.svg  # comment out if you don't need vector files
+[._]*.sw[a-p]
+[._]s[a-rt-v][a-z]
+[._]ss[a-gi-z]
+[._]sw[a-p]
+
+# Session
+Session.vim
+Sessionx.vim
+
+# Temporary
+.netrwhist
+*~
+# Auto-generated tag files
+tags
+# Persistent undo
+[._]*.un~
+
+# End of https://www.toptal.com/developers/gitignore/api/terraform,ansible,vim,git
diff --git a/Makefile b/Makefile
new file mode 100644
index 0000000000000000000000000000000000000000..75e2d6ced62c69fbdc6f97352d76241a0470005c
--- /dev/null
+++ b/Makefile
@@ -0,0 +1,14 @@
+.DEFAULT_GOAL := help
+
+include utils/*.mk
+
+.PHONY: check-machine
+check-machine: ## Check your local machine setup to be prepared for the installation
+	command -v kubectl >/dev/null
+	command -v ansible >/dev/null
+	command -v flux >/dev/null
+	command -v terraform >/dev/null
+
+.PHONY: deploy
+deploy: check-machine ## Deploy infrastructure on Hetzner Cloud
+	cd ./terraform && make apply
diff --git a/terraform/.gitignore b/terraform/.gitignore
new file mode 100644
index 0000000000000000000000000000000000000000..248419ce0fd6d013d5bd38cc2fb2ff56071fcb96
--- /dev/null
+++ b/terraform/.gitignore
@@ -0,0 +1,76 @@
+
+# Created by https://www.toptal.com/developers/gitignore/api/terraform,vim,git
+# Edit at https://www.toptal.com/developers/gitignore?templates=terraform,vim,git
+
+### Git ###
+# Created by git for backups. To disable backups in Git:
+# $ git config --global mergetool.keepBackup false
+*.orig
+
+# Created by git when using merge tools for conflicts
+*.BACKUP.*
+*.BASE.*
+*.LOCAL.*
+*.REMOTE.*
+*_BACKUP_*.txt
+*_BASE_*.txt
+*_LOCAL_*.txt
+*_REMOTE_*.txt
+
+### Terraform ###
+# Local .terraform directories
+**/.terraform/*
+
+# .tfstate files
+*.tfstate
+*.tfstate.*
+
+# Crash log files
+crash.log
+
+# Exclude all .tfvars files, which are likely to contain sentitive data, such as
+# password, private keys, and other secrets. These should not be part of version
+# control as they are data points which are potentially sensitive and subject
+# to change depending on the environment.
+#
+*.tfvars
+
+# Ignore override files as they are usually used to override resources locally and so
+# are not checked in
+override.tf
+override.tf.json
+*_override.tf
+*_override.tf.json
+
+# Include override files you do wish to add to version control using negated pattern
+# !example_override.tf
+
+# Include tfplan files to ignore the plan output of command: terraform plan -out=tfplan
+# example: *tfplan*
+
+# Ignore CLI configuration files
+.terraformrc
+terraform.rc
+
+### Vim ###
+# Swap
+[._]*.s[a-v][a-z]
+!*.svg  # comment out if you don't need vector files
+[._]*.sw[a-p]
+[._]s[a-rt-v][a-z]
+[._]ss[a-gi-z]
+[._]sw[a-p]
+
+# Session
+Session.vim
+Sessionx.vim
+
+# Temporary
+.netrwhist
+*~
+# Auto-generated tag files
+tags
+# Persistent undo
+[._]*.un~
+
+# End of https://www.toptal.com/developers/gitignore/api/terraform,vim,git
diff --git a/terraform/LICENSE b/terraform/LICENSE
new file mode 100644
index 0000000000000000000000000000000000000000..18d3c977d57af2447a4d8793333f55102e8a10b1
--- /dev/null
+++ b/terraform/LICENSE
@@ -0,0 +1,10 @@
+The MIT License (MIT)
+
+Copyright 2021 Christoph (Sheogorath) Kern
+Copyright 2021 Simon Lauger
+
+Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
diff --git a/terraform/Makefile b/terraform/Makefile
new file mode 100644
index 0000000000000000000000000000000000000000..447d0ac9dd008de04b7b1a7dec4bfeece52c762d
--- /dev/null
+++ b/terraform/Makefile
@@ -0,0 +1,32 @@
+.DEFAULT_GOAL := plan
+
+TERRAFORM_PARAMETERS?=
+
+include ../utils/help.mk
+
+.PHONY: preflight
+preflight:
+	# Checking tooling
+	command -v terraform > /dev/null
+	# Checking for required variables
+	env | grep -Pe '^HCLOUD_TOKEN' > /dev/null
+	env | grep -Pe '^CLOUDFLARE_EMAIL' > /dev/null
+	env | grep -Pe '^CLOUDFLARE_API_KEY' > /dev/null
+
+.PHONY: init
+init: ## Initalize terraform
+	terraform init
+
+.PHONY: plan
+plan: preflight init ## Run terraform plan (dry-run)
+	terraform plan ${TERRAFORM_PARAMETERS}
+
+.PHONY: apply
+apply: preflight init ## Deploy the base infrastructure using terraform
+	terraform apply ${TERRAFORM_PARAMETERS}
+
+.PHONY: destroy
+destroy: preflight init ## Tear down deployed the infrastructure
+	terraform destroy ${TERRAFORM_PARAMETERS}
+	# Cleanup remaining files
+	rm -r .terraform .terraform.* terraform.tfstate terraform.tfstate.backup
diff --git a/terraform/README.md b/terraform/README.md
new file mode 100644
index 0000000000000000000000000000000000000000..f0ceb3267b88a5c9b59e375889a77e5c92ee156f
--- /dev/null
+++ b/terraform/README.md
@@ -0,0 +1,43 @@
+SI-GitOps Terraform
+===
+
+This terraform definition is used to do the base deployment of cloud infrastructure for the future Kubernetes clusters.
+
+Preparation
+---
+
+1. Create a Hetzner Cloud account
+2. Create a Project on Hetzner Cloud
+3. Upload your SSH key(s) to Hetzner Cloud
+4. Create an API key for Hetzner Cloud and export it to your shell environment using `export HCLOUD_TOKEN=<your token>`
+5. Create and API key for Cloudflare and export your email address used for Cloudflare using `export CLOUDFLARE_EMAIL=<your cloudflare email>` and the API token using `export CLOUDFLARE_API_TOKEN=<your cloudflare token>`
+6. Install terraform
+
+What does it do?
+---
+
+It'll deploy:
+
+- 3 control plane nodes on Hetzner Cloud with respective DNS entries on Cloudflare
+- 1 load balancer on Hetzner Cloud with a respective DNS entry on Cloudflare for the API
+- Firewall rules for all nodes as well as specific ones for the control plane and ingress nodes
+- A placement group that should prevent all nodes from ending up on the same machine
+- An internal network for all nodes to use
+- All nodes will get the required tools installed and configurations prepared for the Kubernetes bootstrapping
+
+Why?
+---
+
+Because.
+
+Deployment
+---
+
+To deploy the infrastructure you can just use the `make`-CLI this project provides by running `make plan` to preview the project and `make apply` to deploy the project.
+
+Additional options
+---
+
+In order to customise the terraform commands, one is able to pass `TERRAFORM_PARAMETERS` as variable to the make command, which will pass them to the terraform commands. This can be used like `TERRAFORM_PARAMETERS="-auto-approve"` in order to run the commands in a CI environment.
+
+You can also always run `make help` to get a help dialogue for all commands.
diff --git a/terraform/dns.tf b/terraform/dns.tf
new file mode 100644
index 0000000000000000000000000000000000000000..955d869683de810beb5561393ece1333c88edef8
--- /dev/null
+++ b/terraform/dns.tf
@@ -0,0 +1,7 @@
+resource "cloudflare_record" "dns_a_api" {
+  zone_id = var.dns_zone_id
+  name    = "api.${var.dns_domain}"
+  value   = hcloud_load_balancer.lb.ipv4
+  type    = "A"
+  ttl     = 300
+}
diff --git a/terraform/firewall.tf b/terraform/firewall.tf
new file mode 100644
index 0000000000000000000000000000000000000000..5f090e91b98a30ad8dcfe94e1872ae9dee793a0c
--- /dev/null
+++ b/terraform/firewall.tf
@@ -0,0 +1,154 @@
+# https://docs.k8s.io/latest/installing/installing_platform_agnostic/installing-platform-agnostic.html#installation-network-connectivity-user-infra_installing-platform-agnostic
+resource "hcloud_firewall" "k8s-base" {
+  name = "k8s-base"
+  apply_to {
+    label_selector = "k8s.io/node=true"
+  }
+
+  # ICMP is always a good idea
+  #
+  # Network reachability tests
+  rule {
+   direction = "in"
+   protocol  = "icmp"
+   source_ips = [
+      "0.0.0.0/0",
+      "::/0"
+   ]
+  }
+  # Kublet
+  rule {
+      direction       = "in"
+      protocol        = "tcp"
+      port            = "10250"
+      source_ips      = [for s in concat(module.nodes.ipv4_addresses) : "${s}/32"]
+  }
+  # Kubernetes node port
+  rule {
+      direction       = "in"
+      protocol        = "tcp"
+      port            = "30000-32767"
+      source_ips      = [for s in concat(module.nodes.ipv4_addresses) : "${s}/32"]
+  }
+  # Kubernetes node port
+  rule {
+      direction       = "in"
+      protocol        = "udp"
+      port            = "30000-32767"
+      source_ips      = [for s in concat(module.nodes.ipv4_addresses) : "${s}/32"]
+  }
+  # Calico BGP
+  rule {
+      direction       = "in"
+      protocol        = "tcp"
+      port            = "179"
+      source_ips      = [for s in concat(module.nodes.ipv4_addresses) : "${s}/32"]
+  }
+  # Calico VXLAN
+  rule {
+      direction       = "in"
+      protocol        = "udp"
+      port            = "4789"
+      source_ips      = [for s in concat(module.nodes.ipv4_addresses) : "${s}/32"]
+  }
+  # Calico Typha
+  rule {
+      direction       = "in"
+      protocol        = "tcp"
+      port            = "5473"
+      source_ips      = [for s in concat(module.nodes.ipv4_addresses) : "${s}/32"]
+  }
+  # Host level services, including the node exporter on ports 9100-9101.
+  rule {
+      direction       = "in"
+      protocol        = "tcp"
+      port            = "9000-9999"
+      source_ips      = [for s in concat(module.nodes.ipv4_addresses) : "${s}/32"]
+  }
+  # Host level services, including the node exporter on ports 9100-9101.
+  rule {
+      direction       = "in"
+      protocol        = "udp"
+      port            = "9000-9999"
+      source_ips      = [for s in concat(module.nodes.ipv4_addresses) : "${s}/32"]
+  }
+}
+
+
+resource "hcloud_firewall" "k8s-master" {
+  name = "k8s-master"
+  apply_to {
+    label_selector = "k8s.io/master=true"
+  }
+
+  # ICMP is always a good idea
+  #
+  # Network reachability tests
+  rule {
+   direction = "in"
+   protocol  = "icmp"
+   source_ips = [
+      "0.0.0.0/0",
+      "::/0"
+   ]
+  }
+  # Kubernetes API
+  rule {
+      direction       = "in"
+      protocol        = "tcp"
+      port            = "6443"
+      source_ips      = [for s in concat([hcloud_load_balancer.lb.ipv4],module.nodes.ipv4_addresses) : "${s}/32"]
+  }
+  # etcd server and peer ports
+  rule {
+      direction       = "in"
+      protocol        = "tcp"
+      port            = "2379-2380"
+      source_ips      = [for s in module.nodes.ipv4_addresses : "${s}/32"]
+  }
+  # kube-scheduler
+  rule {
+      direction       = "in"
+      protocol        = "tcp"
+      port            = "10251"
+      source_ips      = [for s in module.nodes.ipv4_addresses : "${s}/32"]
+  }
+  # kube-controller-manager
+  rule {
+      direction       = "in"
+      protocol        = "tcp"
+      port            = "10252"
+      source_ips      = [for s in module.nodes.ipv4_addresses : "${s}/32"]
+  }
+}
+
+resource "hcloud_firewall" "k8s-ingress" {
+  name = "k8s-ingress"
+  apply_to {
+    label_selector = "k8s.io/ingress=true"
+  }
+
+  # ICMP is always a good idea
+  #
+  # Network reachability tests
+  rule {
+   direction = "in"
+   protocol  = "icmp"
+   source_ips = [
+      "0.0.0.0/0",
+      "::/0"
+   ]
+  }
+  rule {
+      direction       = "in"
+      protocol        = "tcp"
+      port            = "80"
+      source_ips      = [for s in [hcloud_load_balancer.lb.ipv4] : "${s}/32"]
+  }
+  rule {
+      direction       = "in"
+      protocol        = "tcp"
+      port            = "443"
+      source_ips      = [for s in [hcloud_load_balancer.lb.ipv4] : "${s}/32"]
+  }
+}
diff --git a/terraform/loadbalancer.tf b/terraform/loadbalancer.tf
new file mode 100644
index 0000000000000000000000000000000000000000..68b37e9c3fa680dc2bcc0ac2ec17c87a24c60f45
--- /dev/null
+++ b/terraform/loadbalancer.tf
@@ -0,0 +1,63 @@
+resource "hcloud_load_balancer" "lb" {
+  name               = "lb.${var.dns_domain}"
+  load_balancer_type = "lb11"
+  location           = var.location
+}
+
+resource "hcloud_load_balancer_target" "lb_target" {
+  type             = "label_selector"
+  load_balancer_id = hcloud_load_balancer.lb.id
+  label_selector   = "k8s.io/node=true"
+  use_private_ip   = true
+}
+
+resource "hcloud_load_balancer_network" "lb_network" {
+  load_balancer_id = hcloud_load_balancer.lb.id
+  subnet_id        = hcloud_network_subnet.subnet.id
+  ip               = "172.16.0.254"
+}
+
+resource "hcloud_load_balancer_service" "lb_api" {
+  load_balancer_id = hcloud_load_balancer.lb.id
+  protocol         = "tcp"
+  listen_port      = 6443
+  destination_port = 6443
+
+  health_check {
+    protocol = "tcp"
+    port     = 6443
+    interval = 10
+    timeout  = 1
+    retries  = 3
+  }
+}
+
+resource "hcloud_load_balancer_service" "lb_ingress_http" {
+  load_balancer_id = hcloud_load_balancer.lb.id
+  protocol         = "tcp"
+  listen_port      = 80
+  destination_port = 80
+
+  health_check {
+    protocol = "tcp"
+    port     = 80
+    interval = 10
+    timeout  = 1
+    retries  = 3
+  }
+}
+
+resource "hcloud_load_balancer_service" "lb_ingress_https" {
+  load_balancer_id = hcloud_load_balancer.lb.id
+  protocol         = "tcp"
+  listen_port      = 443
+  destination_port = 443
+
+  health_check {
+    protocol = "tcp"
+    port     = 443
+    interval = 10
+    timeout  = 1
+    retries  = 3
+  }
+}
diff --git a/terraform/main.tf b/terraform/main.tf
new file mode 100644
index 0000000000000000000000000000000000000000..6299239c43010de300fcf689c49457634f3c69d6
--- /dev/null
+++ b/terraform/main.tf
@@ -0,0 +1,26 @@
+resource "hcloud_placement_group" "k8s" {
+  name = var.dns_domain
+  type = "spread"
+  labels = {
+    key = "value"
+  }
+}
+
+module "nodes" {
+  source         = "./modules/hcloud_instance"
+  instance_count = var.replicas_nodes
+  location       = var.location
+  name           = "node"
+  dns_domain     = var.dns_domain
+  dns_zone_id    = var.dns_zone_id
+  image          = var.image
+  labels          = {
+    "k8s.io/node" = "true",
+    "k8s.io/master" = "true"
+  }
+  placement_group_id = hcloud_placement_group.k8s.id
+  ssh_keys       = data.hcloud_ssh_keys.all_keys.ssh_keys.*.name
+  server_type    = "cx21"
+  subnet         = hcloud_network_subnet.subnet.id
+  user_data      = file("templates/cloud-init.tpl")
+}
diff --git a/terraform/modules/hcloud_instance/main.tf b/terraform/modules/hcloud_instance/main.tf
new file mode 100644
index 0000000000000000000000000000000000000000..f2de273961ee52625b1068a9066e9e5e4a1ecc04
--- /dev/null
+++ b/terraform/modules/hcloud_instance/main.tf
@@ -0,0 +1,48 @@
+resource "hcloud_server" "server" {
+  count       = var.instance_count
+  name        = "${format("${var.name}%02d", count.index + 1)}.${var.dns_domain}"
+  image       = var.image
+  server_type = var.server_type
+  keep_disk   = var.keep_disk
+  ssh_keys    = var.ssh_keys
+  user_data   = var.user_data
+  location    = var.location
+  labels      = var.labels
+  backups     = var.backups
+  placement_group_id = var.placement_group_id
+  lifecycle {
+    ignore_changes = [user_data, image, firewall_ids]
+  }
+}
+
+resource "cloudflare_record" "dns-a" {
+  count   = var.instance_count
+  zone_id = var.dns_zone_id
+  name    = element(hcloud_server.server.*.name, count.index)
+  value   = element(hcloud_server.server.*.ipv4_address, count.index)
+  type    = "A"
+  ttl     = 120
+}
+
+resource "cloudflare_record" "dns-aaaa" {
+  count   = var.instance_count
+  zone_id = var.dns_zone_id
+  name    = element(hcloud_server.server.*.name, count.index)
+  value   = "${element(hcloud_server.server.*.ipv6_address, count.index)}1"
+  type    = "AAAA"
+  ttl     = 120
+}
+
+resource "hcloud_rdns" "dns-ptr-ipv4" {
+  count      = var.instance_count
+  server_id  = element(hcloud_server.server.*.id, count.index)
+  ip_address = element(hcloud_server.server.*.ipv4_address, count.index)
+  dns_ptr    = element(hcloud_server.server.*.name, count.index)
+}
+
+resource "hcloud_rdns" "dns-ptr-ipv6" {
+  count      = var.instance_count
+  server_id  = element(hcloud_server.server.*.id, count.index)
+  ip_address = "${element(hcloud_server.server.*.ipv6_address, count.index)}1"
+  dns_ptr    = element(hcloud_server.server.*.name, count.index)
+}
diff --git a/terraform/modules/hcloud_instance/network.tf b/terraform/modules/hcloud_instance/network.tf
new file mode 100644
index 0000000000000000000000000000000000000000..b21ddea3dd177a101a6dd683c8a7c4b7d1ab1eeb
--- /dev/null
+++ b/terraform/modules/hcloud_instance/network.tf
@@ -0,0 +1,5 @@
+resource "hcloud_server_network" "server_network" {
+  server_id = element(hcloud_server.server.*.id, count.index)
+  subnet_id = var.subnet
+  count = length(hcloud_server.server.*.id)
+}
diff --git a/terraform/modules/hcloud_instance/output.tf b/terraform/modules/hcloud_instance/output.tf
new file mode 100644
index 0000000000000000000000000000000000000000..e2ec52ef86167d37f4b2bdaf9332a2c01cbe89c4
--- /dev/null
+++ b/terraform/modules/hcloud_instance/output.tf
@@ -0,0 +1,19 @@
+output "server_ids" {
+  value = hcloud_server.server.*.id
+}
+
+output "server_names" {
+  value = hcloud_server.server.*.name
+}
+
+output "internal_ipv4_addresses" {
+  value = hcloud_server_network.server_network.*.ip
+}
+
+output "ipv4_addresses" {
+  value = hcloud_server.server.*.ipv4_address
+}
+
+output "ipv6_addresses" {
+  value = hcloud_server.server.*.ipv6_address
+}
diff --git a/terraform/modules/hcloud_instance/variables.tf b/terraform/modules/hcloud_instance/variables.tf
new file mode 100644
index 0000000000000000000000000000000000000000..325bf7333667b8e80722a4c2ed16200cd159a248
--- /dev/null
+++ b/terraform/modules/hcloud_instance/variables.tf
@@ -0,0 +1,95 @@
+variable "name" {
+  type        = string
+  description = "Instance nam"
+}
+
+variable "dns_domain" {
+  type        = string
+  description = "DNS domain"
+}
+
+variable "dns_zone_id" {
+  description = "Zone ID"
+  default     = null
+}
+
+variable "dns_internal_ip" {
+  description = "Point DNS record to internal ip"
+  default     = false
+}
+
+variable "instance_count" {
+  type        = number
+  description = "Number of instances to deploy"
+  default     = 1
+}
+
+variable "server_type" {
+  type        = string
+  description = "Hetzner Cloud instance type"
+  default     = "cx11"
+}
+
+variable "image" {
+  type        = string
+  description = "Hetzner Cloud system image"
+  default     = "fedora-34"
+}
+
+variable "user_data" {
+  description = "Cloud-Init user data to use during server creation"
+  default     = null
+}
+
+variable "ssh_keys" {
+  type        = list(any)
+  description = "SSH key IDs or names which should be injected into the server at creation time"
+  default     = []
+}
+
+variable "keep_disk" {
+  type        = bool
+  description = "If true, do not upgrade the disk. This allows downgrading the server type later."
+  default     = true
+}
+
+variable "labels" {
+  type        = map(string)
+  description = "Labels that the instance is tagged with."
+  default     = {}
+}
+
+
+variable "location" {
+  type        = string
+  description = "The location name to create the server in. nbg1, fsn1 or hel1"
+  default     = "nbg1"
+}
+
+variable "placement_group_id" {
+description = "Placement Group ID"
+default     = null
+}
+
+variable "backups" {
+  type        = bool
+  description = "Enable or disable backups"
+  default     = false
+}
+
+variable "volume" {
+  type        = bool
+  description = "Enable or disable an additional volume"
+  default     = false
+}
+
+variable "volume_size" {
+  type        = number
+  description = "Size of the additional data volume"
+  default     = 20
+}
+
+variable "subnet" {
+  type        = string
+  description = "Id of the additional internal network"
+}
diff --git a/terraform/modules/hcloud_instance/versions.tf b/terraform/modules/hcloud_instance/versions.tf
new file mode 120000
index 0000000000000000000000000000000000000000..b7707ec81b9ec1f5743a08b56f8479195b92f036
--- /dev/null
+++ b/terraform/modules/hcloud_instance/versions.tf
@@ -0,0 +1 @@
+../../versions.tf
\ No newline at end of file
diff --git a/terraform/modules/hcloud_instance/volumes.tf b/terraform/modules/hcloud_instance/volumes.tf
new file mode 100644
index 0000000000000000000000000000000000000000..0a080461e1c2ca6adc03b8d94e1179079dc0cb60
--- /dev/null
+++ b/terraform/modules/hcloud_instance/volumes.tf
@@ -0,0 +1,8 @@
+resource "hcloud_volume" "volumes" {
+  name      = "${element(hcloud_server.server.*.name, count.index)}-data"
+  size      = var.volume_size
+  format    = "xfs"
+  automount = false
+  server_id = element(hcloud_server.server.*.id, count.index)
+  count     = var.volume == true ? var.instance_count : 0
+}
diff --git a/terraform/network.tf b/terraform/network.tf
new file mode 100644
index 0000000000000000000000000000000000000000..1bb01681c4e1e2534ab19d93395137f82242fc90
--- /dev/null
+++ b/terraform/network.tf
@@ -0,0 +1,11 @@
+resource "hcloud_network" "network" {
+  name     = var.dns_domain
+  ip_range = var.network_cidr
+}
+
+resource "hcloud_network_subnet" "subnet" {
+  network_id   = hcloud_network.network.id
+  type         = "server"
+  network_zone = "eu-central"
+  ip_range     = var.subnet_cidr
+}
diff --git a/terraform/provider.tf b/terraform/provider.tf
new file mode 100644
index 0000000000000000000000000000000000000000..eac840f084bdc6ce9d8da6a04297c9d5afa3d27e
--- /dev/null
+++ b/terraform/provider.tf
@@ -0,0 +1,14 @@
+provider "cloudflare" {
+}
+
+provider "hcloud" {
+}
+
+provider "template" {
+}
+
+provider "local" {
+}
+
+provider "random" {
+}
diff --git a/terraform/ssh_keys.tf b/terraform/ssh_keys.tf
new file mode 100644
index 0000000000000000000000000000000000000000..db794f29d0e7b7cc56cb41a7ce3a1eedfa203cd5
--- /dev/null
+++ b/terraform/ssh_keys.tf
@@ -0,0 +1,2 @@
+data "hcloud_ssh_keys" "all_keys" {
+}
diff --git a/terraform/templates/cloud-init.tpl b/terraform/templates/cloud-init.tpl
new file mode 100644
index 0000000000000000000000000000000000000000..983a4a3c3269ab5fbc67d2e516c5a97123b338c4
--- /dev/null
+++ b/terraform/templates/cloud-init.tpl
@@ -0,0 +1,43 @@
+#!/bin/bash
+
+# System upgrade
+dnf upgrade -y
+
+# Prepare Kubernetes
+dnf install -y kubernetes kubernetes-kubeadm
+
+# Install Crio as container engine
+dnf module enable -y cri-o:1.20
+dnf install -y cri-o cri-tools
+systemctl enable --now crio
+
+# Load kernel modules for Kubernetes and Calico
+modprobe br_netfilter
+modprobe wireguard
+cat <<EOF | sudo tee /etc/modules-load.d/k8s.conf
+br_netfilter
+wireguard
+EOF
+
+# Prepare sysctls for Kubernetes
+cat <<EOF | sudo tee /etc/sysctl.d/k8s.conf
+net.bridge.bridge-nf-call-ip6tables = 1
+net.bridge.bridge-nf-call-iptables = 1
+net.ipv4.ip_forward = 1
+EOF
+sysctl --system
+
+dnf install -y iptables
+
+# Prepare NetworkManager for Calico
+cat <<EOF | sudo tee /etc/NetworkManager/conf.d/calico.conf
+[keyfile]
+unmanaged-devices=interface-name:cali*;interface-name:tunl*;interface-name:vxlan.calico;interface-name:wireguard.cali
+EOF
+
+# Disable systemd-resolved for CoreDNS
+systemctl disable --now systemd-resolved
+rm /etc/resolv.conf
+systemctl restart NetworkManager
+
+reboot
diff --git a/terraform/variables.tf b/terraform/variables.tf
new file mode 100644
index 0000000000000000000000000000000000000000..b8e7f003b00625926fd5b9751be08649a6e8be32
--- /dev/null
+++ b/terraform/variables.tf
@@ -0,0 +1,50 @@
+variable "replicas_nodes" {
+  type        = number
+  default     = 3
+  description = "Count of nodes"
+}
+
+variable "bootstrap" {
+  type        = bool
+  default     = false
+  description = "Whether to deploy a bootstrap instance"
+}
+
+variable "dns_domain" {
+  type        = string
+  description = "Name of the Cloudflare domain"
+}
+
+variable "dns_zone_id" {
+  type        = string
+  description = "Zone ID of the Cloudflare domain"
+}
+
+variable "ip_loadbalancer_api" {
+  description = "IP of an external loadbalancer for api (optional)"
+  default     = null
+}
+
+variable "network_cidr" {
+  type        = string
+  description = "CIDR for the network"
+  default     = "172.16.0.0/12"
+}
+
+variable "subnet_cidr" {
+  type        = string
+  description = "CIDR for the subnet"
+  default     = "172.16.0.0/24"
+}
+
+variable "location" {
+  type        = string
+  description = "Region"
+  default     = "nbg1"
+}
+
+variable "image" {
+  type        = string
+  description = "Image selector"
+  default     = "fedora-34"
+}
diff --git a/terraform/versions.tf b/terraform/versions.tf
new file mode 100644
index 0000000000000000000000000000000000000000..4922ac71ac2d0003abb5f403d059627742ba9f06
--- /dev/null
+++ b/terraform/versions.tf
@@ -0,0 +1,21 @@
+terraform {
+  required_providers {
+    cloudflare = {
+      source  = "cloudflare/cloudflare"
+      version = "3.1.0"
+    }
+    hcloud = {
+      source  = "hetznercloud/hcloud"
+      version = "1.31.1"
+    }
+    template = {
+      source  = "hashicorp/template"
+      version = "2.2.0"
+    }
+    local = {
+      source  = "hashicorp/local"
+      version = "1.4.0"
+    }
+  }
+  required_version = ">= 0.14"
+}
diff --git a/utils/help.mk b/utils/help.mk
new file mode 100644
index 0000000000000000000000000000000000000000..9fbcf043c04c468fcdd3f065d94b33bbe2b365e5
--- /dev/null
+++ b/utils/help.mk
@@ -0,0 +1,3 @@
+.PHONY: help
+help: ## Show this help
+	@egrep -h '\s##\s' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-20s\033[0m %s\n", $$1, $$2}'