Slide 1

Slide 1 text

Self-hosting Kubernetes at Home with Terraform Because I can’t remember all Helm packages…

Slide 2

Slide 2 text

About me kahnwong Karnsiree Wong karnwong.me Platform Engineer @Data Cafe Company Limited Firmly believes that all configurations should be documented as code Loves automation

Slide 3

Slide 3 text

Table of contents 1. What is Kubernetes 2. Anatomy of a Kubernetes Deployment 3. Enter Helm 1. Folder Structure 4. Enter Terraform 1. Terraform Project Structure 2. Project Setup 3. Setup Namespaces 4. Setup Deployments 1. Terraform Data Structures 5. Setup ConfigMaps 6. Setup Secrets 5. Conclusion

Slide 4

Slide 4 text

What is Kubernetes Containers orchestration platform Can scale workloads Baked in service discovery, load balancing, storage orchestration, and self-healing Ensures high availability and fault tolerance by distributing workloads across multiple nodes Available on major cloud providers Planet scale

Slide 5

Slide 5 text

Anatomy of a Kubernetes Deployment Deployment Service In practice Create Deployment and Service manifest Run kubectl apply -f deployment.yaml & kubectl apply -f service.yaml Sometimes you also need ConfigMap or Secret Which translate to: two more kubectl apply -f $manifest.yaml That’s a total 4 manifest files per deployment And you need to apply 4 commands to get a deployment working At minimum it would require following manifests:

Slide 6

Slide 6 text

Enter Helm Sample Helm values: Helm is a package manager for Kubernetes kind: Deployment name: qa-api replicaCount: 1 # deployment containers: - name: qa-api repository: ghcr.io/kahnwong/qa-api tag: "95c411c" port: 3000 envFrom: - secretRef: name: qa-api # service service: port: 3000 nodePort: 30043

Slide 7

Slide 7 text

Folder Structure Without Helm With Helm But then we still need to run helm command for every deployment manually… . ├── appA │ ├── deployment.yaml │ └── service.yaml ├── appB ├── deployment.yaml └── service.yaml # deploy appA $ kubectl apply -f appA/deployment.yaml $ kubectl apply -f appA/service.yaml # deploy appB $ kubectl apply -f appB/deployment.yaml $ kubectl apply -f appB/service.yaml . ├── chart │ ├── Chart.yaml │ └── templates │ ├── deployment.yaml │ ├── _helpers.tpl │ └── service.yaml └── deployments ├── appA.yaml └── appB.yaml # deploy appA $ helm install appA chart --values deployments/$appA.yaml # deploy appB $ helm install appB chart --values deployments/$appB.yaml

Slide 8

Slide 8 text

Enter Terraform ⚠️⚠️⚠️ For production you should not use tereraform to manage app deployments In practice, you will need other Kubernetes manifests, such as ConfigMap and Secret . Terraform will help you with: Keeping track of all resources within your cluster Find configurations drift if any Update deployments / configurations with a single command ( terraform apply ) Keep you sane :) Use code to manage infrastructure and deployments.

Slide 9

Slide 9 text

Terraform Project Structure . ├── configmaps │ ├── bar.yaml │ └── foo.yaml ├── secrets │ ├── bar.sops.yaml │ └── foo.sops.yaml ├── helm │ └── deployments │ └── default │ ├── bar.yaml │ └── foo.yaml ├── configmaps.tf # k8s - ConfigMap ├── deployments.tf # k8s - Deployment ├── namespaces.tf # k8s - Namespace ├── providers.tf # terraform ├── secrets.tf # k8s - Secret ├── terraform.tf # terraform └── variables.tf # terraform

Slide 10

Slide 10 text

Project Setup provider "helm" { kubernetes { host = var.host client_certificate = base64decode(var.client_certificate) client_key = base64decode(var.client_key) cluster_ca_certificate = base64decode(var.cluster_ca_certificate) } } provider "kubernetes" { host = var.host client_certificate = base64decode(var.client_certificate) client_key = base64decode(var.client_key) cluster_ca_certificate = base64decode(var.cluster_ca_certificate) } provider "sops" {} # for secrets decryption

Slide 11

Slide 11 text

Setup Namespaces locals { namespaces = toset([ "default", "manchester", "quebec", ]) } resource "kubernetes_namespace" "this" { for_each = setsubtract(local.namespaces, toset(["default"])) metadata { name = each.key } }

Slide 12

Slide 12 text

Setup Deployments locals { deployments = tomap({ default = ["foo", "bar"] manchester = ["manchester", "oldham"] quebec = ["montreal", "terrebonne"] }) } locals { deployments_map_raw = flatten([ for namespace, deployments in local.deployments : [ for deployment in deployments : { deployment = deployment namespace = namespace } ] ]) deployments_map = { for index, v in local.deployments_map_raw : v.deployment => v.namespace } } resource "helm_release" "this" { for_each = local.deployments_map name = each.key namespace = each.value repository = "oci://ghcr.io/kahnwong/charts" version = "0.2.0" chart = "base" values = [ file("./helm/deployments/${each.value}/${each.key}.yaml") ] }

Slide 13

Slide 13 text

Terraform Data Structures From To locals { deployments = tomap({ default = ["foo", "bar"] manchester = ["manchester", "oldham"] quebec = ["montreal", "terrebonne"] }) } # $deployment = $namespace { "foo" = "default" "bar" = "default" "manchester" = "manchester" "oldham" = "manchester" "montreal" = "quebec" "terrebonne" = "quebec" }

Slide 14

Slide 14 text

Setup ConfigMaps Local Block locals { configmaps = tomap({ default = [ { deployment = "foo" filename = "conf.yml" }, { deployment = "bar" filename = "conf.yml" } ] }) }

Slide 15

Slide 15 text

Setup ConfigMaps Local Block (cont) locals { configmaps_map_raw = flatten([ for namespace, configmaps in local.configmaps : [ for configmap in configmaps : { namespace = namespace deployment = configmap.deployment filename = configmap.filename } ] ]) configmaps_map = { for index, v in local.configmaps_map_raw : v.deployment => v } }

Slide 16

Slide 16 text

Setup ConfigMaps Resource Block resource "kubernetes_config_map" "configmaps" { for_each = local.configmaps_map metadata { name = each.key namespace = each.value.namespace } data = { (each.value.filename) = file("${path.module}/configmaps/${each.key}.sops.yaml") } }

Slide 17

Slide 17 text

Setup Secrets Local Block locals { secrets = tomap({ default = ["foo", "bar"] }) } locals { secrets_map_raw = flatten([ for namespace, secrets in local.secrets : [ for secret in secrets : { secret = secret namespace = namespace } ] ]) secrets_map = { for index, v in local.secrets_map_raw : "${v.namespace}-${v.secret}" => tomap(v) } }

Slide 18

Slide 18 text

Setup Secrets Resource Block data "sops_file" "secrets" { for_each = local.secrets_map source_file = "./secrets/${each.value.secret}.sops.yaml" } resource "kubernetes_secret" "secrets" { for_each = local.secrets_map metadata { name = each.value.secret namespace = each.value.namespace } data = nonsensitive(data.sops_file.secrets["${each.value.namespace}-${each.value.secret}"].data) depends_on = [data.sops_file.secrets] }

Slide 19

Slide 19 text

Any questions?

Slide 20

Slide 20 text

Conclusion Kubernetes can help you manage multiple workloads Deploying Kubernetes workloads manually can be tedious and prone to errors Terraform can help you keep track of cluster configurations and workloads With proper Terraform local blocks usage, you can even simplify the setup further

Slide 21

Slide 21 text

Thank You Slides Blog GitHub LinkedIn