extract remaining 19 modules from platform, complete stack split [ci skip]
Phase 3: all 27 platform modules now run as independent stacks. Platform reduced to empty shell (outputs only) for backward compat with 72 app stacks that declare dependency "platform". Fixed technitium cross-module dashboard reference by copying file. Woodpecker pipeline applies all 27+1 stacks in parallel via loop. All applied with zero destroys.
This commit is contained in:
parent
f7c3a338a5
commit
263d97bea2
134 changed files with 7930 additions and 270 deletions
|
|
@ -45,18 +45,15 @@ steps:
|
|||
- "chmod 755 /usr/local/bin/terragrunt"
|
||||
# Source Vault token
|
||||
- "source .vault-env"
|
||||
# Apply extracted stacks in parallel
|
||||
- "cd stacks/dbaas && terragrunt apply --non-interactive -auto-approve &"
|
||||
- "cd stacks/authentik && terragrunt apply --non-interactive -auto-approve &"
|
||||
- "cd stacks/crowdsec && terragrunt apply --non-interactive -auto-approve &"
|
||||
- "cd stacks/monitoring && terragrunt apply --non-interactive -auto-approve &"
|
||||
- "cd stacks/nvidia && terragrunt apply --non-interactive -auto-approve &"
|
||||
- "cd stacks/mailserver && terragrunt apply --non-interactive -auto-approve &"
|
||||
- "cd stacks/cloudflared && terragrunt apply --non-interactive -auto-approve &"
|
||||
- "cd stacks/kyverno && terragrunt apply --non-interactive -auto-approve &"
|
||||
# Apply platform stack (remaining core infrastructure services)
|
||||
- "cd stacks/platform && terragrunt apply --non-interactive -auto-approve"
|
||||
- "wait"
|
||||
# Apply all platform stacks in parallel
|
||||
- |
|
||||
for stack in dbaas authentik crowdsec monitoring nvidia mailserver cloudflared kyverno \
|
||||
metallb redis traefik technitium headscale rbac k8s-portal vaultwarden \
|
||||
reverse-proxy metrics-server vpa nfs-csi iscsi-csi cnpg sealed-secrets \
|
||||
uptime-kuma wireguard xray infra-maintenance platform; do
|
||||
(cd stacks/$stack && terragrunt apply --non-interactive -auto-approve) &
|
||||
done
|
||||
wait
|
||||
|
||||
- name: cleanup-and-push
|
||||
image: alpine
|
||||
|
|
|
|||
4
stacks/cnpg/main.tf
Normal file
4
stacks/cnpg/main.tf
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
module "cnpg" {
|
||||
source = "./modules/cnpg"
|
||||
tier = local.tiers.cluster
|
||||
}
|
||||
53
stacks/cnpg/modules/cnpg/main.tf
Normal file
53
stacks/cnpg/modules/cnpg/main.tf
Normal file
|
|
@ -0,0 +1,53 @@
|
|||
variable "tier" { type = string }
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Namespace
|
||||
# -----------------------------------------------------------------------------
|
||||
resource "kubernetes_namespace" "cnpg_system" {
|
||||
metadata {
|
||||
name = "cnpg-system"
|
||||
labels = {
|
||||
tier = var.tier
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# CloudNativePG Operator — manages PostgreSQL clusters via CRDs
|
||||
# https://cloudnative-pg.io/
|
||||
# -----------------------------------------------------------------------------
|
||||
resource "helm_release" "cnpg" {
|
||||
namespace = kubernetes_namespace.cnpg_system.metadata[0].name
|
||||
create_namespace = false
|
||||
name = "cnpg"
|
||||
atomic = true
|
||||
timeout = 300
|
||||
|
||||
repository = "https://cloudnative-pg.github.io/charts"
|
||||
chart = "cloudnative-pg"
|
||||
version = "0.27.1"
|
||||
|
||||
values = [yamlencode({
|
||||
crds = {
|
||||
create = true
|
||||
}
|
||||
|
||||
replicaCount = 1
|
||||
|
||||
resources = {
|
||||
requests = {
|
||||
cpu = "100m"
|
||||
memory = "256Mi"
|
||||
}
|
||||
limits = {
|
||||
memory = "256Mi"
|
||||
}
|
||||
}
|
||||
})]
|
||||
}
|
||||
|
||||
# NOTE: local-path-provisioner is already installed in the cluster
|
||||
# (via cloud-init template) with StorageClass "local-path" (default).
|
||||
# ReclaimPolicy is "Delete" — for CNPG clusters, set
|
||||
# .spec.storage.pvcTemplate.storageClassName = "local-path" in the
|
||||
# Cluster CR. CNPG handles PVC lifecycle independently.
|
||||
1
stacks/cnpg/secrets
Symbolic link
1
stacks/cnpg/secrets
Symbolic link
|
|
@ -0,0 +1 @@
|
|||
../../secrets
|
||||
8
stacks/cnpg/terragrunt.hcl
Normal file
8
stacks/cnpg/terragrunt.hcl
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
include "root" {
|
||||
path = find_in_parent_folders()
|
||||
}
|
||||
|
||||
dependency "infra" {
|
||||
config_path = "../infra"
|
||||
skip_outputs = true
|
||||
}
|
||||
10
stacks/cnpg/tiers.tf
Normal file
10
stacks/cnpg/tiers.tf
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
# Generated by Terragrunt. Sig: nIlQXj57tbuaRZEa
|
||||
locals {
|
||||
tiers = {
|
||||
core = "0-core"
|
||||
cluster = "1-cluster"
|
||||
gpu = "2-gpu"
|
||||
edge = "3-edge"
|
||||
aux = "4-aux"
|
||||
}
|
||||
}
|
||||
21
stacks/headscale/main.tf
Normal file
21
stacks/headscale/main.tf
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
variable "tls_secret_name" { type = string }
|
||||
variable "nfs_server" { type = string }
|
||||
|
||||
data "vault_kv_secret_v2" "secrets" {
|
||||
mount = "secret"
|
||||
name = "platform"
|
||||
}
|
||||
|
||||
locals {
|
||||
homepage_credentials = jsondecode(data.vault_kv_secret_v2.secrets.data["homepage_credentials"])
|
||||
}
|
||||
|
||||
module "headscale" {
|
||||
source = "./modules/headscale"
|
||||
tls_secret_name = var.tls_secret_name
|
||||
nfs_server = var.nfs_server
|
||||
headscale_config = data.vault_kv_secret_v2.secrets.data["headscale_config"]
|
||||
headscale_acl = data.vault_kv_secret_v2.secrets.data["headscale_acl"]
|
||||
homepage_token = try(local.homepage_credentials["headscale"]["api_key"], "")
|
||||
tier = local.tiers.core
|
||||
}
|
||||
324
stacks/headscale/modules/headscale/main.tf
Normal file
324
stacks/headscale/modules/headscale/main.tf
Normal file
|
|
@ -0,0 +1,324 @@
|
|||
|
||||
variable "tls_secret_name" {}
|
||||
variable "tier" { type = string }
|
||||
variable "headscale_config" {}
|
||||
variable "headscale_acl" {}
|
||||
variable "nfs_server" { type = string }
|
||||
variable "homepage_token" {
|
||||
type = string
|
||||
default = ""
|
||||
sensitive = true
|
||||
}
|
||||
|
||||
resource "kubernetes_namespace" "headscale" {
|
||||
metadata {
|
||||
name = "headscale"
|
||||
labels = {
|
||||
tier = var.tier
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module "tls_secret" {
|
||||
source = "../../../../modules/kubernetes/setup_tls_secret"
|
||||
namespace = kubernetes_namespace.headscale.metadata[0].name
|
||||
tls_secret_name = var.tls_secret_name
|
||||
}
|
||||
|
||||
module "nfs_data" {
|
||||
source = "../../../../modules/kubernetes/nfs_volume"
|
||||
name = "headscale-data"
|
||||
namespace = kubernetes_namespace.headscale.metadata[0].name
|
||||
nfs_server = var.nfs_server
|
||||
nfs_path = "/mnt/main/headscale"
|
||||
}
|
||||
|
||||
resource "kubernetes_deployment" "headscale" {
|
||||
metadata {
|
||||
name = "headscale"
|
||||
namespace = kubernetes_namespace.headscale.metadata[0].name
|
||||
labels = {
|
||||
app = "headscale"
|
||||
tier = var.tier
|
||||
# scare to try but probably non-http will fail
|
||||
# "istio-injection" : "enabled"
|
||||
}
|
||||
|
||||
annotations = {
|
||||
"reloader.stakater.com/search" = "true"
|
||||
}
|
||||
}
|
||||
spec {
|
||||
replicas = 1
|
||||
strategy {
|
||||
type = "Recreate"
|
||||
}
|
||||
selector {
|
||||
match_labels = {
|
||||
app = "headscale"
|
||||
}
|
||||
}
|
||||
template {
|
||||
metadata {
|
||||
labels = {
|
||||
app = "headscale"
|
||||
}
|
||||
annotations = {
|
||||
# "diun.enable" = "true"
|
||||
"diun.enable" = "false"
|
||||
"diun.include_tags" = "^\\d+(?:\\.\\d+)?(?:\\.\\d+)?$"
|
||||
}
|
||||
}
|
||||
spec {
|
||||
container {
|
||||
image = "headscale/headscale:0.23.0"
|
||||
# image = "headscale/headscale:0.23.0-debug" # -debug is for debug images
|
||||
name = "headscale"
|
||||
command = ["headscale", "serve"]
|
||||
|
||||
resources {
|
||||
requests = {
|
||||
cpu = "50m"
|
||||
memory = "128Mi"
|
||||
}
|
||||
limits = {
|
||||
memory = "128Mi"
|
||||
}
|
||||
}
|
||||
|
||||
port {
|
||||
container_port = 8080
|
||||
}
|
||||
port {
|
||||
container_port = 9090
|
||||
}
|
||||
port {
|
||||
container_port = 41641
|
||||
}
|
||||
|
||||
liveness_probe {
|
||||
http_get {
|
||||
path = "/health"
|
||||
port = 8080
|
||||
}
|
||||
initial_delay_seconds = 15
|
||||
period_seconds = 30
|
||||
timeout_seconds = 5
|
||||
failure_threshold = 5
|
||||
}
|
||||
readiness_probe {
|
||||
http_get {
|
||||
path = "/health"
|
||||
port = 8080
|
||||
}
|
||||
initial_delay_seconds = 5
|
||||
period_seconds = 30
|
||||
timeout_seconds = 5
|
||||
failure_threshold = 3
|
||||
}
|
||||
|
||||
volume_mount {
|
||||
name = "config-volume"
|
||||
mount_path = "/etc/headscale"
|
||||
}
|
||||
|
||||
volume_mount {
|
||||
mount_path = "/mnt"
|
||||
name = "nfs-config"
|
||||
}
|
||||
}
|
||||
volume {
|
||||
name = "config-volume"
|
||||
config_map {
|
||||
name = "headscale-config"
|
||||
items {
|
||||
key = "config.yaml"
|
||||
path = "config.yaml"
|
||||
}
|
||||
items {
|
||||
key = "acl.yaml"
|
||||
path = "acl.yaml"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
volume {
|
||||
name = "nfs-config"
|
||||
persistent_volume_claim {
|
||||
claim_name = module.nfs_data.claim_name
|
||||
}
|
||||
}
|
||||
# container {
|
||||
# image = "simcu/headscale-ui:0.1.4"
|
||||
# name = "headscale-ui"
|
||||
# port {
|
||||
# container_port = 80
|
||||
# }
|
||||
# }
|
||||
container {
|
||||
image = "ghcr.io/gurucomputing/headscale-ui:latest"
|
||||
# image = "ghcr.io/tale/headplane:0.3.2"
|
||||
name = "headscale-ui"
|
||||
|
||||
resources {
|
||||
requests = {
|
||||
cpu = "25m"
|
||||
memory = "128Mi"
|
||||
}
|
||||
limits = {
|
||||
memory = "128Mi"
|
||||
}
|
||||
}
|
||||
|
||||
port {
|
||||
container_port = 8081
|
||||
# container_port = 3000
|
||||
}
|
||||
env {
|
||||
name = "HTTP_PORT"
|
||||
value = "8081"
|
||||
}
|
||||
# env {
|
||||
# name = "HTTPS_PORT"
|
||||
# value = "8082"
|
||||
# }
|
||||
env {
|
||||
name = "HEADSCALE_URL"
|
||||
value = "http://localhost:8080"
|
||||
}
|
||||
env {
|
||||
name = "COOKIE_SECRET"
|
||||
value = "kekekekke"
|
||||
}
|
||||
env {
|
||||
name = "ROOT_API_KEY"
|
||||
value = "kekekekeke"
|
||||
}
|
||||
}
|
||||
dns_config {
|
||||
option {
|
||||
name = "ndots"
|
||||
value = "2"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
resource "kubernetes_service" "headscale" {
|
||||
metadata {
|
||||
name = "headscale"
|
||||
namespace = kubernetes_namespace.headscale.metadata[0].name
|
||||
labels = {
|
||||
"app" = "headscale"
|
||||
}
|
||||
annotations = {
|
||||
"prometheus.io/scrape" = "true"
|
||||
"prometheus.io/port" = "9090"
|
||||
}
|
||||
# annotations = {
|
||||
# "metallb.universe.tf/allow-shared-ip" : "shared"
|
||||
# }
|
||||
}
|
||||
|
||||
spec {
|
||||
# type = "LoadBalancer"
|
||||
# external_traffic_policy = "Cluster"
|
||||
selector = {
|
||||
app = "headscale"
|
||||
|
||||
}
|
||||
port {
|
||||
name = "headscale"
|
||||
port = "8080"
|
||||
protocol = "TCP"
|
||||
}
|
||||
port {
|
||||
name = "headscale-ui"
|
||||
port = "80"
|
||||
target_port = 8081
|
||||
# target_port = 3000
|
||||
protocol = "TCP"
|
||||
}
|
||||
port {
|
||||
name = "metrics"
|
||||
port = "9090"
|
||||
protocol = "TCP"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module "ingress" {
|
||||
source = "../../../../modules/kubernetes/ingress_factory"
|
||||
namespace = kubernetes_namespace.headscale.metadata[0].name
|
||||
name = "headscale"
|
||||
port = 8080
|
||||
tls_secret_name = var.tls_secret_name
|
||||
extra_annotations = {
|
||||
"gethomepage.dev/enabled" = "true"
|
||||
"gethomepage.dev/name" = "Headscale"
|
||||
"gethomepage.dev/description" = "VPN mesh network"
|
||||
"gethomepage.dev/icon" = "headscale.png"
|
||||
"gethomepage.dev/group" = "Identity & Security"
|
||||
"gethomepage.dev/pod-selector" = ""
|
||||
}
|
||||
}
|
||||
|
||||
module "ingress-ui" {
|
||||
source = "../../../../modules/kubernetes/ingress_factory"
|
||||
namespace = kubernetes_namespace.headscale.metadata[0].name
|
||||
name = "headscale-ui"
|
||||
host = "headscale"
|
||||
service_name = "headscale"
|
||||
port = 8081
|
||||
ingress_path = ["/web"]
|
||||
tls_secret_name = var.tls_secret_name
|
||||
}
|
||||
|
||||
resource "kubernetes_service" "headscale-server" {
|
||||
metadata {
|
||||
name = "headscale-server"
|
||||
namespace = kubernetes_namespace.headscale.metadata[0].name
|
||||
labels = {
|
||||
"app" = "headscale"
|
||||
}
|
||||
annotations = {
|
||||
"metallb.universe.tf/allow-shared-ip" : "shared"
|
||||
}
|
||||
}
|
||||
|
||||
spec {
|
||||
type = "LoadBalancer"
|
||||
external_traffic_policy = "Cluster"
|
||||
selector = {
|
||||
app = "headscale"
|
||||
|
||||
}
|
||||
# port {
|
||||
# name = "headscale-tcp"
|
||||
# port = "41641"
|
||||
# protocol = "TCP"
|
||||
# }
|
||||
port {
|
||||
name = "headscale-udp"
|
||||
port = "41641"
|
||||
protocol = "UDP"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
resource "kubernetes_config_map" "headscale-config" {
|
||||
metadata {
|
||||
name = "headscale-config"
|
||||
namespace = kubernetes_namespace.headscale.metadata[0].name
|
||||
|
||||
annotations = {
|
||||
"reloader.stakater.com/match" = "true"
|
||||
}
|
||||
}
|
||||
|
||||
data = {
|
||||
"config.yaml" = var.headscale_config
|
||||
"acl.yaml" = var.headscale_acl
|
||||
}
|
||||
}
|
||||
1
stacks/headscale/secrets
Symbolic link
1
stacks/headscale/secrets
Symbolic link
|
|
@ -0,0 +1 @@
|
|||
../../secrets
|
||||
8
stacks/headscale/terragrunt.hcl
Normal file
8
stacks/headscale/terragrunt.hcl
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
include "root" {
|
||||
path = find_in_parent_folders()
|
||||
}
|
||||
|
||||
dependency "infra" {
|
||||
config_path = "../infra"
|
||||
skip_outputs = true
|
||||
}
|
||||
10
stacks/headscale/tiers.tf
Normal file
10
stacks/headscale/tiers.tf
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
# Generated by Terragrunt. Sig: nIlQXj57tbuaRZEa
|
||||
locals {
|
||||
tiers = {
|
||||
core = "0-core"
|
||||
cluster = "1-cluster"
|
||||
gpu = "2-gpu"
|
||||
edge = "3-edge"
|
||||
aux = "4-aux"
|
||||
}
|
||||
}
|
||||
15
stacks/infra-maintenance/main.tf
Normal file
15
stacks/infra-maintenance/main.tf
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
variable "nfs_server" { type = string }
|
||||
|
||||
data "vault_kv_secret_v2" "secrets" {
|
||||
mount = "secret"
|
||||
name = "platform"
|
||||
}
|
||||
|
||||
module "infra-maintenance" {
|
||||
source = "./modules/infra-maintenance"
|
||||
nfs_server = var.nfs_server
|
||||
git_user = data.vault_kv_secret_v2.secrets.data["webhook_handler_git_user"]
|
||||
git_token = data.vault_kv_secret_v2.secrets.data["webhook_handler_git_token"]
|
||||
technitium_username = data.vault_kv_secret_v2.secrets.data["technitium_username"]
|
||||
technitium_password = data.vault_kv_secret_v2.secrets.data["technitium_password"]
|
||||
}
|
||||
274
stacks/infra-maintenance/modules/infra-maintenance/main.tf
Normal file
274
stacks/infra-maintenance/modules/infra-maintenance/main.tf
Normal file
|
|
@ -0,0 +1,274 @@
|
|||
# Module to run some infra-specific things like updating the public ip
|
||||
variable "git_user" {}
|
||||
variable "git_token" {}
|
||||
variable "technitium_username" {}
|
||||
variable "technitium_password" {}
|
||||
variable "nfs_server" { type = string }
|
||||
|
||||
|
||||
# DISABLED WHILST USING CLOUDFLARE NS
|
||||
# resource "kubernetes_cron_job_v1" "update-public-ip" {
|
||||
# metadata {
|
||||
# name = "update-public-ip"
|
||||
# namespace = "default"
|
||||
# }
|
||||
# spec {
|
||||
# schedule = "*/5 * * * *"
|
||||
# successful_jobs_history_limit = 1
|
||||
# failed_jobs_history_limit = 1
|
||||
# concurrency_policy = "Forbid"
|
||||
# job_template {
|
||||
# metadata {
|
||||
# name = "update-public-ip"
|
||||
# }
|
||||
# spec {
|
||||
# template {
|
||||
# metadata {
|
||||
# name = "update-public-ip"
|
||||
# }
|
||||
# spec {
|
||||
# priority_class_name = "system-cluster-critical"
|
||||
# container {
|
||||
# name = "update-public-ip"
|
||||
# image = "viktorbarzin/infra"
|
||||
# command = ["./infra_cli"]
|
||||
# args = ["-use-case", "update-public-ip"]
|
||||
|
||||
# env {
|
||||
# name = "GIT_USER"
|
||||
# value = var.git_user
|
||||
# }
|
||||
# env {
|
||||
# name = "GIT_TOKEN"
|
||||
# value = var.git_token
|
||||
# }
|
||||
# env {
|
||||
# name = "TECHNITIUM_USERNAME"
|
||||
# value = var.technitium_username
|
||||
# }
|
||||
# env {
|
||||
# name = "TECHNITIUM_PASSWORD"
|
||||
# value = var.technitium_password
|
||||
# }
|
||||
# }
|
||||
# restart_policy = "Never"
|
||||
# # service_account_name = "descheduler-sa"
|
||||
# # volume {
|
||||
# # name = "policy-volume"
|
||||
# # config_map {
|
||||
# # name = "policy-configmap"
|
||||
# # }
|
||||
# # }
|
||||
# }
|
||||
# }
|
||||
# }
|
||||
# }
|
||||
# }
|
||||
# }
|
||||
|
||||
module "nfs_etcd_backup" {
|
||||
source = "../../../../modules/kubernetes/nfs_volume"
|
||||
name = "infra-etcd-backup"
|
||||
namespace = "default"
|
||||
nfs_server = var.nfs_server
|
||||
nfs_path = "/mnt/main/etcd-backup"
|
||||
}
|
||||
|
||||
# # backup etcd
|
||||
resource "kubernetes_cron_job_v1" "backup-etcd" {
|
||||
metadata {
|
||||
name = "backup-etcd"
|
||||
namespace = "default"
|
||||
}
|
||||
spec {
|
||||
schedule = "0 0 * * *"
|
||||
successful_jobs_history_limit = 1
|
||||
failed_jobs_history_limit = 1
|
||||
concurrency_policy = "Forbid"
|
||||
job_template {
|
||||
metadata {
|
||||
name = "backup-etcd"
|
||||
}
|
||||
spec {
|
||||
template {
|
||||
metadata {
|
||||
name = "backup-etcd"
|
||||
}
|
||||
spec {
|
||||
node_name = "k8s-master"
|
||||
priority_class_name = "system-cluster-critical"
|
||||
host_network = true
|
||||
container {
|
||||
name = "backup-etcd"
|
||||
image = "registry.k8s.io/etcd:3.5.21-0"
|
||||
command = ["etcdctl"]
|
||||
args = ["--endpoints=https://127.0.0.1:2379", "--cacert=/etc/kubernetes/pki/etcd/ca.crt", "--cert=/etc/kubernetes/pki/etcd/healthcheck-client.crt", "--key=/etc/kubernetes/pki/etcd/healthcheck-client.key", "snapshot", "save", "/backup/etcd-snapshot-latest.db"]
|
||||
env {
|
||||
name = "ETCDCTL_API"
|
||||
value = "3"
|
||||
}
|
||||
volume_mount {
|
||||
mount_path = "/backup"
|
||||
name = "backup"
|
||||
}
|
||||
volume_mount {
|
||||
mount_path = "/etc/kubernetes/pki/etcd"
|
||||
name = "etcd-certs"
|
||||
read_only = true
|
||||
}
|
||||
}
|
||||
container {
|
||||
name = "backup-purge"
|
||||
image = "busybox:1.31.1"
|
||||
command = ["/bin/sh"]
|
||||
args = ["-c", "find /backup -type f -mtime +30 -name '*.db' -exec rm -- '{}' \\;"]
|
||||
|
||||
volume_mount {
|
||||
mount_path = "/backup"
|
||||
name = "backup"
|
||||
}
|
||||
}
|
||||
|
||||
volume {
|
||||
name = "backup"
|
||||
persistent_volume_claim {
|
||||
claim_name = module.nfs_etcd_backup.claim_name
|
||||
}
|
||||
}
|
||||
volume {
|
||||
name = "etcd-certs"
|
||||
host_path {
|
||||
path = "/etc/kubernetes/pki/etcd"
|
||||
type = "DirectoryOrCreate"
|
||||
}
|
||||
}
|
||||
restart_policy = "Never"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# Weekly etcd defragmentation — prevents fragmentation buildup that causes slow requests
|
||||
resource "kubernetes_cron_job_v1" "defrag-etcd" {
|
||||
metadata {
|
||||
name = "defrag-etcd"
|
||||
namespace = "default"
|
||||
}
|
||||
spec {
|
||||
schedule = "0 3 * * 0"
|
||||
successful_jobs_history_limit = 1
|
||||
failed_jobs_history_limit = 1
|
||||
concurrency_policy = "Forbid"
|
||||
job_template {
|
||||
metadata {
|
||||
name = "defrag-etcd"
|
||||
}
|
||||
spec {
|
||||
template {
|
||||
metadata {
|
||||
name = "defrag-etcd"
|
||||
}
|
||||
spec {
|
||||
node_name = "k8s-master"
|
||||
priority_class_name = "system-cluster-critical"
|
||||
host_network = true
|
||||
container {
|
||||
name = "defrag-etcd"
|
||||
image = "registry.k8s.io/etcd:3.5.21-0"
|
||||
command = ["etcdctl"]
|
||||
args = ["--endpoints=https://127.0.0.1:2379", "--cacert=/etc/kubernetes/pki/etcd/ca.crt", "--cert=/etc/kubernetes/pki/etcd/healthcheck-client.crt", "--key=/etc/kubernetes/pki/etcd/healthcheck-client.key", "--command-timeout=60s", "defrag"]
|
||||
env {
|
||||
name = "ETCDCTL_API"
|
||||
value = "3"
|
||||
}
|
||||
volume_mount {
|
||||
mount_path = "/etc/kubernetes/pki/etcd"
|
||||
name = "etcd-certs"
|
||||
read_only = true
|
||||
}
|
||||
}
|
||||
volume {
|
||||
name = "etcd-certs"
|
||||
host_path {
|
||||
path = "/etc/kubernetes/pki/etcd"
|
||||
type = "DirectoryOrCreate"
|
||||
}
|
||||
}
|
||||
restart_policy = "Never"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# Clean up evicted/failed pods cluster-wide daily
|
||||
resource "kubernetes_cron_job_v1" "cleanup-failed-pods" {
|
||||
metadata {
|
||||
name = "cleanup-failed-pods"
|
||||
namespace = "default"
|
||||
}
|
||||
spec {
|
||||
schedule = "0 2 * * *"
|
||||
successful_jobs_history_limit = 1
|
||||
failed_jobs_history_limit = 1
|
||||
concurrency_policy = "Forbid"
|
||||
job_template {
|
||||
metadata {
|
||||
name = "cleanup-failed-pods"
|
||||
}
|
||||
spec {
|
||||
template {
|
||||
metadata {
|
||||
name = "cleanup-failed-pods"
|
||||
}
|
||||
spec {
|
||||
service_account_name = kubernetes_service_account.cleanup_sa.metadata[0].name
|
||||
container {
|
||||
name = "cleanup"
|
||||
image = "bitnami/kubectl:latest"
|
||||
command = ["/bin/sh", "-c", "kubectl delete pods -A --field-selector=status.phase=Failed --ignore-not-found"]
|
||||
}
|
||||
restart_policy = "Never"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
resource "kubernetes_service_account" "cleanup_sa" {
|
||||
metadata {
|
||||
name = "failed-pod-cleanup"
|
||||
namespace = "default"
|
||||
}
|
||||
}
|
||||
|
||||
resource "kubernetes_cluster_role" "cleanup_role" {
|
||||
metadata {
|
||||
name = "failed-pod-cleanup"
|
||||
}
|
||||
rule {
|
||||
api_groups = [""]
|
||||
resources = ["pods"]
|
||||
verbs = ["list", "delete"]
|
||||
}
|
||||
}
|
||||
|
||||
resource "kubernetes_cluster_role_binding" "cleanup_binding" {
|
||||
metadata {
|
||||
name = "failed-pod-cleanup"
|
||||
}
|
||||
role_ref {
|
||||
api_group = "rbac.authorization.k8s.io"
|
||||
kind = "ClusterRole"
|
||||
name = kubernetes_cluster_role.cleanup_role.metadata[0].name
|
||||
}
|
||||
subject {
|
||||
kind = "ServiceAccount"
|
||||
name = kubernetes_service_account.cleanup_sa.metadata[0].name
|
||||
namespace = "default"
|
||||
}
|
||||
}
|
||||
1
stacks/infra-maintenance/secrets
Symbolic link
1
stacks/infra-maintenance/secrets
Symbolic link
|
|
@ -0,0 +1 @@
|
|||
../../secrets
|
||||
8
stacks/infra-maintenance/terragrunt.hcl
Normal file
8
stacks/infra-maintenance/terragrunt.hcl
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
include "root" {
|
||||
path = find_in_parent_folders()
|
||||
}
|
||||
|
||||
dependency "infra" {
|
||||
config_path = "../infra"
|
||||
skip_outputs = true
|
||||
}
|
||||
10
stacks/infra-maintenance/tiers.tf
Normal file
10
stacks/infra-maintenance/tiers.tf
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
# Generated by Terragrunt. Sig: nIlQXj57tbuaRZEa
|
||||
locals {
|
||||
tiers = {
|
||||
core = "0-core"
|
||||
cluster = "1-cluster"
|
||||
gpu = "2-gpu"
|
||||
edge = "3-edge"
|
||||
aux = "4-aux"
|
||||
}
|
||||
}
|
||||
14
stacks/iscsi-csi/main.tf
Normal file
14
stacks/iscsi-csi/main.tf
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
variable "nfs_server" { type = string }
|
||||
|
||||
data "vault_kv_secret_v2" "secrets" {
|
||||
mount = "secret"
|
||||
name = "platform"
|
||||
}
|
||||
|
||||
module "iscsi-csi" {
|
||||
source = "./modules/iscsi-csi"
|
||||
tier = local.tiers.cluster
|
||||
truenas_host = var.nfs_server
|
||||
truenas_api_key = data.vault_kv_secret_v2.secrets.data["truenas_api_key"]
|
||||
truenas_ssh_private_key = data.vault_kv_secret_v2.secrets.data["truenas_ssh_private_key"]
|
||||
}
|
||||
148
stacks/iscsi-csi/modules/iscsi-csi/main.tf
Normal file
148
stacks/iscsi-csi/modules/iscsi-csi/main.tf
Normal file
|
|
@ -0,0 +1,148 @@
|
|||
resource "kubernetes_namespace" "iscsi_csi" {
|
||||
metadata {
|
||||
name = "iscsi-csi"
|
||||
labels = {
|
||||
tier = var.tier
|
||||
"resource-governance/custom-quota" = "true"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
resource "helm_release" "democratic_csi" {
|
||||
namespace = kubernetes_namespace.iscsi_csi.metadata[0].name
|
||||
create_namespace = false
|
||||
name = "democratic-csi-iscsi"
|
||||
atomic = true
|
||||
timeout = 300
|
||||
|
||||
repository = "https://democratic-csi.github.io/charts/"
|
||||
chart = "democratic-csi"
|
||||
|
||||
values = [yamlencode({
|
||||
csiDriver = {
|
||||
name = "org.democratic-csi.iscsi"
|
||||
}
|
||||
|
||||
storageClasses = [{
|
||||
name = "iscsi-truenas"
|
||||
defaultClass = false
|
||||
reclaimPolicy = "Retain"
|
||||
volumeBindingMode = "Immediate"
|
||||
allowVolumeExpansion = true
|
||||
parameters = {
|
||||
fsType = "ext4"
|
||||
}
|
||||
mountOptions = []
|
||||
}]
|
||||
|
||||
controller = {
|
||||
replicas = 2
|
||||
driver = {
|
||||
resources = {
|
||||
requests = { cpu = "25m", memory = "192Mi" }
|
||||
limits = { memory = "192Mi" }
|
||||
}
|
||||
}
|
||||
externalProvisioner = {
|
||||
resources = {
|
||||
requests = { cpu = "5m", memory = "64Mi" }
|
||||
limits = { memory = "64Mi" }
|
||||
}
|
||||
}
|
||||
externalAttacher = {
|
||||
resources = {
|
||||
requests = { cpu = "5m", memory = "64Mi" }
|
||||
limits = { memory = "64Mi" }
|
||||
}
|
||||
}
|
||||
externalResizer = {
|
||||
resources = {
|
||||
requests = { cpu = "5m", memory = "64Mi" }
|
||||
limits = { memory = "64Mi" }
|
||||
}
|
||||
}
|
||||
externalSnapshotter = {
|
||||
resources = {
|
||||
requests = { cpu = "5m", memory = "80Mi" }
|
||||
limits = { memory = "80Mi" }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# csiProxy is a top-level chart key, NOT nested under controller/node
|
||||
csiProxy = {
|
||||
resources = {
|
||||
requests = { cpu = "5m", memory = "32Mi" }
|
||||
limits = { memory = "32Mi" }
|
||||
}
|
||||
}
|
||||
|
||||
node = {
|
||||
driver = {
|
||||
resources = {
|
||||
requests = { cpu = "25m", memory = "192Mi" }
|
||||
limits = { memory = "192Mi" }
|
||||
}
|
||||
}
|
||||
driverRegistrar = {
|
||||
resources = {
|
||||
requests = { cpu = "5m", memory = "32Mi" }
|
||||
limits = { memory = "32Mi" }
|
||||
}
|
||||
}
|
||||
cleanup = {
|
||||
resources = {
|
||||
requests = { cpu = "5m", memory = "32Mi" }
|
||||
limits = { memory = "32Mi" }
|
||||
}
|
||||
}
|
||||
|
||||
hostPID = true
|
||||
hostPath = "/lib/modules"
|
||||
}
|
||||
|
||||
driver = {
|
||||
config = {
|
||||
driver = "freenas-iscsi"
|
||||
|
||||
instance_id = "truenas-iscsi"
|
||||
|
||||
httpConnection = {
|
||||
protocol = "http"
|
||||
host = var.truenas_host
|
||||
port = 80
|
||||
apiKey = var.truenas_api_key
|
||||
}
|
||||
|
||||
sshConnection = {
|
||||
host = var.truenas_host
|
||||
port = 22
|
||||
username = "root"
|
||||
privateKey = var.truenas_ssh_private_key
|
||||
}
|
||||
|
||||
zfs = {
|
||||
datasetParentName = "main/iscsi"
|
||||
detachedSnapshotsDatasetParentName = "main/iscsi-snaps"
|
||||
}
|
||||
|
||||
iscsi = {
|
||||
targetPortal = "${var.truenas_host}:3260"
|
||||
namePrefix = "csi-"
|
||||
nameSuffix = ""
|
||||
targetGroups = [{
|
||||
targetGroupPortalGroup = 1
|
||||
targetGroupInitiatorGroup = 1
|
||||
targetGroupAuthType = "None"
|
||||
}]
|
||||
extentInsecureTpc = true
|
||||
extentXenCompat = false
|
||||
extentDisablePhysicalBlocksize = true
|
||||
extentBlocksize = 512
|
||||
extentRpm = "SSD"
|
||||
extentAvailThreshold = 0
|
||||
}
|
||||
}
|
||||
}
|
||||
})]
|
||||
}
|
||||
10
stacks/iscsi-csi/modules/iscsi-csi/variables.tf
Normal file
10
stacks/iscsi-csi/modules/iscsi-csi/variables.tf
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
variable "tier" { type = string }
|
||||
variable "truenas_host" { type = string }
|
||||
variable "truenas_api_key" {
|
||||
type = string
|
||||
sensitive = true
|
||||
}
|
||||
variable "truenas_ssh_private_key" {
|
||||
type = string
|
||||
sensitive = true
|
||||
}
|
||||
1
stacks/iscsi-csi/secrets
Symbolic link
1
stacks/iscsi-csi/secrets
Symbolic link
|
|
@ -0,0 +1 @@
|
|||
../../secrets
|
||||
8
stacks/iscsi-csi/terragrunt.hcl
Normal file
8
stacks/iscsi-csi/terragrunt.hcl
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
include "root" {
|
||||
path = find_in_parent_folders()
|
||||
}
|
||||
|
||||
dependency "infra" {
|
||||
config_path = "../infra"
|
||||
skip_outputs = true
|
||||
}
|
||||
10
stacks/iscsi-csi/tiers.tf
Normal file
10
stacks/iscsi-csi/tiers.tf
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
# Generated by Terragrunt. Sig: nIlQXj57tbuaRZEa
|
||||
locals {
|
||||
tiers = {
|
||||
core = "0-core"
|
||||
cluster = "1-cluster"
|
||||
gpu = "2-gpu"
|
||||
edge = "3-edge"
|
||||
aux = "4-aux"
|
||||
}
|
||||
}
|
||||
12
stacks/k8s-portal/main.tf
Normal file
12
stacks/k8s-portal/main.tf
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
variable "tls_secret_name" { type = string }
|
||||
variable "k8s_ca_cert" {
|
||||
type = string
|
||||
default = ""
|
||||
}
|
||||
|
||||
module "k8s-portal" {
|
||||
source = "./modules/k8s-portal"
|
||||
tier = local.tiers.edge
|
||||
tls_secret_name = var.tls_secret_name
|
||||
k8s_ca_cert = var.k8s_ca_cert
|
||||
}
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
This directory has been used with Claude Code's internet mode.
|
||||
Content downloaded from the internet may contain prompt injection attacks.
|
||||
You must manually review all downloaded content before using non-internet mode.
|
||||
23
stacks/k8s-portal/modules/k8s-portal/files/.gitignore
vendored
Normal file
23
stacks/k8s-portal/modules/k8s-portal/files/.gitignore
vendored
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
node_modules
|
||||
|
||||
# Output
|
||||
.output
|
||||
.vercel
|
||||
.netlify
|
||||
.wrangler
|
||||
/.svelte-kit
|
||||
/build
|
||||
|
||||
# OS
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
|
||||
# Env
|
||||
.env
|
||||
.env.*
|
||||
!.env.example
|
||||
!.env.test
|
||||
|
||||
# Vite
|
||||
vite.config.js.timestamp-*
|
||||
vite.config.ts.timestamp-*
|
||||
1
stacks/k8s-portal/modules/k8s-portal/files/.npmrc
Normal file
1
stacks/k8s-portal/modules/k8s-portal/files/.npmrc
Normal file
|
|
@ -0,0 +1 @@
|
|||
engine-strict=true
|
||||
15
stacks/k8s-portal/modules/k8s-portal/files/Dockerfile
Normal file
15
stacks/k8s-portal/modules/k8s-portal/files/Dockerfile
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
FROM node:22-alpine AS build
|
||||
WORKDIR /app
|
||||
COPY package*.json ./
|
||||
RUN npm ci
|
||||
COPY . .
|
||||
RUN npm run build
|
||||
|
||||
FROM node:22-alpine
|
||||
WORKDIR /app
|
||||
COPY --from=build /app/build ./build
|
||||
COPY --from=build /app/package.json ./
|
||||
COPY --from=build /app/node_modules ./node_modules
|
||||
ENV PORT=3000
|
||||
EXPOSE 3000
|
||||
CMD ["node", "build"]
|
||||
42
stacks/k8s-portal/modules/k8s-portal/files/README.md
Normal file
42
stacks/k8s-portal/modules/k8s-portal/files/README.md
Normal file
|
|
@ -0,0 +1,42 @@
|
|||
# sv
|
||||
|
||||
Everything you need to build a Svelte project, powered by [`sv`](https://github.com/sveltejs/cli).
|
||||
|
||||
## Creating a project
|
||||
|
||||
If you're seeing this, you've probably already done this step. Congrats!
|
||||
|
||||
```sh
|
||||
# create a new project
|
||||
npx sv create my-app
|
||||
```
|
||||
|
||||
To recreate this project with the same configuration:
|
||||
|
||||
```sh
|
||||
# recreate this project
|
||||
npx sv create --template minimal --types ts --install npm .
|
||||
```
|
||||
|
||||
## Developing
|
||||
|
||||
Once you've created a project and installed dependencies with `npm install` (or `pnpm install` or `yarn`), start a development server:
|
||||
|
||||
```sh
|
||||
npm run dev
|
||||
|
||||
# or start the server and open the app in a new browser tab
|
||||
npm run dev -- --open
|
||||
```
|
||||
|
||||
## Building
|
||||
|
||||
To create a production version of your app:
|
||||
|
||||
```sh
|
||||
npm run build
|
||||
```
|
||||
|
||||
You can preview the production build with `npm run preview`.
|
||||
|
||||
> To deploy your app, you may need to install an [adapter](https://svelte.dev/docs/kit/adapters) for your target environment.
|
||||
13
stacks/k8s-portal/modules/k8s-portal/files/src/app.d.ts
vendored
Normal file
13
stacks/k8s-portal/modules/k8s-portal/files/src/app.d.ts
vendored
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
// See https://svelte.dev/docs/kit/types#app.d.ts
|
||||
// for information about these interfaces
|
||||
declare global {
|
||||
namespace App {
|
||||
// interface Error {}
|
||||
// interface Locals {}
|
||||
// interface PageData {}
|
||||
// interface PageState {}
|
||||
// interface Platform {}
|
||||
}
|
||||
}
|
||||
|
||||
export {};
|
||||
11
stacks/k8s-portal/modules/k8s-portal/files/src/app.html
Normal file
11
stacks/k8s-portal/modules/k8s-portal/files/src/app.html
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
%sveltekit.head%
|
||||
</head>
|
||||
<body data-sveltekit-preload-data="hover">
|
||||
<div style="display: contents">%sveltekit.body%</div>
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" width="107" height="128" viewBox="0 0 107 128"><title>svelte-logo</title><path d="M94.157 22.819c-10.4-14.885-30.94-19.297-45.792-9.835L22.282 29.608A29.92 29.92 0 0 0 8.764 49.65a31.5 31.5 0 0 0 3.108 20.231 30 30 0 0 0-4.477 11.183 31.9 31.9 0 0 0 5.448 24.116c10.402 14.887 30.942 19.297 45.791 9.835l26.083-16.624A29.92 29.92 0 0 0 98.235 78.35a31.53 31.53 0 0 0-3.105-20.232 30 30 0 0 0 4.474-11.182 31.88 31.88 0 0 0-5.447-24.116" style="fill:#ff3e00"/><path d="M45.817 106.582a20.72 20.72 0 0 1-22.237-8.243 19.17 19.17 0 0 1-3.277-14.503 18 18 0 0 1 .624-2.435l.49-1.498 1.337.981a33.6 33.6 0 0 0 10.203 5.098l.97.294-.09.968a5.85 5.85 0 0 0 1.052 3.878 6.24 6.24 0 0 0 6.695 2.485 5.8 5.8 0 0 0 1.603-.704L69.27 76.28a5.43 5.43 0 0 0 2.45-3.631 5.8 5.8 0 0 0-.987-4.371 6.24 6.24 0 0 0-6.698-2.487 5.7 5.7 0 0 0-1.6.704l-9.953 6.345a19 19 0 0 1-5.296 2.326 20.72 20.72 0 0 1-22.237-8.243 19.17 19.17 0 0 1-3.277-14.502 17.99 17.99 0 0 1 8.13-12.052l26.081-16.623a19 19 0 0 1 5.3-2.329 20.72 20.72 0 0 1 22.237 8.243 19.17 19.17 0 0 1 3.277 14.503 18 18 0 0 1-.624 2.435l-.49 1.498-1.337-.98a33.6 33.6 0 0 0-10.203-5.1l-.97-.294.09-.968a5.86 5.86 0 0 0-1.052-3.878 6.24 6.24 0 0 0-6.696-2.485 5.8 5.8 0 0 0-1.602.704L37.73 51.72a5.42 5.42 0 0 0-2.449 3.63 5.79 5.79 0 0 0 .986 4.372 6.24 6.24 0 0 0 6.698 2.486 5.8 5.8 0 0 0 1.602-.704l9.952-6.342a19 19 0 0 1 5.295-2.328 20.72 20.72 0 0 1 22.237 8.242 19.17 19.17 0 0 1 3.277 14.503 18 18 0 0 1-8.13 12.053l-26.081 16.622a19 19 0 0 1-5.3 2.328" style="fill:#fff"/></svg>
|
||||
|
After Width: | Height: | Size: 1.5 KiB |
|
|
@ -0,0 +1 @@
|
|||
// place files you want to import through the `$lib` alias in this folder.
|
||||
|
|
@ -0,0 +1,64 @@
|
|||
<script lang="ts">
|
||||
import favicon from '$lib/assets/favicon.svg';
|
||||
import { page } from '$app/stores';
|
||||
|
||||
let { children } = $props();
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<link rel="icon" href={favicon} />
|
||||
</svelte:head>
|
||||
|
||||
<nav>
|
||||
<div class="nav-inner">
|
||||
<a href="/" class="brand">K8s Portal</a>
|
||||
<div class="links">
|
||||
<a href="/onboarding" class:active={$page.url.pathname === '/onboarding'}>Getting Started</a>
|
||||
<a href="/architecture" class:active={$page.url.pathname === '/architecture'}>Architecture</a>
|
||||
<a href="/services" class:active={$page.url.pathname === '/services'}>Services</a>
|
||||
<a href="/contributing" class:active={$page.url.pathname === '/contributing'}>Contributing</a>
|
||||
<a href="/troubleshooting" class:active={$page.url.pathname === '/troubleshooting'}>Help</a>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
{@render children()}
|
||||
|
||||
<style>
|
||||
nav {
|
||||
background: #1a1a2e;
|
||||
padding: 0.75rem 1rem;
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 100;
|
||||
}
|
||||
.nav-inner {
|
||||
max-width: 768px;
|
||||
margin: 0 auto;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 1.5rem;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.brand {
|
||||
color: #e0e0e0;
|
||||
text-decoration: none;
|
||||
font-weight: 700;
|
||||
font-size: 1.1rem;
|
||||
}
|
||||
.links {
|
||||
display: flex;
|
||||
gap: 1rem;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.links a {
|
||||
color: #a0a0c0;
|
||||
text-decoration: none;
|
||||
font-size: 0.9rem;
|
||||
padding: 0.25rem 0;
|
||||
}
|
||||
.links a:hover, .links a.active {
|
||||
color: #ffffff;
|
||||
border-bottom: 2px solid #4fc3f7;
|
||||
}
|
||||
</style>
|
||||
|
|
@ -0,0 +1,33 @@
|
|||
import type { PageServerLoad } from './$types';
|
||||
import { readFileSync } from 'fs';
|
||||
|
||||
interface UserRole {
|
||||
role: string;
|
||||
namespaces: string[];
|
||||
}
|
||||
|
||||
export const load: PageServerLoad = async ({ request }) => {
|
||||
const email = request.headers.get('x-authentik-email') || 'unknown';
|
||||
const username = request.headers.get('x-authentik-username') || 'unknown';
|
||||
const groups = request.headers.get('x-authentik-groups') || '';
|
||||
|
||||
// Read user roles from ConfigMap-mounted file
|
||||
let userRole: UserRole = { role: 'unknown', namespaces: [] };
|
||||
try {
|
||||
const usersJson = readFileSync('/config/users.json', 'utf-8');
|
||||
const users = JSON.parse(usersJson);
|
||||
if (users[email]) {
|
||||
userRole = users[email];
|
||||
}
|
||||
} catch {
|
||||
// ConfigMap not mounted or parse error
|
||||
}
|
||||
|
||||
return {
|
||||
email,
|
||||
username,
|
||||
groups: groups.split('|').filter(Boolean),
|
||||
role: userRole.role,
|
||||
namespaces: userRole.namespaces
|
||||
};
|
||||
};
|
||||
|
|
@ -0,0 +1,102 @@
|
|||
<script lang="ts">
|
||||
let { data } = $props();
|
||||
</script>
|
||||
|
||||
<main>
|
||||
<h1>Kubernetes Access Portal</h1>
|
||||
|
||||
<div class="callout warning">
|
||||
<strong>VPN Required</strong> — The cluster is on a private network. You need Headscale VPN access before kubectl will work.
|
||||
<a href="/onboarding">See the Getting Started guide</a> for VPN setup instructions.
|
||||
</div>
|
||||
|
||||
<section>
|
||||
<h2>Your Identity</h2>
|
||||
<p><strong>Username:</strong> {data.username}</p>
|
||||
<p><strong>Email:</strong> {data.email}</p>
|
||||
<p><strong>Role:</strong> {data.role}</p>
|
||||
{#if data.namespaces.length > 0}
|
||||
<p><strong>Namespaces:</strong> {data.namespaces.join(', ')}</p>
|
||||
{/if}
|
||||
</section>
|
||||
|
||||
{#if data.role === 'namespace-owner'}
|
||||
<section>
|
||||
<h2>Your Namespace</h2>
|
||||
<p><strong>Assigned namespaces:</strong> {data.namespaces.join(', ')}</p>
|
||||
|
||||
<h3>Quick Commands</h3>
|
||||
<pre>
|
||||
# Check your pods
|
||||
kubectl get pods -n {data.namespaces[0]}
|
||||
|
||||
# View quota usage
|
||||
kubectl describe resourcequota -n {data.namespaces[0]}
|
||||
|
||||
# Log into Vault
|
||||
vault login -method=oidc
|
||||
|
||||
# Store a secret
|
||||
vault kv put secret/{data.username}/myapp KEY=value
|
||||
|
||||
# Get K8s deploy token
|
||||
vault write kubernetes/creds/{data.namespaces[0]}-deployer \
|
||||
kubernetes_namespace={data.namespaces[0]}</pre>
|
||||
</section>
|
||||
{/if}
|
||||
|
||||
<section>
|
||||
<h2>Get Started</h2>
|
||||
<ol>
|
||||
{#if data.role === 'namespace-owner'}
|
||||
<li><a href="/onboarding?role=namespace-owner">Complete the namespace-owner onboarding guide</a></li>
|
||||
{:else}
|
||||
<li><a href="/onboarding">Complete the onboarding guide</a> (VPN, kubectl, git)</li>
|
||||
{/if}
|
||||
<li><a href="/setup">Install kubectl and kubelogin</a></li>
|
||||
<li><a href="/download">Download your kubeconfig</a></li>
|
||||
<li>Run <code>kubectl get namespaces</code> to verify access</li>
|
||||
</ol>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2>Resources</h2>
|
||||
<ul>
|
||||
<li><a href="/architecture">Architecture overview</a></li>
|
||||
<li><a href="/services">Service catalog</a></li>
|
||||
<li><a href="/contributing">How to contribute</a></li>
|
||||
<li><a href="/troubleshooting">Troubleshooting</a></li>
|
||||
</ul>
|
||||
</section>
|
||||
</main>
|
||||
|
||||
<style>
|
||||
main {
|
||||
max-width: 768px;
|
||||
margin: 2rem auto;
|
||||
padding: 0 1rem;
|
||||
font-family: system-ui, -apple-system, sans-serif;
|
||||
line-height: 1.6;
|
||||
}
|
||||
code {
|
||||
background: #f0f0f0;
|
||||
padding: 2px 6px;
|
||||
border-radius: 3px;
|
||||
}
|
||||
section {
|
||||
margin: 2rem 0;
|
||||
}
|
||||
.callout {
|
||||
padding: 1rem;
|
||||
border-radius: 6px;
|
||||
margin: 1rem 0;
|
||||
}
|
||||
.callout.warning {
|
||||
background: #fff3cd;
|
||||
border-left: 4px solid #ffc107;
|
||||
}
|
||||
.callout a {
|
||||
color: #856404;
|
||||
font-weight: 600;
|
||||
}
|
||||
</style>
|
||||
|
|
@ -0,0 +1,61 @@
|
|||
<main class="content">
|
||||
<h1>Agent Bootstrap</h1>
|
||||
<p>Point any AI coding agent at this cluster and it can bootstrap itself automatically.</p>
|
||||
|
||||
<section>
|
||||
<h2>For AI Agents</h2>
|
||||
<p>Fetch the machine-readable bootstrap document:</p>
|
||||
<pre>curl -fsSL https://k8s-portal.viktorbarzin.me/agent</pre>
|
||||
<p>This returns a plain-text markdown document with everything an agent needs: setup commands, critical rules, secrets workflow, Terraform conventions, key file paths, and common operations.</p>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2>Usage with Claude Code</h2>
|
||||
<pre>claude "$(curl -fsSL https://k8s-portal.viktorbarzin.me/agent)" "Deploy a new echo service"</pre>
|
||||
<p>Or within a session:</p>
|
||||
<ol>
|
||||
<li>Clone the repo: <code>git clone https://github.com/ViktorBarzin/infra.git && cd infra</code></li>
|
||||
<li>Start Claude Code: <code>claude</code></li>
|
||||
<li>Claude auto-reads <code>AGENTS.md</code> and <code>.claude/CLAUDE.md</code> from the repo</li>
|
||||
</ol>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2>Usage with Codex / Other Agents</h2>
|
||||
<ol>
|
||||
<li>Clone the repo and <code>cd</code> into it</li>
|
||||
<li>Run the setup script: <code>bash <(curl -fsSL https://k8s-portal.viktorbarzin.me/setup/script?os=linux)</code></li>
|
||||
<li>Start the agent — it will read <code>AGENTS.md</code> for instructions</li>
|
||||
</ol>
|
||||
<p>If the agent doesn't auto-read <code>AGENTS.md</code>, feed it the bootstrap doc:</p>
|
||||
<pre>curl -fsSL https://k8s-portal.viktorbarzin.me/agent</pre>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2>What the Agent Gets</h2>
|
||||
<ul>
|
||||
<li>Quick-start commands (setup script, repo clone)</li>
|
||||
<li>Critical rules (no kubectl apply, no plaintext secrets, no NFS restart)</li>
|
||||
<li>Sealed Secrets workflow (kubeseal self-service)</li>
|
||||
<li>Terraform conventions (fileset pattern, tiers, storage, shared vars)</li>
|
||||
<li>Key file paths in the repo</li>
|
||||
<li>Common operations (deploy, fix pods, add secrets)</li>
|
||||
<li>Contributing workflow (branch, PR, review, CI)</li>
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2>The <code>/agent</code> Endpoint</h2>
|
||||
<p>The endpoint is <strong>unauthenticated</strong> — no login required. Agents can <code>curl</code> or <code>WebFetch</code> it directly without a browser session, just like the setup script.</p>
|
||||
<p>Content-Type: <code>text/plain</code> — no HTML parsing needed.</p>
|
||||
</section>
|
||||
</main>
|
||||
|
||||
<style>
|
||||
.content { max-width: 768px; margin: 2rem auto; padding: 0 1rem; font-family: system-ui, -apple-system, sans-serif; line-height: 1.6; }
|
||||
.content h1 { border-bottom: 1px solid #e0e0e0; padding-bottom: 0.5rem; }
|
||||
.content h2 { margin-top: 2rem; color: #333; }
|
||||
.content pre { background: #1e1e1e; color: #d4d4d4; padding: 1rem; border-radius: 6px; overflow-x: auto; }
|
||||
.content code { background: #f0f0f0; padding: 2px 6px; border-radius: 3px; }
|
||||
section { margin: 2rem 0; }
|
||||
</style>
|
||||
|
|
@ -0,0 +1,161 @@
|
|||
import type { RequestHandler } from './$types';
|
||||
|
||||
const BOOTSTRAP_DOC = `# Infrastructure Cluster — AI Agent Bootstrap
|
||||
|
||||
> Fetch this document: \`curl -fsSL https://k8s-portal.viktorbarzin.me/agent\`
|
||||
|
||||
## Quick Start
|
||||
|
||||
\`\`\`bash
|
||||
# 1. Install tools (kubectl, kubelogin, kubeseal)
|
||||
bash <(curl -fsSL https://k8s-portal.viktorbarzin.me/setup/script?os=linux)
|
||||
|
||||
# 2. Clone the infrastructure repo
|
||||
git clone https://github.com/ViktorBarzin/infra.git && cd infra
|
||||
|
||||
# 3. Verify cluster access (opens browser for OIDC login on first run)
|
||||
kubectl get namespaces
|
||||
\`\`\`
|
||||
|
||||
## Critical Rules (MUST FOLLOW)
|
||||
|
||||
- **ALL changes through Terraform/Terragrunt** — NEVER \`kubectl apply/edit/patch/delete\` for persistent changes. Read-only kubectl is fine.
|
||||
- **NEVER put secrets in plaintext** — use Sealed Secrets (\`kubeseal\`) or \`secrets.sops.json\` (SOPS-encrypted).
|
||||
- **NEVER restart NFS on TrueNAS** — causes cluster-wide mount failures across all pods.
|
||||
- **NEVER commit secrets** — triple-check before every commit.
|
||||
- **\`[ci skip]\` in commit messages** when changes were already applied locally.
|
||||
- **Ask before \`git push\`** — always confirm with the user first.
|
||||
|
||||
## Sealed Secrets (Self-Service)
|
||||
|
||||
You can manage your own secrets without SOPS access using \`kubeseal\`:
|
||||
|
||||
\`\`\`bash
|
||||
# 1. Create a sealed secret
|
||||
kubectl create secret generic <name> \\
|
||||
--from-literal=key=value -n <namespace> \\
|
||||
--dry-run=client -o yaml | \\
|
||||
kubeseal --controller-name sealed-secrets \\
|
||||
--controller-namespace sealed-secrets -o yaml > sealed-<name>.yaml
|
||||
|
||||
# 2. Place the file in the stack directory: stacks/<service>/sealed-<name>.yaml
|
||||
|
||||
# 3. Ensure the stack's main.tf has the fileset block (add if missing):
|
||||
\`\`\`
|
||||
|
||||
\`\`\`hcl
|
||||
resource "kubernetes_manifest" "sealed_secrets" {
|
||||
for_each = fileset(path.module, "sealed-*.yaml")
|
||||
manifest = yamldecode(file("\${path.module}/\${each.value}"))
|
||||
}
|
||||
\`\`\`
|
||||
|
||||
\`\`\`bash
|
||||
# 4. Push to PR — CI runs terragrunt apply — controller decrypts into real K8s Secrets
|
||||
\`\`\`
|
||||
|
||||
- Files MUST match the \`sealed-*.yaml\` glob pattern.
|
||||
- Only the in-cluster controller has the private key. \`kubeseal\` uses the public key — safe to distribute.
|
||||
- The \`kubernetes_manifest\` block is safe to add even with zero sealed-*.yaml files (empty for_each).
|
||||
|
||||
## SOPS Secrets (Admin-Only Fallback)
|
||||
|
||||
For secrets requiring admin access (shared infra passwords, API keys):
|
||||
- **\`secrets.sops.json\`** — SOPS-encrypted secrets (JSON format)
|
||||
- **Edit**: \`sops secrets.sops.json\` (opens $EDITOR, re-encrypts on save)
|
||||
- **Add**: \`sops set secrets.sops.json '["new_key"]' '"value"'\`
|
||||
- **Operators without SOPS keys**: comment on your PR asking Viktor to add the secret.
|
||||
|
||||
## Terraform Conventions
|
||||
|
||||
### Execution
|
||||
- **Apply a service**: \`scripts/tg apply --non-interactive\` (auto-decrypts SOPS secrets)
|
||||
- **Plan**: \`scripts/tg plan --non-interactive\`
|
||||
- **kubectl**: \`kubectl --kubeconfig $(pwd)/config\`
|
||||
- **Health check**: \`bash scripts/cluster_healthcheck.sh --quiet\`
|
||||
|
||||
### Key Paths
|
||||
| Path | Purpose |
|
||||
|------|---------|
|
||||
| \`stacks/<service>/main.tf\` | Service definition |
|
||||
| \`stacks/platform/modules/<module>/\` | Core infra modules (~22) |
|
||||
| \`modules/kubernetes/ingress_factory/\` | Standardized ingress (auth, rate limiting, anti-AI) |
|
||||
| \`modules/kubernetes/nfs_volume/\` | NFS volume module (CSI-backed, soft mount) |
|
||||
| \`config.tfvars\` | Non-secret configuration (plaintext) |
|
||||
| \`secrets.sops.json\` | All secrets (SOPS-encrypted JSON) |
|
||||
| \`scripts/cluster_healthcheck.sh\` | 25-check cluster health script |
|
||||
| \`AGENTS.md\` | Full AI agent instructions (auto-loaded by most agents) |
|
||||
|
||||
### Tier System
|
||||
\`0-core\` | \`1-cluster\` | \`2-gpu\` | \`3-edge\` | \`4-aux\`
|
||||
|
||||
Kyverno auto-generates LimitRange + ResourceQuota per namespace based on tier label.
|
||||
- Containers without explicit \`resources {}\` get default limits (256Mi for edge/aux — causes OOMKill for heavy apps)
|
||||
- Always set explicit resources on containers that need more than defaults
|
||||
- Opt-out labels: \`resource-governance/custom-quota=true\` / \`resource-governance/custom-limitrange=true\`
|
||||
|
||||
### Storage
|
||||
- **NFS** (\`nfs-truenas\` StorageClass): For app data. Use the \`nfs_volume\` module.
|
||||
- **iSCSI** (\`iscsi-truenas\` StorageClass): For databases (PostgreSQL, MySQL).
|
||||
|
||||
### Shared Variables (never hardcode)
|
||||
\`var.nfs_server\`, \`var.redis_host\`, \`var.postgresql_host\`, \`var.mysql_host\`, \`var.ollama_host\`, \`var.mail_host\`
|
||||
|
||||
## Architecture
|
||||
|
||||
- Terragrunt-based homelab managing a Kubernetes cluster (5 nodes, v1.34.2) on Proxmox VMs
|
||||
- 70+ services, each in \`stacks/<service>/\` with its own Terraform state
|
||||
- Core platform: \`stacks/platform/modules/\` (Traefik, Kyverno, monitoring, dbaas, sealed-secrets, etc.)
|
||||
- Public domain: \`viktorbarzin.me\` (Cloudflare) | Internal: \`viktorbarzin.lan\` (Technitium DNS)
|
||||
- CI/CD: Woodpecker CI — PRs run plan, merges to master auto-apply platform stack
|
||||
|
||||
## Common Operations
|
||||
|
||||
### Deploy a New Service
|
||||
1. Copy an existing stack as template: \`cp -r stacks/echo stacks/my-service\`
|
||||
2. Edit \`main.tf\` — update image, ports, ingress, resources
|
||||
3. Add DNS in \`config.tfvars\`
|
||||
4. Apply platform first if needed, then the service
|
||||
|
||||
### Fix Crashed Pods
|
||||
1. Run \`bash scripts/cluster_healthcheck.sh --quiet\`
|
||||
2. Safe to delete evicted/failed pods and CrashLoopBackOff pods with >10 restarts
|
||||
3. OOMKilled? Check \`kubectl describe limitrange tier-defaults -n <ns>\` and increase \`resources.limits.memory\`
|
||||
|
||||
### Add a Secret
|
||||
- **Self-service**: Use \`kubeseal\` (see Sealed Secrets section above)
|
||||
- **Admin**: \`sops set secrets.sops.json '["key"]' '"value"'\` then commit
|
||||
|
||||
## Contributing Workflow
|
||||
|
||||
1. Create a branch: \`git checkout -b fix/my-change\`
|
||||
2. Make changes in \`stacks/<service>/main.tf\`
|
||||
3. Push and open a PR: \`git push -u origin fix/my-change\`
|
||||
4. Viktor reviews and merges
|
||||
5. CI applies automatically — Slack notification when done
|
||||
|
||||
## Infrastructure Details
|
||||
|
||||
- **Proxmox**: 192.168.1.127 (Dell R730, 22c/44t, 142GB RAM)
|
||||
- **Nodes**: k8s-master (10.0.20.100), node1 (GPU, Tesla T4), node2-4
|
||||
- **GPU workloads**: \`node_selector = { "gpu": "true" }\` + toleration \`nvidia.com/gpu\`
|
||||
- **Pull-through cache**: 10.0.20.10 — use versioned image tags (cache serves stale :latest manifests)
|
||||
- **MySQL InnoDB Cluster**: 3 instances on iSCSI
|
||||
- **SMTP**: \`var.mail_host\` port 587 STARTTLS
|
||||
|
||||
## Further Reading
|
||||
|
||||
- Full agent instructions: \`AGENTS.md\` in the repo root
|
||||
- Patterns and examples: \`.claude/reference/patterns.md\`
|
||||
- Service catalog: \`.claude/reference/service-catalog.md\`
|
||||
- Onboarding guide: https://k8s-portal.viktorbarzin.me/onboarding
|
||||
`;
|
||||
|
||||
export const GET: RequestHandler = async () => {
|
||||
return new Response(BOOTSTRAP_DOC, {
|
||||
headers: {
|
||||
'Content-Type': 'text/plain; charset=utf-8',
|
||||
'Cache-Control': 'public, max-age=3600'
|
||||
}
|
||||
});
|
||||
};
|
||||
|
|
@ -0,0 +1,75 @@
|
|||
<main class="content">
|
||||
<h1>Architecture</h1>
|
||||
|
||||
<section>
|
||||
<h2>Overview</h2>
|
||||
<p>The infrastructure runs on a single Dell R730 server (22 CPU cores, 142GB RAM) using Proxmox to manage virtual machines. Five of those VMs form a Kubernetes cluster that runs 70+ services.</p>
|
||||
<pre class="output">
|
||||
Proxmox (Dell R730)
|
||||
├── k8s-master (10.0.20.100) — control plane
|
||||
├── k8s-node1 (10.0.20.101) — GPU node (Tesla T4)
|
||||
├── k8s-node2 (10.0.20.102) — worker
|
||||
├── k8s-node3 (10.0.20.103) — worker
|
||||
├── k8s-node4 (10.0.20.104) — worker
|
||||
├── TrueNAS (10.0.10.15) — storage (NFS + iSCSI)
|
||||
└── pfSense (10.0.20.1) — firewall + gateway</pre>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2>Networking</h2>
|
||||
<ul>
|
||||
<li><strong>Public domain</strong>: <code>viktorbarzin.me</code> — managed by Cloudflare</li>
|
||||
<li><strong>Internal domain</strong>: <code>viktorbarzin.lan</code> — managed by Technitium DNS</li>
|
||||
<li><strong>Ingress</strong>: Cloudflare → Traefik → services</li>
|
||||
<li><strong>VPN</strong>: Headscale (self-hosted Tailscale)</li>
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2>Storage</h2>
|
||||
<ul>
|
||||
<li><strong>NFS</strong> (<code>nfs-truenas</code>) — for app data (files, configs, media). Stored on TrueNAS.</li>
|
||||
<li><strong>iSCSI</strong> (<code>iscsi-truenas</code>) — for databases (PostgreSQL, MySQL). Block storage.</li>
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2>Service Tiers</h2>
|
||||
<p>Services are organized into tiers that control resource limits and restart priority:</p>
|
||||
<table>
|
||||
<thead><tr><th>Tier</th><th>Examples</th><th>Priority</th></tr></thead>
|
||||
<tbody>
|
||||
<tr><td><strong>0-core</strong></td><td>Traefik, DNS, VPN, Auth</td><td>Highest — never evicted</td></tr>
|
||||
<tr><td><strong>1-cluster</strong></td><td>Redis, Prometheus, CrowdSec</td><td>High</td></tr>
|
||||
<tr><td><strong>2-gpu</strong></td><td>Ollama, Immich ML, Whisper</td><td>Medium</td></tr>
|
||||
<tr><td><strong>3-edge</strong></td><td>Nextcloud, Paperless, Grafana</td><td>Normal</td></tr>
|
||||
<tr><td><strong>4-aux</strong></td><td>Dashy, PrivateBin, CyberChef</td><td>Low — evicted first under pressure</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2>Infrastructure as Code</h2>
|
||||
<p>Everything is managed with <strong>Terraform</strong> (via <strong>Terragrunt</strong>). Each service has its own stack:</p>
|
||||
<pre class="output">stacks/
|
||||
├── platform/ ← core infra (22 modules)
|
||||
├── url/ ← URL shortener (Shlink)
|
||||
├── immich/ ← photo library
|
||||
├── nextcloud/ ← file storage
|
||||
└── ... (70+ more)</pre>
|
||||
<p>Changes go through git: branch → PR → review → merge → CI applies automatically.</p>
|
||||
</section>
|
||||
</main>
|
||||
|
||||
<style>
|
||||
.content { max-width: 768px; margin: 2rem auto; padding: 0 1rem; font-family: system-ui, -apple-system, sans-serif; line-height: 1.6; }
|
||||
.content h1 { border-bottom: 1px solid #e0e0e0; padding-bottom: 0.5rem; }
|
||||
.content h2 { margin-top: 2rem; color: #333; }
|
||||
.content pre { background: #1e1e1e; color: #d4d4d4; padding: 1rem; border-radius: 6px; overflow-x: auto; }
|
||||
.content pre.output { background: #f5f5f5; color: #333; }
|
||||
.content code { background: #f0f0f0; padding: 2px 6px; border-radius: 3px; }
|
||||
section { margin: 2rem 0; }
|
||||
table { border-collapse: collapse; width: 100%; }
|
||||
th, td { border: 1px solid #ddd; padding: 0.5rem; text-align: left; }
|
||||
th { background: #f5f5f5; }
|
||||
</style>
|
||||
|
|
@ -0,0 +1,115 @@
|
|||
<main class="content">
|
||||
<h1>How to Contribute</h1>
|
||||
|
||||
<section>
|
||||
<h2>Workflow</h2>
|
||||
<ol>
|
||||
<li><strong>Create a branch</strong>: <code>git checkout -b fix/my-change</code></li>
|
||||
<li><strong>Make your changes</strong> in <code>stacks/<service>/main.tf</code></li>
|
||||
<li><strong>Push and open a PR</strong>: <code>git push -u origin fix/my-change</code></li>
|
||||
<li><strong>Viktor reviews</strong> and merges</li>
|
||||
<li><strong>CI applies</strong> automatically — Slack notification when done</li>
|
||||
</ol>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2>What you CAN change</h2>
|
||||
<ul>
|
||||
<li>Service configurations (image tags, environment variables, resource limits)</li>
|
||||
<li>New services (add a new stack under <code>stacks/</code>)</li>
|
||||
<li>Ingress routes, health probes, replica counts</li>
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2>What needs Viktor's review</h2>
|
||||
<ul>
|
||||
<li>CI pipeline changes (<code>.woodpecker/</code>)</li>
|
||||
<li>Terragrunt configuration (<code>terragrunt.hcl</code>)</li>
|
||||
<li>Secrets configuration (<code>.sops.yaml</code>)</li>
|
||||
<li>Core platform modules (<code>stacks/platform/</code>)</li>
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2 class="danger-header">NEVER do these</h2>
|
||||
<div class="callout danger">
|
||||
<ul>
|
||||
<li><strong>Never <code>kubectl apply/edit/patch</code></strong> — all changes go through Terraform</li>
|
||||
<li><strong>Never put secrets in code</strong> — ask Viktor to add them to the encrypted secrets file</li>
|
||||
<li><strong>Never restart NFS on TrueNAS</strong> — causes cluster-wide mount failures</li>
|
||||
<li><strong>Never push directly to master</strong> — always use a PR</li>
|
||||
</ul>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2>Need a new secret?</h2>
|
||||
<p>Comment on your PR: "I need a database password for my-service." Viktor will add it to the encrypted secrets file and push to your branch.</p>
|
||||
<p>Then reference it in your Terraform: <code>var.my_service_db_password</code></p>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2>Namespace Owner Workflow</h2>
|
||||
<p>If you are a namespace owner, you can deploy your own apps:</p>
|
||||
<ol>
|
||||
<li>Clone the infra repo: <code>git clone https://github.com/ViktorBarzin/infra.git</code></li>
|
||||
<li>Copy the template: <code>cp -r stacks/_template stacks/your-app</code></li>
|
||||
<li>Rename: <code>mv stacks/your-app/main.tf.example stacks/your-app/main.tf</code></li>
|
||||
<li>Edit <code>main.tf</code> — replace all <code><placeholders></code></li>
|
||||
<li>Store secrets in Vault: <code>vault kv put secret/your-username/your-app KEY=value</code></li>
|
||||
<li>Add your app domain to your <code>domains</code> list in Vault KV</li>
|
||||
<li>Submit a PR, get it reviewed</li>
|
||||
<li>After merge, admin runs <code>terragrunt apply</code></li>
|
||||
</ol>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2>CI Pipeline Template</h2>
|
||||
<p>Create a <code>.woodpecker.yml</code> in your app's Forgejo repo:</p>
|
||||
<pre>{`steps:
|
||||
- name: build
|
||||
image: woodpeckerci/plugin-docker-buildx
|
||||
settings:
|
||||
repo: your-dockerhub-user/myapp
|
||||
tag: ["\${CI_PIPELINE_NUMBER}", "latest"]
|
||||
username:
|
||||
from_secret: dockerhub-username
|
||||
password:
|
||||
from_secret: dockerhub-token
|
||||
platforms: linux/amd64
|
||||
|
||||
- name: deploy
|
||||
image: hashicorp/vault:1.18.1
|
||||
commands:
|
||||
- export VAULT_ADDR=http://vault-active.vault.svc.cluster.local:8200
|
||||
- export VAULT_TOKEN=$(vault write -field=token auth/kubernetes/login
|
||||
role=ci jwt=$(cat /var/run/secrets/kubernetes.io/serviceaccount/token))
|
||||
- KUBE_TOKEN=$(vault write -field=service_account_token
|
||||
kubernetes/creds/YOUR_NAMESPACE-deployer
|
||||
kubernetes_namespace=YOUR_NAMESPACE)
|
||||
- kubectl --server=https://kubernetes.default.svc
|
||||
--token=$KUBE_TOKEN
|
||||
--certificate-authority=/var/run/secrets/kubernetes.io/serviceaccount/ca.crt
|
||||
-n YOUR_NAMESPACE set image deployment/myapp
|
||||
myapp=your-dockerhub-user/myapp:\${CI_PIPELINE_NUMBER}`}</pre>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2>Need a secret for your app?</h2>
|
||||
<p>As a namespace owner, you manage your own secrets in Vault:</p>
|
||||
<pre>vault kv put secret/your-username/your-app DB_PASSWORD=mysecret API_KEY=abc123</pre>
|
||||
<p>Then reference them in your Terraform using a <code>data "vault_kv_secret_v2"</code> block.</p>
|
||||
</section>
|
||||
</main>
|
||||
|
||||
<style>
|
||||
.content { max-width: 768px; margin: 2rem auto; padding: 0 1rem; font-family: system-ui, -apple-system, sans-serif; line-height: 1.6; }
|
||||
.content h1 { border-bottom: 1px solid #e0e0e0; padding-bottom: 0.5rem; }
|
||||
.content h2 { margin-top: 2rem; color: #333; }
|
||||
.content code { background: #f0f0f0; padding: 2px 6px; border-radius: 3px; }
|
||||
section { margin: 2rem 0; }
|
||||
.callout { padding: 1rem; border-radius: 6px; margin: 1rem 0; }
|
||||
.callout.danger { background: #f8d7da; border-left: 4px solid #dc3545; }
|
||||
.danger-header { color: #dc3545; }
|
||||
</style>
|
||||
|
|
@ -0,0 +1,58 @@
|
|||
import type { RequestHandler } from './$types';
|
||||
import { readFileSync } from 'fs';
|
||||
|
||||
const CLUSTER_SERVER = 'https://10.0.20.100:6443';
|
||||
const OIDC_ISSUER = 'https://authentik.viktorbarzin.me/application/o/kubernetes/';
|
||||
const OIDC_CLIENT_ID = 'kubernetes';
|
||||
|
||||
export const GET: RequestHandler = async ({ request }) => {
|
||||
const email = request.headers.get('x-authentik-email') || 'user';
|
||||
|
||||
// Read CA cert from mounted ConfigMap
|
||||
let caCert = '';
|
||||
try {
|
||||
caCert = readFileSync('/config/ca.crt', 'utf-8');
|
||||
} catch {
|
||||
// CA cert not available
|
||||
}
|
||||
|
||||
const caCertBase64 = Buffer.from(caCert).toString('base64');
|
||||
const sanitizedEmail = email.replace(/[^a-zA-Z0-9@._-]/g, '');
|
||||
|
||||
const kubeconfig = `apiVersion: v1
|
||||
kind: Config
|
||||
clusters:
|
||||
- cluster:
|
||||
server: ${CLUSTER_SERVER}
|
||||
certificate-authority-data: ${caCertBase64}
|
||||
name: home-cluster
|
||||
contexts:
|
||||
- context:
|
||||
cluster: home-cluster
|
||||
user: oidc-${sanitizedEmail}
|
||||
name: home-cluster
|
||||
current-context: home-cluster
|
||||
users:
|
||||
- name: oidc-${sanitizedEmail}
|
||||
user:
|
||||
exec:
|
||||
apiVersion: client.authentication.k8s.io/v1beta1
|
||||
command: kubectl
|
||||
args:
|
||||
- oidc-login
|
||||
- get-token
|
||||
- --oidc-issuer-url=${OIDC_ISSUER}
|
||||
- --oidc-client-id=${OIDC_CLIENT_ID}
|
||||
- --oidc-extra-scope=email
|
||||
- --oidc-extra-scope=profile
|
||||
- --oidc-extra-scope=groups
|
||||
interactiveMode: IfAvailable
|
||||
`;
|
||||
|
||||
return new Response(kubeconfig, {
|
||||
headers: {
|
||||
'Content-Type': 'application/yaml',
|
||||
'Content-Disposition': `attachment; filename="kubeconfig-home-cluster.yaml"`
|
||||
}
|
||||
});
|
||||
};
|
||||
|
|
@ -0,0 +1,146 @@
|
|||
<script>
|
||||
import { page } from '$app/stores';
|
||||
let showNamespaceOwner = $derived($page.url.searchParams.get('role') === 'namespace-owner');
|
||||
</script>
|
||||
|
||||
<main class="content">
|
||||
<h1>Getting Started</h1>
|
||||
<p>Welcome! Follow these steps to get access to the home Kubernetes cluster.</p>
|
||||
|
||||
<div class="role-tabs">
|
||||
<a href="/onboarding" class:active={!showNamespaceOwner}>General User</a>
|
||||
<a href="/onboarding?role=namespace-owner" class:active={showNamespaceOwner}>Namespace Owner</a>
|
||||
</div>
|
||||
|
||||
<section>
|
||||
<h2>Step 0 — Join the VPN</h2>
|
||||
<p>The cluster is on a private network (<code>10.0.20.0/24</code>). You need VPN access first.</p>
|
||||
<ol>
|
||||
<li>Install <a href="https://tailscale.com/download" target="_blank">Tailscale</a> for your OS</li>
|
||||
<li>Run this in your terminal:
|
||||
<pre>tailscale login --login-server https://headscale.viktorbarzin.me</pre>
|
||||
</li>
|
||||
<li>A browser window will open with a registration URL</li>
|
||||
<li>Send that URL to Viktor via email (<a href="mailto:vbarzin@gmail.com">vbarzin@gmail.com</a>) or Slack</li>
|
||||
<li>Wait for approval (usually within a few hours)</li>
|
||||
<li>Once approved, test: <pre>ping 10.0.20.100</pre></li>
|
||||
</ol>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2>Step 1 — Log in to the portal</h2>
|
||||
<p>Visit <a href="https://k8s-portal.viktorbarzin.me">k8s-portal.viktorbarzin.me</a> and sign in with your Authentik account.</p>
|
||||
<p>If you don't have an account yet, ask Viktor to create one.</p>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2>Step 2 — Set up kubectl</h2>
|
||||
<p>Run one of these commands in your terminal to install everything automatically:</p>
|
||||
<h3>macOS</h3>
|
||||
<p class="prereq">Requires <a href="https://brew.sh" target="_blank">Homebrew</a>. Install it first if you don't have it.</p>
|
||||
<pre>bash <(curl -fsSL https://k8s-portal.viktorbarzin.me/setup/script?os=mac)</pre>
|
||||
<h3>Linux</h3>
|
||||
<pre>bash <(curl -fsSL https://k8s-portal.viktorbarzin.me/setup/script?os=linux)</pre>
|
||||
<h3>Windows</h3>
|
||||
<p>Use <a href="https://learn.microsoft.com/en-us/windows/wsl/install" target="_blank">WSL2</a> and follow the Linux instructions.</p>
|
||||
</section>
|
||||
|
||||
{#if showNamespaceOwner}
|
||||
<section>
|
||||
<h2>Step 3 — Log into Vault</h2>
|
||||
<p>Vault manages your secrets and issues dynamic Kubernetes credentials.</p>
|
||||
<pre>vault login -method=oidc</pre>
|
||||
<p>This opens your browser for Authentik SSO. After login, your token is saved to <code>~/.vault-token</code>.</p>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2>Step 4 — Verify kubectl access</h2>
|
||||
<p>Run this command. It will open your browser for OIDC login the first time:</p>
|
||||
<pre>kubectl get pods -n YOUR_NAMESPACE</pre>
|
||||
<p>You should see an empty list (no resources) or your running pods.</p>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2>Step 5 — Clone the infra repo</h2>
|
||||
<pre>git clone https://github.com/ViktorBarzin/infra.git
|
||||
cd infra</pre>
|
||||
<p>This is where all the infrastructure configuration lives.</p>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2>Step 6 — Create your first app stack</h2>
|
||||
<ol>
|
||||
<li>Copy the template: <pre>cp -r stacks/_template stacks/myapp
|
||||
mv stacks/myapp/main.tf.example stacks/myapp/main.tf</pre></li>
|
||||
<li>Edit <code>stacks/myapp/main.tf</code> — replace all <code><placeholders></code></li>
|
||||
<li>Store secrets in Vault:
|
||||
<pre>vault kv put secret/YOUR_USERNAME/myapp DB_PASSWORD=secret123</pre>
|
||||
</li>
|
||||
<li>Add your app domain to <code>domains</code> list in Vault KV <code>k8s_users</code></li>
|
||||
<li>Submit a PR:
|
||||
<pre>git checkout -b feat/myapp
|
||||
git add stacks/myapp/
|
||||
git commit -m "add myapp stack"
|
||||
git push -u origin feat/myapp</pre>
|
||||
</li>
|
||||
<li>Viktor reviews and merges</li>
|
||||
<li>After merge: <code>cd stacks/myapp && terragrunt apply</code></li>
|
||||
</ol>
|
||||
</section>
|
||||
{:else}
|
||||
<section>
|
||||
<h2>Step 3 — Verify access</h2>
|
||||
<p>Run this command. It will open your browser for login the first time:</p>
|
||||
<pre>kubectl get namespaces</pre>
|
||||
<p>You should see output like:</p>
|
||||
<pre class="output">NAME STATUS AGE
|
||||
default Active 200d
|
||||
kube-system Active 200d
|
||||
monitoring Active 200d
|
||||
...</pre>
|
||||
<p>If you get a connection error, make sure your VPN is connected (<code>tailscale status</code>).</p>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2>Step 4 — Clone the repo</h2>
|
||||
<pre>git clone https://github.com/ViktorBarzin/infra.git
|
||||
cd infra</pre>
|
||||
<p>This is where all the infrastructure configuration lives.</p>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2>Step 5 — Install your AI assistant (optional)</h2>
|
||||
<p>Install <a href="https://github.com/openai/codex" target="_blank">Codex CLI</a> for AI-assisted cluster management:</p>
|
||||
<pre>npm install -g @openai/codex</pre>
|
||||
<p>Codex reads the <code>AGENTS.md</code> file in the repo and knows how to work with the cluster.</p>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2>Step 6 — Your first change</h2>
|
||||
<ol>
|
||||
<li>Create a branch: <pre>git checkout -b my-first-change</pre></li>
|
||||
<li>Edit a service file (e.g., change an image tag in <code>stacks/echo/main.tf</code>)</li>
|
||||
<li>Commit and push: <pre>git add . && git commit -m "my first change" && git push -u origin my-first-change</pre></li>
|
||||
<li>Open a Pull Request on GitHub</li>
|
||||
<li>Viktor reviews and merges</li>
|
||||
<li>Woodpecker CI automatically applies the change to the cluster</li>
|
||||
<li>Slack notification confirms it worked</li>
|
||||
</ol>
|
||||
</section>
|
||||
{/if}
|
||||
</main>
|
||||
|
||||
<style>
|
||||
.content { max-width: 768px; margin: 2rem auto; padding: 0 1rem; font-family: system-ui, -apple-system, sans-serif; line-height: 1.6; }
|
||||
.content h1 { border-bottom: 1px solid #e0e0e0; padding-bottom: 0.5rem; }
|
||||
.content h2 { margin-top: 2rem; color: #333; }
|
||||
.content h3 { color: #666; margin: 1rem 0 0.25rem; }
|
||||
.content pre { background: #1e1e1e; color: #d4d4d4; padding: 1rem; border-radius: 6px; overflow-x: auto; }
|
||||
.content pre.output { background: #f5f5f5; color: #333; }
|
||||
.content code { background: #f0f0f0; padding: 2px 6px; border-radius: 3px; }
|
||||
.content .prereq { font-size: 0.9rem; color: #666; font-style: italic; }
|
||||
section { margin: 2rem 0; }
|
||||
.role-tabs { display: flex; gap: 0; margin: 1.5rem 0; border-bottom: 2px solid #e0e0e0; }
|
||||
.role-tabs a { padding: 0.5rem 1.5rem; text-decoration: none; color: #666; border-bottom: 2px solid transparent; margin-bottom: -2px; }
|
||||
.role-tabs a.active { color: #333; border-bottom-color: #333; font-weight: 600; }
|
||||
</style>
|
||||
|
|
@ -0,0 +1,58 @@
|
|||
<main class="content">
|
||||
<h1>Service Catalog</h1>
|
||||
<p>70+ services running on the cluster. Here are the most commonly used:</p>
|
||||
|
||||
<section>
|
||||
<h2>Core Services</h2>
|
||||
<table>
|
||||
<thead><tr><th>Service</th><th>URL</th><th>Description</th></tr></thead>
|
||||
<tbody>
|
||||
<tr><td>Grafana</td><td><a href="https://grafana.viktorbarzin.me">grafana.viktorbarzin.me</a></td><td>Monitoring dashboards</td></tr>
|
||||
<tr><td>Uptime Kuma</td><td><a href="https://uptime.viktorbarzin.me">uptime.viktorbarzin.me</a></td><td>Service uptime monitoring</td></tr>
|
||||
<tr><td>Authentik</td><td><a href="https://authentik.viktorbarzin.me">authentik.viktorbarzin.me</a></td><td>Identity provider (SSO)</td></tr>
|
||||
<tr><td>Woodpecker CI</td><td><a href="https://ci.viktorbarzin.me">ci.viktorbarzin.me</a></td><td>CI/CD pipeline</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2>User-Facing Services</h2>
|
||||
<table>
|
||||
<thead><tr><th>Service</th><th>URL</th><th>Description</th></tr></thead>
|
||||
<tbody>
|
||||
<tr><td>Nextcloud</td><td><a href="https://nextcloud.viktorbarzin.me">nextcloud.viktorbarzin.me</a></td><td>File storage, calendar, contacts</td></tr>
|
||||
<tr><td>Immich</td><td><a href="https://immich.viktorbarzin.me">immich.viktorbarzin.me</a></td><td>Photo library (Google Photos alternative)</td></tr>
|
||||
<tr><td>Vaultwarden</td><td><a href="https://vault.viktorbarzin.me">vault.viktorbarzin.me</a></td><td>Password manager</td></tr>
|
||||
<tr><td>Paperless-ngx</td><td><a href="https://pdf.viktorbarzin.me">pdf.viktorbarzin.me</a></td><td>Document management</td></tr>
|
||||
<tr><td>Navidrome</td><td><a href="https://music.viktorbarzin.me">music.viktorbarzin.me</a></td><td>Music streaming</td></tr>
|
||||
<tr><td>Tandoor</td><td><a href="https://recipes.viktorbarzin.me">recipes.viktorbarzin.me</a></td><td>Recipe manager</td></tr>
|
||||
<tr><td>Linkwarden</td><td><a href="https://bookmarks.viktorbarzin.me">bookmarks.viktorbarzin.me</a></td><td>Bookmark manager</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2>Developer Tools</h2>
|
||||
<table>
|
||||
<thead><tr><th>Service</th><th>URL</th><th>Description</th></tr></thead>
|
||||
<tbody>
|
||||
<tr><td>Forgejo</td><td><a href="https://forgejo.viktorbarzin.me">forgejo.viktorbarzin.me</a></td><td>Git server (Gitea fork)</td></tr>
|
||||
<tr><td>CyberChef</td><td><a href="https://cyberchef.viktorbarzin.me">cyberchef.viktorbarzin.me</a></td><td>Data transformation tool</td></tr>
|
||||
<tr><td>Excalidraw</td><td><a href="https://draw.viktorbarzin.me">draw.viktorbarzin.me</a></td><td>Whiteboard drawing</td></tr>
|
||||
<tr><td>PrivateBin</td><td><a href="https://paste.viktorbarzin.me">paste.viktorbarzin.me</a></td><td>Encrypted paste bin</td></tr>
|
||||
<tr><td>JSON Crack</td><td><a href="https://jsoncrack.viktorbarzin.me">jsoncrack.viktorbarzin.me</a></td><td>JSON visualizer</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</section>
|
||||
</main>
|
||||
|
||||
<style>
|
||||
.content { max-width: 768px; margin: 2rem auto; padding: 0 1rem; font-family: system-ui, -apple-system, sans-serif; line-height: 1.6; }
|
||||
.content h1 { border-bottom: 1px solid #e0e0e0; padding-bottom: 0.5rem; }
|
||||
.content h2 { margin-top: 2rem; color: #333; }
|
||||
section { margin: 2rem 0; }
|
||||
table { border-collapse: collapse; width: 100%; }
|
||||
th, td { border: 1px solid #ddd; padding: 0.5rem; text-align: left; }
|
||||
th { background: #f5f5f5; }
|
||||
a { color: #1a73e8; }
|
||||
</style>
|
||||
|
|
@ -0,0 +1,69 @@
|
|||
<main>
|
||||
<h1>Setup Instructions</h1>
|
||||
|
||||
<section>
|
||||
<h2>Quick Setup (one command)</h2>
|
||||
<p>Run this in your terminal to install everything and configure kubectl automatically:</p>
|
||||
<h3>macOS</h3>
|
||||
<pre>bash <(curl -fsSL https://k8s-portal.viktorbarzin.me/setup/script?os=mac)</pre>
|
||||
<h3>Linux</h3>
|
||||
<pre>bash <(curl -fsSL https://k8s-portal.viktorbarzin.me/setup/script?os=linux)</pre>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2>Manual Setup</h2>
|
||||
|
||||
<h3>1. Install kubectl</h3>
|
||||
<h4>macOS</h4>
|
||||
<pre>brew install kubectl</pre>
|
||||
<h4>Linux</h4>
|
||||
<pre>curl -LO "https://dl.k8s.io/release/$(curl -L -s https://dl.k8s.io/release/stable.txt)/bin/linux/amd64/kubectl"
|
||||
chmod +x kubectl && sudo mv kubectl /usr/local/bin/</pre>
|
||||
|
||||
<h3>2. Install kubelogin (OIDC plugin)</h3>
|
||||
<h4>macOS</h4>
|
||||
<pre>brew install int128/kubelogin/kubelogin</pre>
|
||||
<h4>Linux</h4>
|
||||
<pre>curl -LO https://github.com/int128/kubelogin/releases/latest/download/kubelogin_linux_amd64.zip
|
||||
unzip kubelogin_linux_amd64.zip && sudo mv kubelogin /usr/local/bin/kubectl-oidc_login
|
||||
rm kubelogin_linux_amd64.zip</pre>
|
||||
|
||||
<h3>3. Download and use your kubeconfig</h3>
|
||||
<pre>
|
||||
mkdir -p ~/.kube
|
||||
|
||||
# Download from the portal (requires auth cookie from browser)
|
||||
# Or use the download button on the portal homepage
|
||||
|
||||
# Set the KUBECONFIG environment variable
|
||||
export KUBECONFIG=~/.kube/config-home
|
||||
|
||||
# Test access (opens browser for login)
|
||||
kubectl get namespaces
|
||||
</pre>
|
||||
</section>
|
||||
|
||||
<p><a href="/">← Back to portal</a></p>
|
||||
</main>
|
||||
|
||||
<style>
|
||||
main {
|
||||
max-width: 640px;
|
||||
margin: 2rem auto;
|
||||
font-family: system-ui;
|
||||
}
|
||||
pre {
|
||||
background: #1e1e1e;
|
||||
color: #d4d4d4;
|
||||
padding: 1rem;
|
||||
border-radius: 6px;
|
||||
overflow-x: auto;
|
||||
}
|
||||
section {
|
||||
margin: 2rem 0;
|
||||
}
|
||||
h4 {
|
||||
margin: 0.5rem 0 0.25rem;
|
||||
color: #666;
|
||||
}
|
||||
</style>
|
||||
|
|
@ -0,0 +1,266 @@
|
|||
import type { RequestHandler } from './$types';
|
||||
import { readFileSync } from 'fs';
|
||||
|
||||
const CLUSTER_SERVER = 'https://10.0.20.100:6443';
|
||||
const OIDC_ISSUER = 'https://authentik.viktorbarzin.me/application/o/kubernetes/';
|
||||
const OIDC_CLIENT_ID = 'kubernetes';
|
||||
|
||||
export const GET: RequestHandler = async ({ url }) => {
|
||||
const os = url.searchParams.get('os') || 'mac';
|
||||
|
||||
let caCert = '';
|
||||
try {
|
||||
caCert = readFileSync('/config/ca.crt', 'utf-8');
|
||||
} catch {
|
||||
// CA cert not available
|
||||
}
|
||||
const caCertBase64 = Buffer.from(caCert).toString('base64');
|
||||
|
||||
const kubeconfigContent = `apiVersion: v1
|
||||
kind: Config
|
||||
clusters:
|
||||
- cluster:
|
||||
server: ${CLUSTER_SERVER}
|
||||
certificate-authority-data: ${caCertBase64}
|
||||
name: home-cluster
|
||||
contexts:
|
||||
- context:
|
||||
cluster: home-cluster
|
||||
user: oidc-user
|
||||
name: home-cluster
|
||||
current-context: home-cluster
|
||||
users:
|
||||
- name: oidc-user
|
||||
user:
|
||||
exec:
|
||||
apiVersion: client.authentication.k8s.io/v1beta1
|
||||
command: kubectl
|
||||
args:
|
||||
- oidc-login
|
||||
- get-token
|
||||
- --oidc-issuer-url=${OIDC_ISSUER}
|
||||
- --oidc-client-id=${OIDC_CLIENT_ID}
|
||||
- --oidc-extra-scope=email
|
||||
- --oidc-extra-scope=profile
|
||||
- --oidc-extra-scope=groups
|
||||
interactiveMode: IfAvailable`;
|
||||
|
||||
let script: string;
|
||||
|
||||
if (os === 'linux') {
|
||||
script = `#!/bin/bash
|
||||
set -e
|
||||
|
||||
echo "=== Kubernetes Cluster Setup ==="
|
||||
echo ""
|
||||
|
||||
# Use sudo if available, otherwise install directly (e.g. in containers running as root)
|
||||
SUDO=""
|
||||
if [ "$(id -u)" -ne 0 ] && command -v sudo &>/dev/null; then
|
||||
SUDO="sudo"
|
||||
fi
|
||||
|
||||
# Determine install directory
|
||||
INSTALL_DIR="/usr/local/bin"
|
||||
if [ ! -w "\$INSTALL_DIR" ] && [ -z "\$SUDO" ]; then
|
||||
INSTALL_DIR="\$HOME/.local/bin"
|
||||
mkdir -p "\$INSTALL_DIR"
|
||||
export PATH="\$INSTALL_DIR:\$PATH"
|
||||
fi
|
||||
|
||||
# Install kubectl
|
||||
if command -v kubectl &>/dev/null; then
|
||||
echo "[OK] kubectl already installed"
|
||||
else
|
||||
echo "[..] Installing kubectl..."
|
||||
KUBECTL_VERSION=\$(curl -L -s https://dl.k8s.io/release/stable.txt)
|
||||
curl -fsSLO "https://dl.k8s.io/release/\${KUBECTL_VERSION}/bin/linux/amd64/kubectl"
|
||||
chmod +x kubectl && \$SUDO mv kubectl "\$INSTALL_DIR/"
|
||||
echo "[OK] kubectl installed"
|
||||
fi
|
||||
|
||||
# Install kubelogin
|
||||
if command -v kubectl-oidc_login &>/dev/null; then
|
||||
echo "[OK] kubelogin already installed"
|
||||
else
|
||||
echo "[..] Installing kubelogin..."
|
||||
KUBELOGIN_VERSION=\$(curl -fsSL -o /dev/null -w "%{url_effective}" https://github.com/int128/kubelogin/releases/latest | grep -o '[^/]*\$')
|
||||
curl -fsSLO "https://github.com/int128/kubelogin/releases/download/\${KUBELOGIN_VERSION}/kubelogin_linux_amd64.zip"
|
||||
unzip -o kubelogin_linux_amd64.zip kubelogin -d /tmp
|
||||
\$SUDO mv /tmp/kubelogin "\$INSTALL_DIR/kubectl-oidc_login"
|
||||
rm -f kubelogin_linux_amd64.zip
|
||||
echo "[OK] kubelogin installed"
|
||||
fi
|
||||
|
||||
# Install kubeseal
|
||||
if command -v kubeseal &>/dev/null; then
|
||||
echo "[OK] kubeseal already installed"
|
||||
else
|
||||
echo "[..] Installing kubeseal..."
|
||||
KUBESEAL_VERSION=\$(curl -fsSL -o /dev/null -w "%{url_effective}" https://github.com/bitnami-labs/sealed-secrets/releases/latest | grep -o '[^/]*\$')
|
||||
curl -fsSLO "https://github.com/bitnami-labs/sealed-secrets/releases/download/\${KUBESEAL_VERSION}/kubeseal-\${KUBESEAL_VERSION#v}-linux-amd64.tar.gz"
|
||||
tar -xzf "kubeseal-\${KUBESEAL_VERSION#v}-linux-amd64.tar.gz" kubeseal
|
||||
\$SUDO mv kubeseal "\$INSTALL_DIR/"
|
||||
rm -f "kubeseal-\${KUBESEAL_VERSION#v}-linux-amd64.tar.gz"
|
||||
echo "[OK] kubeseal installed"
|
||||
fi
|
||||
|
||||
# Install Vault CLI
|
||||
if command -v vault &>/dev/null; then
|
||||
echo "[OK] vault already installed"
|
||||
else
|
||||
echo "[..] Installing Vault CLI..."
|
||||
VAULT_VERSION="1.18.1"
|
||||
curl -fsSLO "https://releases.hashicorp.com/vault/\${VAULT_VERSION}/vault_\${VAULT_VERSION}_linux_amd64.zip"
|
||||
unzip -o "vault_\${VAULT_VERSION}_linux_amd64.zip" vault -d /tmp
|
||||
\$SUDO mv /tmp/vault "\$INSTALL_DIR/"
|
||||
rm -f "vault_\${VAULT_VERSION}_linux_amd64.zip"
|
||||
echo "[OK] vault installed"
|
||||
fi
|
||||
|
||||
# Install Terragrunt
|
||||
if command -v terragrunt &>/dev/null; then
|
||||
echo "[OK] terragrunt already installed"
|
||||
else
|
||||
echo "[..] Installing terragrunt..."
|
||||
TG_VERSION=\$(curl -fsSL -o /dev/null -w "%{url_effective}" https://github.com/gruntwork-io/terragrunt/releases/latest | grep -o '[^/]*\$')
|
||||
curl -fsSLO "https://github.com/gruntwork-io/terragrunt/releases/download/\${TG_VERSION}/terragrunt_linux_amd64"
|
||||
chmod +x terragrunt_linux_amd64
|
||||
\$SUDO mv terragrunt_linux_amd64 "\$INSTALL_DIR/terragrunt"
|
||||
echo "[OK] terragrunt installed"
|
||||
fi
|
||||
|
||||
# Install Terraform
|
||||
if command -v terraform &>/dev/null; then
|
||||
echo "[OK] terraform already installed"
|
||||
else
|
||||
echo "[..] Installing terraform..."
|
||||
TF_VERSION="1.9.8"
|
||||
curl -fsSLO "https://releases.hashicorp.com/terraform/\${TF_VERSION}/terraform_\${TF_VERSION}_linux_amd64.zip"
|
||||
unzip -o "terraform_\${TF_VERSION}_linux_amd64.zip" terraform -d /tmp
|
||||
\$SUDO mv /tmp/terraform "\$INSTALL_DIR/"
|
||||
rm -f "terraform_\${TF_VERSION}_linux_amd64.zip"
|
||||
echo "[OK] terraform installed"
|
||||
fi
|
||||
|
||||
# Write kubeconfig
|
||||
mkdir -p ~/.kube
|
||||
cat > ~/.kube/config-home << 'KUBECONFIG_EOF'
|
||||
${kubeconfigContent}
|
||||
KUBECONFIG_EOF
|
||||
echo "[OK] Kubeconfig written to ~/.kube/config-home"
|
||||
|
||||
# Add KUBECONFIG to shell profile
|
||||
SHELL_RC=~/.bashrc
|
||||
[ -f ~/.zshrc ] && SHELL_RC=~/.zshrc
|
||||
if ! grep -q 'config-home' "\$SHELL_RC" 2>/dev/null; then
|
||||
echo 'export KUBECONFIG=~/.kube/config-home' >> "\$SHELL_RC"
|
||||
echo "[OK] Added KUBECONFIG to \$SHELL_RC"
|
||||
fi
|
||||
export KUBECONFIG=~/.kube/config-home
|
||||
|
||||
echo ""
|
||||
echo "=== Setup complete! ==="
|
||||
echo ""
|
||||
echo "Run 'kubectl get namespaces' to test (opens browser for login)."
|
||||
echo "You may need to restart your shell or run: export KUBECONFIG=~/.kube/config-home"
|
||||
`;
|
||||
} else {
|
||||
script = `#!/bin/bash
|
||||
set -e
|
||||
|
||||
echo "=== Kubernetes Cluster Setup ==="
|
||||
echo ""
|
||||
|
||||
# Check for Homebrew
|
||||
if ! command -v brew &>/dev/null; then
|
||||
echo "[!!] Homebrew not found. Install it first:"
|
||||
echo ' /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"'
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Install kubectl
|
||||
if command -v kubectl &>/dev/null; then
|
||||
echo "[OK] kubectl already installed ($(kubectl version --client -o json 2>/dev/null | grep -o '"gitVersion":"[^"]*"' | cut -d'"' -f4))"
|
||||
else
|
||||
echo "[..] Installing kubectl..."
|
||||
brew install kubectl
|
||||
echo "[OK] kubectl installed"
|
||||
fi
|
||||
|
||||
# Install kubelogin
|
||||
if command -v kubectl-oidc_login &>/dev/null; then
|
||||
echo "[OK] kubelogin already installed"
|
||||
else
|
||||
echo "[..] Installing kubelogin..."
|
||||
brew install int128/kubelogin/kubelogin
|
||||
echo "[OK] kubelogin installed"
|
||||
fi
|
||||
|
||||
# Install kubeseal
|
||||
if command -v kubeseal &>/dev/null; then
|
||||
echo "[OK] kubeseal already installed"
|
||||
else
|
||||
echo "[..] Installing kubeseal..."
|
||||
brew install kubeseal
|
||||
echo "[OK] kubeseal installed"
|
||||
fi
|
||||
|
||||
# Install Vault CLI
|
||||
if command -v vault &>/dev/null; then
|
||||
echo "[OK] vault already installed"
|
||||
else
|
||||
echo "[..] Installing Vault CLI..."
|
||||
brew tap hashicorp/tap
|
||||
brew install hashicorp/tap/vault
|
||||
echo "[OK] vault installed"
|
||||
fi
|
||||
|
||||
# Install Terragrunt
|
||||
if command -v terragrunt &>/dev/null; then
|
||||
echo "[OK] terragrunt already installed"
|
||||
else
|
||||
echo "[..] Installing terragrunt..."
|
||||
brew install terragrunt
|
||||
echo "[OK] terragrunt installed"
|
||||
fi
|
||||
|
||||
# Install Terraform
|
||||
if command -v terraform &>/dev/null; then
|
||||
echo "[OK] terraform already installed"
|
||||
else
|
||||
echo "[..] Installing terraform..."
|
||||
brew install hashicorp/tap/terraform
|
||||
echo "[OK] terraform installed"
|
||||
fi
|
||||
|
||||
# Write kubeconfig
|
||||
mkdir -p ~/.kube
|
||||
cat > ~/.kube/config-home << 'KUBECONFIG_EOF'
|
||||
${kubeconfigContent}
|
||||
KUBECONFIG_EOF
|
||||
echo "[OK] Kubeconfig written to ~/.kube/config-home"
|
||||
|
||||
# Add KUBECONFIG to shell profile
|
||||
SHELL_RC=~/.zshrc
|
||||
[ ! -f ~/.zshrc ] && SHELL_RC=~/.bashrc
|
||||
if ! grep -q 'config-home' "\$SHELL_RC" 2>/dev/null; then
|
||||
echo 'export KUBECONFIG=~/.kube/config-home' >> "\$SHELL_RC"
|
||||
echo "[OK] Added KUBECONFIG to \$SHELL_RC"
|
||||
fi
|
||||
export KUBECONFIG=~/.kube/config-home
|
||||
|
||||
echo ""
|
||||
echo "=== Setup complete! ==="
|
||||
echo ""
|
||||
echo "Run 'kubectl get namespaces' to test (opens browser for login)."
|
||||
echo "You may need to restart your shell or run: export KUBECONFIG=~/.kube/config-home"
|
||||
`;
|
||||
}
|
||||
|
||||
return new Response(script, {
|
||||
headers: {
|
||||
'Content-Type': 'text/plain; charset=utf-8'
|
||||
}
|
||||
});
|
||||
};
|
||||
|
|
@ -0,0 +1,63 @@
|
|||
<main class="content">
|
||||
<h1>Troubleshooting</h1>
|
||||
|
||||
<section>
|
||||
<h2>"kubectl can't connect to the server"</h2>
|
||||
<ol>
|
||||
<li>Check your VPN: <code>tailscale status</code> — should show "connected"</li>
|
||||
<li>Check KUBECONFIG: <code>echo $KUBECONFIG</code> — should be <code>~/.kube/config-home</code></li>
|
||||
<li>Test connectivity: <code>ping 10.0.20.100</code></li>
|
||||
<li>If ping works but kubectl doesn't, re-run the <a href="/setup">setup script</a></li>
|
||||
</ol>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2>"Forbidden" or "Permission denied"</h2>
|
||||
<p>You may not have access to that namespace. Your access is scoped to specific namespaces.</p>
|
||||
<p>Try: <code>kubectl get namespaces</code> to see which namespaces you can access.</p>
|
||||
<p>Need access to another namespace? Ask Viktor.</p>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2>"Pod is CrashLoopBackOff"</h2>
|
||||
<ol>
|
||||
<li>Check pod logs: <code>kubectl logs -n <namespace> <pod-name> --tail=50</code></li>
|
||||
<li>Check previous crash: <code>kubectl logs -n <namespace> <pod-name> --previous</code></li>
|
||||
<li>Check events: <code>kubectl describe pod -n <namespace> <pod-name></code></li>
|
||||
<li>Common causes: OOMKilled (need more memory), bad config, database connection failure</li>
|
||||
</ol>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2>"PR CI failed"</h2>
|
||||
<ol>
|
||||
<li>Check the Woodpecker CI dashboard: <a href="https://ci.viktorbarzin.me">ci.viktorbarzin.me</a></li>
|
||||
<li>Read the build logs — the error is usually at the bottom</li>
|
||||
<li>Fix the issue, commit, and push — CI will re-run</li>
|
||||
</ol>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2>"I need a new secret / database password"</h2>
|
||||
<p>Secrets are managed by Viktor in an encrypted file. You cannot add them yourself.</p>
|
||||
<ol>
|
||||
<li>Comment on your PR: "Need DB password for <service>"</li>
|
||||
<li>Viktor adds the secret and pushes to your branch</li>
|
||||
<li>Reference it as <code>var.<service>_db_password</code> in your Terraform</li>
|
||||
</ol>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2>Still stuck?</h2>
|
||||
<p>Email Viktor at <a href="mailto:vbarzin@gmail.com">vbarzin@gmail.com</a> or message on Slack.</p>
|
||||
</section>
|
||||
</main>
|
||||
|
||||
<style>
|
||||
.content { max-width: 768px; margin: 2rem auto; padding: 0 1rem; font-family: system-ui, -apple-system, sans-serif; line-height: 1.6; }
|
||||
.content h1 { border-bottom: 1px solid #e0e0e0; padding-bottom: 0.5rem; }
|
||||
.content h2 { margin-top: 2rem; color: #333; }
|
||||
.content pre { background: #1e1e1e; color: #d4d4d4; padding: 1rem; border-radius: 6px; overflow-x: auto; }
|
||||
.content code { background: #f0f0f0; padding: 2px 6px; border-radius: 3px; }
|
||||
section { margin: 2rem 0; }
|
||||
</style>
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
# allow crawling everything by default
|
||||
User-agent: *
|
||||
Disallow:
|
||||
10
stacks/k8s-portal/modules/k8s-portal/files/svelte.config.js
Normal file
10
stacks/k8s-portal/modules/k8s-portal/files/svelte.config.js
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
import adapter from '@sveltejs/adapter-node';
|
||||
|
||||
/** @type {import('@sveltejs/kit').Config} */
|
||||
const config = {
|
||||
kit: {
|
||||
adapter: adapter()
|
||||
}
|
||||
};
|
||||
|
||||
export default config;
|
||||
20
stacks/k8s-portal/modules/k8s-portal/files/tsconfig.json
Normal file
20
stacks/k8s-portal/modules/k8s-portal/files/tsconfig.json
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
{
|
||||
"extends": "./.svelte-kit/tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"rewriteRelativeImportExtensions": true,
|
||||
"allowJs": true,
|
||||
"checkJs": true,
|
||||
"esModuleInterop": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"resolveJsonModule": true,
|
||||
"skipLibCheck": true,
|
||||
"sourceMap": true,
|
||||
"strict": true,
|
||||
"moduleResolution": "bundler"
|
||||
}
|
||||
// Path aliases are handled by https://svelte.dev/docs/kit/configuration#alias
|
||||
// except $lib which is handled by https://svelte.dev/docs/kit/configuration#files
|
||||
//
|
||||
// To make changes to top-level options such as include and exclude, we recommend extending
|
||||
// the generated config; see https://svelte.dev/docs/kit/configuration#typescript
|
||||
}
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
import { sveltekit } from '@sveltejs/kit/vite';
|
||||
import { defineConfig } from 'vite';
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [sveltekit()]
|
||||
});
|
||||
166
stacks/k8s-portal/modules/k8s-portal/main.tf
Normal file
166
stacks/k8s-portal/modules/k8s-portal/main.tf
Normal file
|
|
@ -0,0 +1,166 @@
|
|||
variable "tls_secret_name" {}
|
||||
variable "tier" { type = string }
|
||||
variable "k8s_ca_cert" {
|
||||
type = string
|
||||
default = ""
|
||||
}
|
||||
|
||||
resource "kubernetes_namespace" "k8s_portal" {
|
||||
metadata {
|
||||
name = "k8s-portal"
|
||||
labels = {
|
||||
tier = var.tier
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module "tls_secret" {
|
||||
source = "../../../../modules/kubernetes/setup_tls_secret"
|
||||
namespace = kubernetes_namespace.k8s_portal.metadata[0].name
|
||||
tls_secret_name = var.tls_secret_name
|
||||
}
|
||||
|
||||
resource "kubernetes_config_map" "k8s_portal_config" {
|
||||
metadata {
|
||||
name = "k8s-portal-config"
|
||||
namespace = kubernetes_namespace.k8s_portal.metadata[0].name
|
||||
}
|
||||
|
||||
data = {
|
||||
"ca.crt" = var.k8s_ca_cert
|
||||
}
|
||||
}
|
||||
|
||||
resource "kubernetes_deployment" "k8s_portal" {
|
||||
metadata {
|
||||
name = "k8s-portal"
|
||||
namespace = kubernetes_namespace.k8s_portal.metadata[0].name
|
||||
labels = {
|
||||
app = "k8s-portal"
|
||||
tier = var.tier
|
||||
}
|
||||
}
|
||||
|
||||
spec {
|
||||
replicas = 1
|
||||
strategy {
|
||||
type = "Recreate"
|
||||
}
|
||||
revision_history_limit = 3
|
||||
selector {
|
||||
match_labels = {
|
||||
app = "k8s-portal"
|
||||
}
|
||||
}
|
||||
|
||||
template {
|
||||
metadata {
|
||||
labels = {
|
||||
app = "k8s-portal"
|
||||
}
|
||||
}
|
||||
|
||||
spec {
|
||||
container {
|
||||
name = "portal"
|
||||
image = "viktorbarzin/k8s-portal:latest"
|
||||
port {
|
||||
container_port = 3000
|
||||
}
|
||||
|
||||
volume_mount {
|
||||
name = "config"
|
||||
mount_path = "/config/ca.crt"
|
||||
sub_path = "ca.crt"
|
||||
read_only = true
|
||||
}
|
||||
volume_mount {
|
||||
name = "user-roles"
|
||||
mount_path = "/config/users.json"
|
||||
sub_path = "users.json"
|
||||
read_only = true
|
||||
}
|
||||
resources {
|
||||
requests = {
|
||||
cpu = "10m"
|
||||
memory = "128Mi"
|
||||
}
|
||||
limits = {
|
||||
memory = "128Mi"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
volume {
|
||||
name = "config"
|
||||
config_map {
|
||||
name = kubernetes_config_map.k8s_portal_config.metadata[0].name
|
||||
}
|
||||
}
|
||||
volume {
|
||||
name = "user-roles"
|
||||
config_map {
|
||||
name = "k8s-user-roles"
|
||||
}
|
||||
}
|
||||
dns_config {
|
||||
option {
|
||||
name = "ndots"
|
||||
value = "2"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
lifecycle {
|
||||
ignore_changes = [
|
||||
spec[0].template[0].spec[0].dns_config,
|
||||
spec[0].template[0].spec[0].container[0].image, # CI updates image tag
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
resource "kubernetes_service" "k8s_portal" {
|
||||
metadata {
|
||||
name = "k8s-portal"
|
||||
namespace = kubernetes_namespace.k8s_portal.metadata[0].name
|
||||
}
|
||||
|
||||
spec {
|
||||
selector = {
|
||||
app = "k8s-portal"
|
||||
}
|
||||
port {
|
||||
port = 80
|
||||
target_port = 3000
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module "ingress" {
|
||||
source = "../../../../modules/kubernetes/ingress_factory"
|
||||
namespace = kubernetes_namespace.k8s_portal.metadata[0].name
|
||||
name = "k8s-portal"
|
||||
tls_secret_name = var.tls_secret_name
|
||||
protected = true # Require Authentik login
|
||||
extra_annotations = {
|
||||
"gethomepage.dev/enabled" = "true"
|
||||
"gethomepage.dev/name" = "K8s Portal"
|
||||
"gethomepage.dev/description" = "Kubernetes portal"
|
||||
"gethomepage.dev/icon" = "kubernetes.png"
|
||||
"gethomepage.dev/group" = "Core Platform"
|
||||
"gethomepage.dev/pod-selector" = ""
|
||||
}
|
||||
}
|
||||
|
||||
# Unprotected ingress for the setup script and agent endpoint (needs to be curl-able without auth)
|
||||
module "ingress_setup_script" {
|
||||
source = "../../../../modules/kubernetes/ingress_factory"
|
||||
namespace = kubernetes_namespace.k8s_portal.metadata[0].name
|
||||
name = "k8s-portal-setup"
|
||||
host = "k8s-portal"
|
||||
service_name = "k8s-portal"
|
||||
ingress_path = ["/setup/script", "/agent"]
|
||||
tls_secret_name = var.tls_secret_name
|
||||
protected = false
|
||||
}
|
||||
1
stacks/k8s-portal/secrets
Symbolic link
1
stacks/k8s-portal/secrets
Symbolic link
|
|
@ -0,0 +1 @@
|
|||
../../secrets
|
||||
8
stacks/k8s-portal/terragrunt.hcl
Normal file
8
stacks/k8s-portal/terragrunt.hcl
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
include "root" {
|
||||
path = find_in_parent_folders()
|
||||
}
|
||||
|
||||
dependency "infra" {
|
||||
config_path = "../infra"
|
||||
skip_outputs = true
|
||||
}
|
||||
10
stacks/k8s-portal/tiers.tf
Normal file
10
stacks/k8s-portal/tiers.tf
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
# Generated by Terragrunt. Sig: nIlQXj57tbuaRZEa
|
||||
locals {
|
||||
tiers = {
|
||||
core = "0-core"
|
||||
cluster = "1-cluster"
|
||||
gpu = "2-gpu"
|
||||
edge = "3-edge"
|
||||
aux = "4-aux"
|
||||
}
|
||||
}
|
||||
4
stacks/metallb/main.tf
Normal file
4
stacks/metallb/main.tf
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
module "metallb" {
|
||||
source = "./modules/metallb"
|
||||
tier = local.tiers.core
|
||||
}
|
||||
40
stacks/metallb/modules/metallb/main.tf
Normal file
40
stacks/metallb/modules/metallb/main.tf
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
# Creates namespace and everythin needed
|
||||
# Do not use until https://github.com/colinwilson/terraform-kubernetes-metallb/issues/5 is solved
|
||||
# module "metallb" {
|
||||
# source = "colinwilson/metallb/kubernetes"
|
||||
# version = "0.1.7"
|
||||
# }
|
||||
variable "tier" { type = string }
|
||||
|
||||
resource "kubernetes_namespace" "metallb" {
|
||||
metadata {
|
||||
name = "metallb-system"
|
||||
labels = {
|
||||
app = "metallb"
|
||||
# "istio-injection" : "disabled"
|
||||
# tier = var.tier
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module "metallb" {
|
||||
source = "ViktorBarzin/metallb/kubernetes"
|
||||
version = "0.1.5"
|
||||
depends_on = [kubernetes_namespace.metallb]
|
||||
}
|
||||
|
||||
resource "kubernetes_config_map" "config" {
|
||||
metadata {
|
||||
name = "config"
|
||||
namespace = kubernetes_namespace.metallb.metadata[0].name
|
||||
}
|
||||
data = {
|
||||
config = <<EOT
|
||||
address-pools:
|
||||
- name: default
|
||||
protocol: layer2
|
||||
addresses:
|
||||
- 10.0.20.200-10.0.20.220
|
||||
EOT
|
||||
}
|
||||
}
|
||||
1
stacks/metallb/secrets
Symbolic link
1
stacks/metallb/secrets
Symbolic link
|
|
@ -0,0 +1 @@
|
|||
../../secrets
|
||||
8
stacks/metallb/terragrunt.hcl
Normal file
8
stacks/metallb/terragrunt.hcl
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
include "root" {
|
||||
path = find_in_parent_folders()
|
||||
}
|
||||
|
||||
dependency "infra" {
|
||||
config_path = "../infra"
|
||||
skip_outputs = true
|
||||
}
|
||||
10
stacks/metallb/tiers.tf
Normal file
10
stacks/metallb/tiers.tf
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
# Generated by Terragrunt. Sig: nIlQXj57tbuaRZEa
|
||||
locals {
|
||||
tiers = {
|
||||
core = "0-core"
|
||||
cluster = "1-cluster"
|
||||
gpu = "2-gpu"
|
||||
edge = "3-edge"
|
||||
aux = "4-aux"
|
||||
}
|
||||
}
|
||||
7
stacks/metrics-server/main.tf
Normal file
7
stacks/metrics-server/main.tf
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
variable "tls_secret_name" { type = string }
|
||||
|
||||
module "metrics-server" {
|
||||
source = "./modules/metrics-server"
|
||||
tier = local.tiers.cluster
|
||||
tls_secret_name = var.tls_secret_name
|
||||
}
|
||||
29
stacks/metrics-server/modules/metrics-server/main.tf
Normal file
29
stacks/metrics-server/modules/metrics-server/main.tf
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
variable "tls_secret_name" {}
|
||||
variable "tier" { type = string }
|
||||
|
||||
resource "kubernetes_namespace" "metrics-server" {
|
||||
metadata {
|
||||
name = "metrics-server"
|
||||
labels = {
|
||||
tier = var.tier
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module "tls_secret" {
|
||||
source = "../../../../modules/kubernetes/setup_tls_secret"
|
||||
namespace = kubernetes_namespace.metrics-server.metadata[0].name
|
||||
tls_secret_name = var.tls_secret_name
|
||||
}
|
||||
|
||||
resource "helm_release" "metrics-server" {
|
||||
namespace = kubernetes_namespace.metrics-server.metadata[0].name
|
||||
create_namespace = false
|
||||
name = "metrics-server"
|
||||
atomic = true
|
||||
|
||||
repository = "https://kubernetes-sigs.github.io/metrics-server/"
|
||||
chart = "metrics-server"
|
||||
|
||||
values = [templatefile("${path.module}/values.yaml", {})]
|
||||
}
|
||||
8
stacks/metrics-server/modules/metrics-server/values.yaml
Normal file
8
stacks/metrics-server/modules/metrics-server/values.yaml
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
args:
|
||||
- "--kubelet-insecure-tls"
|
||||
resources:
|
||||
requests:
|
||||
cpu: 50m
|
||||
memory: 200Mi
|
||||
limits:
|
||||
memory: 200Mi
|
||||
1
stacks/metrics-server/secrets
Symbolic link
1
stacks/metrics-server/secrets
Symbolic link
|
|
@ -0,0 +1 @@
|
|||
../../secrets
|
||||
8
stacks/metrics-server/terragrunt.hcl
Normal file
8
stacks/metrics-server/terragrunt.hcl
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
include "root" {
|
||||
path = find_in_parent_folders()
|
||||
}
|
||||
|
||||
dependency "infra" {
|
||||
config_path = "../infra"
|
||||
skip_outputs = true
|
||||
}
|
||||
10
stacks/metrics-server/tiers.tf
Normal file
10
stacks/metrics-server/tiers.tf
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
# Generated by Terragrunt. Sig: nIlQXj57tbuaRZEa
|
||||
locals {
|
||||
tiers = {
|
||||
core = "0-core"
|
||||
cluster = "1-cluster"
|
||||
gpu = "2-gpu"
|
||||
edge = "3-edge"
|
||||
aux = "4-aux"
|
||||
}
|
||||
}
|
||||
7
stacks/nfs-csi/main.tf
Normal file
7
stacks/nfs-csi/main.tf
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
variable "nfs_server" { type = string }
|
||||
|
||||
module "nfs-csi" {
|
||||
source = "./modules/nfs-csi"
|
||||
tier = local.tiers.cluster
|
||||
nfs_server = var.nfs_server
|
||||
}
|
||||
90
stacks/nfs-csi/modules/nfs-csi/main.tf
Normal file
90
stacks/nfs-csi/modules/nfs-csi/main.tf
Normal file
|
|
@ -0,0 +1,90 @@
|
|||
variable "tier" { type = string }
|
||||
variable "nfs_server" { type = string }
|
||||
|
||||
resource "kubernetes_namespace" "nfs_csi" {
|
||||
metadata {
|
||||
name = "nfs-csi"
|
||||
labels = {
|
||||
tier = var.tier
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
resource "helm_release" "nfs_csi_driver" {
|
||||
namespace = kubernetes_namespace.nfs_csi.metadata[0].name
|
||||
create_namespace = false
|
||||
name = "csi-driver-nfs"
|
||||
atomic = true
|
||||
timeout = 300
|
||||
|
||||
repository = "https://raw.githubusercontent.com/kubernetes-csi/csi-driver-nfs/master/charts"
|
||||
chart = "csi-driver-nfs"
|
||||
|
||||
values = [yamlencode({
|
||||
controller = {
|
||||
replicas = 2
|
||||
resources = {
|
||||
csiProvisioner = {
|
||||
requests = { cpu = "10m", memory = "128Mi" }
|
||||
limits = { memory = "128Mi" }
|
||||
}
|
||||
csiResizer = {
|
||||
requests = { cpu = "10m", memory = "128Mi" }
|
||||
limits = { memory = "128Mi" }
|
||||
}
|
||||
csiSnapshotter = {
|
||||
requests = { cpu = "10m", memory = "128Mi" }
|
||||
limits = { memory = "128Mi" }
|
||||
}
|
||||
nfs = {
|
||||
requests = { cpu = "10m", memory = "128Mi" }
|
||||
limits = { memory = "128Mi" }
|
||||
}
|
||||
livenessProbe = {
|
||||
requests = { cpu = "10m", memory = "64Mi" }
|
||||
limits = { memory = "64Mi" }
|
||||
}
|
||||
}
|
||||
}
|
||||
node = {
|
||||
resources = {
|
||||
nfs = {
|
||||
requests = { cpu = "10m", memory = "128Mi" }
|
||||
limits = { memory = "128Mi" }
|
||||
}
|
||||
livenessProbe = {
|
||||
requests = { cpu = "10m", memory = "64Mi" }
|
||||
limits = { memory = "64Mi" }
|
||||
}
|
||||
nodeDriverRegistrar = {
|
||||
requests = { cpu = "10m", memory = "64Mi" }
|
||||
limits = { memory = "64Mi" }
|
||||
}
|
||||
}
|
||||
}
|
||||
storageClass = {
|
||||
create = false
|
||||
}
|
||||
})]
|
||||
}
|
||||
|
||||
resource "kubernetes_storage_class" "nfs_truenas" {
|
||||
metadata {
|
||||
name = "nfs-truenas"
|
||||
}
|
||||
storage_provisioner = "nfs.csi.k8s.io"
|
||||
reclaim_policy = "Retain"
|
||||
volume_binding_mode = "Immediate"
|
||||
|
||||
mount_options = [
|
||||
"soft",
|
||||
"timeo=30",
|
||||
"retrans=3",
|
||||
"actimeo=5",
|
||||
]
|
||||
|
||||
parameters = {
|
||||
server = var.nfs_server
|
||||
share = "/mnt/main"
|
||||
}
|
||||
}
|
||||
1
stacks/nfs-csi/secrets
Symbolic link
1
stacks/nfs-csi/secrets
Symbolic link
|
|
@ -0,0 +1 @@
|
|||
../../secrets
|
||||
8
stacks/nfs-csi/terragrunt.hcl
Normal file
8
stacks/nfs-csi/terragrunt.hcl
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
include "root" {
|
||||
path = find_in_parent_folders()
|
||||
}
|
||||
|
||||
dependency "infra" {
|
||||
config_path = "../infra"
|
||||
skip_outputs = true
|
||||
}
|
||||
10
stacks/nfs-csi/tiers.tf
Normal file
10
stacks/nfs-csi/tiers.tf
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
# Generated by Terragrunt. Sig: nIlQXj57tbuaRZEa
|
||||
locals {
|
||||
tiers = {
|
||||
core = "0-core"
|
||||
cluster = "1-cluster"
|
||||
gpu = "2-gpu"
|
||||
edge = "3-edge"
|
||||
aux = "4-aux"
|
||||
}
|
||||
}
|
||||
|
|
@ -1,271 +1,20 @@
|
|||
# =============================================================================
|
||||
# Platform Stack — Core & Cluster Services
|
||||
# Platform Stack — Empty Shell (all modules extracted)
|
||||
# =============================================================================
|
||||
#
|
||||
# This stack groups core/cluster services that form the platform layer.
|
||||
# These services are always present (no DEFCON gating) and provide the
|
||||
# foundational infrastructure that application stacks depend on.
|
||||
# All modules have been extracted to independent stacks.
|
||||
# This stack remains as a dependency target for 72+ app stacks
|
||||
# that declare `dependency "platform" { skip_outputs = true }`.
|
||||
#
|
||||
# Services included:
|
||||
# metallb, infra-maintenance, redis, traefik, technitium, headscale,
|
||||
# rbac, k8s-portal, vaultwarden, reverse-proxy, metrics-server, vpa,
|
||||
# nfs-csi, iscsi-csi, cnpg, sealed-secrets, uptime-kuma, wireguard, xray
|
||||
#
|
||||
# Extracted to independent stacks:
|
||||
# dbaas, authentik, crowdsec, monitoring, nvidia, mailserver, cloudflared, kyverno
|
||||
# Outputs are kept as variable pass-throughs for any stacks
|
||||
# that may read them (though most use skip_outputs = true).
|
||||
# =============================================================================
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Tier Definitions
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
# =============================================================================
|
||||
# Variable Declarations
|
||||
# =============================================================================
|
||||
|
||||
# --- Core (non-secret, from config.tfvars) ---
|
||||
variable "tls_secret_name" {
|
||||
type = string
|
||||
}
|
||||
variable "nfs_server" { type = string }
|
||||
variable "tls_secret_name" { type = string }
|
||||
variable "redis_host" { type = string }
|
||||
variable "postgresql_host" { type = string }
|
||||
variable "mysql_host" { type = string }
|
||||
variable "ollama_host" { type = string }
|
||||
variable "mail_host" { type = string }
|
||||
variable "k8s_ca_cert" {
|
||||
type = string
|
||||
default = ""
|
||||
}
|
||||
variable "ssh_private_key" {
|
||||
type = string
|
||||
default = ""
|
||||
sensitive = true
|
||||
}
|
||||
|
||||
# --- Vault KV secrets ---
|
||||
data "vault_kv_secret_v2" "secrets" {
|
||||
mount = "secret"
|
||||
name = "platform"
|
||||
}
|
||||
|
||||
locals {
|
||||
homepage_credentials = jsondecode(data.vault_kv_secret_v2.secrets.data["homepage_credentials"])
|
||||
k8s_users = jsondecode(data.vault_kv_secret_v2.secrets.data["k8s_users"])
|
||||
xray_reality_clients = jsondecode(data.vault_kv_secret_v2.secrets.data["xray_reality_clients"])
|
||||
xray_reality_short_ids = jsondecode(data.vault_kv_secret_v2.secrets.data["xray_reality_short_ids"])
|
||||
}
|
||||
|
||||
# =============================================================================
|
||||
# Module Calls
|
||||
# =============================================================================
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# MetalLB — L2 load balancer
|
||||
# -----------------------------------------------------------------------------
|
||||
module "metallb" {
|
||||
source = "./modules/metallb"
|
||||
tier = local.tiers.core
|
||||
}
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Redis — Shared Redis instance
|
||||
# -----------------------------------------------------------------------------
|
||||
module "redis" {
|
||||
source = "./modules/redis"
|
||||
tls_secret_name = var.tls_secret_name
|
||||
nfs_server = var.nfs_server
|
||||
tier = local.tiers.cluster
|
||||
}
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Traefik — Ingress controller (Helm)
|
||||
# -----------------------------------------------------------------------------
|
||||
module "traefik" {
|
||||
source = "./modules/traefik"
|
||||
tier = local.tiers.core
|
||||
crowdsec_api_key = data.vault_kv_secret_v2.secrets.data["ingress_crowdsec_api_key"]
|
||||
redis_host = var.redis_host
|
||||
tls_secret_name = var.tls_secret_name
|
||||
auth_fallback_htpasswd = data.vault_kv_secret_v2.secrets.data["auth_fallback_htpasswd"]
|
||||
}
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Technitium — DNS server
|
||||
# -----------------------------------------------------------------------------
|
||||
module "technitium" {
|
||||
source = "./modules/technitium"
|
||||
tls_secret_name = var.tls_secret_name
|
||||
nfs_server = var.nfs_server
|
||||
mysql_host = var.mysql_host
|
||||
homepage_token = local.homepage_credentials["technitium"]["token"]
|
||||
technitium_db_password = data.vault_kv_secret_v2.secrets.data["technitium_db_password"]
|
||||
technitium_username = data.vault_kv_secret_v2.secrets.data["technitium_username"]
|
||||
technitium_password = data.vault_kv_secret_v2.secrets.data["technitium_password"]
|
||||
tier = local.tiers.core
|
||||
}
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Headscale — Tailscale control server
|
||||
# -----------------------------------------------------------------------------
|
||||
module "headscale" {
|
||||
source = "./modules/headscale"
|
||||
tls_secret_name = var.tls_secret_name
|
||||
nfs_server = var.nfs_server
|
||||
headscale_config = data.vault_kv_secret_v2.secrets.data["headscale_config"]
|
||||
headscale_acl = data.vault_kv_secret_v2.secrets.data["headscale_acl"]
|
||||
homepage_token = try(local.homepage_credentials["headscale"]["api_key"], "")
|
||||
tier = local.tiers.core
|
||||
}
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# RBAC — Kubernetes OIDC RBAC (depends on Authentik)
|
||||
# -----------------------------------------------------------------------------
|
||||
module "rbac" {
|
||||
source = "./modules/rbac"
|
||||
tier = local.tiers.cluster
|
||||
tls_secret_name = var.tls_secret_name
|
||||
k8s_users = local.k8s_users
|
||||
ssh_private_key = var.ssh_private_key
|
||||
}
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# K8s Portal — Self-service Kubernetes portal (depends on Authentik)
|
||||
# -----------------------------------------------------------------------------
|
||||
module "k8s-portal" {
|
||||
source = "./modules/k8s-portal"
|
||||
tier = local.tiers.edge
|
||||
tls_secret_name = var.tls_secret_name
|
||||
k8s_ca_cert = var.k8s_ca_cert
|
||||
}
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Vaultwarden — Password manager
|
||||
# -----------------------------------------------------------------------------
|
||||
module "vaultwarden" {
|
||||
source = "./modules/vaultwarden"
|
||||
tls_secret_name = var.tls_secret_name
|
||||
mail_host = var.mail_host
|
||||
smtp_password = data.vault_kv_secret_v2.secrets.data["vaultwarden_smtp_password"]
|
||||
tier = local.tiers.edge
|
||||
nfs_server = var.nfs_server
|
||||
}
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Reverse Proxy — Generic reverse proxy
|
||||
# -----------------------------------------------------------------------------
|
||||
module "reverse-proxy" {
|
||||
source = "./modules/reverse_proxy"
|
||||
tls_secret_name = var.tls_secret_name
|
||||
truenas_homepage_token = local.homepage_credentials["reverse_proxy"]["truenas_token"]
|
||||
pfsense_homepage_token = local.homepage_credentials["reverse_proxy"]["pfsense_token"]
|
||||
haos_homepage_token = try(local.homepage_credentials["home_assistant"]["token"], "")
|
||||
}
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Metrics Server — Kubernetes metrics
|
||||
# -----------------------------------------------------------------------------
|
||||
module "metrics-server" {
|
||||
source = "./modules/metrics-server"
|
||||
tier = local.tiers.cluster
|
||||
tls_secret_name = var.tls_secret_name
|
||||
}
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# VPA + Goldilocks — Vertical Pod Autoscaler & resource dashboard
|
||||
# -----------------------------------------------------------------------------
|
||||
module "vpa" {
|
||||
source = "./modules/vpa"
|
||||
tls_secret_name = var.tls_secret_name
|
||||
tier = local.tiers.cluster
|
||||
}
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# NFS CSI — CSI driver for NFS with soft mount options (no stale mount hangs)
|
||||
# -----------------------------------------------------------------------------
|
||||
module "nfs-csi" {
|
||||
source = "./modules/nfs-csi"
|
||||
tier = local.tiers.cluster
|
||||
nfs_server = var.nfs_server
|
||||
}
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# iSCSI CSI — democratic-csi for TrueNAS iSCSI (database storage)
|
||||
# -----------------------------------------------------------------------------
|
||||
module "iscsi-csi" {
|
||||
source = "./modules/iscsi-csi"
|
||||
tier = local.tiers.cluster
|
||||
truenas_host = var.nfs_server # Same TrueNAS host
|
||||
truenas_api_key = data.vault_kv_secret_v2.secrets.data["truenas_api_key"]
|
||||
truenas_ssh_private_key = data.vault_kv_secret_v2.secrets.data["truenas_ssh_private_key"]
|
||||
}
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# CNPG — CloudNativePG Operator + local-path-provisioner for database storage
|
||||
# -----------------------------------------------------------------------------
|
||||
module "cnpg" {
|
||||
source = "./modules/cnpg"
|
||||
tier = local.tiers.cluster
|
||||
}
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Sealed Secrets — encrypts secrets for safe git storage
|
||||
# -----------------------------------------------------------------------------
|
||||
module "sealed-secrets" {
|
||||
source = "./modules/sealed-secrets"
|
||||
tier = local.tiers.cluster
|
||||
}
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Uptime Kuma — Status monitoring
|
||||
# -----------------------------------------------------------------------------
|
||||
module "uptime-kuma" {
|
||||
source = "./modules/uptime-kuma"
|
||||
tls_secret_name = var.tls_secret_name
|
||||
nfs_server = var.nfs_server
|
||||
tier = local.tiers.cluster
|
||||
}
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# WireGuard — VPN server
|
||||
# -----------------------------------------------------------------------------
|
||||
module "wireguard" {
|
||||
source = "./modules/wireguard"
|
||||
tls_secret_name = var.tls_secret_name
|
||||
wg_0_conf = data.vault_kv_secret_v2.secrets.data["wireguard_wg_0_conf"]
|
||||
wg_0_key = data.vault_kv_secret_v2.secrets.data["wireguard_wg_0_key"]
|
||||
firewall_sh = data.vault_kv_secret_v2.secrets.data["wireguard_firewall_sh"]
|
||||
tier = local.tiers.core
|
||||
}
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Xray — Proxy/tunnel
|
||||
# -----------------------------------------------------------------------------
|
||||
module "xray" {
|
||||
source = "./modules/xray"
|
||||
tls_secret_name = var.tls_secret_name
|
||||
tier = local.tiers.core
|
||||
|
||||
xray_reality_clients = local.xray_reality_clients
|
||||
xray_reality_private_key = data.vault_kv_secret_v2.secrets.data["xray_reality_private_key"]
|
||||
xray_reality_short_ids = local.xray_reality_short_ids
|
||||
}
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Infra Maintenance — Automated maintenance jobs
|
||||
# -----------------------------------------------------------------------------
|
||||
module "infra-maintenance" {
|
||||
source = "./modules/infra-maintenance"
|
||||
nfs_server = var.nfs_server
|
||||
git_user = data.vault_kv_secret_v2.secrets.data["webhook_handler_git_user"]
|
||||
git_token = data.vault_kv_secret_v2.secrets.data["webhook_handler_git_token"]
|
||||
technitium_username = data.vault_kv_secret_v2.secrets.data["technitium_username"]
|
||||
technitium_password = data.vault_kv_secret_v2.secrets.data["technitium_password"]
|
||||
}
|
||||
|
||||
# =============================================================================
|
||||
# Outputs (consumed by service stacks via Terragrunt dependency)
|
||||
# =============================================================================
|
||||
|
||||
output "tls_secret_name" {
|
||||
value = var.tls_secret_name
|
||||
|
|
|
|||
23
stacks/rbac/main.tf
Normal file
23
stacks/rbac/main.tf
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
variable "tls_secret_name" { type = string }
|
||||
variable "ssh_private_key" {
|
||||
type = string
|
||||
default = ""
|
||||
sensitive = true
|
||||
}
|
||||
|
||||
data "vault_kv_secret_v2" "secrets" {
|
||||
mount = "secret"
|
||||
name = "platform"
|
||||
}
|
||||
|
||||
locals {
|
||||
k8s_users = jsondecode(data.vault_kv_secret_v2.secrets.data["k8s_users"])
|
||||
}
|
||||
|
||||
module "rbac" {
|
||||
source = "./modules/rbac"
|
||||
tier = local.tiers.cluster
|
||||
tls_secret_name = var.tls_secret_name
|
||||
k8s_users = local.k8s_users
|
||||
ssh_private_key = var.ssh_private_key
|
||||
}
|
||||
58
stacks/rbac/modules/rbac/apiserver-oidc.tf
Normal file
58
stacks/rbac/modules/rbac/apiserver-oidc.tf
Normal file
|
|
@ -0,0 +1,58 @@
|
|||
# Configure kube-apiserver for OIDC authentication
|
||||
# This SSHs to k8s-master and adds OIDC flags to the static pod manifest.
|
||||
# Kubelet auto-restarts the API server when the manifest changes.
|
||||
|
||||
variable "k8s_master_host" {
|
||||
type = string
|
||||
default = "10.0.20.100"
|
||||
}
|
||||
|
||||
variable "ssh_private_key" {
|
||||
type = string
|
||||
sensitive = true
|
||||
}
|
||||
|
||||
variable "oidc_issuer_url" {
|
||||
type = string
|
||||
default = "https://authentik.viktorbarzin.me/application/o/kubernetes/"
|
||||
}
|
||||
|
||||
variable "oidc_client_id" {
|
||||
type = string
|
||||
default = "kubernetes"
|
||||
}
|
||||
|
||||
resource "null_resource" "apiserver_oidc_config" {
|
||||
connection {
|
||||
type = "ssh"
|
||||
user = "wizard"
|
||||
host = var.k8s_master_host
|
||||
private_key = var.ssh_private_key
|
||||
}
|
||||
|
||||
provisioner "remote-exec" {
|
||||
inline = [
|
||||
# Check if OIDC flags already configured with the correct values
|
||||
"if grep -q 'oidc-issuer-url=${var.oidc_issuer_url}' /etc/kubernetes/manifests/kube-apiserver.yaml && grep -q 'oidc-client-id=${var.oidc_client_id}' /etc/kubernetes/manifests/kube-apiserver.yaml; then echo 'OIDC flags already configured with correct values'; exit 0; fi",
|
||||
|
||||
# Remove any existing OIDC flags (in case values changed)
|
||||
"sudo sed -i '/--oidc-issuer-url/d; /--oidc-client-id/d; /--oidc-username-claim/d; /--oidc-groups-claim/d' /etc/kubernetes/manifests/kube-apiserver.yaml",
|
||||
|
||||
# Backup the manifest
|
||||
"sudo cp /etc/kubernetes/manifests/kube-apiserver.yaml /etc/kubernetes/manifests/kube-apiserver.yaml.bak",
|
||||
|
||||
# Add OIDC flags after the last --tls-private-key-file flag (safe insertion point)
|
||||
"sudo sed -i '/- --tls-private-key-file/a\\ - --oidc-issuer-url=${var.oidc_issuer_url}\\n - --oidc-client-id=${var.oidc_client_id}\\n - --oidc-username-claim=email\\n - --oidc-groups-claim=groups' /etc/kubernetes/manifests/kube-apiserver.yaml",
|
||||
|
||||
# Wait for API server to restart (kubelet watches the manifest)
|
||||
"echo 'Waiting for API server to restart...'",
|
||||
"sleep 30",
|
||||
"sudo kubectl --kubeconfig=/etc/kubernetes/admin.conf get nodes || echo 'API server still restarting, check manually'",
|
||||
]
|
||||
}
|
||||
|
||||
triggers = {
|
||||
oidc_issuer_url = var.oidc_issuer_url
|
||||
oidc_client_id = var.oidc_client_id
|
||||
}
|
||||
}
|
||||
174
stacks/rbac/modules/rbac/audit-policy.tf
Normal file
174
stacks/rbac/modules/rbac/audit-policy.tf
Normal file
|
|
@ -0,0 +1,174 @@
|
|||
# Deploy audit policy to k8s-master and configure kube-apiserver to use it.
|
||||
# Audit logs are written to /var/log/kubernetes/audit.log on the master node.
|
||||
# Alloy (log collector DaemonSet) will pick them up and ship to Loki.
|
||||
|
||||
resource "null_resource" "audit_policy" {
|
||||
connection {
|
||||
type = "ssh"
|
||||
user = "wizard"
|
||||
host = var.k8s_master_host
|
||||
private_key = var.ssh_private_key
|
||||
}
|
||||
|
||||
# Upload audit policy file
|
||||
provisioner "file" {
|
||||
content = yamlencode({
|
||||
apiVersion = "audit.k8s.io/v1"
|
||||
kind = "Policy"
|
||||
rules = [
|
||||
{
|
||||
# Don't log requests to the API discovery endpoints (very noisy)
|
||||
level = "None"
|
||||
resources = [{
|
||||
group = ""
|
||||
resources = ["endpoints", "services", "services/status"]
|
||||
}]
|
||||
users = ["system:kube-proxy"]
|
||||
},
|
||||
{
|
||||
# Don't log watch requests (very noisy)
|
||||
level = "None"
|
||||
verbs = ["watch"]
|
||||
},
|
||||
{
|
||||
# Don't log health checks
|
||||
level = "None"
|
||||
nonResourceURLs = ["/healthz*", "/readyz*", "/livez*"]
|
||||
},
|
||||
{
|
||||
# Log secret access at Metadata level only (no request/response bodies)
|
||||
level = "Metadata"
|
||||
resources = [{
|
||||
group = ""
|
||||
resources = ["secrets"]
|
||||
}]
|
||||
},
|
||||
{
|
||||
# Log all other mutating requests at RequestResponse level
|
||||
level = "RequestResponse"
|
||||
verbs = ["create", "update", "patch", "delete"]
|
||||
},
|
||||
{
|
||||
# Log read requests at Metadata level
|
||||
level = "Metadata"
|
||||
verbs = ["get", "list"]
|
||||
},
|
||||
]
|
||||
})
|
||||
destination = "/tmp/audit-policy.yaml"
|
||||
}
|
||||
|
||||
provisioner "remote-exec" {
|
||||
inline = [
|
||||
# Move audit policy to proper location
|
||||
"sudo mkdir -p /etc/kubernetes/policies",
|
||||
"sudo mv /tmp/audit-policy.yaml /etc/kubernetes/policies/audit-policy.yaml",
|
||||
"sudo chown root:root /etc/kubernetes/policies/audit-policy.yaml",
|
||||
|
||||
# Create audit log directory
|
||||
"sudo mkdir -p /var/log/kubernetes",
|
||||
|
||||
# Idempotently add audit flags, volumes, and volumeMounts using Python
|
||||
# to avoid sed duplication bugs on re-runs
|
||||
<<-SCRIPT
|
||||
sudo python3 -c "
|
||||
import yaml
|
||||
|
||||
path = '/etc/kubernetes/manifests/kube-apiserver.yaml'
|
||||
with open(path) as f:
|
||||
doc = yaml.safe_load(f)
|
||||
|
||||
container = doc['spec']['containers'][0]
|
||||
cmd = container['command']
|
||||
|
||||
# Add audit flags if missing
|
||||
audit_flags = {
|
||||
'--audit-policy-file=/etc/kubernetes/policies/audit-policy.yaml': True,
|
||||
'--audit-log-path=/var/log/kubernetes/audit.log': True,
|
||||
'--audit-log-maxage=7': True,
|
||||
'--audit-log-maxbackup=3': True,
|
||||
'--audit-log-maxsize=100': True,
|
||||
}
|
||||
existing = set(cmd)
|
||||
for flag in audit_flags:
|
||||
if flag not in existing:
|
||||
cmd.append(flag)
|
||||
|
||||
# Add volumes if missing (deduplicate by name)
|
||||
vol_names = {v['name'] for v in doc['spec']['volumes']}
|
||||
for vol in [
|
||||
{'name': 'audit-policy', 'hostPath': {'path': '/etc/kubernetes/policies', 'type': 'DirectoryOrCreate'}},
|
||||
{'name': 'audit-log', 'hostPath': {'path': '/var/log/kubernetes', 'type': 'DirectoryOrCreate'}},
|
||||
]:
|
||||
if vol['name'] not in vol_names:
|
||||
doc['spec']['volumes'].append(vol)
|
||||
vol_names.add(vol['name'])
|
||||
|
||||
# Add volumeMounts if missing (deduplicate by mountPath)
|
||||
mount_paths = {vm['mountPath'] for vm in container['volumeMounts']}
|
||||
for vm in [
|
||||
{'mountPath': '/etc/kubernetes/policies', 'name': 'audit-policy', 'readOnly': True},
|
||||
{'mountPath': '/var/log/kubernetes', 'name': 'audit-log'},
|
||||
]:
|
||||
if vm['mountPath'] not in mount_paths:
|
||||
container['volumeMounts'].append(vm)
|
||||
mount_paths.add(vm['mountPath'])
|
||||
|
||||
with open(path, 'w') as f:
|
||||
yaml.dump(doc, f, default_flow_style=False, sort_keys=False)
|
||||
|
||||
print('Audit config applied (idempotent)')
|
||||
"
|
||||
SCRIPT
|
||||
,
|
||||
|
||||
# Wait for API server to restart
|
||||
"echo 'Waiting for API server to restart with audit logging...'",
|
||||
"sleep 30",
|
||||
"sudo kubectl --kubeconfig=/etc/kubernetes/admin.conf get nodes || echo 'API server still restarting'",
|
||||
]
|
||||
}
|
||||
|
||||
triggers = {
|
||||
policy_version = "v1" # Bump to force re-apply of manifest flags
|
||||
policy_hash = sha256(yamlencode({
|
||||
apiVersion = "audit.k8s.io/v1"
|
||||
kind = "Policy"
|
||||
rules = [
|
||||
{
|
||||
level = "None"
|
||||
resources = [{
|
||||
group = ""
|
||||
resources = ["endpoints", "services", "services/status"]
|
||||
}]
|
||||
users = ["system:kube-proxy"]
|
||||
},
|
||||
{
|
||||
level = "None"
|
||||
verbs = ["watch"]
|
||||
},
|
||||
{
|
||||
level = "None"
|
||||
nonResourceURLs = ["/healthz*", "/readyz*", "/livez*"]
|
||||
},
|
||||
{
|
||||
level = "Metadata"
|
||||
resources = [{
|
||||
group = ""
|
||||
resources = ["secrets"]
|
||||
}]
|
||||
},
|
||||
{
|
||||
level = "RequestResponse"
|
||||
verbs = ["create", "update", "patch", "delete"]
|
||||
},
|
||||
{
|
||||
level = "Metadata"
|
||||
verbs = ["get", "list"]
|
||||
},
|
||||
]
|
||||
}))
|
||||
}
|
||||
|
||||
depends_on = [null_resource.apiserver_oidc_config]
|
||||
}
|
||||
263
stacks/rbac/modules/rbac/main.tf
Normal file
263
stacks/rbac/modules/rbac/main.tf
Normal file
|
|
@ -0,0 +1,263 @@
|
|||
variable "tls_secret_name" {}
|
||||
variable "tier" { type = string }
|
||||
|
||||
variable "k8s_users" {
|
||||
type = map(object({
|
||||
role = string # "admin", "power-user", "namespace-owner"
|
||||
email = string # OIDC email claim
|
||||
namespaces = optional(list(string), []) # for namespace-owners
|
||||
domains = optional(list(string), []) # subdomains for user apps
|
||||
quota = optional(object({
|
||||
cpu_requests = optional(string, "2")
|
||||
memory_requests = optional(string, "4Gi")
|
||||
memory_limits = optional(string, "8Gi")
|
||||
pods = optional(string, "20")
|
||||
}), {})
|
||||
}))
|
||||
default = {}
|
||||
}
|
||||
|
||||
# --- Admin role ---
|
||||
# Binds to built-in cluster-admin ClusterRole
|
||||
|
||||
resource "kubernetes_cluster_role_binding" "admin_users" {
|
||||
for_each = nonsensitive({ for name, user in var.k8s_users : name => user if user.role == "admin" })
|
||||
|
||||
metadata {
|
||||
name = "oidc-admin-${each.key}"
|
||||
}
|
||||
|
||||
role_ref {
|
||||
api_group = "rbac.authorization.k8s.io"
|
||||
kind = "ClusterRole"
|
||||
name = "cluster-admin"
|
||||
}
|
||||
|
||||
subject {
|
||||
kind = "User"
|
||||
name = each.value.email
|
||||
api_group = "rbac.authorization.k8s.io"
|
||||
}
|
||||
}
|
||||
|
||||
# --- Power-user role ---
|
||||
# Can manage workloads cluster-wide but cannot modify RBAC, nodes, or persistent volumes
|
||||
|
||||
resource "kubernetes_cluster_role" "power_user" {
|
||||
metadata {
|
||||
name = "oidc-power-user"
|
||||
}
|
||||
|
||||
# Core resources
|
||||
rule {
|
||||
api_groups = [""]
|
||||
resources = ["pods", "pods/log", "pods/exec", "services", "endpoints", "configmaps", "secrets", "persistentvolumeclaims", "events", "namespaces"]
|
||||
verbs = ["get", "list", "watch"]
|
||||
}
|
||||
|
||||
rule {
|
||||
api_groups = [""]
|
||||
resources = ["pods", "services", "configmaps", "secrets", "persistentvolumeclaims"]
|
||||
verbs = ["create", "update", "patch", "delete"]
|
||||
}
|
||||
|
||||
# Apps
|
||||
rule {
|
||||
api_groups = ["apps"]
|
||||
resources = ["deployments", "statefulsets", "daemonsets", "replicasets"]
|
||||
verbs = ["get", "list", "watch", "create", "update", "patch", "delete"]
|
||||
}
|
||||
|
||||
# Batch
|
||||
rule {
|
||||
api_groups = ["batch"]
|
||||
resources = ["jobs", "cronjobs"]
|
||||
verbs = ["get", "list", "watch", "create", "update", "patch", "delete"]
|
||||
}
|
||||
|
||||
# Networking
|
||||
rule {
|
||||
api_groups = ["networking.k8s.io"]
|
||||
resources = ["ingresses", "networkpolicies"]
|
||||
verbs = ["get", "list", "watch", "create", "update", "patch", "delete"]
|
||||
}
|
||||
|
||||
# Autoscaling
|
||||
rule {
|
||||
api_groups = ["autoscaling"]
|
||||
resources = ["horizontalpodautoscalers"]
|
||||
verbs = ["get", "list", "watch", "create", "update", "patch", "delete"]
|
||||
}
|
||||
|
||||
# Read-only on cluster-level resources
|
||||
rule {
|
||||
api_groups = [""]
|
||||
resources = ["nodes"]
|
||||
verbs = ["get", "list", "watch"]
|
||||
}
|
||||
|
||||
rule {
|
||||
api_groups = ["storage.k8s.io"]
|
||||
resources = ["storageclasses"]
|
||||
verbs = ["get", "list", "watch"]
|
||||
}
|
||||
|
||||
rule {
|
||||
api_groups = ["rbac.authorization.k8s.io"]
|
||||
resources = ["clusterroles", "clusterrolebindings", "roles", "rolebindings"]
|
||||
verbs = ["get", "list", "watch"]
|
||||
}
|
||||
}
|
||||
|
||||
resource "kubernetes_cluster_role_binding" "power_users" {
|
||||
for_each = nonsensitive({ for name, user in var.k8s_users : name => user if user.role == "power-user" })
|
||||
|
||||
metadata {
|
||||
name = "oidc-power-user-${each.key}"
|
||||
}
|
||||
|
||||
role_ref {
|
||||
api_group = "rbac.authorization.k8s.io"
|
||||
kind = "ClusterRole"
|
||||
name = kubernetes_cluster_role.power_user.metadata[0].name
|
||||
}
|
||||
|
||||
subject {
|
||||
kind = "User"
|
||||
name = each.value.email
|
||||
api_group = "rbac.authorization.k8s.io"
|
||||
}
|
||||
}
|
||||
|
||||
# --- Namespace-owner role ---
|
||||
# Full admin within assigned namespaces + read-only cluster-wide
|
||||
|
||||
locals {
|
||||
# Flatten user->namespace pairs for iteration
|
||||
namespace_owner_pairs = flatten([
|
||||
for name, user in var.k8s_users : [
|
||||
for ns in user.namespaces : {
|
||||
user_key = name
|
||||
namespace = ns
|
||||
email = user.email
|
||||
quota = user.quota
|
||||
}
|
||||
] if user.role == "namespace-owner"
|
||||
])
|
||||
}
|
||||
|
||||
resource "kubernetes_role_binding" "namespace_owner" {
|
||||
for_each = nonsensitive({ for pair in local.namespace_owner_pairs : "${pair.user_key}-${pair.namespace}" => pair })
|
||||
|
||||
metadata {
|
||||
name = "namespace-owner-${each.value.user_key}"
|
||||
namespace = each.value.namespace
|
||||
}
|
||||
|
||||
role_ref {
|
||||
api_group = "rbac.authorization.k8s.io"
|
||||
kind = "ClusterRole"
|
||||
name = "admin" # Built-in ClusterRole with full namespace access
|
||||
}
|
||||
|
||||
subject {
|
||||
kind = "User"
|
||||
name = each.value.email
|
||||
api_group = "rbac.authorization.k8s.io"
|
||||
}
|
||||
}
|
||||
|
||||
# Read-only cluster-wide access for namespace owners
|
||||
resource "kubernetes_cluster_role" "namespace_owner_readonly" {
|
||||
metadata {
|
||||
name = "oidc-namespace-owner-readonly"
|
||||
}
|
||||
|
||||
rule {
|
||||
api_groups = [""]
|
||||
resources = ["namespaces", "nodes"]
|
||||
verbs = ["get", "list", "watch"]
|
||||
}
|
||||
|
||||
rule {
|
||||
api_groups = [""]
|
||||
resources = ["pods", "services", "configmaps", "events"]
|
||||
verbs = ["get", "list", "watch"]
|
||||
}
|
||||
|
||||
rule {
|
||||
api_groups = ["apps"]
|
||||
resources = ["deployments", "statefulsets", "daemonsets"]
|
||||
verbs = ["get", "list", "watch"]
|
||||
}
|
||||
}
|
||||
|
||||
resource "kubernetes_cluster_role_binding" "namespace_owner_readonly" {
|
||||
for_each = nonsensitive({ for name, user in var.k8s_users : name => user if user.role == "namespace-owner" })
|
||||
|
||||
metadata {
|
||||
name = "oidc-ns-owner-readonly-${each.key}"
|
||||
}
|
||||
|
||||
role_ref {
|
||||
api_group = "rbac.authorization.k8s.io"
|
||||
kind = "ClusterRole"
|
||||
name = kubernetes_cluster_role.namespace_owner_readonly.metadata[0].name
|
||||
}
|
||||
|
||||
subject {
|
||||
kind = "User"
|
||||
name = each.value.email
|
||||
api_group = "rbac.authorization.k8s.io"
|
||||
}
|
||||
}
|
||||
|
||||
# Resource quotas per user namespace
|
||||
resource "kubernetes_resource_quota" "user_namespace_quota" {
|
||||
for_each = nonsensitive({ for pair in local.namespace_owner_pairs : "${pair.user_key}-${pair.namespace}" => pair })
|
||||
|
||||
metadata {
|
||||
name = "user-quota"
|
||||
namespace = each.value.namespace
|
||||
}
|
||||
|
||||
spec {
|
||||
hard = {
|
||||
"requests.cpu" = each.value.quota.cpu_requests
|
||||
"requests.memory" = each.value.quota.memory_requests
|
||||
"limits.memory" = each.value.quota.memory_limits
|
||||
"pods" = each.value.quota.pods
|
||||
}
|
||||
}
|
||||
|
||||
depends_on = [kubernetes_role_binding.namespace_owner]
|
||||
}
|
||||
|
||||
# ConfigMap with user-role mapping for the self-service portal
|
||||
resource "kubernetes_config_map" "user_roles" {
|
||||
metadata {
|
||||
name = "k8s-user-roles"
|
||||
namespace = "k8s-portal"
|
||||
}
|
||||
|
||||
data = {
|
||||
"users.json" = jsonencode({
|
||||
for name, user in var.k8s_users : user.email => {
|
||||
role = user.role
|
||||
namespaces = user.namespaces
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
# TLS secret in each user namespace (so they can create HTTPS ingresses)
|
||||
module "user_namespace_tls" {
|
||||
for_each = nonsensitive(toset(flatten([
|
||||
for name, user in var.k8s_users : user.namespaces
|
||||
if user.role == "namespace-owner"
|
||||
])))
|
||||
|
||||
source = "../../../../modules/kubernetes/setup_tls_secret"
|
||||
namespace = each.value
|
||||
tls_secret_name = var.tls_secret_name
|
||||
}
|
||||
1
stacks/rbac/secrets
Symbolic link
1
stacks/rbac/secrets
Symbolic link
|
|
@ -0,0 +1 @@
|
|||
../../secrets
|
||||
8
stacks/rbac/terragrunt.hcl
Normal file
8
stacks/rbac/terragrunt.hcl
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
include "root" {
|
||||
path = find_in_parent_folders()
|
||||
}
|
||||
|
||||
dependency "infra" {
|
||||
config_path = "../infra"
|
||||
skip_outputs = true
|
||||
}
|
||||
10
stacks/rbac/tiers.tf
Normal file
10
stacks/rbac/tiers.tf
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
# Generated by Terragrunt. Sig: nIlQXj57tbuaRZEa
|
||||
locals {
|
||||
tiers = {
|
||||
core = "0-core"
|
||||
cluster = "1-cluster"
|
||||
gpu = "2-gpu"
|
||||
edge = "3-edge"
|
||||
aux = "4-aux"
|
||||
}
|
||||
}
|
||||
9
stacks/redis/main.tf
Normal file
9
stacks/redis/main.tf
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
variable "tls_secret_name" { type = string }
|
||||
variable "nfs_server" { type = string }
|
||||
|
||||
module "redis" {
|
||||
source = "./modules/redis"
|
||||
tls_secret_name = var.tls_secret_name
|
||||
nfs_server = var.nfs_server
|
||||
tier = local.tiers.cluster
|
||||
}
|
||||
310
stacks/redis/modules/redis/main.tf
Normal file
310
stacks/redis/modules/redis/main.tf
Normal file
|
|
@ -0,0 +1,310 @@
|
|||
variable "tls_secret_name" {}
|
||||
variable "tier" { type = string }
|
||||
variable "nfs_server" { type = string }
|
||||
|
||||
resource "kubernetes_namespace" "redis" {
|
||||
metadata {
|
||||
name = "redis"
|
||||
labels = {
|
||||
tier = var.tier
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module "tls_secret" {
|
||||
source = "../../../../modules/kubernetes/setup_tls_secret"
|
||||
namespace = kubernetes_namespace.redis.metadata[0].name
|
||||
tls_secret_name = var.tls_secret_name
|
||||
}
|
||||
|
||||
# Redis with Sentinel HA via Bitnami Helm chart
|
||||
# Architecture: 1 master + 1 replica + 2 sentinels (one per node)
|
||||
# Sentinel automatically promotes a replica if master fails
|
||||
# HAProxy sits in front and routes only to the current master (see below)
|
||||
resource "helm_release" "redis" {
|
||||
namespace = kubernetes_namespace.redis.metadata[0].name
|
||||
create_namespace = false
|
||||
name = "redis"
|
||||
atomic = true
|
||||
timeout = 600
|
||||
|
||||
repository = "oci://10.0.20.10:5000/bitnamicharts"
|
||||
chart = "redis"
|
||||
version = "25.3.2"
|
||||
|
||||
values = [yamlencode({
|
||||
architecture = "replication"
|
||||
|
||||
auth = {
|
||||
enabled = false
|
||||
}
|
||||
|
||||
sentinel = {
|
||||
enabled = true
|
||||
quorum = 2
|
||||
masterSet = "mymaster"
|
||||
automateCluster = true
|
||||
|
||||
resources = {
|
||||
requests = {
|
||||
cpu = "50m"
|
||||
memory = "64Mi"
|
||||
}
|
||||
limits = {
|
||||
memory = "64Mi"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
master = {
|
||||
persistence = {
|
||||
enabled = true
|
||||
storageClass = "iscsi-truenas"
|
||||
size = "2Gi"
|
||||
}
|
||||
|
||||
resources = {
|
||||
requests = {
|
||||
cpu = "100m"
|
||||
memory = "64Mi"
|
||||
}
|
||||
limits = {
|
||||
memory = "64Mi"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
replica = {
|
||||
replicaCount = 2
|
||||
|
||||
persistence = {
|
||||
enabled = true
|
||||
storageClass = "iscsi-truenas"
|
||||
size = "2Gi"
|
||||
}
|
||||
|
||||
resources = {
|
||||
requests = {
|
||||
cpu = "50m"
|
||||
memory = "64Mi"
|
||||
}
|
||||
limits = {
|
||||
memory = "64Mi"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# Metrics for Prometheus
|
||||
metrics = {
|
||||
enabled = false
|
||||
}
|
||||
|
||||
# Use the existing service name so clients don't need changes
|
||||
# Sentinel-enabled Bitnami chart creates a headless service
|
||||
# and a regular service pointing at the master
|
||||
nameOverride = "redis"
|
||||
})]
|
||||
}
|
||||
|
||||
# HAProxy-based master-only proxy for simple redis:// clients.
|
||||
# Health-checks each Redis node via INFO replication and only routes
|
||||
# to the current master. On Sentinel failover, HAProxy detects the
|
||||
# new master within seconds via its health check interval.
|
||||
# Previously this was a K8s Service that routed to all nodes, causing
|
||||
# READONLY errors when clients hit a replica.
|
||||
|
||||
resource "kubernetes_config_map" "haproxy" {
|
||||
metadata {
|
||||
name = "redis-haproxy"
|
||||
namespace = kubernetes_namespace.redis.metadata[0].name
|
||||
}
|
||||
data = {
|
||||
"haproxy.cfg" = <<-EOT
|
||||
global
|
||||
maxconn 256
|
||||
|
||||
defaults
|
||||
mode tcp
|
||||
timeout connect 5s
|
||||
timeout client 30s
|
||||
timeout server 30s
|
||||
timeout check 3s
|
||||
|
||||
frontend redis_front
|
||||
bind *:6379
|
||||
default_backend redis_master
|
||||
|
||||
frontend sentinel_front
|
||||
bind *:26379
|
||||
default_backend redis_sentinel
|
||||
|
||||
backend redis_master
|
||||
option tcp-check
|
||||
tcp-check connect
|
||||
tcp-check send "PING\r\n"
|
||||
tcp-check expect string +PONG
|
||||
tcp-check send "INFO replication\r\n"
|
||||
tcp-check expect string role:master
|
||||
tcp-check send "QUIT\r\n"
|
||||
tcp-check expect string +OK
|
||||
server redis-node-0 redis-node-0.redis-headless.redis.svc.cluster.local:6379 check inter 3s fall 3 rise 2
|
||||
server redis-node-1 redis-node-1.redis-headless.redis.svc.cluster.local:6379 check inter 3s fall 3 rise 2
|
||||
|
||||
backend redis_sentinel
|
||||
balance roundrobin
|
||||
server redis-node-0 redis-node-0.redis-headless.redis.svc.cluster.local:26379 check inter 5s
|
||||
server redis-node-1 redis-node-1.redis-headless.redis.svc.cluster.local:26379 check inter 5s
|
||||
EOT
|
||||
}
|
||||
}
|
||||
|
||||
resource "kubernetes_deployment" "haproxy" {
|
||||
metadata {
|
||||
name = "redis-haproxy"
|
||||
namespace = kubernetes_namespace.redis.metadata[0].name
|
||||
labels = {
|
||||
app = "redis-haproxy"
|
||||
}
|
||||
}
|
||||
spec {
|
||||
replicas = 2
|
||||
selector {
|
||||
match_labels = {
|
||||
app = "redis-haproxy"
|
||||
}
|
||||
}
|
||||
template {
|
||||
metadata {
|
||||
labels = {
|
||||
app = "redis-haproxy"
|
||||
}
|
||||
}
|
||||
spec {
|
||||
container {
|
||||
name = "haproxy"
|
||||
image = "docker.io/library/haproxy:3.1-alpine"
|
||||
port {
|
||||
container_port = 6379
|
||||
name = "redis"
|
||||
}
|
||||
port {
|
||||
container_port = 26379
|
||||
name = "sentinel"
|
||||
}
|
||||
volume_mount {
|
||||
name = "config"
|
||||
mount_path = "/usr/local/etc/haproxy"
|
||||
read_only = true
|
||||
}
|
||||
resources {
|
||||
requests = {
|
||||
cpu = "10m"
|
||||
memory = "16Mi"
|
||||
}
|
||||
limits = {
|
||||
memory = "16Mi"
|
||||
}
|
||||
}
|
||||
liveness_probe {
|
||||
tcp_socket {
|
||||
port = 6379
|
||||
}
|
||||
initial_delay_seconds = 5
|
||||
period_seconds = 10
|
||||
}
|
||||
}
|
||||
volume {
|
||||
name = "config"
|
||||
config_map {
|
||||
name = kubernetes_config_map.haproxy.metadata[0].name
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
depends_on = [helm_release.redis]
|
||||
}
|
||||
|
||||
resource "kubernetes_service" "redis" {
|
||||
metadata {
|
||||
name = "redis"
|
||||
namespace = kubernetes_namespace.redis.metadata[0].name
|
||||
}
|
||||
spec {
|
||||
selector = {
|
||||
app = "redis-haproxy"
|
||||
}
|
||||
port {
|
||||
name = "tcp-redis"
|
||||
port = 6379
|
||||
target_port = 6379
|
||||
}
|
||||
port {
|
||||
name = "tcp-sentinel"
|
||||
port = 26379
|
||||
target_port = 26379
|
||||
}
|
||||
}
|
||||
|
||||
depends_on = [kubernetes_deployment.haproxy]
|
||||
}
|
||||
|
||||
module "nfs_backup" {
|
||||
source = "../../../../modules/kubernetes/nfs_volume"
|
||||
name = "redis-backup"
|
||||
namespace = kubernetes_namespace.redis.metadata[0].name
|
||||
nfs_server = var.nfs_server
|
||||
nfs_path = "/mnt/main/redis-backup"
|
||||
}
|
||||
|
||||
# Hourly backup: copy RDB snapshot from master to NFS
|
||||
resource "kubernetes_cron_job_v1" "redis-backup" {
|
||||
metadata {
|
||||
name = "redis-backup"
|
||||
namespace = kubernetes_namespace.redis.metadata[0].name
|
||||
}
|
||||
spec {
|
||||
concurrency_policy = "Replace"
|
||||
failed_jobs_history_limit = 3
|
||||
schedule = "0 * * * *"
|
||||
starting_deadline_seconds = 10
|
||||
successful_jobs_history_limit = 3
|
||||
job_template {
|
||||
metadata {}
|
||||
spec {
|
||||
backoff_limit = 2
|
||||
ttl_seconds_after_finished = 60
|
||||
template {
|
||||
metadata {}
|
||||
spec {
|
||||
container {
|
||||
name = "redis-backup"
|
||||
image = "redis:7-alpine"
|
||||
command = ["/bin/sh", "-c", <<-EOT
|
||||
set -eux
|
||||
# Trigger a fresh RDB save on the master
|
||||
redis-cli -h redis.redis BGSAVE
|
||||
sleep 5
|
||||
# Copy the RDB via redis-cli --rdb
|
||||
redis-cli -h redis.redis --rdb /backup/dump.rdb
|
||||
echo "Backup complete: $(ls -lh /backup/dump.rdb)"
|
||||
EOT
|
||||
]
|
||||
volume_mount {
|
||||
name = "backup"
|
||||
mount_path = "/backup"
|
||||
}
|
||||
}
|
||||
volume {
|
||||
name = "backup"
|
||||
persistent_volume_claim {
|
||||
claim_name = module.nfs_backup.claim_name
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
1
stacks/redis/secrets
Symbolic link
1
stacks/redis/secrets
Symbolic link
|
|
@ -0,0 +1 @@
|
|||
../../secrets
|
||||
8
stacks/redis/terragrunt.hcl
Normal file
8
stacks/redis/terragrunt.hcl
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
include "root" {
|
||||
path = find_in_parent_folders()
|
||||
}
|
||||
|
||||
dependency "infra" {
|
||||
config_path = "../infra"
|
||||
skip_outputs = true
|
||||
}
|
||||
10
stacks/redis/tiers.tf
Normal file
10
stacks/redis/tiers.tf
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
# Generated by Terragrunt. Sig: nIlQXj57tbuaRZEa
|
||||
locals {
|
||||
tiers = {
|
||||
core = "0-core"
|
||||
cluster = "1-cluster"
|
||||
gpu = "2-gpu"
|
||||
edge = "3-edge"
|
||||
aux = "4-aux"
|
||||
}
|
||||
}
|
||||
18
stacks/reverse-proxy/main.tf
Normal file
18
stacks/reverse-proxy/main.tf
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
variable "tls_secret_name" { type = string }
|
||||
|
||||
data "vault_kv_secret_v2" "secrets" {
|
||||
mount = "secret"
|
||||
name = "platform"
|
||||
}
|
||||
|
||||
locals {
|
||||
homepage_credentials = jsondecode(data.vault_kv_secret_v2.secrets.data["homepage_credentials"])
|
||||
}
|
||||
|
||||
module "reverse-proxy" {
|
||||
source = "./modules/reverse_proxy"
|
||||
tls_secret_name = var.tls_secret_name
|
||||
truenas_homepage_token = local.homepage_credentials["reverse_proxy"]["truenas_token"]
|
||||
pfsense_homepage_token = local.homepage_credentials["reverse_proxy"]["pfsense_token"]
|
||||
haos_homepage_token = try(local.homepage_credentials["home_assistant"]["token"], "")
|
||||
}
|
||||
163
stacks/reverse-proxy/modules/reverse_proxy/factory/main.tf
Normal file
163
stacks/reverse-proxy/modules/reverse_proxy/factory/main.tf
Normal file
|
|
@ -0,0 +1,163 @@
|
|||
variable "name" {}
|
||||
variable "namespace" {
|
||||
default = "reverse-proxy"
|
||||
}
|
||||
variable "external_name" {}
|
||||
variable "port" {
|
||||
default = "80"
|
||||
}
|
||||
variable "tls_secret_name" {}
|
||||
variable "backend_protocol" {
|
||||
default = "HTTP"
|
||||
}
|
||||
variable "protected" {
|
||||
type = bool
|
||||
default = true
|
||||
}
|
||||
variable "ingress_path" {
|
||||
type = list(string)
|
||||
default = ["/"]
|
||||
}
|
||||
variable "max_body_size" {
|
||||
type = string
|
||||
default = "50m"
|
||||
}
|
||||
variable "extra_annotations" {
|
||||
default = {}
|
||||
}
|
||||
variable "rybbit_site_id" {
|
||||
default = null
|
||||
type = string
|
||||
}
|
||||
variable "custom_content_security_policy" {
|
||||
default = null
|
||||
type = string
|
||||
}
|
||||
variable "strip_auth_headers" {
|
||||
type = bool
|
||||
default = false
|
||||
}
|
||||
variable "extra_middlewares" {
|
||||
type = list(string)
|
||||
default = []
|
||||
}
|
||||
|
||||
|
||||
resource "kubernetes_service" "proxied-service" {
|
||||
metadata {
|
||||
name = var.name
|
||||
namespace = var.namespace
|
||||
labels = {
|
||||
"app" = var.name
|
||||
}
|
||||
}
|
||||
|
||||
spec {
|
||||
type = "ExternalName"
|
||||
external_name = var.external_name
|
||||
|
||||
port {
|
||||
name = var.backend_protocol == "HTTPS" ? "https-${var.name}" : "${var.name}-web"
|
||||
port = var.port
|
||||
protocol = "TCP"
|
||||
target_port = var.port
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
resource "kubernetes_ingress_v1" "proxied-ingress" {
|
||||
metadata {
|
||||
name = var.name
|
||||
namespace = var.namespace
|
||||
annotations = merge({
|
||||
"traefik.ingress.kubernetes.io/router.middlewares" = join(",", compact(concat([
|
||||
"traefik-rate-limit@kubernetescrd",
|
||||
var.custom_content_security_policy == null ? "traefik-csp-headers@kubernetescrd" : null,
|
||||
"traefik-crowdsec@kubernetescrd",
|
||||
var.protected ? "traefik-authentik-forward-auth@kubernetescrd" : null,
|
||||
var.strip_auth_headers ? "traefik-strip-auth-headers@kubernetescrd" : null,
|
||||
var.rybbit_site_id != null ? "traefik-strip-accept-encoding@kubernetescrd" : null,
|
||||
var.rybbit_site_id != null ? "${var.namespace}-rybbit-analytics-${var.name}@kubernetescrd" : null,
|
||||
var.custom_content_security_policy != null ? "${var.namespace}-custom-csp-${var.name}@kubernetescrd" : null,
|
||||
], var.extra_middlewares)))
|
||||
"traefik.ingress.kubernetes.io/router.entrypoints" = "websecure"
|
||||
"traefik.ingress.kubernetes.io/service.serversscheme" = var.backend_protocol == "HTTPS" ? "https" : null
|
||||
"traefik.ingress.kubernetes.io/service.serverstransport" = var.backend_protocol == "HTTPS" ? "traefik-insecure-skip-verify@kubernetescrd" : null
|
||||
}, var.extra_annotations)
|
||||
}
|
||||
|
||||
spec {
|
||||
ingress_class_name = "traefik"
|
||||
tls {
|
||||
hosts = ["${var.name}.viktorbarzin.me"]
|
||||
secret_name = var.tls_secret_name
|
||||
}
|
||||
rule {
|
||||
host = "${var.name}.viktorbarzin.me"
|
||||
http {
|
||||
dynamic "path" {
|
||||
for_each = var.ingress_path
|
||||
|
||||
content {
|
||||
path = path.value
|
||||
backend {
|
||||
service {
|
||||
|
||||
name = var.name
|
||||
port {
|
||||
number = var.port
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# Rybbit analytics middleware (rewrite-body plugin with content-type filtering) - created per service when rybbit_site_id is set
|
||||
resource "kubernetes_manifest" "rybbit_analytics" {
|
||||
count = var.rybbit_site_id != null ? 1 : 0
|
||||
|
||||
manifest = {
|
||||
apiVersion = "traefik.io/v1alpha1"
|
||||
kind = "Middleware"
|
||||
metadata = {
|
||||
name = "rybbit-analytics-${var.name}"
|
||||
namespace = var.namespace
|
||||
}
|
||||
spec = {
|
||||
plugin = {
|
||||
rewrite-body = {
|
||||
rewrites = [{
|
||||
regex = "</head>"
|
||||
replacement = "<script src=\"https://rybbit.viktorbarzin.me/api/script.js\" data-site-id=\"${var.rybbit_site_id}\" defer></script></head>"
|
||||
}]
|
||||
monitoring = {
|
||||
types = ["text/html"]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# Custom CSP headers middleware - created per service when custom_content_security_policy is set
|
||||
resource "kubernetes_manifest" "custom_csp" {
|
||||
count = var.custom_content_security_policy != null ? 1 : 0
|
||||
|
||||
manifest = {
|
||||
apiVersion = "traefik.io/v1alpha1"
|
||||
kind = "Middleware"
|
||||
metadata = {
|
||||
name = "custom-csp-${var.name}"
|
||||
namespace = var.namespace
|
||||
}
|
||||
spec = {
|
||||
headers = {
|
||||
contentSecurityPolicy = var.custom_content_security_policy
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
356
stacks/reverse-proxy/modules/reverse_proxy/main.tf
Normal file
356
stacks/reverse-proxy/modules/reverse_proxy/main.tf
Normal file
|
|
@ -0,0 +1,356 @@
|
|||
# Reverse proxy for things in my infra that are
|
||||
# outside of K8S but would be nice to use the Nginx-ingress
|
||||
|
||||
variable "tls_secret_name" {}
|
||||
variable "truenas_homepage_token" {}
|
||||
variable "pfsense_homepage_token" {}
|
||||
variable "haos_homepage_token" {
|
||||
type = string
|
||||
default = ""
|
||||
sensitive = true
|
||||
}
|
||||
|
||||
resource "kubernetes_namespace" "reverse-proxy" {
|
||||
metadata {
|
||||
name = "reverse-proxy"
|
||||
}
|
||||
}
|
||||
|
||||
module "tls_secret" {
|
||||
source = "../../../../modules/kubernetes/setup_tls_secret"
|
||||
namespace = "reverse-proxy"
|
||||
tls_secret_name = var.tls_secret_name
|
||||
depends_on = [kubernetes_namespace.reverse-proxy]
|
||||
}
|
||||
|
||||
# https://pfsense.viktorbarzin.me/
|
||||
module "pfsense" {
|
||||
source = "./factory"
|
||||
name = "pfsense"
|
||||
external_name = "pfsense.viktorbarzin.lan"
|
||||
tls_secret_name = var.tls_secret_name
|
||||
port = 443
|
||||
backend_protocol = "HTTPS"
|
||||
|
||||
extra_annotations = {
|
||||
"gethomepage.dev/enabled" : "true"
|
||||
"gethomepage.dev/description" : "Cluster Firewall"
|
||||
"gethomepage.dev/group" : "Identity & Security"
|
||||
"gethomepage.dev/icon" : "pfsense.png"
|
||||
"gethomepage.dev/name" : "pFsense"
|
||||
"gethomepage.dev/widget.type" : "pfsense"
|
||||
"gethomepage.dev/widget.version" : "2"
|
||||
"gethomepage.dev/widget.url" : "https://10.0.20.1"
|
||||
"gethomepage.dev/widget.username" : "admin"
|
||||
"gethomepage.dev/widget.password" : var.pfsense_homepage_token
|
||||
"gethomepage.dev/widget.fields" = "[\"load\", \"memory\", \"temp\", \"disk\"]"
|
||||
"gethomepage.dev/widget.wan" = "vtnet0"
|
||||
}
|
||||
depends_on = [kubernetes_namespace.reverse-proxy]
|
||||
rybbit_site_id = "b029580e5a7c"
|
||||
}
|
||||
|
||||
# https://nas.viktorbarzin.me/
|
||||
module "nas" {
|
||||
source = "./factory"
|
||||
name = "nas"
|
||||
external_name = "nas.viktorbarzin.lan"
|
||||
port = 5001
|
||||
tls_secret_name = var.tls_secret_name
|
||||
backend_protocol = "HTTPS"
|
||||
max_body_size = "0m"
|
||||
depends_on = [kubernetes_namespace.reverse-proxy]
|
||||
rybbit_site_id = "1e11f8449f7d"
|
||||
extra_annotations = {
|
||||
"gethomepage.dev/enabled" = "true"
|
||||
"gethomepage.dev/name" = "Synology NAS"
|
||||
"gethomepage.dev/description" = "Network storage"
|
||||
"gethomepage.dev/icon" = "synology.png"
|
||||
"gethomepage.dev/group" = "Infrastructure"
|
||||
"gethomepage.dev/pod-selector" = ""
|
||||
}
|
||||
}
|
||||
|
||||
# https://files.viktorbarzin.me/
|
||||
module "nas-files" {
|
||||
source = "./factory"
|
||||
name = "files"
|
||||
external_name = "nas.viktorbarzin.lan"
|
||||
port = 5001
|
||||
tls_secret_name = var.tls_secret_name
|
||||
backend_protocol = "HTTPS"
|
||||
protected = false # allow anyone to download files
|
||||
ingress_path = ["/sharing", "/scripts", "/webman", "/wfmlogindialog.js", "/fsdownload"]
|
||||
max_body_size = "0m"
|
||||
depends_on = [kubernetes_namespace.reverse-proxy]
|
||||
extra_annotations = { "gethomepage.dev/enabled" = "false" }
|
||||
}
|
||||
|
||||
# https://idrac.viktorbarzin.me/
|
||||
module "idrac" {
|
||||
source = "./factory"
|
||||
name = "idrac"
|
||||
external_name = "idrac.viktorbarzin.lan"
|
||||
port = 443
|
||||
tls_secret_name = var.tls_secret_name
|
||||
backend_protocol = "HTTPS"
|
||||
strip_auth_headers = true
|
||||
extra_annotations = {
|
||||
"gethomepage.dev/enabled" = "true"
|
||||
"gethomepage.dev/name" = "iDRAC"
|
||||
"gethomepage.dev/description" = "Server management"
|
||||
"gethomepage.dev/icon" = "dell.png"
|
||||
"gethomepage.dev/group" = "Infrastructure"
|
||||
"gethomepage.dev/pod-selector" = ""
|
||||
}
|
||||
depends_on = [kubernetes_namespace.reverse-proxy]
|
||||
}
|
||||
|
||||
# Can either listen on https or http; can't do both :/
|
||||
# TODO: Not working yet
|
||||
module "tp-link-gateway" {
|
||||
source = "./factory"
|
||||
name = "gw"
|
||||
external_name = "gw.viktorbarzin.lan"
|
||||
port = 443
|
||||
tls_secret_name = var.tls_secret_name
|
||||
backend_protocol = "HTTPS"
|
||||
depends_on = [kubernetes_namespace.reverse-proxy]
|
||||
protected = true
|
||||
strip_auth_headers = true
|
||||
extra_annotations = { "gethomepage.dev/enabled" = "false" }
|
||||
}
|
||||
|
||||
# https://truenas.viktorbarzin.me/
|
||||
module "truenas" {
|
||||
source = "./factory"
|
||||
name = "truenas"
|
||||
external_name = "truenas.viktorbarzin.lan"
|
||||
port = 80
|
||||
tls_secret_name = var.tls_secret_name
|
||||
max_body_size = "0m"
|
||||
|
||||
extra_annotations = {
|
||||
"gethomepage.dev/enabled" : "true"
|
||||
"gethomepage.dev/description" : "TrueNAS"
|
||||
"gethomepage.dev/group" : "Infrastructure"
|
||||
"gethomepage.dev/icon" : "truenas.png"
|
||||
"gethomepage.dev/name" : "TrueNAS"
|
||||
"gethomepage.dev/widget.type" : "truenas"
|
||||
"gethomepage.dev/widget.url" : "https://truenas.viktorbarzin.lan"
|
||||
"gethomepage.dev/widget.key" : var.truenas_homepage_token
|
||||
# "gethomepage.dev/widget.enablePools" : "true"
|
||||
# "gethomepage.dev/pod-selector" : ""
|
||||
}
|
||||
depends_on = [kubernetes_namespace.reverse-proxy]
|
||||
rybbit_site_id = "b66fbd3cb58a"
|
||||
}
|
||||
|
||||
# https://r730.viktorbarzin.me/
|
||||
module "r730" {
|
||||
source = "./factory"
|
||||
name = "r730"
|
||||
external_name = "r730.viktorbarzin.lan"
|
||||
port = 443
|
||||
tls_secret_name = var.tls_secret_name
|
||||
backend_protocol = "HTTPS"
|
||||
depends_on = [kubernetes_namespace.reverse-proxy]
|
||||
extra_annotations = {
|
||||
"gethomepage.dev/enabled" = "true"
|
||||
"gethomepage.dev/name" = "R730"
|
||||
"gethomepage.dev/description" = "Dell PowerEdge server"
|
||||
"gethomepage.dev/icon" = "dell.png"
|
||||
"gethomepage.dev/group" = "Infrastructure"
|
||||
"gethomepage.dev/pod-selector" = ""
|
||||
}
|
||||
}
|
||||
|
||||
# https://proxmox.viktorbarzin.me/
|
||||
module "proxmox" {
|
||||
source = "./factory"
|
||||
name = "proxmox"
|
||||
external_name = "proxmox.viktorbarzin.lan"
|
||||
port = 8006
|
||||
tls_secret_name = var.tls_secret_name
|
||||
backend_protocol = "HTTPS"
|
||||
max_body_size = "0" # unlimited
|
||||
depends_on = [kubernetes_namespace.reverse-proxy]
|
||||
rybbit_site_id = "190a7ad3e1c7"
|
||||
extra_annotations = {
|
||||
"gethomepage.dev/enabled" = "true"
|
||||
"gethomepage.dev/name" = "Proxmox"
|
||||
"gethomepage.dev/description" = "Hypervisor"
|
||||
"gethomepage.dev/icon" = "proxmox.png"
|
||||
"gethomepage.dev/group" = "Infrastructure"
|
||||
"gethomepage.dev/pod-selector" = ""
|
||||
}
|
||||
}
|
||||
|
||||
# https://registry.viktorbarzin.me/
|
||||
module "docker-registry-ui" {
|
||||
source = "./factory"
|
||||
name = "registry"
|
||||
external_name = "docker-registry.viktorbarzin.lan"
|
||||
port = 8080
|
||||
tls_secret_name = var.tls_secret_name
|
||||
depends_on = [kubernetes_namespace.reverse-proxy]
|
||||
extra_annotations = {
|
||||
# Override middleware chain to remove rate-limit; the UI fires many API calls to list repos/tags
|
||||
"traefik.ingress.kubernetes.io/router.middlewares" = "traefik-csp-headers@kubernetescrd,traefik-crowdsec@kubernetescrd,traefik-authentik-forward-auth@kubernetescrd"
|
||||
"gethomepage.dev/enabled" = "true"
|
||||
"gethomepage.dev/name" = "Docker Registry"
|
||||
"gethomepage.dev/description" = "Container registry"
|
||||
"gethomepage.dev/icon" = "docker.png"
|
||||
"gethomepage.dev/group" = "Infrastructure"
|
||||
"gethomepage.dev/pod-selector" = ""
|
||||
}
|
||||
}
|
||||
|
||||
# https://valchedrym.viktorbarzin.me/
|
||||
module "valchedrym" {
|
||||
source = "./factory"
|
||||
name = "valchedrym"
|
||||
external_name = "valchedrym.viktorbarzin.lan"
|
||||
tls_secret_name = var.tls_secret_name
|
||||
port = 80
|
||||
backend_protocol = "HTTP"
|
||||
depends_on = [kubernetes_namespace.reverse-proxy]
|
||||
extra_annotations = { "gethomepage.dev/enabled" = "false" }
|
||||
}
|
||||
|
||||
# https://ip150.viktorbarzin.me/
|
||||
# Server has funky behaviour based on headers; works on some browrsers not others...
|
||||
# module "valchedrym-ip150" {
|
||||
# source = "./factory"
|
||||
# name = "ip150"
|
||||
# # external_name = "valchedrym.ddns.net"
|
||||
# external_name = "192.168.0.10"
|
||||
# port = 80
|
||||
# backend_protocol = "HTTP"
|
||||
# use_proxy_protocol = false
|
||||
# tls_secret_name = var.tls_secret_name
|
||||
# protected = false
|
||||
# depends_on = [kubernetes_namespace.reverse-proxy]
|
||||
# }
|
||||
|
||||
# https://mladost3.viktorbarzin.me/
|
||||
module "mladost3" {
|
||||
source = "./factory"
|
||||
name = "mladost3"
|
||||
external_name = "mladost3.ddns.net"
|
||||
port = 8080
|
||||
tls_secret_name = var.tls_secret_name
|
||||
depends_on = [kubernetes_namespace.reverse-proxy]
|
||||
extra_annotations = { "gethomepage.dev/enabled" = "false" }
|
||||
}
|
||||
|
||||
# # https://server-switch.viktorbarzin.me/
|
||||
# module "server-switch" {
|
||||
# source = "./factory"
|
||||
# name = "server-switch"
|
||||
# external_name = "server-switch.viktorbarzin.lan"
|
||||
# port = 80
|
||||
# tls_secret_name = var.tls_secret_name
|
||||
# depends_on = [kubernetes_namespace.reverse-proxy]
|
||||
# }
|
||||
|
||||
# https://ha-sofia.viktorbarzin.me/
|
||||
module "ha-sofia" {
|
||||
source = "./factory"
|
||||
name = "ha-sofia"
|
||||
external_name = "ha-sofia.viktorbarzin.lan"
|
||||
port = 8123
|
||||
tls_secret_name = var.tls_secret_name
|
||||
depends_on = [kubernetes_namespace.reverse-proxy]
|
||||
protected = false
|
||||
rybbit_site_id = "590fc392690a"
|
||||
extra_annotations = {
|
||||
"gethomepage.dev/enabled" = "true"
|
||||
"gethomepage.dev/name" = "Home Assistant Sofia"
|
||||
"gethomepage.dev/description" = "Smart home hub"
|
||||
"gethomepage.dev/icon" = "home-assistant.png"
|
||||
"gethomepage.dev/group" = "Smart Home"
|
||||
"gethomepage.dev/pod-selector" = ""
|
||||
}
|
||||
}
|
||||
|
||||
# https://ha-london.viktorbarzin.me/
|
||||
module "ha-london" {
|
||||
source = "./factory"
|
||||
name = "ha-london"
|
||||
external_name = "ha-london.viktorbarzin.lan"
|
||||
port = 8123
|
||||
tls_secret_name = var.tls_secret_name
|
||||
depends_on = [kubernetes_namespace.reverse-proxy]
|
||||
protected = false
|
||||
extra_annotations = {
|
||||
"gethomepage.dev/enabled" = "true"
|
||||
"gethomepage.dev/name" = "Home Assistant London"
|
||||
"gethomepage.dev/description" = "Smart home hub"
|
||||
"gethomepage.dev/icon" = "home-assistant.png"
|
||||
"gethomepage.dev/group" = "Smart Home"
|
||||
"gethomepage.dev/pod-selector" = ""
|
||||
}
|
||||
}
|
||||
|
||||
# https://london.viktorbarzin.me/
|
||||
module "london" {
|
||||
source = "./factory"
|
||||
name = "london"
|
||||
external_name = "openwrt-london.viktorbarzin.lan"
|
||||
port = 443
|
||||
tls_secret_name = var.tls_secret_name
|
||||
backend_protocol = "HTTPS"
|
||||
protected = true
|
||||
depends_on = [kubernetes_namespace.reverse-proxy]
|
||||
extra_annotations = {
|
||||
"gethomepage.dev/enabled" : "false"
|
||||
"gethomepage.dev/description" : "OpenWRT London"
|
||||
# gethomepage.dev/group: Media
|
||||
"gethomepage.dev/icon" : "openwrt.png"
|
||||
"gethomepage.dev/name" : "OpenWRT London"
|
||||
"gethomepage.dev/widget.type" : "openwrt"
|
||||
"gethomepage.dev/widget.url" : "https://100.64.0.14"
|
||||
# "gethomepage.dev/widget.token" = var.homepage_token
|
||||
"gethomepage.dev/widget.username" : "homepage"
|
||||
"gethomepage.dev/widget.password" : "" # add later as Flint2's openwrt is a little odd
|
||||
"gethomepage.dev/pod-selector" : ""
|
||||
}
|
||||
}
|
||||
module "pi-lights" {
|
||||
source = "./factory"
|
||||
name = "pi"
|
||||
external_name = "ha-london.viktorbarzin.lan"
|
||||
port = 5000
|
||||
tls_secret_name = var.tls_secret_name
|
||||
protected = true
|
||||
depends_on = [kubernetes_namespace.reverse-proxy]
|
||||
extra_annotations = { "gethomepage.dev/enabled" = "false" }
|
||||
}
|
||||
|
||||
# module "ups" { # .NET app doesn't work well behind host
|
||||
# source = "./factory"
|
||||
# name = "ups"
|
||||
# external_name = "ups.viktorbarzin.lan"
|
||||
# backend_protocol = "HTTPS"
|
||||
# port = 443
|
||||
# tls_secret_name = var.tls_secret_name
|
||||
# # protected = true
|
||||
# protected = false
|
||||
# depends_on = [kubernetes_namespace.reverse-proxy]
|
||||
# extra_annotations = {
|
||||
# "nginx.ingress.kubernetes.io/upstream-vhost" : "",
|
||||
# # "nginx.ingress.kubernetes.io/proxy-set-header" : "Host: <>",
|
||||
# }
|
||||
# }
|
||||
|
||||
module "mbp14" {
|
||||
source = "./factory"
|
||||
name = "mbp14"
|
||||
external_name = "mbp14.viktorbarzin.lan"
|
||||
port = 4020
|
||||
tls_secret_name = var.tls_secret_name
|
||||
protected = true
|
||||
depends_on = [kubernetes_namespace.reverse-proxy]
|
||||
extra_annotations = { "gethomepage.dev/enabled" = "false" }
|
||||
}
|
||||
1
stacks/reverse-proxy/secrets
Symbolic link
1
stacks/reverse-proxy/secrets
Symbolic link
|
|
@ -0,0 +1 @@
|
|||
../../secrets
|
||||
8
stacks/reverse-proxy/terragrunt.hcl
Normal file
8
stacks/reverse-proxy/terragrunt.hcl
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
include "root" {
|
||||
path = find_in_parent_folders()
|
||||
}
|
||||
|
||||
dependency "infra" {
|
||||
config_path = "../infra"
|
||||
skip_outputs = true
|
||||
}
|
||||
10
stacks/reverse-proxy/tiers.tf
Normal file
10
stacks/reverse-proxy/tiers.tf
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
# Generated by Terragrunt. Sig: nIlQXj57tbuaRZEa
|
||||
locals {
|
||||
tiers = {
|
||||
core = "0-core"
|
||||
cluster = "1-cluster"
|
||||
gpu = "2-gpu"
|
||||
edge = "3-edge"
|
||||
aux = "4-aux"
|
||||
}
|
||||
}
|
||||
4
stacks/sealed-secrets/main.tf
Normal file
4
stacks/sealed-secrets/main.tf
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
module "sealed-secrets" {
|
||||
source = "./modules/sealed-secrets"
|
||||
tier = local.tiers.cluster
|
||||
}
|
||||
45
stacks/sealed-secrets/modules/sealed-secrets/main.tf
Normal file
45
stacks/sealed-secrets/modules/sealed-secrets/main.tf
Normal file
|
|
@ -0,0 +1,45 @@
|
|||
variable "tier" { type = string }
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Namespace
|
||||
# -----------------------------------------------------------------------------
|
||||
resource "kubernetes_namespace" "sealed_secrets" {
|
||||
metadata {
|
||||
name = "sealed-secrets"
|
||||
labels = {
|
||||
tier = var.tier
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Sealed Secrets — encrypts secrets for safe git storage
|
||||
# https://github.com/bitnami-labs/sealed-secrets
|
||||
# -----------------------------------------------------------------------------
|
||||
resource "helm_release" "sealed_secrets" {
|
||||
namespace = kubernetes_namespace.sealed_secrets.metadata[0].name
|
||||
create_namespace = false
|
||||
name = "sealed-secrets"
|
||||
atomic = true
|
||||
timeout = 300
|
||||
|
||||
repository = "https://bitnami-labs.github.io/sealed-secrets"
|
||||
chart = "sealed-secrets"
|
||||
version = "2.18.3"
|
||||
|
||||
values = [yamlencode({
|
||||
crds = {
|
||||
create = true
|
||||
}
|
||||
|
||||
resources = {
|
||||
requests = {
|
||||
cpu = "50m"
|
||||
memory = "192Mi"
|
||||
}
|
||||
limits = {
|
||||
memory = "192Mi"
|
||||
}
|
||||
}
|
||||
})]
|
||||
}
|
||||
1
stacks/sealed-secrets/secrets
Symbolic link
1
stacks/sealed-secrets/secrets
Symbolic link
|
|
@ -0,0 +1 @@
|
|||
../../secrets
|
||||
8
stacks/sealed-secrets/terragrunt.hcl
Normal file
8
stacks/sealed-secrets/terragrunt.hcl
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
include "root" {
|
||||
path = find_in_parent_folders()
|
||||
}
|
||||
|
||||
dependency "infra" {
|
||||
config_path = "../infra"
|
||||
skip_outputs = true
|
||||
}
|
||||
10
stacks/sealed-secrets/tiers.tf
Normal file
10
stacks/sealed-secrets/tiers.tf
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
# Generated by Terragrunt. Sig: nIlQXj57tbuaRZEa
|
||||
locals {
|
||||
tiers = {
|
||||
core = "0-core"
|
||||
cluster = "1-cluster"
|
||||
gpu = "2-gpu"
|
||||
edge = "3-edge"
|
||||
aux = "4-aux"
|
||||
}
|
||||
}
|
||||
24
stacks/technitium/main.tf
Normal file
24
stacks/technitium/main.tf
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
variable "tls_secret_name" { type = string }
|
||||
variable "nfs_server" { type = string }
|
||||
variable "mysql_host" { type = string }
|
||||
|
||||
data "vault_kv_secret_v2" "secrets" {
|
||||
mount = "secret"
|
||||
name = "platform"
|
||||
}
|
||||
|
||||
locals {
|
||||
homepage_credentials = jsondecode(data.vault_kv_secret_v2.secrets.data["homepage_credentials"])
|
||||
}
|
||||
|
||||
module "technitium" {
|
||||
source = "./modules/technitium"
|
||||
tls_secret_name = var.tls_secret_name
|
||||
nfs_server = var.nfs_server
|
||||
mysql_host = var.mysql_host
|
||||
homepage_token = local.homepage_credentials["technitium"]["token"]
|
||||
technitium_db_password = data.vault_kv_secret_v2.secrets.data["technitium_db_password"]
|
||||
technitium_username = data.vault_kv_secret_v2.secrets.data["technitium_username"]
|
||||
technitium_password = data.vault_kv_secret_v2.secrets.data["technitium_password"]
|
||||
tier = local.tiers.core
|
||||
}
|
||||
|
|
@ -0,0 +1,488 @@
|
|||
{
|
||||
"annotations": {
|
||||
"list": [
|
||||
{
|
||||
"builtIn": 1,
|
||||
"datasource": { "type": "datasource", "uid": "grafana" },
|
||||
"enable": true,
|
||||
"hide": true,
|
||||
"iconColor": "rgba(0, 211, 255, 1)",
|
||||
"name": "Annotations & Alerts",
|
||||
"type": "dashboard"
|
||||
}
|
||||
]
|
||||
},
|
||||
"description": "Technitium DNS query logs from MySQL",
|
||||
"editable": true,
|
||||
"fiscalYearStartMonth": 0,
|
||||
"graphTooltip": 1,
|
||||
"id": null,
|
||||
"links": [],
|
||||
"panels": [
|
||||
{
|
||||
"title": "Total Queries",
|
||||
"type": "stat",
|
||||
"datasource": { "type": "mysql", "uid": "technitium-mysql" },
|
||||
"gridPos": { "h": 4, "w": 4, "x": 0, "y": 0 },
|
||||
"fieldConfig": {
|
||||
"defaults": {
|
||||
"color": { "mode": "thresholds" },
|
||||
"thresholds": {
|
||||
"steps": [
|
||||
{ "color": "green", "value": null }
|
||||
]
|
||||
}
|
||||
},
|
||||
"overrides": []
|
||||
},
|
||||
"options": {
|
||||
"colorMode": "value",
|
||||
"graphMode": "none",
|
||||
"justifyMode": "auto",
|
||||
"textMode": "auto",
|
||||
"reduceOptions": { "calcs": ["lastNotNull"], "fields": "", "values": false }
|
||||
},
|
||||
"targets": [
|
||||
{
|
||||
"rawSql": "SELECT COUNT(*) as total_queries FROM dns_logs WHERE $__timeFilter(timestamp)",
|
||||
"format": "table",
|
||||
"refId": "A"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"title": "Cached %",
|
||||
"type": "stat",
|
||||
"datasource": { "type": "mysql", "uid": "technitium-mysql" },
|
||||
"gridPos": { "h": 4, "w": 4, "x": 4, "y": 0 },
|
||||
"fieldConfig": {
|
||||
"defaults": {
|
||||
"color": { "mode": "thresholds" },
|
||||
"unit": "percentunit",
|
||||
"thresholds": {
|
||||
"steps": [
|
||||
{ "color": "red", "value": null },
|
||||
{ "color": "yellow", "value": 0.3 },
|
||||
{ "color": "green", "value": 0.5 }
|
||||
]
|
||||
}
|
||||
},
|
||||
"overrides": []
|
||||
},
|
||||
"options": {
|
||||
"colorMode": "value",
|
||||
"graphMode": "none",
|
||||
"justifyMode": "auto",
|
||||
"textMode": "auto",
|
||||
"reduceOptions": { "calcs": ["lastNotNull"], "fields": "", "values": false }
|
||||
},
|
||||
"targets": [
|
||||
{
|
||||
"rawSql": "SELECT SUM(CASE WHEN response_type = 3 THEN 1 ELSE 0 END) / COUNT(*) as cached_pct FROM dns_logs WHERE $__timeFilter(timestamp)",
|
||||
"format": "table",
|
||||
"refId": "A"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"title": "Blocked %",
|
||||
"type": "stat",
|
||||
"datasource": { "type": "mysql", "uid": "technitium-mysql" },
|
||||
"gridPos": { "h": 4, "w": 4, "x": 8, "y": 0 },
|
||||
"fieldConfig": {
|
||||
"defaults": {
|
||||
"color": { "mode": "thresholds" },
|
||||
"unit": "percentunit",
|
||||
"thresholds": {
|
||||
"steps": [
|
||||
{ "color": "green", "value": null },
|
||||
{ "color": "yellow", "value": 0.1 },
|
||||
{ "color": "red", "value": 0.3 }
|
||||
]
|
||||
}
|
||||
},
|
||||
"overrides": []
|
||||
},
|
||||
"options": {
|
||||
"colorMode": "value",
|
||||
"graphMode": "none",
|
||||
"justifyMode": "auto",
|
||||
"textMode": "auto",
|
||||
"reduceOptions": { "calcs": ["lastNotNull"], "fields": "", "values": false }
|
||||
},
|
||||
"targets": [
|
||||
{
|
||||
"rawSql": "SELECT SUM(CASE WHEN response_type = 4 THEN 1 ELSE 0 END) / COUNT(*) as blocked_pct FROM dns_logs WHERE $__timeFilter(timestamp)",
|
||||
"format": "table",
|
||||
"refId": "A"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"title": "NxDomain %",
|
||||
"type": "stat",
|
||||
"datasource": { "type": "mysql", "uid": "technitium-mysql" },
|
||||
"gridPos": { "h": 4, "w": 4, "x": 12, "y": 0 },
|
||||
"fieldConfig": {
|
||||
"defaults": {
|
||||
"color": { "mode": "thresholds" },
|
||||
"unit": "percentunit",
|
||||
"thresholds": {
|
||||
"steps": [
|
||||
{ "color": "green", "value": null },
|
||||
{ "color": "yellow", "value": 0.2 },
|
||||
{ "color": "red", "value": 0.5 }
|
||||
]
|
||||
}
|
||||
},
|
||||
"overrides": []
|
||||
},
|
||||
"options": {
|
||||
"colorMode": "value",
|
||||
"graphMode": "none",
|
||||
"justifyMode": "auto",
|
||||
"textMode": "auto",
|
||||
"reduceOptions": { "calcs": ["lastNotNull"], "fields": "", "values": false }
|
||||
},
|
||||
"targets": [
|
||||
{
|
||||
"rawSql": "SELECT SUM(CASE WHEN rcode = 3 THEN 1 ELSE 0 END) / COUNT(*) as nxdomain_pct FROM dns_logs WHERE $__timeFilter(timestamp)",
|
||||
"format": "table",
|
||||
"refId": "A"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"title": "Avg Response Time",
|
||||
"type": "stat",
|
||||
"datasource": { "type": "mysql", "uid": "technitium-mysql" },
|
||||
"gridPos": { "h": 4, "w": 4, "x": 16, "y": 0 },
|
||||
"fieldConfig": {
|
||||
"defaults": {
|
||||
"color": { "mode": "thresholds" },
|
||||
"unit": "ms",
|
||||
"thresholds": {
|
||||
"steps": [
|
||||
{ "color": "green", "value": null },
|
||||
{ "color": "yellow", "value": 50 },
|
||||
{ "color": "red", "value": 200 }
|
||||
]
|
||||
}
|
||||
},
|
||||
"overrides": []
|
||||
},
|
||||
"options": {
|
||||
"colorMode": "value",
|
||||
"graphMode": "none",
|
||||
"justifyMode": "auto",
|
||||
"textMode": "auto",
|
||||
"reduceOptions": { "calcs": ["lastNotNull"], "fields": "", "values": false }
|
||||
},
|
||||
"targets": [
|
||||
{
|
||||
"rawSql": "SELECT AVG(response_rtt) as avg_rtt_ms FROM dns_logs WHERE $__timeFilter(timestamp) AND response_rtt IS NOT NULL",
|
||||
"format": "table",
|
||||
"refId": "A"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"title": "Queries by Protocol",
|
||||
"type": "stat",
|
||||
"datasource": { "type": "mysql", "uid": "technitium-mysql" },
|
||||
"gridPos": { "h": 4, "w": 4, "x": 20, "y": 0 },
|
||||
"fieldConfig": {
|
||||
"defaults": {
|
||||
"color": { "mode": "palette-classic" }
|
||||
},
|
||||
"overrides": []
|
||||
},
|
||||
"options": {
|
||||
"colorMode": "background",
|
||||
"graphMode": "none",
|
||||
"justifyMode": "auto",
|
||||
"textMode": "auto",
|
||||
"reduceOptions": { "calcs": ["lastNotNull"], "fields": "", "values": true }
|
||||
},
|
||||
"targets": [
|
||||
{
|
||||
"rawSql": "SELECT SUM(CASE WHEN protocol = 0 THEN 1 ELSE 0 END) as UDP, SUM(CASE WHEN protocol = 1 THEN 1 ELSE 0 END) as TCP, SUM(CASE WHEN protocol = 3 THEN 1 ELSE 0 END) as DoH, SUM(CASE WHEN protocol = 4 THEN 1 ELSE 0 END) as DoT FROM dns_logs WHERE $__timeFilter(timestamp)",
|
||||
"format": "table",
|
||||
"refId": "A"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"title": "Queries Over Time",
|
||||
"type": "timeseries",
|
||||
"datasource": { "type": "mysql", "uid": "technitium-mysql" },
|
||||
"gridPos": { "h": 8, "w": 24, "x": 0, "y": 4 },
|
||||
"fieldConfig": {
|
||||
"defaults": {
|
||||
"color": { "mode": "palette-classic" },
|
||||
"custom": {
|
||||
"axisBorderShow": false,
|
||||
"axisCenteredZero": false,
|
||||
"axisColorMode": "text",
|
||||
"axisLabel": "",
|
||||
"axisPlacement": "auto",
|
||||
"barAlignment": 0,
|
||||
"drawStyle": "bars",
|
||||
"fillOpacity": 50,
|
||||
"gradientMode": "none",
|
||||
"hideFrom": { "legend": false, "tooltip": false, "viz": false },
|
||||
"lineWidth": 1,
|
||||
"pointSize": 5,
|
||||
"scaleDistribution": { "type": "linear" },
|
||||
"showPoints": "never",
|
||||
"spanNulls": false,
|
||||
"stacking": { "group": "A", "mode": "normal" }
|
||||
}
|
||||
},
|
||||
"overrides": []
|
||||
},
|
||||
"options": {
|
||||
"legend": { "calcs": ["sum"], "displayMode": "list", "placement": "bottom" },
|
||||
"tooltip": { "mode": "multi", "sort": "desc" }
|
||||
},
|
||||
"targets": [
|
||||
{
|
||||
"rawSql": "SELECT $__timeGroup(timestamp, $__interval) as time, SUM(CASE WHEN response_type = 1 THEN 1 ELSE 0 END) as Authoritative, SUM(CASE WHEN response_type = 2 THEN 1 ELSE 0 END) as Recursive, SUM(CASE WHEN response_type = 3 THEN 1 ELSE 0 END) as Cached, SUM(CASE WHEN response_type = 4 THEN 1 ELSE 0 END) as Blocked, SUM(CASE WHEN response_type = 5 THEN 1 ELSE 0 END) as Dropped FROM dns_logs WHERE $__timeFilter(timestamp) GROUP BY time ORDER BY time",
|
||||
"format": "time_series",
|
||||
"refId": "A"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"title": "Response Codes",
|
||||
"type": "piechart",
|
||||
"datasource": { "type": "mysql", "uid": "technitium-mysql" },
|
||||
"gridPos": { "h": 8, "w": 8, "x": 0, "y": 12 },
|
||||
"fieldConfig": {
|
||||
"defaults": {
|
||||
"color": { "mode": "palette-classic" }
|
||||
},
|
||||
"overrides": [
|
||||
{ "matcher": { "id": "byName", "options": "NOERROR" }, "properties": [{ "id": "color", "value": { "fixedColor": "green", "mode": "fixed" } }] },
|
||||
{ "matcher": { "id": "byName", "options": "NXDOMAIN" }, "properties": [{ "id": "color", "value": { "fixedColor": "yellow", "mode": "fixed" } }] },
|
||||
{ "matcher": { "id": "byName", "options": "SERVFAIL" }, "properties": [{ "id": "color", "value": { "fixedColor": "red", "mode": "fixed" } }] },
|
||||
{ "matcher": { "id": "byName", "options": "REFUSED" }, "properties": [{ "id": "color", "value": { "fixedColor": "orange", "mode": "fixed" } }] }
|
||||
]
|
||||
},
|
||||
"options": {
|
||||
"legend": { "displayMode": "table", "placement": "right", "values": ["value", "percent"] },
|
||||
"pieType": "donut",
|
||||
"reduceOptions": { "calcs": ["lastNotNull"], "fields": "", "values": true },
|
||||
"tooltip": { "mode": "single" }
|
||||
},
|
||||
"targets": [
|
||||
{
|
||||
"rawSql": "SELECT SUM(CASE WHEN rcode = 0 THEN 1 ELSE 0 END) as NOERROR, SUM(CASE WHEN rcode = 2 THEN 1 ELSE 0 END) as SERVFAIL, SUM(CASE WHEN rcode = 3 THEN 1 ELSE 0 END) as NXDOMAIN, SUM(CASE WHEN rcode = 5 THEN 1 ELSE 0 END) as REFUSED, SUM(CASE WHEN rcode NOT IN (0,2,3,5) THEN 1 ELSE 0 END) as Other FROM dns_logs WHERE $__timeFilter(timestamp)",
|
||||
"format": "table",
|
||||
"refId": "A"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"title": "Response Types",
|
||||
"type": "piechart",
|
||||
"datasource": { "type": "mysql", "uid": "technitium-mysql" },
|
||||
"gridPos": { "h": 8, "w": 8, "x": 8, "y": 12 },
|
||||
"fieldConfig": {
|
||||
"defaults": {
|
||||
"color": { "mode": "palette-classic" }
|
||||
},
|
||||
"overrides": [
|
||||
{ "matcher": { "id": "byName", "options": "Cached" }, "properties": [{ "id": "color", "value": { "fixedColor": "green", "mode": "fixed" } }] },
|
||||
{ "matcher": { "id": "byName", "options": "Blocked" }, "properties": [{ "id": "color", "value": { "fixedColor": "red", "mode": "fixed" } }] },
|
||||
{ "matcher": { "id": "byName", "options": "Recursive" }, "properties": [{ "id": "color", "value": { "fixedColor": "blue", "mode": "fixed" } }] },
|
||||
{ "matcher": { "id": "byName", "options": "Authoritative" }, "properties": [{ "id": "color", "value": { "fixedColor": "purple", "mode": "fixed" } }] }
|
||||
]
|
||||
},
|
||||
"options": {
|
||||
"legend": { "displayMode": "table", "placement": "right", "values": ["value", "percent"] },
|
||||
"pieType": "donut",
|
||||
"reduceOptions": { "calcs": ["lastNotNull"], "fields": "", "values": true },
|
||||
"tooltip": { "mode": "single" }
|
||||
},
|
||||
"targets": [
|
||||
{
|
||||
"rawSql": "SELECT SUM(CASE WHEN response_type = 1 THEN 1 ELSE 0 END) as Authoritative, SUM(CASE WHEN response_type = 2 THEN 1 ELSE 0 END) as Recursive, SUM(CASE WHEN response_type = 3 THEN 1 ELSE 0 END) as Cached, SUM(CASE WHEN response_type = 4 THEN 1 ELSE 0 END) as Blocked, SUM(CASE WHEN response_type = 5 THEN 1 ELSE 0 END) as Dropped FROM dns_logs WHERE $__timeFilter(timestamp)",
|
||||
"format": "table",
|
||||
"refId": "A"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"title": "Query Types",
|
||||
"type": "piechart",
|
||||
"datasource": { "type": "mysql", "uid": "technitium-mysql" },
|
||||
"gridPos": { "h": 8, "w": 8, "x": 16, "y": 12 },
|
||||
"fieldConfig": {
|
||||
"defaults": {
|
||||
"color": { "mode": "palette-classic" }
|
||||
},
|
||||
"overrides": []
|
||||
},
|
||||
"options": {
|
||||
"legend": { "displayMode": "table", "placement": "right", "values": ["value", "percent"] },
|
||||
"pieType": "donut",
|
||||
"reduceOptions": { "calcs": ["lastNotNull"], "fields": "", "values": true },
|
||||
"tooltip": { "mode": "single" }
|
||||
},
|
||||
"targets": [
|
||||
{
|
||||
"rawSql": "SELECT SUM(CASE WHEN qtype = 1 THEN 1 ELSE 0 END) as A, SUM(CASE WHEN qtype = 28 THEN 1 ELSE 0 END) as AAAA, SUM(CASE WHEN qtype = 5 THEN 1 ELSE 0 END) as CNAME, SUM(CASE WHEN qtype = 15 THEN 1 ELSE 0 END) as MX, SUM(CASE WHEN qtype = 16 THEN 1 ELSE 0 END) as TXT, SUM(CASE WHEN qtype = 33 THEN 1 ELSE 0 END) as SRV, SUM(CASE WHEN qtype = 12 THEN 1 ELSE 0 END) as PTR, SUM(CASE WHEN qtype = 6 THEN 1 ELSE 0 END) as SOA, SUM(CASE WHEN qtype = 2 THEN 1 ELSE 0 END) as NS, SUM(CASE WHEN qtype = 65 THEN 1 ELSE 0 END) as HTTPS, SUM(CASE WHEN qtype NOT IN (1,2,5,6,12,15,16,28,33,65) THEN 1 ELSE 0 END) as Other FROM dns_logs WHERE $__timeFilter(timestamp)",
|
||||
"format": "table",
|
||||
"refId": "A"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"title": "Top 20 Queried Domains",
|
||||
"type": "table",
|
||||
"datasource": { "type": "mysql", "uid": "technitium-mysql" },
|
||||
"gridPos": { "h": 10, "w": 12, "x": 0, "y": 20 },
|
||||
"fieldConfig": {
|
||||
"defaults": {
|
||||
"custom": { "filterable": true }
|
||||
},
|
||||
"overrides": [
|
||||
{ "matcher": { "id": "byName", "options": "count" }, "properties": [{ "id": "custom.width", "value": 100 }] }
|
||||
]
|
||||
},
|
||||
"options": {
|
||||
"showHeader": true,
|
||||
"sortBy": [{ "desc": true, "displayName": "count" }]
|
||||
},
|
||||
"targets": [
|
||||
{
|
||||
"rawSql": "SELECT qname as domain, COUNT(*) as count FROM dns_logs WHERE $__timeFilter(timestamp) GROUP BY qname ORDER BY count DESC LIMIT 20",
|
||||
"format": "table",
|
||||
"refId": "A"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"title": "Top 20 Clients",
|
||||
"type": "table",
|
||||
"datasource": { "type": "mysql", "uid": "technitium-mysql" },
|
||||
"gridPos": { "h": 10, "w": 12, "x": 12, "y": 20 },
|
||||
"fieldConfig": {
|
||||
"defaults": {
|
||||
"custom": { "filterable": true }
|
||||
},
|
||||
"overrides": [
|
||||
{ "matcher": { "id": "byName", "options": "count" }, "properties": [{ "id": "custom.width", "value": 100 }] }
|
||||
]
|
||||
},
|
||||
"options": {
|
||||
"showHeader": true,
|
||||
"sortBy": [{ "desc": true, "displayName": "count" }]
|
||||
},
|
||||
"targets": [
|
||||
{
|
||||
"rawSql": "SELECT client_ip, COUNT(*) as count FROM dns_logs WHERE $__timeFilter(timestamp) GROUP BY client_ip ORDER BY count DESC LIMIT 20",
|
||||
"format": "table",
|
||||
"refId": "A"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"title": "Average Response Time Over Time",
|
||||
"type": "timeseries",
|
||||
"datasource": { "type": "mysql", "uid": "technitium-mysql" },
|
||||
"gridPos": { "h": 8, "w": 24, "x": 0, "y": 30 },
|
||||
"fieldConfig": {
|
||||
"defaults": {
|
||||
"color": { "mode": "palette-classic" },
|
||||
"unit": "ms",
|
||||
"custom": {
|
||||
"axisBorderShow": false,
|
||||
"axisLabel": "Response Time (ms)",
|
||||
"axisPlacement": "auto",
|
||||
"drawStyle": "line",
|
||||
"fillOpacity": 20,
|
||||
"gradientMode": "none",
|
||||
"lineWidth": 2,
|
||||
"pointSize": 5,
|
||||
"showPoints": "never",
|
||||
"spanNulls": true
|
||||
}
|
||||
},
|
||||
"overrides": []
|
||||
},
|
||||
"options": {
|
||||
"legend": { "calcs": ["mean", "max"], "displayMode": "list", "placement": "bottom" },
|
||||
"tooltip": { "mode": "multi", "sort": "desc" }
|
||||
},
|
||||
"targets": [
|
||||
{
|
||||
"rawSql": "SELECT $__timeGroup(timestamp, $__interval) as time, AVG(response_rtt) as avg_rtt, MAX(response_rtt) as max_rtt FROM dns_logs WHERE $__timeFilter(timestamp) AND response_rtt IS NOT NULL GROUP BY time ORDER BY time",
|
||||
"format": "time_series",
|
||||
"refId": "A"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"title": "Top 20 NxDomain Domains",
|
||||
"type": "table",
|
||||
"datasource": { "type": "mysql", "uid": "technitium-mysql" },
|
||||
"gridPos": { "h": 10, "w": 12, "x": 0, "y": 38 },
|
||||
"fieldConfig": {
|
||||
"defaults": {
|
||||
"custom": { "filterable": true }
|
||||
},
|
||||
"overrides": [
|
||||
{ "matcher": { "id": "byName", "options": "count" }, "properties": [{ "id": "custom.width", "value": 100 }] }
|
||||
]
|
||||
},
|
||||
"options": {
|
||||
"showHeader": true,
|
||||
"sortBy": [{ "desc": true, "displayName": "count" }]
|
||||
},
|
||||
"targets": [
|
||||
{
|
||||
"rawSql": "SELECT qname as domain, COUNT(*) as count FROM dns_logs WHERE $__timeFilter(timestamp) AND rcode = 3 GROUP BY qname ORDER BY count DESC LIMIT 20",
|
||||
"format": "table",
|
||||
"refId": "A"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"title": "Top 20 Blocked Domains",
|
||||
"type": "table",
|
||||
"datasource": { "type": "mysql", "uid": "technitium-mysql" },
|
||||
"gridPos": { "h": 10, "w": 12, "x": 12, "y": 38 },
|
||||
"fieldConfig": {
|
||||
"defaults": {
|
||||
"custom": { "filterable": true }
|
||||
},
|
||||
"overrides": [
|
||||
{ "matcher": { "id": "byName", "options": "count" }, "properties": [{ "id": "custom.width", "value": 100 }] }
|
||||
]
|
||||
},
|
||||
"options": {
|
||||
"showHeader": true,
|
||||
"sortBy": [{ "desc": true, "displayName": "count" }]
|
||||
},
|
||||
"targets": [
|
||||
{
|
||||
"rawSql": "SELECT qname as domain, COUNT(*) as count FROM dns_logs WHERE $__timeFilter(timestamp) AND response_type = 4 GROUP BY qname ORDER BY count DESC LIMIT 20",
|
||||
"format": "table",
|
||||
"refId": "A"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"refresh": "5m",
|
||||
"schemaVersion": 39,
|
||||
"tags": ["dns", "technitium", "mysql"],
|
||||
"templating": { "list": [] },
|
||||
"time": { "from": "now-24h", "to": "now" },
|
||||
"timepicker": {},
|
||||
"timezone": "",
|
||||
"title": "Technitium DNS",
|
||||
"uid": "technitium-dns",
|
||||
"version": 1
|
||||
}
|
||||
278
stacks/technitium/modules/technitium/ha.tf
Normal file
278
stacks/technitium/modules/technitium/ha.tf
Normal file
|
|
@ -0,0 +1,278 @@
|
|||
# =============================================================================
|
||||
# Technitium DNS — High Availability (Primary-Secondary)
|
||||
# =============================================================================
|
||||
#
|
||||
# Secondary DNS instance replicates zones from primary via AXFR.
|
||||
# Both pods share the `dns-server=true` label so the DNS LoadBalancer
|
||||
# in main.tf routes queries to whichever pod is healthy.
|
||||
|
||||
module "nfs_secondary_config" {
|
||||
source = "../../../../modules/kubernetes/nfs_volume"
|
||||
name = "technitium-secondary-config"
|
||||
namespace = kubernetes_namespace.technitium.metadata[0].name
|
||||
nfs_server = var.nfs_server
|
||||
nfs_path = "/mnt/main/technitium-secondary"
|
||||
}
|
||||
|
||||
# Primary-only service for zone transfers (AXFR) and API access
|
||||
resource "kubernetes_service" "technitium_primary" {
|
||||
metadata {
|
||||
name = "technitium-primary"
|
||||
namespace = kubernetes_namespace.technitium.metadata[0].name
|
||||
labels = {
|
||||
"app" = "technitium"
|
||||
}
|
||||
}
|
||||
|
||||
spec {
|
||||
selector = {
|
||||
app = "technitium"
|
||||
}
|
||||
port {
|
||||
name = "dns-tcp"
|
||||
port = 53
|
||||
protocol = "TCP"
|
||||
}
|
||||
port {
|
||||
name = "dns-udp"
|
||||
port = 53
|
||||
protocol = "UDP"
|
||||
}
|
||||
port {
|
||||
name = "api"
|
||||
port = 5380
|
||||
protocol = "TCP"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# Secondary DNS deployment — zone-transfer replica
|
||||
resource "kubernetes_deployment" "technitium_secondary" {
|
||||
metadata {
|
||||
name = "technitium-secondary"
|
||||
namespace = kubernetes_namespace.technitium.metadata[0].name
|
||||
labels = {
|
||||
app = "technitium-secondary"
|
||||
tier = var.tier
|
||||
}
|
||||
}
|
||||
spec {
|
||||
replicas = 1
|
||||
strategy {
|
||||
type = "RollingUpdate"
|
||||
rolling_update {
|
||||
max_unavailable = "0"
|
||||
max_surge = "1"
|
||||
}
|
||||
}
|
||||
selector {
|
||||
match_labels = {
|
||||
app = "technitium-secondary"
|
||||
}
|
||||
}
|
||||
template {
|
||||
metadata {
|
||||
labels = {
|
||||
app = "technitium-secondary"
|
||||
"dns-server" = "true"
|
||||
}
|
||||
}
|
||||
spec {
|
||||
affinity {
|
||||
pod_anti_affinity {
|
||||
required_during_scheduling_ignored_during_execution {
|
||||
label_selector {
|
||||
match_expressions {
|
||||
key = "dns-server"
|
||||
operator = "In"
|
||||
values = ["true"]
|
||||
}
|
||||
}
|
||||
topology_key = "kubernetes.io/hostname"
|
||||
}
|
||||
}
|
||||
}
|
||||
container {
|
||||
image = "technitium/dns-server:latest"
|
||||
name = "technitium"
|
||||
env {
|
||||
name = "DNS_SERVER_ADMIN_PASSWORD"
|
||||
value = var.technitium_password
|
||||
}
|
||||
env {
|
||||
name = "DNS_SERVER_ENABLE_BLOCKING"
|
||||
value = "true"
|
||||
}
|
||||
resources {
|
||||
requests = {
|
||||
cpu = "25m"
|
||||
memory = "512Mi"
|
||||
}
|
||||
limits = {
|
||||
memory = "512Mi"
|
||||
}
|
||||
}
|
||||
port {
|
||||
container_port = 5380
|
||||
}
|
||||
port {
|
||||
container_port = 53
|
||||
}
|
||||
port {
|
||||
container_port = 80
|
||||
}
|
||||
liveness_probe {
|
||||
tcp_socket {
|
||||
port = 53
|
||||
}
|
||||
initial_delay_seconds = 10
|
||||
period_seconds = 10
|
||||
}
|
||||
readiness_probe {
|
||||
tcp_socket {
|
||||
port = 53
|
||||
}
|
||||
initial_delay_seconds = 5
|
||||
period_seconds = 5
|
||||
}
|
||||
volume_mount {
|
||||
mount_path = "/etc/dns"
|
||||
name = "nfs-config"
|
||||
}
|
||||
}
|
||||
volume {
|
||||
name = "nfs-config"
|
||||
persistent_volume_claim {
|
||||
claim_name = module.nfs_secondary_config.claim_name
|
||||
}
|
||||
}
|
||||
dns_config {
|
||||
option {
|
||||
name = "ndots"
|
||||
value = "2"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# Secondary web service — internal only, used by setup Job
|
||||
resource "kubernetes_service" "technitium_secondary_web" {
|
||||
metadata {
|
||||
name = "technitium-secondary-web"
|
||||
namespace = kubernetes_namespace.technitium.metadata[0].name
|
||||
labels = {
|
||||
"app" = "technitium-secondary"
|
||||
}
|
||||
}
|
||||
|
||||
spec {
|
||||
selector = {
|
||||
app = "technitium-secondary"
|
||||
}
|
||||
port {
|
||||
name = "api"
|
||||
port = 5380
|
||||
protocol = "TCP"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# PodDisruptionBudget — keep at least 1 DNS pod running during voluntary disruptions
|
||||
resource "kubernetes_pod_disruption_budget_v1" "technitium_dns" {
|
||||
metadata {
|
||||
name = "technitium-dns"
|
||||
namespace = kubernetes_namespace.technitium.metadata[0].name
|
||||
}
|
||||
spec {
|
||||
min_available = "1"
|
||||
selector {
|
||||
match_labels = {
|
||||
"dns-server" = "true"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# Setup Job — configures secondary zones via Technitium REST API
|
||||
resource "kubernetes_job" "technitium_secondary_setup" {
|
||||
metadata {
|
||||
name = "technitium-secondary-setup"
|
||||
namespace = kubernetes_namespace.technitium.metadata[0].name
|
||||
}
|
||||
spec {
|
||||
backoff_limit = 5
|
||||
template {
|
||||
metadata {}
|
||||
spec {
|
||||
restart_policy = "OnFailure"
|
||||
container {
|
||||
name = "setup"
|
||||
image = "curlimages/curl:latest"
|
||||
command = ["/bin/sh", "-c", <<-SCRIPT
|
||||
set -e
|
||||
PRIMARY="http://technitium-primary.technitium.svc.cluster.local:5380"
|
||||
SECONDARY="http://technitium-secondary-web.technitium.svc.cluster.local:5380"
|
||||
|
||||
# Wait for both to be ready
|
||||
until curl -sf "$PRIMARY/api/user/login?user=$TECH_USER&pass=$TECH_PASS" -o /tmp/p.json; do echo "Waiting for primary..."; sleep 5; done
|
||||
until curl -sf "$SECONDARY/api/user/login?user=$TECH_USER&pass=$TECH_PASS" -o /tmp/s.json; do echo "Waiting for secondary..."; sleep 5; done
|
||||
P_TOKEN=$(cat /tmp/p.json | sed -n 's/.*"token":"\([^"]*\)".*/\1/p')
|
||||
S_TOKEN=$(cat /tmp/s.json | sed -n 's/.*"token":"\([^"]*\)".*/\1/p')
|
||||
|
||||
# Get zones from primary (split JSON into lines so sed can match each zone)
|
||||
curl -sf "$PRIMARY/api/zones/list?token=$P_TOKEN" | tr ',' '\n' | sed -n 's/.*"name":"\([^"]*\)".*/\1/p' > /tmp/zones.txt
|
||||
echo "Found zones:"; cat /tmp/zones.txt
|
||||
|
||||
# Enable zone transfers on primary for each zone
|
||||
while read -r zone; do
|
||||
echo "Enabling zone transfer for: $zone"
|
||||
curl -sf "$PRIMARY/api/zones/options/set?token=$P_TOKEN&zone=$zone&zoneTransfer=Allow" || true
|
||||
done < /tmp/zones.txt
|
||||
|
||||
# Create secondary zones on secondary instance (ignore "already exists" errors)
|
||||
while read -r zone; do
|
||||
echo "Creating secondary zone: $zone"
|
||||
curl -sf "$SECONDARY/api/zones/create?token=$S_TOKEN&zone=$zone&type=Secondary&primaryNameServerAddresses=$PRIMARY_IP" || true
|
||||
done < /tmp/zones.txt
|
||||
|
||||
# Force resync all secondary zones to pull latest data
|
||||
while read -r zone; do
|
||||
echo "Resyncing: $zone"
|
||||
curl -sf "$SECONDARY/api/zones/resync?token=$S_TOKEN&zone=$zone" || true
|
||||
done < /tmp/zones.txt
|
||||
|
||||
echo "Secondary zone setup complete"
|
||||
SCRIPT
|
||||
]
|
||||
env {
|
||||
name = "TECH_USER"
|
||||
value = var.technitium_username
|
||||
}
|
||||
env {
|
||||
name = "TECH_PASS"
|
||||
value = var.technitium_password
|
||||
}
|
||||
env {
|
||||
name = "PRIMARY_IP"
|
||||
value = kubernetes_service.technitium_primary.spec[0].cluster_ip
|
||||
}
|
||||
}
|
||||
dns_config {
|
||||
option {
|
||||
name = "ndots"
|
||||
value = "2"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
depends_on = [
|
||||
kubernetes_deployment.technitium,
|
||||
kubernetes_deployment.technitium_secondary,
|
||||
kubernetes_service.technitium_primary,
|
||||
kubernetes_service.technitium_secondary_web,
|
||||
]
|
||||
}
|
||||
356
stacks/technitium/modules/technitium/main.tf
Normal file
356
stacks/technitium/modules/technitium/main.tf
Normal file
|
|
@ -0,0 +1,356 @@
|
|||
variable "tls_secret_name" {}
|
||||
variable "tier" { type = string }
|
||||
variable "homepage_token" {}
|
||||
variable "technitium_db_password" {}
|
||||
variable "nfs_server" { type = string }
|
||||
variable "mysql_host" { type = string }
|
||||
variable "technitium_username" { type = string }
|
||||
variable "technitium_password" {
|
||||
type = string
|
||||
sensitive = true
|
||||
}
|
||||
|
||||
resource "kubernetes_namespace" "technitium" {
|
||||
metadata {
|
||||
name = "technitium"
|
||||
labels = {
|
||||
tier = var.tier
|
||||
}
|
||||
# stale cache error when trying to resolve
|
||||
# labels = {
|
||||
# "istio-injection" : "enabled"
|
||||
# }
|
||||
}
|
||||
}
|
||||
|
||||
module "tls_secret" {
|
||||
source = "../../../../modules/kubernetes/setup_tls_secret"
|
||||
namespace = kubernetes_namespace.technitium.metadata[0].name
|
||||
tls_secret_name = var.tls_secret_name
|
||||
}
|
||||
|
||||
# CoreDNS Corefile - manages cluster DNS resolution
|
||||
# The viktorbarzin.lan block forwards to Technitium via LoadBalancer.
|
||||
# A template regex in the viktorbarzin.lan block short-circuits junk queries
|
||||
# caused by ndots:5 search domain expansion (e.g. www.cloudflare.com.viktorbarzin.lan,
|
||||
# redis.redis.svc.cluster.local.viktorbarzin.lan) by returning NXDOMAIN for any
|
||||
# query with 2+ labels before .viktorbarzin.lan. Legitimate single-label queries
|
||||
# (e.g. idrac.viktorbarzin.lan) fall through to Technitium.
|
||||
resource "kubernetes_config_map" "coredns" {
|
||||
metadata {
|
||||
name = "coredns"
|
||||
namespace = "kube-system"
|
||||
}
|
||||
|
||||
data = {
|
||||
Corefile = <<-EOF
|
||||
.:53 {
|
||||
#log
|
||||
errors
|
||||
health {
|
||||
lameduck 5s
|
||||
}
|
||||
ready
|
||||
kubernetes cluster.local in-addr.arpa ip6.arpa {
|
||||
pods insecure
|
||||
fallthrough in-addr.arpa ip6.arpa
|
||||
ttl 30
|
||||
}
|
||||
prometheus :9153
|
||||
forward . 8.8.8.8 1.1.1.1 10.0.20.1
|
||||
cache {
|
||||
success 10000 300 6
|
||||
denial 10000 300 60
|
||||
}
|
||||
loop
|
||||
reload
|
||||
loadbalance
|
||||
}
|
||||
viktorbarzin.lan:53 {
|
||||
#log
|
||||
errors
|
||||
template ANY ANY viktorbarzin.lan {
|
||||
match ".*\..*\.viktorbarzin\.lan\.$"
|
||||
rcode NXDOMAIN
|
||||
fallthrough
|
||||
}
|
||||
forward . 10.0.20.204 # Technitium LoadBalancer
|
||||
cache {
|
||||
success 10000 300 6
|
||||
denial 10000 300 60
|
||||
}
|
||||
}
|
||||
EOF
|
||||
}
|
||||
}
|
||||
|
||||
module "nfs_config" {
|
||||
source = "../../../../modules/kubernetes/nfs_volume"
|
||||
name = "technitium-config"
|
||||
namespace = kubernetes_namespace.technitium.metadata[0].name
|
||||
nfs_server = var.nfs_server
|
||||
nfs_path = "/mnt/main/technitium"
|
||||
}
|
||||
|
||||
resource "kubernetes_deployment" "technitium" {
|
||||
# resource "kubernetes_daemonset" "technitium" {
|
||||
metadata {
|
||||
name = "technitium"
|
||||
namespace = kubernetes_namespace.technitium.metadata[0].name
|
||||
labels = {
|
||||
app = "technitium"
|
||||
tier = var.tier
|
||||
}
|
||||
}
|
||||
spec {
|
||||
strategy {
|
||||
type = "RollingUpdate"
|
||||
rolling_update {
|
||||
max_unavailable = "0"
|
||||
max_surge = "1"
|
||||
}
|
||||
}
|
||||
# replicas = 1
|
||||
selector {
|
||||
match_labels = {
|
||||
app = "technitium"
|
||||
}
|
||||
}
|
||||
template {
|
||||
metadata {
|
||||
annotations = {
|
||||
"diun.enable" = "false"
|
||||
# "diun.include_tags" = "^\\d+(?:\\.\\d+)?(?:\\.\\d+)?$"
|
||||
"diun.include_tags" = "latest"
|
||||
}
|
||||
labels = {
|
||||
app = "technitium"
|
||||
"dns-server" = "true"
|
||||
}
|
||||
}
|
||||
spec {
|
||||
affinity {
|
||||
# Prefer nodes running Traefik for network locality
|
||||
pod_affinity {
|
||||
preferred_during_scheduling_ignored_during_execution {
|
||||
weight = 100
|
||||
pod_affinity_term {
|
||||
label_selector {
|
||||
match_expressions {
|
||||
key = "app.kubernetes.io/name"
|
||||
operator = "In"
|
||||
values = ["traefik"]
|
||||
}
|
||||
}
|
||||
topology_key = "kubernetes.io/hostname"
|
||||
}
|
||||
}
|
||||
}
|
||||
# Spread DNS pods across nodes for HA
|
||||
pod_anti_affinity {
|
||||
required_during_scheduling_ignored_during_execution {
|
||||
label_selector {
|
||||
match_expressions {
|
||||
key = "dns-server"
|
||||
operator = "In"
|
||||
values = ["true"]
|
||||
}
|
||||
}
|
||||
topology_key = "kubernetes.io/hostname"
|
||||
}
|
||||
}
|
||||
}
|
||||
container {
|
||||
image = "technitium/dns-server:latest"
|
||||
name = "technitium"
|
||||
resources {
|
||||
requests = {
|
||||
cpu = "25m"
|
||||
memory = "512Mi"
|
||||
}
|
||||
limits = {
|
||||
memory = "512Mi"
|
||||
}
|
||||
}
|
||||
port {
|
||||
container_port = 5380
|
||||
}
|
||||
port {
|
||||
container_port = 53
|
||||
}
|
||||
port {
|
||||
container_port = 80
|
||||
}
|
||||
liveness_probe {
|
||||
tcp_socket {
|
||||
port = 53
|
||||
}
|
||||
initial_delay_seconds = 10
|
||||
period_seconds = 10
|
||||
}
|
||||
readiness_probe {
|
||||
tcp_socket {
|
||||
port = 53
|
||||
}
|
||||
initial_delay_seconds = 5
|
||||
period_seconds = 5
|
||||
}
|
||||
volume_mount {
|
||||
mount_path = "/etc/dns"
|
||||
name = "nfs-config"
|
||||
}
|
||||
volume_mount {
|
||||
mount_path = "/etc/tls/"
|
||||
name = "tls-cert"
|
||||
}
|
||||
}
|
||||
volume {
|
||||
name = "nfs-config"
|
||||
persistent_volume_claim {
|
||||
claim_name = module.nfs_config.claim_name
|
||||
}
|
||||
}
|
||||
volume {
|
||||
name = "tls-cert"
|
||||
secret {
|
||||
secret_name = var.tls_secret_name
|
||||
}
|
||||
}
|
||||
dns_config {
|
||||
option {
|
||||
name = "ndots"
|
||||
value = "2"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
resource "kubernetes_service" "technitium-web" {
|
||||
metadata {
|
||||
name = "technitium-web"
|
||||
namespace = kubernetes_namespace.technitium.metadata[0].name
|
||||
labels = {
|
||||
"app" = "technitium"
|
||||
}
|
||||
# annotations = {
|
||||
# "metallb.universe.tf/allow-shared-ip" : "shared"
|
||||
# }
|
||||
}
|
||||
|
||||
spec {
|
||||
# type = "LoadBalancer"
|
||||
# external_traffic_policy = "Cluster"
|
||||
selector = {
|
||||
app = "technitium"
|
||||
}
|
||||
port {
|
||||
name = "technitium-dns"
|
||||
port = "5380"
|
||||
protocol = "TCP"
|
||||
}
|
||||
port {
|
||||
name = "technitium-doh"
|
||||
port = "80"
|
||||
protocol = "TCP"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
resource "kubernetes_service" "technitium-dns" {
|
||||
metadata {
|
||||
name = "technitium-dns"
|
||||
namespace = kubernetes_namespace.technitium.metadata[0].name
|
||||
labels = {
|
||||
"app" = "technitium"
|
||||
}
|
||||
}
|
||||
|
||||
spec {
|
||||
type = "LoadBalancer"
|
||||
port {
|
||||
name = "technitium-dns"
|
||||
port = 53
|
||||
protocol = "UDP"
|
||||
}
|
||||
external_traffic_policy = "Local"
|
||||
selector = {
|
||||
"dns-server" = "true"
|
||||
}
|
||||
}
|
||||
}
|
||||
module "ingress" {
|
||||
source = "../../../../modules/kubernetes/ingress_factory"
|
||||
namespace = kubernetes_namespace.technitium.metadata[0].name
|
||||
name = "technitium"
|
||||
tls_secret_name = var.tls_secret_name
|
||||
port = 5380
|
||||
service_name = "technitium-web"
|
||||
extra_annotations = {
|
||||
"gethomepage.dev/enabled" = "true"
|
||||
"gethomepage.dev/description" = "Internal DNS Server and Recursive Resolver"
|
||||
"gethomepage.dev/group" = "Infrastructure"
|
||||
"gethomepage.dev/icon" : "technitium.png"
|
||||
"gethomepage.dev/name" = "Technitium"
|
||||
"gethomepage.dev/widget.type" = "technitium"
|
||||
"gethomepage.dev/widget.url" = "http://technitium-web.technitium.svc.cluster.local:5380"
|
||||
"gethomepage.dev/widget.key" = var.homepage_token
|
||||
|
||||
"gethomepage.dev/widget.range" = "LastWeek"
|
||||
"gethomepage.dev/widget.fields" = "[\"totalQueries\", \"totalCached\", \"totalBlocked\", \"totalRecursive\"]"
|
||||
"gethomepage.dev/pod-selector" = ""
|
||||
}
|
||||
}
|
||||
|
||||
module "ingress-doh" {
|
||||
source = "../../../../modules/kubernetes/ingress_factory"
|
||||
namespace = kubernetes_namespace.technitium.metadata[0].name
|
||||
name = "technitium-doh"
|
||||
tls_secret_name = var.tls_secret_name
|
||||
host = "dns"
|
||||
service_name = "technitium-web"
|
||||
}
|
||||
|
||||
# Grafana datasource for Technitium DNS query logs in MySQL
|
||||
resource "kubernetes_config_map" "grafana_technitium_datasource" {
|
||||
metadata {
|
||||
name = "grafana-technitium-datasource"
|
||||
namespace = "monitoring"
|
||||
labels = {
|
||||
grafana_datasource = "1"
|
||||
}
|
||||
}
|
||||
data = {
|
||||
"technitium-datasource.yaml" = yamlencode({
|
||||
apiVersion = 1
|
||||
datasources = [{
|
||||
name = "Technitium MySQL"
|
||||
type = "mysql"
|
||||
access = "proxy"
|
||||
url = "${var.mysql_host}:3306"
|
||||
database = "technitium"
|
||||
user = "technitium"
|
||||
uid = "technitium-mysql"
|
||||
secureJsonData = {
|
||||
password = var.technitium_db_password
|
||||
}
|
||||
}]
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
# Grafana dashboard for Technitium DNS query logs
|
||||
resource "kubernetes_config_map" "grafana_technitium_dashboard" {
|
||||
metadata {
|
||||
name = "grafana-technitium-dashboard"
|
||||
namespace = "monitoring"
|
||||
labels = {
|
||||
grafana_dashboard = "1"
|
||||
}
|
||||
}
|
||||
data = {
|
||||
"technitium-dns.json" = file("${path.module}/dashboards/technitium-dns.json")
|
||||
}
|
||||
}
|
||||
|
||||
1
stacks/technitium/secrets
Symbolic link
1
stacks/technitium/secrets
Symbolic link
|
|
@ -0,0 +1 @@
|
|||
../../secrets
|
||||
8
stacks/technitium/terragrunt.hcl
Normal file
8
stacks/technitium/terragrunt.hcl
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
include "root" {
|
||||
path = find_in_parent_folders()
|
||||
}
|
||||
|
||||
dependency "infra" {
|
||||
config_path = "../infra"
|
||||
skip_outputs = true
|
||||
}
|
||||
10
stacks/technitium/tiers.tf
Normal file
10
stacks/technitium/tiers.tf
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
# Generated by Terragrunt. Sig: nIlQXj57tbuaRZEa
|
||||
locals {
|
||||
tiers = {
|
||||
core = "0-core"
|
||||
cluster = "1-cluster"
|
||||
gpu = "2-gpu"
|
||||
edge = "3-edge"
|
||||
aux = "4-aux"
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue