Compare commits

...

12 Commits

Author SHA1 Message Date
Max
9e90135b00 feat: simple hello service 2026-01-01 19:05:44 -05:00
Max
ac8b6d8f9a fix: automatically create dns recods 2026-01-01 18:59:53 -05:00
Max
4768c94b24 feat: got it all working 2026-01-01 15:07:44 -05:00
Max
380ddb8920 feat: up to final step 2025-12-31 17:23:04 -05:00
Max
9a323672bc feat: up to hetzner cloud controller creation 2025-12-31 16:19:17 -05:00
Max
9066a8e600 fix: remove aws provider 2025-12-31 14:12:34 -05:00
Max
31118df33a fix: forgot to remove lb attachment 2025-12-31 14:05:05 -05:00
Max
d4ae65180b feat: removed lb, added managed cert 2025-12-31 14:03:47 -05:00
Max
f39b29288e feat: attach lb to network 2025-12-31 12:13:02 -05:00
Max
a916da7d45 feat: new load balancer, with dns via route 53 2025-12-31 12:10:52 -05:00
Max
b8938983dc feat: add control plane to inventory 2025-12-31 11:10:17 -05:00
Max
42642ae933 feat: add control plane node 2025-12-31 11:07:40 -05:00
13 changed files with 487 additions and 33 deletions

View File

@@ -1,3 +1,9 @@
# hetzner-cluster
Create a private Kubernetes cluster on Hetzner Cloud.
Create a private Kubernetes cluster on Hetzner Cloud.
## Resources
<https://community.hetzner.com/tutorials/kubernetes-on-hetzner-with-crio-flannel-and-hetzner-balancer>
<https://community.hetzner.com/tutorials/how-to-set-up-nat-for-cloud-networks>

View File

@@ -6,6 +6,7 @@ tasks:
tf:destroy: ansible-playbook playbooks/destroy.yml {{.CLI_ARGS}}
configure-nat: ansible-playbook playbooks/configure_nat.yml {{.CLI_ARGS}}
configure-servers: ansible-playbook playbooks/configure_servers.yml {{.CLI_ARGS}}
deploy: ansible-playbook playbooks/install_k8s.yml {{.CLI_ARGS}}
enter:
cmd: ssh -i {{.KEY}} -p 22 root@{{.IP}}

View File

@@ -3,4 +3,5 @@ nat
[servers]
node-a
node-b
node-b
control

292
playbooks/install_k8s.yml Normal file
View File

@@ -0,0 +1,292 @@
- name: Configure compute for the cluster.
hosts: servers
gather_facts: false
vars:
kubernetes_version: v1.30
tasks:
- name: Download Kubernetes key.
ansible.builtin.apt_key:
url: https://pkgs.k8s.io/core:/stable:/{{ kubernetes_version }}/deb/Release.key
state: present
- name: Download Kubernetes repository.
ansible.builtin.apt_repository:
repo: "deb https://pkgs.k8s.io/core:/stable:/{{ kubernetes_version }}/deb/ /"
state: present
- name: Download CRI-O key.
ansible.builtin.apt_key:
url: https://pkgs.k8s.io/addons:/cri-o:/prerelease:/main/deb/Release.key
state: present
- name: Download CRI-O repository.
ansible.builtin.apt_repository:
repo: "deb https://pkgs.k8s.io/addons:/cri-o:/prerelease:/main/deb/ /"
state: present
- name: Download Helm key.
ansible.builtin.apt_key:
url: https://packages.buildkite.com/helm-linux/helm-debian/gpgkey
state: present
- name: Download Helm repository.
ansible.builtin.apt_repository:
repo: "deb https://packages.buildkite.com/helm-linux/helm-debian/any/ any main"
state: present
- name: Install packages.
ansible.builtin.apt:
state: present
update_cache: true
name: [cri-o, kubelet, kubeadm, kubectl, python3-pip, helm, git]
- name: Install Kubernetes Python packages.
ansible.builtin.pip:
name: [kubernetes, pyyaml]
state: present
break_system_packages: true
- name: Enable IPv4 forwarding.
ansible.posix.sysctl:
name: net.ipv4.ip_forward
value: '1'
sysctl_set: true
notify: Reboot the nodes.
- name: Enable `br_netfilter` module.
community.general.modprobe:
name: br_netfilter
state: present
notify: Reboot the nodes.
handlers:
- name: Reboot the nodes.
ansible.builtin.reboot:
- name: Spawn new cluster on control node.
hosts: control
gather_facts: false
vars:
config_template: ../templates/InitConfiguration.yml.jinja2
config:
bootstrap_token: "{{ secrets.bootstrap_token }}"
node_ip: 10.0.2.11
node_name: control
vars_files:
- ../vault.yml
tasks:
- name: Test for cluster.
kubernetes.core.k8s_cluster_info:
register: api_status
ignore_errors: true
- name: Copy configuration over.
ansible.builtin.template:
src: "{{ config_template }}"
dest: InitConfiguration.yml
mode: preserve
when: "api_status.failed"
- name: Initialize cluster.
ansible.builtin.command:
kubeadm init --config InitConfiguration.yml
changed_when: true
when: "api_status.failed"
- name: Apply the Kubernetes config to the shell.
ansible.builtin.lineinfile:
path: /etc/environment
line: 'KUBECONFIG=/etc/kubernetes/admin.conf'
when: "api_status.failed"
- name: Join worker nodes to cluster.
hosts: [node-a, node-b]
vars:
join_template: ../templates/JoinConfiguration.yml.jinja2
join_control_ip: 10.0.2.11
join_bootstrap_token: "{{ secrets.bootstrap_token }}"
vars_files:
- ../vault.yml
tasks:
- name: Copy join configuration over.
vars:
join_worker_ip: "{{ ansible_default_ipv4.address }}"
join_worker_name: "{{ ansible_hostname }}"
ansible.builtin.template:
src: "{{ join_template }}"
dest: JoinConfiguration.yml
mode: preserve
- name: Join the nodes.
ansible.builtin.command:
kubeadm join --config JoinConfiguration.yml
changed_when: true
- name: Install Helm Diff.
gather_facts: false
hosts: control
tasks:
- name: Install it.
kubernetes.core.helm_plugin:
plugin_path: https://github.com/databus23/helm-diff
state: present
- name: Install CNI.
gather_facts: false
hosts: control
tasks:
- name: Create Flannel namespace.
kubernetes.core.k8s:
state: present
kind: Namespace
name: kube-flannel
- name: Add privilege to the namespace.
kubernetes.core.k8s:
state: patched
kind: Namespace
name: kube-flannel
definition:
metadata:
labels:
pod-security.kubernetes.io/enforce: privileged
- name: Add Flannel repository.
kubernetes.core.helm_repository:
name: flannel
url: https://flannel-io.github.io/flannel/
state: present
- name: Install Flannel.
kubernetes.core.helm:
name: flannel
chart_ref: flannel/flannel
namespace: kube-flannel
values:
podCidr: 10.244.0.0/16
state: present
- name: Patch CoreDNS deployment.
kubernetes.core.k8s_json_patch:
name: coredns
namespace: kube-system
kind: Deployment
patch:
- op: add
path: /spec/template/spec/tolerations/-
value:
key: node.cloudprovider.kubernetes.io/uninitialized
value: "true"
effect: NoSchedule
- name: Install Hetzner Cloud Controller.
gather_facts: false
hosts: control
vars_files:
- ../vault.yml
- ../secrets/tf_outputs.yml
tasks:
- name: Create `hcloud` secret.
kubernetes.core.k8s:
name: hcloud
namespace: kube-system
kind: Secret
state: present
definition:
apiVersion: v1
kind: Secret
metadata:
name: hcloud
namespace: kube-system
type: Opaque
data:
token: "{{ secrets.hcloud_token | b64encode }}"
network: "{{ private_network_id.value | b64encode }}"
- name: Add Cloud Controller repository.
kubernetes.core.helm_repository:
name: hcloud
url: https://charts.hetzner.cloud
state: present
- name: Copy over values file.
vars:
values_template: ../templates/HCCMValues.yml.jinja2
ansible.builtin.template:
src: "{{ values_template }}"
dest: HCCMValues.yml
mode: preserve
- name: Install it.
kubernetes.core.helm:
name: hccm
chart_ref: hcloud/hcloud-cloud-controller-manager
namespace: kube-system
state: present
update_repo_cache: true
force: true
values_files: [HCCMValues.yml]
- name: Install `nginx` Controller.
gather_facts: false
hosts: control
tasks:
# This makes sure Hetzer gives the nodes a proper Provider ID.
- name: Add schedule taint to nodes.
kubernetes.core.k8s_taint:
state: present
name: "{{ item }}"
taints:
- key: node.cloudprovider.kubernetes.io/uninitialized
value: "true"
effect: NoSchedule
loop: [node-a, node-b]
- name: Add `ingress-nginx` repository.
kubernetes.core.helm_repository:
name: ingress-nginx
url: https://kubernetes.github.io/ingress-nginx
state: present
- name: Copy over values file.
vars:
values_template: ../templates/IngressValues.yml.jinja2
ansible.builtin.template:
src: "{{ values_template }}"
dest: IngressValues.yml
mode: preserve
- name: Install it.
kubernetes.core.helm:
name: ingress-nginx-controller
chart_ref: ingress-nginx/ingress-nginx
namespace: kube-system
state: present
update_repo_cache: true
values_files: [IngressValues.yml]
- name: Connect DNS to the Load Balancer.
gather_facts: false
vars_files:
- ../vault.yml
hosts: localhost
tasks:
- name: Get the IP address of the LB.
hetzner.hcloud.load_balancer_info:
api_token: "{{ secrets.hcloud_token }}"
name: "hetzner-lb"
register: hetzner_lb_info
- name: Connect DNS.
vars:
ip_address: "{{ hetzner_lb_info.hcloud_load_balancer_info[0].ipv4_address }}"
hetzner.hcloud.zone_rrset:
api_token: "{{ secrets.hcloud_token }}"
zone: "{{ secrets.zone_name }}"
name: "{{ item }}"
type: A
records:
- value: "{{ ip_address }}"
state: present
loop: ["*", "@"]

View File

@@ -0,0 +1,3 @@
networking:
enabled: "true"
clusterCIDR: "10.244.0.0/16"

View File

@@ -0,0 +1,51 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: hello-app
spec:
replicas: 2
selector:
matchLabels:
app: hello
template:
metadata:
labels:
app: hello
spec:
containers:
- name: hello
image: hashicorp/http-echo
args: ["-text=Hello from Kubernetes!"]
ports:
- containerPort: 5678
---
apiVersion: v1
kind: Service
metadata:
name: hello-service
spec:
selector:
app: hello
ports:
- port: 80
targetPort: 5678
---
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: hello-ingress
annotations:
nginx.ingress.kubernetes.io/rewrite-target: /
spec:
ingressClassName: nginx
rules:
- host: hello.maximhutz.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: hello-service
port:
number: 80

View File

@@ -0,0 +1,18 @@
controller:
dnsPolicy: ClusterFirstWithHostNet
hostNetwork: true
kind: DaemonSet
service:
annotations:
load-balancer.hetzner.cloud/name: "hetzner-lb"
load-balancer.hetzner.cloud/location: "fsn1"
load-balancer.hetzner.cloud/type: "lb11"
load-balancer.hetzner.cloud/ipv6-disabled: "true"
load-balancer.hetzner.cloud/use-private-ip: "true"
load-balancer.hetzner.cloud/protocol: "https"
load-balancer.hetzner.cloud/network-zone: "eu-central"
load-balancer.hetzner.cloud/http-certificates: "Main Certificate"
load-balancer.hetzner.cloud/http-redirect-http: "true"
enableHttp: false
targetPorts:
https: http

View File

@@ -0,0 +1,42 @@
apiVersion: kubeadm.k8s.io/v1beta3
bootstrapTokens:
- groups:
- system:bootstrappers:kubeadm:default-node-token
token: {{ config.bootstrap_token }}
ttl: 24h0m0s
usages:
- signing
- authentication
kind: InitConfiguration
localAPIEndpoint:
advertiseAddress: {{ config.node_ip }}
bindPort: 6443
nodeRegistration:
criSocket: unix:///var/run/crio/crio.sock
imagePullPolicy: IfNotPresent
kubeletExtraArgs:
cloud-provider: external
node-ip: {{ config.node_ip }}
name: {{ config.node_name }}
taints: null
---
apiServer:
timeoutForControlPlane: 4m0s
certSANs:
- {{ config.node_ip }}
apiVersion: kubeadm.k8s.io/v1beta3
certificatesDir: /etc/kubernetes/pki
clusterName: kubernetes
controllerManager: {}
dns: {}
etcd:
local:
dataDir: /var/lib/etcd
imageRepository: registry.k8s.io
kind: ClusterConfiguration
kubernetesVersion: 1.30.0
networking:
dnsDomain: cluster.local
serviceSubnet: 10.96.0.0/12
podSubnet: 10.244.0.0/16
scheduler: {}

View File

@@ -0,0 +1,18 @@
apiVersion: kubeadm.k8s.io/v1beta3
caCertPath: /etc/kubernetes/pki/ca.crt
discovery:
bootstrapToken:
apiServerEndpoint: {{ join_control_ip }}:6443
token: {{ join_bootstrap_token }}
unsafeSkipCAVerification: true
timeout: 5m0s
tlsBootstrapToken: {{ join_bootstrap_token }}
kind: JoinConfiguration
nodeRegistration:
criSocket: unix:///var/run/crio/crio.sock
imagePullPolicy: IfNotPresent
kubeletExtraArgs:
cloud-provider: external
node-ip: {{ join_worker_ip }}
name: {{ join_worker_name }}
taints: null

View File

@@ -19,3 +19,9 @@ resource "hcloud_network_route" "gateway" {
destination = "0.0.0.0/0"
gateway = local.nat-private-ip
}
// A managed certificate for the domain, to be used by the load balancer.
resource "hcloud_managed_certificate" "managed_cert" {
name = "Main Certificate"
domain_names = ["*.${local.domain}", "${local.domain}"]
}

View File

@@ -1,3 +1,7 @@
output "nat_public_ip" {
value = hcloud_server.nat.ipv4_address
}
output "private_network_id" {
value = hcloud_network.net.id
}

View File

@@ -3,10 +3,14 @@ locals {
subnet-cidr = "10.0.2.0/24"
nat-private-ip = "10.0.2.8"
lb-private-ip = "10.0.2.128"
servers = {
node-a = "10.0.2.9"
node-b = "10.0.2.10"
control = "10.0.2.11"
node-a = "10.0.2.9"
node-b = "10.0.2.10"
}
domain = "maximhutz.com"
}
variable "public_key_file" {

View File

@@ -1,30 +1,38 @@
$ANSIBLE_VAULT;1.1;AES256
39353161316464393931613836373735326631623763363337313366303631326661356239346261
3939383335343235643439393434353266376662336136640a616231346663633738336265393437
61386631343036353765643139373764353437323562376263316562346430613837663833383638
3866353634633564310a343435393662626239666564363631666165326364313730653031393466
63316539346466663535306232633835306664353262376366623630653730386565333262326430
65656435386530623463643436626236393065376639636161316161313236376565353761653835
65313039313339323933343765303561643631346236396638306131613936666165353661613731
31333162613364333230396638363663613435613637313536623330336463613363623034613565
38326332343537323364373930623435313231666264646562383930633731306130323938393838
35646261646635303030373265396139336363656463633230646332643832336239323137633337
62343438616266623630336235663563666432383732393963396436613934653931633036346332
66376433376265363539313435383965316564653232333737333137383065363362346562346463
65343033353163316430656132663261626562323762303165633666613233353132376531303532
66376131396633366435336165326432356535663232643631316464613065316139623363356537
66326634656330303537376633633463616663633761643561306665326364333464333163386635
37376362643065656265386232373066313562396139636131626330303364353636613139383565
63393733303638393335303765343366353063633433666363323631303334396537323430366361
65313131383339383265323436356630623265313436653565346133383335303862613234663539
65366434623639346261373361393065386466623061613165326436313935376137363236333663
36653933623836316163663531373139303334333162393637613633303261653934623830313337
63363931393639373533393236646361313938643062356436626266373339316437323930383464
61306132303463623162646264613439386137396464613930353439646135313036356138323039
37366430386661333862666364313563663239383435613161613436313831633863356465623437
62343639336638346663363036313965623833616632653065313031393137656562663563613637
62396130396238333232323632656334333030373265613230306332643166353135656335346535
38316265383866306333356263643233333861663036316164313932306134396133356332336461
32323833323430653830396430393731343961393638656534663963333632646364303063356432
33623436646539373864386535383832333461313936613363346433666636623561373837383537
6237
33653833333639353437316264356533353739383838306564656334653238636239633366393665
6239383662613562396562326238663733633962303365630a663235633337613137636563353932
31626533303532613566356638346663643337393839653536613866386339666537643435396133
3030373532333061300a323835623262626531663666386130333034316237626635306536303764
33343338333234383937363064616364623864353338653366623333356336333939656433373663
65653433386435313634653037623462636333303535373432306463353833663466653236363630
32646664656634643331616331396230346263363838373133383737396461623434333837633832
34653333373364396438376130383265373534383936373131646437333535306631386662626439
31336634343466333732343639353239656632356632663333663464303965656563343737376630
61343337353561386139653739313562643738306535636264623430396365323531373133663063
37393330313461613865313633363136383663643331356461633662626232613937393761393561
32663437393332643334356330386536663965303634646261393137316662653637623064353263
66366234303664373861343766363563303539646239366637303666323062616339653234306438
32636237346563646365393636383964663336653833323566316135373463383933623037653563
32626263323136333330393735363839333062303963316364626363323330363735356266653834
61303339373965653964383331646664353130363131303664313433303131386138646236663966
63353463393538303939383566396264623739633331383834323334626635636231353930313838
32346134643830613365613337613331386533356633386630616562653264326531383632646463
66373465393336643538373935373738376665656561316162386238393433613465643132373032
36393665643166336265643730383833343064306432626233356639303832326463613164643361
65633336613130643330633062373865303763616132643337333261643438323662626261313237
30663963633364353933623134363062643565393766323465643130613234646535386462333763
31653737613334653231356634316532626530376530363562363439373831663065623830663739
37613165626630323262306335306136376639623238633635613430373065303766386131616235
62323739363564386138336330373930333462306231323238613363623233326164623130383338
31303733663165346532323839353364643833646633333562303932373161613263323930363963
34623732613465386563376433656330663039346162643130333866333431313733646263623234
36646662646630666436633862643161393561646236333437323231646134376338633664356664
39346434636262636663313232633733396464383666633730363438356638636533633937396236
39623739383239636365353364373438383334363636613864663932303863326266373666623130
39643833323637393837363365343363396534326461656433646562323765356339346437383262
31353636316530393935666266383834376236316633643630306462666531396263643261643833
64326530316636376230306234336233373361333933613233643937306333306361323938636264
66306565316663623862616331656532626165363732646137366338653561626261653763356166
63356336623761613731306464636438346462623535306431646666623061376565353132303765
64353265396137663264626264333439376538373634636533383338376134306561636339623437
6466