Compare commits

...

7 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
11 changed files with 467 additions and 65 deletions

View File

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

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

@@ -1,28 +1,6 @@
# This file is maintained automatically by "terraform init". # This file is maintained automatically by "terraform init".
# Manual edits may be lost in future updates. # Manual edits may be lost in future updates.
provider "registry.terraform.io/hashicorp/aws" {
version = "6.27.0"
hashes = [
"h1:emgTfB1LXSFYh9uAwgsRMoMIN5Wz7jNNKq3rqC0EHWk=",
"zh:177a24b806c72e8484b5cabc93b2b38e3d770ae6f745a998b54d6619fd0e8129",
"zh:4ac4a85c14fb868a3306b542e6a56c10bd6c6d5a67bc0c9b8f6a9060cf5f3be7",
"zh:552652185bc85c8ba1da1d65dea47c454728a5c6839c458b6dcd3ce71c19ccfc",
"zh:60284b8172d09aee91eae0856f09855eaf040ce3a58d6933602ae17c53f8ed04",
"zh:6be38d156756ca61fb8e7c752cc5d769cd709686700ac4b230f40a6e95b5dbc9",
"zh:7a409138fae4ef42e3a637e37cb9efedf96459e28a3c764fc4e855e8db9a7485",
"zh:8070cf5224ed1ed3a3e9a59f7c30ff88bf071c7567165275d477c1738a56c064",
"zh:894439ef340a9a79f69cd759e27ad11c7826adeca27be1b1ca82b3c9702fa300",
"zh:89d035eebf08a97c89374ff06040955ddc09f275ecca609d0c9d58d149bef5cf",
"zh:985b1145d724fc1f38369099e4a5087141885740fd6c0b1dbc492171e73c2e49",
"zh:9b12af85486a96aedd8d7984b0ff811a4b42e3d88dad1a3fb4c0b580d04fa425",
"zh:a80b47ae8d1475201c86bd94a5dcb9dd4da5e8b73102a90820b68b66b76d50fd",
"zh:d3395be1556210f82199b9166a6b2e677cee9c4b67e96e63f6c3a98325ad7ab0",
"zh:db0b869d09657f6f1e4110b56093c5fcdf9dbdd97c020db1e577b239c0adcbce",
"zh:ffc72e680370ae7c21f9bd3082c6317730df805c6797427839a6b6b7e9a26a01",
]
}
provider "registry.terraform.io/hetznercloud/hcloud" { provider "registry.terraform.io/hetznercloud/hcloud" {
version = "1.57.0" version = "1.57.0"
constraints = "~> 1.45" constraints = "~> 1.45"

View File

@@ -12,13 +12,6 @@ resource "hcloud_network_subnet" "subnet" {
ip_range = local.subnet-cidr ip_range = local.subnet-cidr
} }
// Attach the load balancer to the compute subnet.
resource "hcloud_load_balancer_network" "attachment" {
load_balancer_id = hcloud_load_balancer.lb.id
subnet_id = hcloud_network_subnet.subnet.id
ip = local.lb-private-ip
}
// Provide internet to the private servers, by sending all internet traffic to // Provide internet to the private servers, by sending all internet traffic to
// the NAT. // the NAT.
resource "hcloud_network_route" "gateway" { resource "hcloud_network_route" "gateway" {
@@ -29,6 +22,6 @@ resource "hcloud_network_route" "gateway" {
// A managed certificate for the domain, to be used by the load balancer. // A managed certificate for the domain, to be used by the load balancer.
resource "hcloud_managed_certificate" "managed_cert" { resource "hcloud_managed_certificate" "managed_cert" {
name = "managed_cert" name = "Main Certificate"
domain_names = ["*.${local.domain}", "${local.domain}"] domain_names = ["*.${local.domain}", "${local.domain}"]
} }

View File

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

View File

@@ -1,36 +1,38 @@
$ANSIBLE_VAULT;1.1;AES256 $ANSIBLE_VAULT;1.1;AES256
35323436626135616534616432353038616532613138393165366534663361636633303336643766 33653833333639353437316264356533353739383838306564656334653238636239633366393665
6538656163363065363832333961653736373231366230340a363539376639323635303131343966 6239383662613562396562326238663733633962303365630a663235633337613137636563353932
38626563666630663934333633303636323964666339353035396566326634336134666436383061 31626533303532613566356638346663643337393839653536613866386339666537643435396133
3838623462363565640a383832306334646530326133653235323730386563343462316332333236 3030373532333061300a323835623262626531663666386130333034316237626635306536303764
65646265356132343761323661636235363838643838343936353862646336376461633433666364 33343338333234383937363064616364623864353338653366623333356336333939656433373663
36613763346439636263303166323662633237353533363064613437636561616538343832656531 65653433386435313634653037623462636333303535373432306463353833663466653236363630
31323064663534363234303633386562313565366366366533383761663466326333393135343534 32646664656634643331616331396230346263363838373133383737396461623434333837633832
31646263333736353132356136343765396362373864616261363666636262313631303663653765 34653333373364396438376130383265373534383936373131646437333535306631386662626439
64356133656536333366663237366562343266383462323630346434363065303466373035386131 31336634343466333732343639353239656632356632663333663464303965656563343737376630
63356434396433353731656233346465656536303437656232353831373062386161313165326164 61343337353561386139653739313562643738306535636264623430396365323531373133663063
61393031613733396131643762363465303833326633326364343631383733383966343965306530 37393330313461613865313633363136383663643331356461633662626232613937393761393561
65316638633132633562316637373166623631636237346435613733336333616138323534633266 32663437393332643334356330386536663965303634646261393137316662653637623064353263
33623362653166646639373137396265353064326435646138336635383232663437336238343366 66366234303664373861343766363563303539646239366637303666323062616339653234306438
66613735306235353563383762383763653964313766616539336362616332663138353233326261 32636237346563646365393636383964663336653833323566316135373463383933623037653563
36313631663832633163633966613864323331356362376437393563323061303864646665623332 32626263323136333330393735363839333062303963316364626363323330363735356266653834
65363236323339346232303763326636333935306363323164626635663833356661386532303430 61303339373965653964383331646664353130363131303664313433303131386138646236663966
31346133313639356361316563343761623466633030633030333138353538656633643630653862 63353463393538303939383566396264623739633331383834323334626635636231353930313838
34633638666562656434346636613136323461353565323133613461646638366135353638393834 32346134643830613365613337613331386533356633386630616562653264326531383632646463
36373030643030303365353531313665643666383433333562626532323363366432336333623733 66373465393336643538373935373738376665656561316162386238393433613465643132373032
62306462616538313139646437333363616364343766636562366235613635373335613164336361 36393665643166336265643730383833343064306432626233356639303832326463613164643361
33313130353361373164643833636231383233323034313861343134363964653031396530643537 65633336613130643330633062373865303763616132643337333261643438323662626261313237
36626561303839653234313466356330633666386166623335623864653935316566366539626238 30663963633364353933623134363062643565393766323465643130613234646535386462333763
62663438643730653463343634626361633766343366626266386538326136323932626636313864 31653737613334653231356634316532626530376530363562363439373831663065623830663739
66393465646366633036396538336662643230646631623436386462383434643665383233393930 37613165626630323262306335306136376639623238633635613430373065303766386131616235
38306265393832386262616261643863306438656662666333306662366464393736623939656565 62323739363564386138336330373930333462306231323238613363623233326164623130383338
33353637353861636562323639306562343235316566353130663563623538656131363132356231 31303733663165346532323839353364643833646633333562303932373161613263323930363963
61636239346138353833633065633030653138306262633830303739356662353063363032656439 34623732613465386563376433656330663039346162643130333866333431313733646263623234
34623336336233626362316638663335613639363533393833623533306334636532623264323966 36646662646630666436633862643161393561646236333437323231646134376338633664356664
64393234656463643438303630306565353534663066363065383533336130323434393131326166 39346434636262636663313232633733396464383666633730363438356638636533633937396236
61636538356265343937626435653763366333326364376461616138313637316535636238376263 39623739383239636365353364373438383334363636613864663932303863326266373666623130
63316430323863316366653338653261373438626563643433346133363031313464323964326230 39643833323637393837363365343363396534326461656433646562323765356339346437383262
30626461333437316332616462373334643736623930666432343966623436623834613038626137 31353636316530393935666266383834376236316633643630306462666531396263643261643833
35613730303166303739316132306164383965636532663563613166373337363030636630346164 64326530316636376230306234336233373361333933613233643937306333306361323938636264
63396166353534373338626261366437653662353166366263653562316562313433616232323636 66306565316663623862616331656532626165363732646137366338653561626261653763356166
373163323937626435303963323632343631 63356336623761613731306464636438346462623535306431646666623061376565353132303765
64353265396137663264626264333439376538373634636533383338376134306561636339623437
6466