├── .shellcheckrc ├── .github ├── release.yaml ├── workflows │ ├── labeler.yaml │ ├── label-sync.yaml │ ├── e2e.yaml │ └── release.yaml ├── tests │ ├── nodes.yaml │ ├── private.yaml │ └── public.yaml ├── labels.yaml └── labeler.yaml ├── templates ├── config │ ├── kubernetes │ │ ├── apps │ │ │ ├── cert-manager │ │ │ │ └── cert-manager │ │ │ │ │ ├── issuers │ │ │ │ │ ├── .mjfilter.py │ │ │ │ │ ├── kustomization.yaml.j2 │ │ │ │ │ ├── secret.sops.yaml.j2 │ │ │ │ │ ├── secret-generator.yaml.j2 │ │ │ │ │ └── clusterissuers.sops.yaml.j2 │ │ │ │ │ ├── kustomization.yaml.j2 │ │ │ │ │ └── values.yaml.j2 │ │ │ ├── kube-system │ │ │ │ ├── cilium │ │ │ │ │ ├── kustomization.yaml.j2 │ │ │ │ │ ├── config │ │ │ │ │ │ ├── kustomization.yaml.j2 │ │ │ │ │ │ └── networks.yaml.j2 │ │ │ │ │ └── values.yaml.j2 │ │ │ │ ├── reloader │ │ │ │ │ └── values.yaml.j2 │ │ │ │ ├── metrics-server │ │ │ │ │ └── values.yaml.j2 │ │ │ │ ├── spegel │ │ │ │ │ └── values.yaml.j2 │ │ │ │ └── coredns │ │ │ │ │ └── values.yaml.j2 │ │ │ ├── network │ │ │ │ ├── envoy-gateway │ │ │ │ │ ├── kustomization.yaml.j2 │ │ │ │ │ └── config │ │ │ │ │ │ ├── kustomization.yaml.j2 │ │ │ │ │ │ ├── secret-generator.yaml.j2 │ │ │ │ │ │ ├── certificate.sops.yaml.j2 │ │ │ │ │ │ ├── podmonitor.yaml.j2 │ │ │ │ │ │ └── envoy.sops.yaml.j2 │ │ │ │ ├── cloudflare-dns │ │ │ │ │ ├── kustomization.yaml.j2 │ │ │ │ │ ├── config │ │ │ │ │ │ ├── kustomization.yaml.j2 │ │ │ │ │ │ ├── secret.sops.yaml.j2 │ │ │ │ │ │ └── secret-generator.yaml.j2 │ │ │ │ │ └── values.sops.yaml.j2 │ │ │ │ ├── cloudflare-tunnel │ │ │ │ │ ├── kustomization.yaml.j2 │ │ │ │ │ ├── config │ │ │ │ │ │ ├── secret.sops.yaml.j2 │ │ │ │ │ │ ├── kustomization.yaml.j2 │ │ │ │ │ │ ├── dnsendpoint.sops.yaml.j2 │ │ │ │ │ │ ├── secret-generator.yaml.j2 │ │ │ │ │ │ └── config.sops.yaml.j2 │ │ │ │ │ └── values.yaml.j2 │ │ │ │ └── k8s-gateway │ │ │ │ │ └── values.sops.yaml.j2 │ │ │ ├── argo-system │ │ │ │ └── argo-cd │ │ │ │ │ ├── kustomization.yaml.j2 │ │ │ │ │ ├── config │ │ │ │ │ ├── kustomization.yaml.j2 │ │ │ │ │ └── http-route.yaml.j2 │ │ │ │ │ ├── values.sops.yaml.j2 │ │ │ │ │ └── values.yaml.j2 │ │ │ └── default │ │ │ │ └── echo │ │ │ │ └── values.sops.yaml.j2 │ │ ├── argo │ │ │ ├── repositories │ │ │ │ ├── kustomization.yaml.j2 │ │ │ │ ├── secret-generator.yaml.j2 │ │ │ │ └── github.sops.yaml.j2 │ │ │ ├── settings │ │ │ │ ├── kustomization.yaml.j2 │ │ │ │ └── cluster-settings.yaml.j2 │ │ │ └── apps │ │ │ │ ├── network │ │ │ │ ├── envoy-gateway.yaml.j2 │ │ │ │ ├── k8s-gateway.yaml.j2 │ │ │ │ ├── cloudflare-tunnel.yaml.j2 │ │ │ │ └── cloudflare-dns.yaml.j2 │ │ │ │ ├── default │ │ │ │ └── echo.yaml.j2 │ │ │ │ ├── kube-system │ │ │ │ ├── coredns.yaml.j2 │ │ │ │ ├── reloader.yaml.j2 │ │ │ │ ├── spegel.yaml.j2 │ │ │ │ ├── metrics-server.yaml.j2 │ │ │ │ └── cilium.yaml.j2 │ │ │ │ ├── cert-manager │ │ │ │ └── cert-manager.yaml.j2 │ │ │ │ └── argo-system │ │ │ │ └── argo-cd.yaml.j2 │ │ └── components │ │ │ └── common │ │ │ ├── helm-secrets-private-keys.sops.yaml.j2 │ │ │ ├── settings.yaml.j2 │ │ │ ├── repositories.yaml.j2 │ │ │ └── apps.yaml.j2 │ ├── talos │ │ ├── patches │ │ │ ├── global │ │ │ │ ├── machine-kubelet.yaml.j2 │ │ │ │ ├── machine-time.yaml.j2 │ │ │ │ ├── machine-network.yaml.j2 │ │ │ │ ├── machine-files.yaml.j2 │ │ │ │ └── machine-sysctls.yaml.j2 │ │ │ ├── controller │ │ │ │ └── cluster.yaml.j2 │ │ │ └── README.md.j2 │ │ ├── talenv.yaml.j2 │ │ └── talconfig.yaml.j2 │ ├── .sops.yaml.j2 │ └── bootstrap │ │ └── helmfile.d │ │ ├── 00-crds.yaml.j2 │ │ └── 01-apps.yaml.j2 ├── overrides │ └── readme.partial.yaml.j2 └── scripts │ └── plugin.py ├── .vscode ├── extensions.json └── settings.json ├── .gitignore ├── .editorconfig ├── .gitattributes ├── makejinja.toml ├── .taskfiles ├── template │ ├── resources │ │ ├── nodes.schema.cue │ │ ├── cluster.schema.cue │ │ └── kubeconform.sh │ └── Taskfile.yaml ├── bootstrap │ └── Taskfile.yaml └── talos │ └── Taskfile.yaml ├── Taskfile.yaml ├── nodes.sample.yaml ├── LICENSE ├── .mise.toml ├── scripts ├── lib │ └── common.sh └── bootstrap-apps.sh ├── cluster.sample.yaml ├── .renovaterc.json5 └── README.md /.shellcheckrc: -------------------------------------------------------------------------------- 1 | disable=SC1091 2 | disable=SC2155 3 | -------------------------------------------------------------------------------- /.github/release.yaml: -------------------------------------------------------------------------------- 1 | changelog: 2 | exclude: 3 | authors: 4 | - github-actions 5 | - renovate 6 | -------------------------------------------------------------------------------- /templates/config/kubernetes/apps/cert-manager/cert-manager/issuers/.mjfilter.py: -------------------------------------------------------------------------------- 1 | main = lambda data: data.get("cloudflare", {}).get("enabled", False) == True 2 | -------------------------------------------------------------------------------- /templates/config/kubernetes/apps/kube-system/cilium/kustomization.yaml.j2: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: kustomize.config.k8s.io/v1beta1 3 | kind: Kustomization 4 | resources: 5 | - ./config 6 | -------------------------------------------------------------------------------- /templates/config/kubernetes/apps/network/envoy-gateway/kustomization.yaml.j2: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: kustomize.config.k8s.io/v1beta1 3 | kind: Kustomization 4 | resources: 5 | - ./config 6 | -------------------------------------------------------------------------------- /templates/config/kubernetes/apps/argo-system/argo-cd/kustomization.yaml.j2: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: kustomize.config.k8s.io/v1beta1 3 | kind: Kustomization 4 | resources: 5 | - ./config 6 | 7 | -------------------------------------------------------------------------------- /templates/config/kubernetes/apps/cert-manager/cert-manager/kustomization.yaml.j2: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: kustomize.config.k8s.io/v1beta1 3 | kind: Kustomization 4 | resources: 5 | - ./issuers 6 | -------------------------------------------------------------------------------- /templates/config/kubernetes/apps/network/cloudflare-dns/kustomization.yaml.j2: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: kustomize.config.k8s.io/v1beta1 3 | kind: Kustomization 4 | resources: 5 | - ./config 6 | -------------------------------------------------------------------------------- /templates/config/kubernetes/apps/network/cloudflare-tunnel/kustomization.yaml.j2: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: kustomize.config.k8s.io/v1beta1 3 | kind: Kustomization 4 | resources: 5 | - ./config 6 | -------------------------------------------------------------------------------- /templates/config/kubernetes/argo/repositories/kustomization.yaml.j2: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: kustomize.config.k8s.io/v1beta1 3 | kind: Kustomization 4 | generators: 5 | - ./secret-generator.yaml 6 | -------------------------------------------------------------------------------- /templates/config/kubernetes/argo/settings/kustomization.yaml.j2: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: kustomize.config.k8s.io/v1beta1 3 | kind: Kustomization 4 | resources: 5 | - ./cluster-settings.yaml 6 | -------------------------------------------------------------------------------- /templates/config/kubernetes/apps/argo-system/argo-cd/config/kustomization.yaml.j2: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: kustomize.config.k8s.io/v1beta1 3 | kind: Kustomization 4 | resources: 5 | - ./http-route.yaml 6 | -------------------------------------------------------------------------------- /templates/config/kubernetes/apps/kube-system/cilium/config/kustomization.yaml.j2: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: kustomize.config.k8s.io/v1beta1 3 | kind: Kustomization 4 | resources: 5 | - ./networks.yaml 6 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | "blueglassblock.better-json5", 4 | "irongeek.vscode-env", 5 | "redhat.vscode-yaml", 6 | "signageos.signageos-vscode-sops" 7 | ] 8 | } 9 | -------------------------------------------------------------------------------- /templates/config/kubernetes/apps/network/cloudflare-dns/config/kustomization.yaml.j2: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: kustomize.config.k8s.io/v1beta1 3 | kind: Kustomization 4 | generators: 5 | - ./secret-generator.yaml 6 | -------------------------------------------------------------------------------- /templates/config/kubernetes/apps/cert-manager/cert-manager/issuers/kustomization.yaml.j2: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: kustomize.config.k8s.io/v1beta1 3 | kind: Kustomization 4 | generators: 5 | - ./secret-generator.yaml 6 | -------------------------------------------------------------------------------- /templates/config/talos/patches/global/machine-kubelet.yaml.j2: -------------------------------------------------------------------------------- 1 | machine: 2 | kubelet: 3 | extraConfig: 4 | serializeImagePulls: false 5 | nodeIP: 6 | validSubnets: 7 | - #{ node_cidr }# 8 | -------------------------------------------------------------------------------- /templates/config/talos/patches/global/machine-time.yaml.j2: -------------------------------------------------------------------------------- 1 | machine: 2 | time: 3 | disabled: false 4 | servers: 5 | #% for item in node_ntp_servers %# 6 | - #{ item }# 7 | #% endfor %# 8 | -------------------------------------------------------------------------------- /templates/config/kubernetes/apps/network/cloudflare-dns/config/secret.sops.yaml.j2: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: v1 3 | kind: Secret 4 | metadata: 5 | name: cloudflare-dns-secret 6 | stringData: 7 | api-token: "#{ cloudflare_token }#" 8 | -------------------------------------------------------------------------------- /templates/config/kubernetes/apps/cert-manager/cert-manager/issuers/secret.sops.yaml.j2: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: v1 3 | kind: Secret 4 | metadata: 5 | name: cert-manager-secret 6 | stringData: 7 | api-token: "#{ cloudflare_token }#" 8 | -------------------------------------------------------------------------------- /templates/config/talos/patches/global/machine-network.yaml.j2: -------------------------------------------------------------------------------- 1 | machine: 2 | network: 3 | disableSearchDomain: true 4 | nameservers: 5 | #% for item in node_dns_servers %# 6 | - #{ item }# 7 | #% endfor %# 8 | -------------------------------------------------------------------------------- /templates/config/talos/talenv.yaml.j2: -------------------------------------------------------------------------------- 1 | # renovate: datasource=docker depName=ghcr.io/siderolabs/installer 2 | talosVersion: v1.11.5 3 | # renovate: datasource=docker depName=ghcr.io/siderolabs/kubelet 4 | kubernetesVersion: v1.34.2 5 | -------------------------------------------------------------------------------- /templates/config/kubernetes/apps/kube-system/reloader/values.yaml.j2: -------------------------------------------------------------------------------- 1 | --- 2 | fullnameOverride: reloader 3 | reloader: 4 | readOnlyRootFileSystem: true 5 | podMonitor: 6 | enabled: true 7 | namespace: "{{ .Release.Namespace }}" 8 | -------------------------------------------------------------------------------- /templates/config/kubernetes/components/common/helm-secrets-private-keys.sops.yaml.j2: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: v1 3 | kind: Secret 4 | metadata: 5 | name: helm-secrets-private-keys 6 | stringData: 7 | key.txt: | 8 | #{ age_key('private') }# 9 | -------------------------------------------------------------------------------- /templates/config/kubernetes/apps/network/cloudflare-tunnel/config/secret.sops.yaml.j2: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: v1 3 | kind: Secret 4 | metadata: 5 | name: cloudflare-tunnel-secret 6 | stringData: 7 | TUNNEL_TOKEN: "#{ cloudflare_tunnel_secret() }#" 8 | -------------------------------------------------------------------------------- /templates/config/kubernetes/apps/network/envoy-gateway/config/kustomization.yaml.j2: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: kustomize.config.k8s.io/v1beta1 3 | kind: Kustomization 4 | 5 | generators: 6 | - ./secret-generator.yaml 7 | 8 | resources: 9 | - ./podmonitor.yaml 10 | -------------------------------------------------------------------------------- /templates/config/kubernetes/apps/network/cloudflare-tunnel/config/kustomization.yaml.j2: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: kustomize.config.k8s.io/v1beta1 3 | kind: Kustomization 4 | generators: 5 | - ./secret-generator.yaml 6 | 7 | generatorOptions: 8 | disableNameSuffixHash: true 9 | -------------------------------------------------------------------------------- /templates/config/kubernetes/apps/argo-system/argo-cd/values.sops.yaml.j2: -------------------------------------------------------------------------------- 1 | fullnameOverride: "argocd" 2 | configs: 3 | secret: 4 | argocdServerAdminPassword: #{ argo_password }# 5 | argocdServerAdminPasswordMtime: #{ current_datetime() }# 6 | githubSecret: #{ github_push_token() }# 7 | -------------------------------------------------------------------------------- /templates/config/kubernetes/argo/repositories/secret-generator.yaml.j2: -------------------------------------------------------------------------------- 1 | apiVersion: viaduct.ai/v1 2 | kind: ksops 3 | metadata: 4 | name: repositories-secret-generator 5 | annotations: 6 | config.kubernetes.io/function: | 7 | exec: 8 | path: ksops 9 | files: 10 | - ./github.sops.yaml 11 | -------------------------------------------------------------------------------- /templates/config/kubernetes/apps/network/cloudflare-dns/config/secret-generator.yaml.j2: -------------------------------------------------------------------------------- 1 | apiVersion: viaduct.ai/v1 2 | kind: ksops 3 | metadata: 4 | name: cloudflare-dns-secret-generator 5 | annotations: 6 | config.kubernetes.io/function: | 7 | exec: 8 | path: ksops 9 | files: 10 | - ./secret.sops.yaml 11 | -------------------------------------------------------------------------------- /templates/config/kubernetes/apps/cert-manager/cert-manager/values.yaml.j2: -------------------------------------------------------------------------------- 1 | --- 2 | crds: 3 | enabled: true 4 | replicaCount: 1 5 | dns01RecursiveNameservers: https://1.1.1.1:443/dns-query,https://1.0.0.1:443/dns-query 6 | dns01RecursiveNameserversOnly: true 7 | prometheus: 8 | enabled: true 9 | servicemonitor: 10 | enabled: true 11 | -------------------------------------------------------------------------------- /templates/config/kubernetes/apps/network/envoy-gateway/config/secret-generator.yaml.j2: -------------------------------------------------------------------------------- 1 | apiVersion: viaduct.ai/v1 2 | kind: ksops 3 | metadata: 4 | name: envoy-gateway-secret-generator 5 | annotations: 6 | config.kubernetes.io/function: | 7 | exec: 8 | path: ksops 9 | files: 10 | - ./certificate.sops.yaml 11 | - ./envoy.sops.yaml 12 | -------------------------------------------------------------------------------- /templates/config/kubernetes/apps/cert-manager/cert-manager/issuers/secret-generator.yaml.j2: -------------------------------------------------------------------------------- 1 | apiVersion: viaduct.ai/v1 2 | kind: ksops 3 | metadata: 4 | name: cert-manager-secret-generator 5 | annotations: 6 | config.kubernetes.io/function: | 7 | exec: 8 | path: ksops 9 | files: 10 | - ./secret.sops.yaml 11 | - ./clusterissuers.sops.yaml 12 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Secrets 2 | *.pub 3 | *.key 4 | *.decrypted~*.yaml 5 | /age.key 6 | /cloudflare-tunnel.json 7 | /github-deploy.key 8 | /github-deploy.key.pub 9 | /github-push-token.txt 10 | # Template config files 11 | /cluster.yaml 12 | /nodes.yaml 13 | # Kubernetes 14 | kubeconfig 15 | talosconfig 16 | # Misc. 17 | .private/ 18 | .task/ 19 | .venv/ 20 | .DS_Store 21 | Thumbs.db 22 | -------------------------------------------------------------------------------- /templates/config/kubernetes/apps/kube-system/metrics-server/values.yaml.j2: -------------------------------------------------------------------------------- 1 | --- 2 | args: 3 | - --kubelet-insecure-tls 4 | - --kubelet-preferred-address-types=InternalIP,ExternalIP,Hostname 5 | - --kubelet-use-node-status-port 6 | - --metric-resolution=10s 7 | - --kubelet-request-timeout=2s 8 | metrics: 9 | enabled: true 10 | serviceMonitor: 11 | enabled: true 12 | -------------------------------------------------------------------------------- /templates/config/kubernetes/apps/kube-system/spegel/values.yaml.j2: -------------------------------------------------------------------------------- 1 | #% if spegel_enabled %# 2 | --- 3 | spegel: 4 | containerdSock: /run/containerd/containerd.sock 5 | containerdRegistryConfigPath: /etc/cri/conf.d/hosts 6 | service: 7 | registry: 8 | hostPort: 29999 9 | serviceMonitor: 10 | enabled: true 11 | grafanaDashboard: 12 | enabled: true 13 | #% endif %# 14 | -------------------------------------------------------------------------------- /templates/config/kubernetes/apps/network/cloudflare-tunnel/config/dnsendpoint.sops.yaml.j2: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: externaldns.k8s.io/v1alpha1 3 | kind: DNSEndpoint 4 | metadata: 5 | name: cloudflare-tunnel 6 | spec: 7 | endpoints: 8 | - dnsName: "external.#{ cloudflare_domain }#" 9 | recordType: CNAME 10 | targets: ["#{ cloudflare_tunnel_id() }#.cfargotunnel.com"] 11 | -------------------------------------------------------------------------------- /templates/config/kubernetes/apps/network/k8s-gateway/values.sops.yaml.j2: -------------------------------------------------------------------------------- 1 | --- 2 | fullnameOverride: k8s-gateway 3 | domain: "#{ cloudflare_domain }#" 4 | ttl: 1 5 | service: 6 | type: LoadBalancer 7 | port: 53 8 | annotations: 9 | io.cilium/lb-ipam-ips: "#{ cluster_dns_gateway_addr }#" 10 | externalTrafficPolicy: Cluster 11 | watchedResources: ["HTTPRoute", "Service"] 12 | -------------------------------------------------------------------------------- /templates/config/talos/patches/global/machine-files.yaml.j2: -------------------------------------------------------------------------------- 1 | machine: 2 | files: 3 | - op: create 4 | path: /etc/cri/conf.d/20-customization.part 5 | content: | 6 | [plugins."io.containerd.cri.v1.images"] 7 | discard_unpacked_layers = false 8 | [plugins."io.containerd.cri.v1.runtime"] 9 | device_ownership_from_security_context = true 10 | -------------------------------------------------------------------------------- /templates/config/.sops.yaml.j2: -------------------------------------------------------------------------------- 1 | --- 2 | creation_rules: 3 | - path_regex: talos/.*\.sops\.ya?ml 4 | mac_only_encrypted: true 5 | age: "#{ age_key('public') }#" 6 | - path_regex: (bootstrap|kubernetes|secret)/.*\.sops\.ya?ml 7 | encrypted_regex: "^(data|stringData)$" 8 | mac_only_encrypted: true 9 | age: "#{ age_key('public') }#" 10 | stores: 11 | yaml: 12 | indent: 2 13 | -------------------------------------------------------------------------------- /templates/config/kubernetes/apps/network/cloudflare-tunnel/config/secret-generator.yaml.j2: -------------------------------------------------------------------------------- 1 | apiVersion: viaduct.ai/v1 2 | kind: ksops 3 | metadata: 4 | name: cloudflare-tunnel-secret-generator 5 | annotations: 6 | config.kubernetes.io/function: | 7 | exec: 8 | path: ksops 9 | files: 10 | - ./secret.sops.yaml 11 | - ./dnsendpoint.sops.yaml 12 | - ./config.sops.yaml 13 | 14 | -------------------------------------------------------------------------------- /templates/config/kubernetes/apps/argo-system/argo-cd/config/http-route.yaml.j2: -------------------------------------------------------------------------------- 1 | apiVersion: gateway.networking.k8s.io/v1 2 | kind: HTTPRoute 3 | metadata: 4 | name: argo 5 | spec: 6 | parentRefs: 7 | - name: envoy-external 8 | namespace: kube-system 9 | hostnames: ["argo.#{ cloudflare_domain }#"] 10 | rules: 11 | - backendRefs: 12 | - name: argocd-server 13 | port: 80 14 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | ; https://editorconfig.org/ 2 | 3 | root = true 4 | 5 | [*] 6 | indent_style = space 7 | indent_size = 2 8 | end_of_line = lf 9 | charset = utf-8 10 | trim_trailing_whitespace = true 11 | insert_final_newline = true 12 | 13 | [*.cue] 14 | indent_style = tab 15 | indent_size = 4 16 | 17 | [*.md] 18 | indent_size = 4 19 | trim_trailing_whitespace = false 20 | 21 | [*.sh] 22 | indent_size = 4 23 | -------------------------------------------------------------------------------- /templates/config/kubernetes/argo/settings/cluster-settings.yaml.j2: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: argoproj.io/v1alpha1 3 | kind: AppProject 4 | metadata: 5 | name: kubernetes 6 | namespace: argo-system 7 | annotations: 8 | argocd.argoproj.io/sync-wave: '0' 9 | spec: 10 | destinations: 11 | - name: "*" 12 | namespace: "*" 13 | server: "*" 14 | sourceRepos: 15 | - "*" 16 | clusterResourceWhitelist: 17 | - group: "*" 18 | kind: "*" 19 | -------------------------------------------------------------------------------- /templates/overrides/readme.partial.yaml.j2: -------------------------------------------------------------------------------- 1 | #| Place user jinja template overrides in this file's directory |# 2 | #| Docs: https://mirkolenz.github.io/makejinja/makejinja.html |# 3 | #| Example: https://github.com/mirkolenz/makejinja/blob/main/tests/data/makejinja.toml |# 4 | #| Example: https://github.com/mirkolenz/makejinja/blob/main/tests/data/input1/not-empty.yaml.jinja |# 5 | #| Example: https://github.com/mirkolenz/makejinja/blob/main/tests/data/input2/not-empty.yaml.jinja |# 6 | -------------------------------------------------------------------------------- /.github/workflows/labeler.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | name: "Labeler" 3 | 4 | on: 5 | workflow_dispatch: 6 | pull_request_target: 7 | branches: ["main"] 8 | 9 | jobs: 10 | labeler: 11 | name: Labeler 12 | runs-on: ubuntu-latest 13 | permissions: 14 | contents: write 15 | pull-requests: write 16 | steps: 17 | - name: Labeler 18 | uses: actions/labeler@634933edcd8ababfe52f92936142cc22ac488b1b # v6.0.1 19 | with: 20 | configuration-path: .github/labeler.yaml 21 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto eol=lf 2 | *.env linguist-detectable linguist-language=SHELL 3 | *.json linguist-detectable linguist-language=JSON 4 | *.json5 linguist-detectable linguist-language=JSON5 5 | *.md linguist-detectable linguist-language=MARKDOWN 6 | *.sh linguist-detectable linguist-language=SHELL 7 | *.toml linguist-detectable linguist-language=TOML 8 | *.yml linguist-detectable linguist-language=YAML 9 | *.yaml linguist-detectable linguist-language=YAML 10 | *.yaml.j2 linguist-detectable linguist-language=YAML 11 | -------------------------------------------------------------------------------- /templates/config/kubernetes/apps/network/envoy-gateway/config/certificate.sops.yaml.j2: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: cert-manager.io/v1 3 | kind: Certificate 4 | metadata: 5 | name: "#{ cloudflare_domain.replace('.', '-') }#-production" 6 | spec: 7 | secretName: "#{ cloudflare_domain.replace('.', '-') }#-production-tls" 8 | issuerRef: 9 | name: letsencrypt-production 10 | kind: ClusterIssuer 11 | commonName: "#{ cloudflare_domain }#" 12 | dnsNames: ["#{ cloudflare_domain }#", "*.#{ cloudflare_domain }#"] 13 | 14 | 15 | -------------------------------------------------------------------------------- /templates/config/kubernetes/apps/network/envoy-gateway/config/podmonitor.yaml.j2: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: monitoring.coreos.com/v1 3 | kind: PodMonitor 4 | metadata: 5 | name: envoy-proxy 6 | spec: 7 | jobLabel: envoy-proxy 8 | namespaceSelector: 9 | matchNames: 10 | - network 11 | podMetricsEndpoints: 12 | - port: metrics 13 | path: /stats/prometheus 14 | honorLabels: true 15 | selector: 16 | matchLabels: 17 | app.kubernetes.io/component: proxy 18 | app.kubernetes.io/name: envoy 19 | -------------------------------------------------------------------------------- /makejinja.toml: -------------------------------------------------------------------------------- 1 | [makejinja] 2 | inputs = ["./templates/overrides","./templates/config"] 3 | output = "./" 4 | exclude_patterns = ["*.partial.yaml.j2"] 5 | data = ["./cluster.yaml", "./nodes.yaml"] 6 | import_paths = ["./templates/scripts"] 7 | loaders = ["plugin:Plugin"] 8 | jinja_suffix = ".j2" 9 | copy_metadata = true 10 | force = true 11 | undefined = "chainable" 12 | 13 | [makejinja.delimiter] 14 | block_start = "#%" 15 | block_end = "%#" 16 | comment_start = "#|" 17 | comment_end = "#|" 18 | variable_start = "#{" 19 | variable_end = "}#" 20 | -------------------------------------------------------------------------------- /.github/tests/nodes.yaml: -------------------------------------------------------------------------------- 1 | nodes: 2 | - name: k8s-0 3 | address: 10.10.10.100 4 | controller: true 5 | disk: /dev/sdfake 6 | mac_addr: 00:00:00:00:00:00 7 | schematic_id: "376567988ad370138ad8b2698212367b8edcb69b5fd68c80be1f2ec7d603b4ba" 8 | - name: k8s-1 9 | address: 10.10.10.101 10 | controller: false 11 | disk: /dev/sdfake 12 | mac_addr: 00:00:00:00:00:01 13 | schematic_id: "376567988ad370138ad8b2698212367b8edcb69b5fd68c80be1f2ec7d603b4ba" 14 | mtu: 1500 15 | secureboot: true 16 | encrypt_disk: true 17 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.bracketPairColorization.enabled": true, 3 | "files.associations": { 4 | "**/*.json5": "json5" 5 | }, 6 | "files.trimTrailingWhitespace": true, 7 | "sops.defaults.ageKeyFile": "age.key", 8 | "vs-kubernetes": { 9 | "vs-kubernetes.kubeconfig": "./kubeconfig", 10 | "vs-kubernetes.knownKubeconfigs": [ 11 | "./kubeconfig" 12 | ] 13 | }, 14 | "yaml.schemaStore.enable": true, 15 | "yaml.schemas": { 16 | "kubernetes": "./kubernetes/**/*.yaml" 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /templates/config/kubernetes/apps/network/cloudflare-tunnel/config/config.sops.yaml.j2: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: v1 3 | kind: ConfigMap 4 | metadata: 5 | name: cloudflare-tunnel-configmap 6 | data: 7 | config.yaml: | 8 | --- 9 | originRequest: 10 | originServerName: "external.#{ cloudflare_domain }#" 11 | 12 | ingress: 13 | - hostname: "#{ cloudflare_domain }#" 14 | service: &svc https://envoy-external.kube-system.svc.cluster.local 15 | - hostname: "*.#{ cloudflare_domain }#" 16 | service: *svc 17 | - service: http_status:404 18 | -------------------------------------------------------------------------------- /templates/config/kubernetes/apps/cert-manager/cert-manager/issuers/clusterissuers.sops.yaml.j2: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: cert-manager.io/v1 3 | kind: ClusterIssuer 4 | metadata: 5 | name: letsencrypt-production 6 | spec: 7 | acme: 8 | server: https://acme-v02.api.letsencrypt.org/directory 9 | privateKeySecretRef: 10 | name: letsencrypt-production 11 | solvers: 12 | - dns01: 13 | cloudflare: 14 | apiTokenSecretRef: 15 | name: cert-manager-secret 16 | key: api-token 17 | selector: 18 | dnsZones: ["#{ cloudflare_domain }#"] 19 | -------------------------------------------------------------------------------- /templates/config/talos/patches/global/machine-sysctls.yaml.j2: -------------------------------------------------------------------------------- 1 | machine: 2 | sysctls: 3 | fs.inotify.max_user_watches: "1048576" # Watchdog 4 | fs.inotify.max_user_instances: "8192" # Watchdog 5 | net.core.rmem_max: "7500000" # Cloudflared | QUIC 6 | net.core.wmem_max: "7500000" # Cloudflared | QUIC 7 | net.ipv4.neigh.default.gc_thresh1: "4096" # Prevent ARP cache overflows 8 | net.ipv4.neigh.default.gc_thresh2: "8192" # Prevent ARP cache overflows 9 | net.ipv4.neigh.default.gc_thresh3: "16384" # Prevent ARP cache overflows 10 | user.max_user_namespaces: "11255" # User Namespaces 11 | -------------------------------------------------------------------------------- /templates/config/kubernetes/argo/repositories/github.sops.yaml.j2: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: v1 3 | kind: Secret 4 | metadata: 5 | name: github 6 | namespace: argo-system 7 | annotations: 8 | argocd.argoproj.io/sync-wave: '1' 9 | labels: 10 | argocd.argoproj.io/secret-type: repository 11 | stringData: 12 | type: git 13 | #% if repository_visibility == 'private' %# 14 | url: "ssh://git@github.com:#{ repository_name }#.git" 15 | sshPrivateKey: | 16 | #% filter indent(width=4, first=False) %# 17 | #{ github_deploy_key() }# 18 | #%- endfilter %# 19 | #% else %# 20 | url: "https://github.com/#{ repository_name }#.git" 21 | #% endif %# 22 | -------------------------------------------------------------------------------- /templates/config/talos/patches/controller/cluster.yaml.j2: -------------------------------------------------------------------------------- 1 | cluster: 2 | allowSchedulingOnControlPlanes: true 3 | apiServer: 4 | admissionControl: 5 | $$patch: delete 6 | extraArgs: 7 | # https://kubernetes.io/docs/tasks/extend-kubernetes/configure-aggregation-layer/ 8 | enable-aggregator-routing: true 9 | controllerManager: 10 | extraArgs: 11 | bind-address: 0.0.0.0 12 | coreDNS: 13 | disabled: true 14 | etcd: 15 | extraArgs: 16 | listen-metrics-urls: http://0.0.0.0:2381 17 | advertisedSubnets: 18 | - #{ node_cidr }# 19 | proxy: 20 | disabled: true 21 | scheduler: 22 | extraArgs: 23 | bind-address: 0.0.0.0 24 | -------------------------------------------------------------------------------- /.github/workflows/label-sync.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | name: "Label Sync" 3 | 4 | on: 5 | workflow_dispatch: 6 | push: 7 | branches: ["main"] 8 | paths: [".github/labels.yaml"] 9 | 10 | jobs: 11 | label-sync: 12 | name: Label Sync 13 | runs-on: ubuntu-latest 14 | permissions: 15 | contents: write 16 | pull-requests: write 17 | issues: write 18 | steps: 19 | - name: Checkout 20 | uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 21 | 22 | - name: Sync Labels 23 | uses: EndBug/label-sync@52074158190acb45f3077f9099fea818aa43f97a # v2.3.3 24 | with: 25 | config-file: .github/labels.yaml 26 | delete-other-labels: true 27 | -------------------------------------------------------------------------------- /.github/tests/private.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | node_cidr: "10.10.10.0/24" 3 | # node_default_gateway: "" 4 | # node_vlan_tag: 5 | # cluster_pod_cidr: "" 6 | # cluster_svc_cidr: "" 7 | # node_dns_servers: [] 8 | # node_ntp_servers: [] 9 | cluster_api_addr: "10.10.10.254" 10 | # cluster_api_tls_sans: [] 11 | cluster_gateway_addr: "10.10.10.252" 12 | cluster_dns_gateway_addr: "10.10.10.253" 13 | repository_name: "ajaykumar4/cluster-template" 14 | # repository_branch: "" 15 | repository_visibility: "private" 16 | cloudflare_domain: "example.com" 17 | cloudflare_token: "fake" 18 | cloudflare_gateway_addr: "10.10.10.251" 19 | # cilium_bgp_router_addr: "" 20 | # cilium_bgp_router_asn: "" 21 | # cilium_bgp_node_asn: "" 22 | # cilium_loadbalancer_mode: "" 23 | argo_password: "argo" 24 | -------------------------------------------------------------------------------- /templates/config/kubernetes/apps/network/cloudflare-dns/values.sops.yaml.j2: -------------------------------------------------------------------------------- 1 | --- 2 | fullnameOverride: cloudflare-dns 3 | provider: cloudflare 4 | env: 5 | - name: CF_API_TOKEN 6 | valueFrom: 7 | secretKeyRef: 8 | name: &secret cloudflare-dns-secret 9 | key: api-token 10 | extraArgs: 11 | - --cloudflare-dns-records-per-page=1000 12 | - --cloudflare-proxied 13 | - --crd-source-apiversion=externaldns.k8s.io/v1alpha1 14 | - --crd-source-kind=DNSEndpoint 15 | - --gateway-name=envoy-external 16 | triggerLoopOnEvent: true 17 | policy: sync 18 | sources: ["crd", "gateway-httproute"] 19 | txtPrefix: k8s. 20 | txtOwnerId: default 21 | domainFilters: ["#{ cloudflare_domain }#"] 22 | serviceMonitor: 23 | enabled: true 24 | podAnnotations: 25 | secret.reloader.stakater.com/reload: *secret 26 | -------------------------------------------------------------------------------- /.taskfiles/template/resources/nodes.schema.cue: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "net" 5 | "list" 6 | ) 7 | 8 | #Config: { 9 | nodes: [...#Node] 10 | _nodes_check: { 11 | name: list.UniqueItems() & [for item in nodes {item.name}] 12 | address: list.UniqueItems() & [for item in nodes {item.address}] 13 | mac_addr: list.UniqueItems() & [for item in nodes {item.mac_addr}] 14 | } 15 | } 16 | 17 | #Node: { 18 | name: =~"^[a-z0-9][a-z0-9\\-]{0,61}[a-z0-9]$|^[a-z0-9]$" & !="global" & !="controller" & !="worker" 19 | address: net.IPv4 20 | controller: bool 21 | disk: string 22 | mac_addr: =~"^([0-9a-f]{2}[:]){5}([0-9a-f]{2})$" 23 | schematic_id: =~"^[a-z0-9]{64}$" 24 | mtu?: >=1450 & <=9000 25 | secureboot?: bool 26 | encrypt_disk?: bool 27 | } 28 | 29 | #Config 30 | -------------------------------------------------------------------------------- /templates/config/talos/patches/README.md.j2: -------------------------------------------------------------------------------- 1 | # Talos Patching 2 | 3 | This directory contains Kustomization patches that are added to the talhelper configuration file. 4 | 5 | 6 | 7 | ## Patch Directories 8 | 9 | Under this `patches` directory, there are several sub-directories that can contain patches that are added to the talhelper configuration file. 10 | Each directory is optional and therefore might not created by default. 11 | 12 | - `global/`: patches that are applied to both the controller and worker configurations 13 | - `controller/`: patches that are applied to the controller configurations 14 | - `worker/`: patches that are applied to the worker configurations 15 | - `${node-hostname}/`: patches that are applied to the node with the specified name 16 | -------------------------------------------------------------------------------- /.github/tests/public.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | node_cidr: "10.10.10.0/24" 3 | node_default_gateway: "10.10.10.1" 4 | node_vlan_tag: "100" 5 | cluster_pod_cidr: "10.42.0.0/16" 6 | cluster_svc_cidr: "10.43.0.0/16" 7 | node_dns_servers: ["1.1.1.1"] 8 | node_ntp_servers: ["162.159.200.123"] 9 | cluster_api_addr: "10.10.10.254" 10 | cluster_api_tls_sans: ["example.com"] 11 | cluster_gateway_addr: "10.10.10.252" 12 | cluster_dns_gateway_addr: "10.10.10.253" 13 | repository_name: "ajaykumar4/cluster-template" 14 | repository_branch: "main" 15 | repository_visibility: "public" 16 | cloudflare_domain: "example.com" 17 | cloudflare_token: "fake" 18 | cloudflare_gateway_addr: "10.10.10.251" 19 | cilium_loadbalancer_mode: "dsr" 20 | cilium_bgp_router_addr: "10.10.1.1" 21 | cilium_bgp_router_asn: "64513" 22 | cilium_bgp_node_asn: "64514" 23 | argo_password: "argo" 24 | -------------------------------------------------------------------------------- /templates/config/bootstrap/helmfile.d/00-crds.yaml.j2: -------------------------------------------------------------------------------- 1 | --- 2 | 3 | # This helmfile is for extracting and installing Custom Resource Definitions (CRDs) from Helm charts. 4 | # It is not intended to be used with helmfile apply or helmfile sync. 5 | 6 | helmDefaults: 7 | args: 8 | - --include-crds 9 | - --no-hooks 10 | postRenderer: bash 11 | postRendererArgs: 12 | - -c 13 | - yq eval-all --exit-status 'select(.kind == "CustomResourceDefinition")' 14 | 15 | releases: 16 | - name: cloudflare-dns 17 | namespace: network 18 | chart: oci://ghcr.io/home-operations/charts-mirror/external-dns 19 | version: 1.19.0 20 | 21 | - name: envoy-gateway 22 | namespace: network 23 | chart: oci://mirror.gcr.io/envoyproxy/gateway-helm 24 | version: v1.6.0 25 | 26 | - name: kube-prometheus-stack 27 | namespace: observability 28 | chart: oci://ghcr.io/prometheus-community/charts/kube-prometheus-stack 29 | version: 79.8.2 30 | -------------------------------------------------------------------------------- /Taskfile.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | version: '3' 3 | 4 | set: [pipefail] 5 | shopt: [globstar] 6 | 7 | vars: 8 | BOOTSTRAP_DIR: '{{.ROOT_DIR}}/bootstrap' 9 | KUBERNETES_DIR: '{{.ROOT_DIR}}/kubernetes' 10 | SCRIPTS_DIR: '{{.ROOT_DIR}}/scripts' 11 | TALOS_DIR: '{{.ROOT_DIR}}/talos' 12 | PRIVATE_DIR: '{{.ROOT_DIR}}/.private' 13 | TALOSCONFIG: '{{.ROOT_DIR}}/talos/clusterconfig/talosconfig' 14 | 15 | env: 16 | KUBECONFIG: '{{.ROOT_DIR}}/kubeconfig' 17 | SOPS_AGE_KEY_FILE: '{{.ROOT_DIR}}/age.key' 18 | TALOSCONFIG: '{{.TALOSCONFIG}}' 19 | 20 | includes: 21 | bootstrap: .taskfiles/bootstrap 22 | talos: .taskfiles/talos 23 | template: .taskfiles/template 24 | 25 | tasks: 26 | 27 | default: task --list 28 | 29 | reconcile: 30 | desc: Force Argo to pull in changes from your Git repository 31 | cmd: argocd app list --project kubernetes -o name | xargs -I {} argocd app wait {} --sync --timeout 300 32 | preconditions: 33 | - test -f {{.KUBECONFIG}} 34 | - which argocd 35 | -------------------------------------------------------------------------------- /nodes.sample.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | nodes: [] 3 | # - name: "" # (REQUIRED) Name of the node (must match [a-z0-9-\]+) 4 | # address: "" # (REQUIRED) IP address of the node (must be in the node_cidr) 5 | # controller: true # (REQUIRED) Set to true if this is a controller node 6 | # disk: "" # (REQUIRED) Device path or serial number of the disk for this node (talosctl get disks -n --insecure) 7 | # mac_addr: "" # (REQUIRED) MAC address of the NIC for this node (talosctl get links -n --insecure) 8 | # schematic_id: "" # (REQUIRED) Schematic ID from https://factory.talos.dev/ 9 | # mtu: 1500 # (ADVANCED/OPTIONAL) MTU for the NIC. DEFAULT: 1500 10 | # secureboot: false # (ADVANCED/OPTIONAL) SecureBoot mode on UEFI platforms. Ref: https://www.talos.dev/latest/talos-guides/install/bare-metal-platforms/secureboot 11 | # encrypt_disk: false # (ADVANCED/OPTIONAL) TPM-based disk encryption. Ref: https://www.talos.dev/latest/talos-guides/install/bare-metal-platforms/secureboot 12 | # ... 13 | -------------------------------------------------------------------------------- /templates/config/kubernetes/components/common/settings.yaml.j2: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: argoproj.io/v1alpha1 3 | kind: Application 4 | metadata: 5 | name: projects 6 | namespace: argo-system 7 | annotations: 8 | argocd.argoproj.io/sync-wave: '1' 9 | spec: 10 | project: default 11 | source: 12 | repoURL: "https://github.com/#{ repository_name }#.git" 13 | path: kubernetes/argo/settings 14 | targetRevision: #{ repository_branch }# 15 | destination: 16 | name: in-cluster 17 | namespace: argo-system 18 | syncPolicy: 19 | automated: 20 | allowEmpty: true 21 | prune: true 22 | selfHeal: true 23 | retry: 24 | limit: 1 25 | backoff: 26 | duration: 10s 27 | factor: 2 28 | maxDuration: 3m 29 | syncOptions: 30 | - CreateNamespace=true 31 | - ApplyOutOfSyncOnly=true 32 | - ServerSideApply=true 33 | - PruneLast=true 34 | - RespectIgnoreDifferences=true 35 | ignoreDifferences: 36 | - group: argoproj.io 37 | kind: Application 38 | jsonPointers: 39 | - /spec/syncPolicy 40 | -------------------------------------------------------------------------------- /templates/config/kubernetes/components/common/repositories.yaml.j2: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: argoproj.io/v1alpha1 3 | kind: Application 4 | metadata: 5 | name: repositories 6 | namespace: argo-system 7 | annotations: 8 | argocd.argoproj.io/sync-wave: '1' 9 | spec: 10 | project: default 11 | source: 12 | repoURL: "https://github.com/#{ repository_name }#.git" 13 | path: kubernetes/argo/repositories 14 | targetRevision: #{ repository_branch }# 15 | destination: 16 | name: in-cluster 17 | namespace: argo-system 18 | syncPolicy: 19 | automated: 20 | allowEmpty: true 21 | prune: true 22 | selfHeal: true 23 | retry: 24 | limit: 1 25 | backoff: 26 | duration: 10s 27 | factor: 2 28 | maxDuration: 3m 29 | syncOptions: 30 | - CreateNamespace=true 31 | - ApplyOutOfSyncOnly=true 32 | - ServerSideApply=true 33 | - PruneLast=true 34 | - RespectIgnoreDifferences=true 35 | ignoreDifferences: 36 | - group: argoproj.io 37 | kind: Application 38 | jsonPointers: 39 | - /spec/syncPolicy 40 | -------------------------------------------------------------------------------- /.github/labels.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | # Areas 3 | - name: area/bootstrap 4 | color: "0e8a16" 5 | - name: area/docs 6 | color: "0e8a16" 7 | - name: area/github 8 | color: "0e8a16" 9 | - name: area/kubernetes 10 | color: "0e8a16" 11 | - name: area/mise 12 | color: "0e8a16" 13 | - name: area/renovate 14 | color: "0e8a16" 15 | - name: area/scripts 16 | color: "0e8a16" 17 | - name: area/talos 18 | color: "0e8a16" 19 | - name: area/templates 20 | color: "0e8a16" 21 | - name: area/taskfile 22 | color: "0e8a16" 23 | # Renovate Types 24 | - name: renovate/container 25 | color: "027fa0" 26 | - name: renovate/github-action 27 | color: "027fa0" 28 | - name: renovate/grafana-dashboard 29 | color: "027fa0" 30 | - name: renovate/github-release 31 | color: "027fa0" 32 | - name: renovate/helm 33 | color: "027fa0" 34 | # Semantic Types 35 | - name: type/digest 36 | color: "ffeC19" 37 | - name: type/patch 38 | color: "ffeC19" 39 | - name: type/minor 40 | color: "ff9800" 41 | - name: type/major 42 | color: "f6412d" 43 | # Uncategorized 44 | - name: community 45 | color: "370fb2" 46 | - name: hold 47 | color: "ee0701" 48 | -------------------------------------------------------------------------------- /templates/config/kubernetes/components/common/apps.yaml.j2: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: argoproj.io/v1alpha1 3 | kind: Application 4 | metadata: 5 | name: apps 6 | namespace: argo-system 7 | annotations: 8 | argocd.argoproj.io/sync-wave: '1' 9 | spec: 10 | project: default 11 | source: 12 | repoURL: "https://github.com/#{ repository_name }#.git" 13 | path: kubernetes/argo/apps 14 | targetRevision: #{ repository_branch }# 15 | directory: 16 | recurse: true 17 | destination: 18 | name: in-cluster 19 | namespace: argo-system 20 | syncPolicy: 21 | automated: 22 | allowEmpty: true 23 | prune: true 24 | selfHeal: true 25 | retry: 26 | limit: 1 27 | backoff: 28 | duration: 10s 29 | factor: 2 30 | maxDuration: 3m 31 | syncOptions: 32 | - CreateNamespace=true 33 | - ApplyOutOfSyncOnly=true 34 | - ServerSideApply=true 35 | - PruneLast=true 36 | - RespectIgnoreDifferences=true 37 | ignoreDifferences: 38 | - group: argoproj.io 39 | kind: Application 40 | jsonPointers: 41 | - /spec/syncPolicy 42 | -------------------------------------------------------------------------------- /.github/labeler.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | area/bootstrap: 3 | - changed-files: 4 | - any-glob-to-any-file: 5 | - bootstrap/**/* 6 | area/docs: 7 | - changed-files: 8 | - any-glob-to-any-file: 9 | - README.md 10 | area/github: 11 | - changed-files: 12 | - any-glob-to-any-file: 13 | - .github/**/* 14 | area/kubernetes: 15 | - changed-files: 16 | - any-glob-to-any-file: 17 | - kubernetes/**/* 18 | area/mise: 19 | - changed-files: 20 | - any-glob-to-any-file: 21 | - .mise.toml 22 | area/renovate: 23 | - changed-files: 24 | - any-glob-to-any-file: 25 | - .renovate/**/* 26 | - .renovaterc.json5 27 | area/scripts: 28 | - changed-files: 29 | - any-glob-to-any-file: 30 | - scripts/**/* 31 | area/talos: 32 | - changed-files: 33 | - any-glob-to-any-file: 34 | - talos/**/* 35 | area/taskfile: 36 | - changed-files: 37 | - any-glob-to-any-file: 38 | - .taskfiles/**/* 39 | - Taskfile.yaml 40 | area/templates: 41 | - changed-files: 42 | - any-glob-to-any-file: 43 | - templates/**/* 44 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 ajaykumar4 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /templates/config/kubernetes/argo/apps/network/envoy-gateway.yaml.j2: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: argoproj.io/v1alpha1 3 | kind: Application 4 | metadata: 5 | name: envoy-gateway 6 | namespace: argo-system 7 | annotations: 8 | argocd.argoproj.io/sync-wave: '0' 9 | spec: 10 | project: kubernetes 11 | sources: 12 | - repoURL: "https://github.com/#{ repository_name }#.git" 13 | path: kubernetes/apps/network/envoy-gateway 14 | targetRevision: #{ repository_branch }# 15 | ref: repo 16 | - repoURL: mirror.gcr.io/envoyproxy 17 | chart: gateway-helm 18 | targetRevision: v1.6.0 19 | helm: 20 | releaseName: envoy-gateway 21 | destination: 22 | name: in-cluster 23 | namespace: network 24 | syncPolicy: 25 | automated: 26 | allowEmpty: true 27 | prune: true 28 | selfHeal: true 29 | retry: 30 | limit: 1 31 | backoff: 32 | duration: 10s 33 | factor: 2 34 | maxDuration: 3m 35 | syncOptions: 36 | - CreateNamespace=true 37 | - ApplyOutOfSyncOnly=true 38 | - ServerSideApply=true 39 | - PruneLast=true 40 | - RespectIgnoreDifferences=true 41 | -------------------------------------------------------------------------------- /templates/config/kubernetes/argo/apps/default/echo.yaml.j2: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: argoproj.io/v1alpha1 3 | kind: Application 4 | metadata: 5 | name: echo 6 | namespace: argo-system 7 | annotations: 8 | argocd.argoproj.io/sync-wave: '0' 9 | spec: 10 | project: kubernetes 11 | sources: 12 | - repoURL: "https://github.com/#{ repository_name }#.git" 13 | path: kubernetes/apps/default/echo 14 | targetRevision: #{ repository_branch }# 15 | ref: repo 16 | - repoURL: ghcr.io/bjw-s-labs/helm 17 | chart: app-template 18 | targetRevision: 4.4.0 19 | helm: 20 | releaseName: echo 21 | valueFiles: 22 | - $repo/kubernetes/apps/default/echo/values.sops.yaml 23 | destination: 24 | name: in-cluster 25 | namespace: default 26 | syncPolicy: 27 | automated: 28 | allowEmpty: true 29 | prune: true 30 | selfHeal: true 31 | retry: 32 | limit: 1 33 | backoff: 34 | duration: 10s 35 | factor: 2 36 | maxDuration: 3m 37 | syncOptions: 38 | - CreateNamespace=true 39 | - ApplyOutOfSyncOnly=true 40 | - ServerSideApply=true 41 | - PruneLast=true 42 | - RespectIgnoreDifferences=true 43 | -------------------------------------------------------------------------------- /templates/config/kubernetes/argo/apps/kube-system/coredns.yaml.j2: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: argoproj.io/v1alpha1 3 | kind: Application 4 | metadata: 5 | name: coredns 6 | namespace: argo-system 7 | annotations: 8 | argocd.argoproj.io/sync-wave: '0' 9 | spec: 10 | project: kubernetes 11 | sources: 12 | - repoURL: "https://github.com/#{ repository_name }#.git" 13 | path: kubernetes/apps/kube-system/coredns 14 | targetRevision: #{ repository_branch }# 15 | ref: repo 16 | - repoURL: ghcr.io/coredns/charts 17 | chart: coredns 18 | targetRevision: 1.45.0 19 | helm: 20 | releaseName: coredns 21 | valueFiles: 22 | - $repo/kubernetes/apps/kube-system/coredns/values.yaml 23 | destination: 24 | name: in-cluster 25 | namespace: kube-system 26 | syncPolicy: 27 | automated: 28 | allowEmpty: true 29 | prune: true 30 | selfHeal: true 31 | retry: 32 | limit: 1 33 | backoff: 34 | duration: 10s 35 | factor: 2 36 | maxDuration: 3m 37 | syncOptions: 38 | - CreateNamespace=true 39 | - ApplyOutOfSyncOnly=true 40 | - ServerSideApply=true 41 | - PruneLast=true 42 | - RespectIgnoreDifferences=true 43 | -------------------------------------------------------------------------------- /templates/config/kubernetes/argo/apps/kube-system/reloader.yaml.j2: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: argoproj.io/v1alpha1 3 | kind: Application 4 | metadata: 5 | name: reloader 6 | namespace: argo-system 7 | annotations: 8 | argocd.argoproj.io/sync-wave: '0' 9 | spec: 10 | project: kubernetes 11 | sources: 12 | - repoURL: "https://github.com/#{ repository_name }#.git" 13 | path: kubernetes/apps/kube-system/reloader 14 | targetRevision: #{ repository_branch }# 15 | ref: repo 16 | - repoURL: ghcr.io/stakater/charts 17 | chart: reloader 18 | targetRevision: 2.2.5 19 | helm: 20 | releaseName: reloader 21 | valueFiles: 22 | - $repo/kubernetes/apps/kube-system/reloader/values.yaml 23 | destination: 24 | name: in-cluster 25 | namespace: kube-system 26 | syncPolicy: 27 | automated: 28 | allowEmpty: true 29 | prune: true 30 | selfHeal: true 31 | retry: 32 | limit: 1 33 | backoff: 34 | duration: 10s 35 | factor: 2 36 | maxDuration: 3m 37 | syncOptions: 38 | - CreateNamespace=true 39 | - ApplyOutOfSyncOnly=true 40 | - ServerSideApply=true 41 | - PruneLast=true 42 | - RespectIgnoreDifferences=true 43 | -------------------------------------------------------------------------------- /templates/config/kubernetes/argo/apps/network/k8s-gateway.yaml.j2: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: argoproj.io/v1alpha1 3 | kind: Application 4 | metadata: 5 | name: k8s-gateway 6 | namespace: argo-system 7 | annotations: 8 | argocd.argoproj.io/sync-wave: '0' 9 | spec: 10 | project: kubernetes 11 | sources: 12 | - repoURL: "https://github.com/#{ repository_name }#.git" 13 | path: kubernetes/apps/network/k8s-gateway 14 | targetRevision: #{ repository_branch }# 15 | ref: repo 16 | - repoURL: https://ori-edge.github.io/k8s_gateway 17 | chart: k8s-gateway 18 | targetRevision: 2.4.0 19 | helm: 20 | releaseName: k8s-gateway 21 | valueFiles: 22 | - $repo/kubernetes/apps/network/k8s-gateway/values.sops.yaml 23 | destination: 24 | name: in-cluster 25 | namespace: network 26 | syncPolicy: 27 | automated: 28 | allowEmpty: true 29 | prune: true 30 | selfHeal: true 31 | retry: 32 | limit: 1 33 | backoff: 34 | duration: 10s 35 | factor: 2 36 | maxDuration: 3m 37 | syncOptions: 38 | - CreateNamespace=true 39 | - ApplyOutOfSyncOnly=true 40 | - ServerSideApply=true 41 | - PruneLast=true 42 | - RespectIgnoreDifferences=true 43 | -------------------------------------------------------------------------------- /templates/config/kubernetes/argo/apps/network/cloudflare-tunnel.yaml.j2: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: argoproj.io/v1alpha1 3 | kind: Application 4 | metadata: 5 | name: cloudflare-tunnel 6 | namespace: argo-system 7 | annotations: 8 | argocd.argoproj.io/sync-wave: '0' 9 | spec: 10 | project: kubernetes 11 | sources: 12 | - repoURL: "https://github.com/#{ repository_name }#.git" 13 | path: kubernetes/apps/network/cloudflare-tunnel 14 | targetRevision: #{ repository_branch }# 15 | ref: repo 16 | - repoURL: ghcr.io/bjw-s/helm 17 | chart: app-template 18 | targetRevision: 3.7.3 19 | helm: 20 | releaseName: cloudflare-tunnel 21 | valueFiles: 22 | - $repo/kubernetes/apps/network/cloudflare-tunnel/values.yaml 23 | destination: 24 | name: in-cluster 25 | namespace: network 26 | syncPolicy: 27 | automated: 28 | allowEmpty: true 29 | prune: true 30 | selfHeal: true 31 | retry: 32 | limit: 1 33 | backoff: 34 | duration: 10s 35 | factor: 2 36 | maxDuration: 3m 37 | syncOptions: 38 | - CreateNamespace=true 39 | - ApplyOutOfSyncOnly=true 40 | - ServerSideApply=true 41 | - PruneLast=true 42 | - RespectIgnoreDifferences=true 43 | -------------------------------------------------------------------------------- /templates/config/kubernetes/argo/apps/cert-manager/cert-manager.yaml.j2: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: argoproj.io/v1alpha1 3 | kind: Application 4 | metadata: 5 | name: cert-manager 6 | namespace: argo-system 7 | annotations: 8 | argocd.argoproj.io/sync-wave: '0' 9 | spec: 10 | project: kubernetes 11 | sources: 12 | - repoURL: "https://github.com/#{ repository_name }#.git" 13 | path: kubernetes/apps/cert-manager/cert-manager 14 | targetRevision: #{ repository_branch }# 15 | ref: repo 16 | - repoURL: https://charts.jetstack.io 17 | chart: cert-manager 18 | targetRevision: v1.19.1 19 | helm: 20 | releaseName: cert-manager 21 | valueFiles: 22 | - $repo/kubernetes/apps/cert-manager/cert-manager/values.yaml 23 | destination: 24 | name: in-cluster 25 | namespace: cert-manager 26 | syncPolicy: 27 | automated: 28 | allowEmpty: true 29 | prune: true 30 | selfHeal: true 31 | retry: 32 | limit: 1 33 | backoff: 34 | duration: 10s 35 | factor: 2 36 | maxDuration: 3m 37 | syncOptions: 38 | - CreateNamespace=true 39 | - ApplyOutOfSyncOnly=true 40 | - ServerSideApply=true 41 | - PruneLast=true 42 | - RespectIgnoreDifferences=true 43 | -------------------------------------------------------------------------------- /templates/config/kubernetes/argo/apps/kube-system/spegel.yaml.j2: -------------------------------------------------------------------------------- 1 | #% if spegel_enabled %# 2 | --- 3 | apiVersion: argoproj.io/v1alpha1 4 | kind: Application 5 | metadata: 6 | name: spegel 7 | namespace: argo-system 8 | annotations: 9 | argocd.argoproj.io/sync-wave: '0' 10 | spec: 11 | project: kubernetes 12 | sources: 13 | - repoURL: "https://github.com/#{ repository_name }#.git" 14 | path: kubernetes/apps/kube-system/spegel 15 | targetRevision: #{ repository_branch }# 16 | ref: repo 17 | - repoURL: ghcr.io/spegel-org/helm-charts 18 | chart: spegel 19 | targetRevision: 0.5.1 20 | helm: 21 | releaseName: spegel 22 | valueFiles: 23 | - $repo/kubernetes/apps/kube-system/spegel/values.yaml 24 | destination: 25 | name: in-cluster 26 | namespace: kube-system 27 | syncPolicy: 28 | automated: 29 | allowEmpty: true 30 | prune: true 31 | selfHeal: true 32 | retry: 33 | limit: 1 34 | backoff: 35 | duration: 10s 36 | factor: 2 37 | maxDuration: 3m 38 | syncOptions: 39 | - CreateNamespace=true 40 | - ApplyOutOfSyncOnly=true 41 | - ServerSideApply=true 42 | - PruneLast=true 43 | - RespectIgnoreDifferences=true 44 | #% endif %# 45 | -------------------------------------------------------------------------------- /templates/config/kubernetes/argo/apps/network/cloudflare-dns.yaml.j2: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: argoproj.io/v1alpha1 3 | kind: Application 4 | metadata: 5 | name: cloudflare-dns 6 | namespace: argo-system 7 | annotations: 8 | argocd.argoproj.io/sync-wave: '0' 9 | spec: 10 | project: kubernetes 11 | sources: 12 | - repoURL: "https://github.com/#{ repository_name }#.git" 13 | path: kubernetes/apps/network/cloudflare-dns 14 | targetRevision: #{ repository_branch }# 15 | ref: repo 16 | - repoURL: https://kubernetes-sigs.github.io/external-dns 17 | chart: external-dns 18 | targetRevision: 1.19.0 19 | helm: 20 | releaseName: cloudflare-dns 21 | valueFiles: 22 | - $repo/kubernetes/apps/network/cloudflare-dns/values.sops.yaml 23 | destination: 24 | name: in-cluster 25 | namespace: network 26 | syncPolicy: 27 | automated: 28 | allowEmpty: true 29 | prune: true 30 | selfHeal: true 31 | retry: 32 | limit: 1 33 | backoff: 34 | duration: 10s 35 | factor: 2 36 | maxDuration: 3m 37 | syncOptions: 38 | - CreateNamespace=true 39 | - ApplyOutOfSyncOnly=true 40 | - ServerSideApply=true 41 | - PruneLast=true 42 | - RespectIgnoreDifferences=true 43 | -------------------------------------------------------------------------------- /templates/config/kubernetes/argo/apps/kube-system/metrics-server.yaml.j2: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: argoproj.io/v1alpha1 3 | kind: Application 4 | metadata: 5 | name: metrics-server 6 | namespace: argo-system 7 | annotations: 8 | argocd.argoproj.io/sync-wave: '0' 9 | spec: 10 | project: kubernetes 11 | sources: 12 | - repoURL: "https://github.com/#{ repository_name }#.git" 13 | path: kubernetes/apps/kube-system/metrics-server 14 | targetRevision: #{ repository_branch }# 15 | ref: repo 16 | - repoURL: https://kubernetes-sigs.github.io/metrics-server 17 | chart: metrics-server 18 | targetRevision: 3.13.0 19 | helm: 20 | releaseName: metrics-server 21 | valueFiles: 22 | - $repo/kubernetes/apps/kube-system/metrics-server/values.yaml 23 | destination: 24 | name: in-cluster 25 | namespace: kube-system 26 | syncPolicy: 27 | automated: 28 | allowEmpty: true 29 | prune: true 30 | selfHeal: true 31 | retry: 32 | limit: 1 33 | backoff: 34 | duration: 10s 35 | factor: 2 36 | maxDuration: 3m 37 | syncOptions: 38 | - CreateNamespace=true 39 | - ApplyOutOfSyncOnly=true 40 | - ServerSideApply=true 41 | - PruneLast=true 42 | - RespectIgnoreDifferences=true 43 | -------------------------------------------------------------------------------- /.taskfiles/bootstrap/Taskfile.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | version: '3' 3 | 4 | tasks: 5 | 6 | talos: 7 | desc: Bootstrap the Talos cluster 8 | dir: '{{.TALOS_DIR}}' 9 | cmds: 10 | - '[ -f talsecret.sops.yaml ] || talhelper gensecret | sops --filename-override talos/talsecret.sops.yaml --encrypt /dev/stdin > talsecret.sops.yaml' 11 | - talhelper genconfig 12 | - talhelper gencommand apply --extra-flags="--insecure" | bash 13 | - until talhelper gencommand bootstrap | bash; do sleep 10; done 14 | - until talhelper gencommand kubeconfig --extra-flags="{{.ROOT_DIR}} --force" | bash; do sleep 10; done 15 | preconditions: 16 | - test -f {{.ROOT_DIR}}/.sops.yaml 17 | - test -f {{.SOPS_AGE_KEY_FILE}} 18 | - test -f {{.TALOS_DIR}}/talconfig.yaml 19 | - which talhelper talosctl sops 20 | 21 | apps: 22 | desc: Bootstrap apps into the Talos cluster 23 | cmd: bash {{.SCRIPTS_DIR}}/bootstrap-apps.sh 24 | preconditions: 25 | - msg: Unsupported bash version, run `brew install bash` to upgrade 26 | sh: '{{if eq OS "darwin"}}test -f /opt/homebrew/bin/bash || test -f /usr/local/bin/bash{{end}}' 27 | - test -f {{.KUBECONFIG}} 28 | - test -f {{.ROOT_DIR}}/.sops.yaml 29 | - test -f {{.SCRIPTS_DIR}}/bootstrap-apps.sh 30 | - test -f {{.SOPS_AGE_KEY_FILE}} 31 | -------------------------------------------------------------------------------- /.taskfiles/template/resources/cluster.schema.cue: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "net" 5 | ) 6 | 7 | #Config: { 8 | node_cidr: net.IPCIDR & !=cluster_pod_cidr & !=cluster_svc_cidr 9 | node_dns_servers?: [...net.IPv4] 10 | node_ntp_servers?: [...net.IPv4] 11 | node_default_gateway?: net.IPv4 & !="" 12 | node_vlan_tag?: string & !="" 13 | cluster_pod_cidr: *"10.42.0.0/16" | net.IPCIDR & !=node_cidr & !=cluster_svc_cidr 14 | cluster_svc_cidr: *"10.43.0.0/16" | net.IPCIDR & !=node_cidr & !=cluster_pod_cidr 15 | cluster_api_addr: net.IPv4 16 | cluster_api_tls_sans?: [...net.FQDN] 17 | cluster_gateway_addr: net.IPv4 & !=cluster_api_addr & !=cluster_dns_gateway_addr & !=cloudflare_gateway_addr 18 | cluster_dns_gateway_addr: net.IPv4 & !=cluster_api_addr & !=cluster_gateway_addr & !=cloudflare_gateway_addr 19 | repository_name: string 20 | repository_branch?: string & !="" 21 | repository_visibility?: *"public" | "private" 22 | cloudflare_domain: net.FQDN 23 | cloudflare_token: string 24 | cloudflare_gateway_addr: net.IPv4 & !=cluster_api_addr & !=cluster_gateway_addr & !=cluster_dns_gateway_addr 25 | cilium_bgp_router_addr?: net.IPv4 & !="" 26 | cilium_bgp_router_asn?: string & !="" 27 | cilium_bgp_node_asn?: string & !="" 28 | cilium_loadbalancer_mode?: *"dsr" | "snat" 29 | argo_password: string 30 | } 31 | 32 | #Config 33 | -------------------------------------------------------------------------------- /.mise.toml: -------------------------------------------------------------------------------- 1 | [env] 2 | _.python.venv = { path = "{{config_root}}/.venv", create = true } 3 | KUBECONFIG = "{{config_root}}/kubeconfig" 4 | SOPS_AGE_KEY_FILE = "{{config_root}}/age.key" 5 | TALOSCONFIG = "{{config_root}}/talos/clusterconfig/talosconfig" 6 | 7 | [tasks.deps] 8 | description = "Install Helm dependencies" 9 | run = [ 10 | "helm plugin list | grep -q 'secrets' && helm plugin update secrets || helm plugin install https://github.com/jkroepke/helm-secrets", 11 | "helm plugin list | grep -q 'diff' && helm plugin update diff || helm plugin install https://github.com/databus23/helm-diff" 12 | ] 13 | 14 | [tools] 15 | "python" = "3.14.2" 16 | "pipx:makejinja" = "2.8.2" 17 | "aqua:argoproj/argo-cd" = "3.2.2" 18 | "aqua:budimanjojo/talhelper" = "3.0.43" 19 | "aqua:cilium/cilium-cli" = "0.18.9" 20 | "aqua:cli/cli" = "2.83.2" 21 | "aqua:cloudflare/cloudflared" = "2025.11.1" 22 | "aqua:cue-lang/cue" = "0.15.1" 23 | "aqua:FiloSottile/age" = "1.2.1" 24 | "aqua:getsops/sops" = "3.11.0" 25 | "aqua:go-task/task" = "3.46.3" 26 | "aqua:helm/helm" = "4.0.4" 27 | "aqua:helmfile/helmfile" = "1.2.3" 28 | "aqua:jqlang/jq" = "1.7.1" 29 | "aqua:kubernetes-sigs/kustomize" = "5.6.0" 30 | "aqua:kubernetes/kubectl" = "1.33.1" 31 | "aqua:mikefarah/yq" = "4.50.1" 32 | "aqua:siderolabs/talos" = "1.11.6" 33 | "aqua:yannh/kubeconform" = "0.7.0" 34 | "asdf:janpieper/asdf-ksops" = "4.3.3" 35 | -------------------------------------------------------------------------------- /templates/config/kubernetes/argo/apps/argo-system/argo-cd.yaml.j2: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: argoproj.io/v1alpha1 3 | kind: Application 4 | metadata: 5 | name: argo 6 | namespace: argo-system 7 | annotations: 8 | argocd.argoproj.io/sync-wave: '0' 9 | labels: 10 | argocd.argoproj.io/instance: argo 11 | spec: 12 | project: kubernetes 13 | sources: 14 | - repoURL: "https://github.com/#{ repository_name }#.git" 15 | path: kubernetes/apps/argo-system/argo-cd 16 | targetRevision: #{ repository_branch }# 17 | ref: repo 18 | - repoURL: ghcr.io/argoproj/argo-helm 19 | chart: argo-cd 20 | targetRevision: 9.1.3 21 | helm: 22 | releaseName: argo-cd 23 | valueFiles: 24 | - $repo/kubernetes/apps/argo-system/argo-cd/values.yaml 25 | - $repo/kubernetes/apps/argo-system/argo-cd/values.sops.yaml 26 | destination: 27 | name: in-cluster 28 | namespace: argo-system 29 | syncPolicy: 30 | automated: 31 | allowEmpty: true 32 | prune: true 33 | selfHeal: true 34 | retry: 35 | limit: 1 36 | backoff: 37 | duration: 10s 38 | factor: 2 39 | maxDuration: 3m 40 | syncOptions: 41 | - CreateNamespace=true 42 | - ApplyOutOfSyncOnly=true 43 | - ServerSideApply=true 44 | - PruneLast=true 45 | - RespectIgnoreDifferences=true 46 | -------------------------------------------------------------------------------- /templates/config/bootstrap/helmfile.d/01-apps.yaml.j2: -------------------------------------------------------------------------------- 1 | --- 2 | 3 | helmDefaults: 4 | cleanupOnFail: true 5 | wait: true 6 | waitForJobs: true 7 | 8 | releases: 9 | - name: cilium 10 | namespace: kube-system 11 | chart: oci://ghcr.io/home-operations/charts-mirror/cilium 12 | version: 1.18.4 13 | values: ['../../kubernetes/apps/kube-system/cilium/values.yaml'] 14 | 15 | - name: coredns 16 | namespace: kube-system 17 | chart: oci://ghcr.io/coredns/charts/coredns 18 | version: 1.45.0 19 | values: ['../../kubernetes/apps/kube-system/coredns/values.yaml'] 20 | 21 | #% if spegel_enabled %# 22 | - name: spegel 23 | namespace: kube-system 24 | chart: oci://ghcr.io/spegel-org/helm-charts/spegel 25 | version: 0.5.1 26 | values: ['../../kubernetes/apps/kube-system/spegel/values.yaml'] 27 | needs: ['kube-system/coredns'] 28 | #% endif %# 29 | 30 | - name: cert-manager 31 | namespace: cert-manager 32 | chart: oci://quay.io/jetstack/charts/cert-manager 33 | version: v1.19.1 34 | values: ['../../kubernetes/apps/cert-manager/cert-manager/values.yaml'] 35 | #% if spegel_enabled %# 36 | needs: ['kube-system/spegel'] 37 | #% else %# 38 | needs: ['kube-system/coredns'] 39 | #% endif %# 40 | 41 | - name: argo-cd 42 | namespace: argo-system 43 | chart: oci://ghcr.io/argoproj/argo-helm/argo-cd 44 | version: 9.1.4 45 | values: ['../../kubernetes/apps/argo-system/argo-cd/values.yaml'] 46 | secrets: ['../../kubernetes/apps/argo-system/argo-cd/values.sops.yaml'] 47 | 48 | -------------------------------------------------------------------------------- /templates/config/kubernetes/apps/kube-system/coredns/values.yaml.j2: -------------------------------------------------------------------------------- 1 | --- 2 | fullnameOverride: coredns 3 | image: 4 | repository: mirror.gcr.io/coredns/coredns 5 | k8sAppLabelOverride: kube-dns 6 | serviceAccount: 7 | create: true 8 | service: 9 | name: kube-dns 10 | clusterIP: "#{ cluster_svc_cidr | nthhost(10) }#" 11 | replicaCount: 2 12 | servers: 13 | - zones: 14 | - zone: . 15 | scheme: dns:// 16 | use_tcp: true 17 | port: 53 18 | plugins: 19 | - name: errors 20 | - name: health 21 | configBlock: |- 22 | lameduck 5s 23 | - name: ready 24 | - name: kubernetes 25 | parameters: cluster.local in-addr.arpa ip6.arpa 26 | configBlock: |- 27 | pods verified 28 | fallthrough in-addr.arpa ip6.arpa 29 | - name: autopath 30 | parameters: "@kubernetes" 31 | - name: forward 32 | parameters: . /etc/resolv.conf 33 | - name: cache 34 | configBlock: |- 35 | prefetch 20 36 | serve_stale 37 | - name: loop 38 | - name: reload 39 | - name: loadbalance 40 | - name: prometheus 41 | parameters: 0.0.0.0:9153 42 | - name: log 43 | configBlock: |- 44 | class error 45 | affinity: 46 | nodeAffinity: 47 | requiredDuringSchedulingIgnoredDuringExecution: 48 | nodeSelectorTerms: 49 | - matchExpressions: 50 | - key: node-role.kubernetes.io/control-plane 51 | operator: Exists 52 | tolerations: 53 | - key: CriticalAddonsOnly 54 | operator: Exists 55 | - key: node-role.kubernetes.io/control-plane 56 | operator: Exists 57 | effect: NoSchedule 58 | -------------------------------------------------------------------------------- /templates/config/kubernetes/apps/default/echo/values.sops.yaml.j2: -------------------------------------------------------------------------------- 1 | controllers: 2 | echo: 3 | strategy: RollingUpdate 4 | containers: 5 | app: 6 | image: 7 | repository: ghcr.io/mendhak/http-https-echo 8 | tag: 37 9 | env: 10 | HTTP_PORT: &port 8080 11 | LOG_WITHOUT_NEWLINE: true 12 | LOG_IGNORE_PATH: /healthz 13 | PROMETHEUS_ENABLED: true 14 | probes: 15 | liveness: &probes 16 | enabled: true 17 | custom: true 18 | spec: 19 | httpGet: 20 | path: /healthz 21 | port: *port 22 | initialDelaySeconds: 0 23 | periodSeconds: 10 24 | timeoutSeconds: 1 25 | failureThreshold: 3 26 | readiness: *probes 27 | securityContext: 28 | allowPrivilegeEscalation: false 29 | readOnlyRootFilesystem: true 30 | capabilities: { drop: ["ALL"] } 31 | resources: 32 | requests: 33 | cpu: 10m 34 | limits: 35 | memory: 64Mi 36 | defaultPodOptions: 37 | securityContext: 38 | runAsNonRoot: true 39 | runAsUser: 65534 40 | runAsGroup: 65534 41 | service: 42 | app: 43 | controller: echo 44 | ports: 45 | http: 46 | port: *port 47 | serviceMonitor: 48 | app: 49 | serviceName: echo 50 | endpoints: 51 | - port: http 52 | route: 53 | app: 54 | hostnames: ["echo.#{ cloudflare_domain }#"] 55 | parentRefs: 56 | - name: envoy-external 57 | namespace: kube-system 58 | sectionName: https 59 | rules: 60 | - backendRefs: 61 | - identifier: app 62 | port: *port 63 | -------------------------------------------------------------------------------- /templates/config/kubernetes/argo/apps/kube-system/cilium.yaml.j2: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: argoproj.io/v1alpha1 3 | kind: Application 4 | metadata: 5 | name: cilium 6 | namespace: argo-system 7 | annotations: 8 | argocd.argoproj.io/sync-wave: '0' 9 | spec: 10 | project: kubernetes 11 | sources: 12 | - repoURL: "https://github.com/#{ repository_name }#.git" 13 | path: kubernetes/apps/kube-system/cilium 14 | targetRevision: #{ repository_branch }# 15 | ref: repo 16 | - repoURL: https://helm.cilium.io 17 | chart: cilium 18 | targetRevision: 1.18.4 19 | helm: 20 | releaseName: cilium 21 | valueFiles: 22 | - $repo/kubernetes/apps/kube-system/cilium/values.yaml 23 | destination: 24 | name: in-cluster 25 | namespace: kube-system 26 | syncPolicy: 27 | automated: 28 | allowEmpty: true 29 | prune: true 30 | selfHeal: true 31 | retry: 32 | limit: 1 33 | backoff: 34 | duration: 10s 35 | factor: 2 36 | maxDuration: 3m 37 | syncOptions: 38 | - CreateNamespace=true 39 | - ApplyOutOfSyncOnly=true 40 | - ServerSideApply=true 41 | - PruneLast=true 42 | - RespectIgnoreDifferences=true 43 | ignoreDifferences: 44 | - group: "" 45 | kind: ConfigMap 46 | name: hubble-ca-cert 47 | jsonPointers: 48 | - /data/ca.crt 49 | - group: "" 50 | kind: Secret 51 | name: hubble-relay-client-certs 52 | jsonPointers: 53 | - /data/ca.crt 54 | - /data/tls.crt 55 | - /data/tls.key 56 | - group: "" 57 | kind: Secret 58 | name: hubble-server-certs 59 | jsonPointers: 60 | - /data/ca.crt 61 | - /data/tls.crt 62 | - /data/tls.key 63 | -------------------------------------------------------------------------------- /templates/config/kubernetes/apps/kube-system/cilium/config/networks.yaml.j2: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: cilium.io/v2alpha1 3 | kind: CiliumLoadBalancerIPPool 4 | metadata: 5 | name: pool 6 | spec: 7 | allowFirstLastIPs: "No" 8 | blocks: 9 | - cidr: "#{ node_cidr }#" 10 | --- 11 | apiVersion: cilium.io/v2alpha1 12 | kind: CiliumL2AnnouncementPolicy 13 | metadata: 14 | name: l2-policy 15 | spec: 16 | loadBalancerIPs: true 17 | # NOTE: interfaces might need to be set if you have more than one active NIC on your hosts 18 | # interfaces: 19 | # - ^eno[0-9]+ 20 | # - ^eth[0-9]+ 21 | nodeSelector: 22 | matchLabels: 23 | kubernetes.io/os: linux 24 | #% if cilium_bgp_enabled %# 25 | --- 26 | apiVersion: cilium.io/v2alpha1 27 | kind: CiliumBGPAdvertisement 28 | metadata: 29 | name: bgp-advertisement-config 30 | labels: 31 | advertise: bgp 32 | spec: 33 | advertisements: 34 | - advertisementType: Service 35 | service: 36 | addresses: 37 | - LoadBalancerIP 38 | selector: 39 | matchExpressions: 40 | - { key: somekey, operator: NotIn, values: ["never-used-value"] } 41 | --- 42 | apiVersion: cilium.io/v2alpha1 43 | kind: CiliumBGPPeerConfig 44 | metadata: 45 | name: bgp-peer-config-v4 46 | spec: 47 | families: 48 | - afi: ipv4 49 | safi: unicast 50 | advertisements: 51 | matchLabels: 52 | advertise: bgp 53 | --- 54 | apiVersion: cilium.io/v2alpha1 55 | kind: CiliumBGPClusterConfig 56 | metadata: 57 | name: bgp-cluster-config 58 | spec: 59 | nodeSelector: 60 | matchLabels: 61 | kubernetes.io/os: linux 62 | bgpInstances: 63 | - name: instance-#{ cilium_bgp_node_asn }# 64 | localASN: #{ cilium_bgp_node_asn }# 65 | peers: 66 | - name: peer-#{ cilium_bgp_router_asn }#-v4 67 | peerASN: #{ cilium_bgp_router_asn }# 68 | peerAddress: #{ cilium_bgp_router_addr }# 69 | peerConfigRef: 70 | name: bgp-peer-config-v4 71 | #% endif %# 72 | -------------------------------------------------------------------------------- /templates/config/kubernetes/apps/network/cloudflare-tunnel/values.yaml.j2: -------------------------------------------------------------------------------- 1 | --- 2 | controllers: 3 | cloudflare-tunnel: 4 | strategy: RollingUpdate 5 | annotations: 6 | reloader.stakater.com/auto: "true" 7 | containers: 8 | app: 9 | image: 10 | repository: docker.io/cloudflare/cloudflared 11 | tag: 2025.11.1 12 | env: 13 | NO_AUTOUPDATE: true 14 | TUNNEL_METRICS: 0.0.0.0:8080 15 | TUNNEL_POST_QUANTUM: true # disable when using http2 16 | TUNNEL_TRANSPORT_PROTOCOL: quic # or http2 17 | envFrom: 18 | - secretRef: 19 | name: cloudflare-tunnel-secret 20 | args: ["tunnel", "run"] 21 | probes: 22 | liveness: &probes 23 | enabled: true 24 | custom: true 25 | spec: 26 | httpGet: 27 | path: /ready 28 | port: &port 8080 29 | initialDelaySeconds: 0 30 | periodSeconds: 10 31 | timeoutSeconds: 1 32 | failureThreshold: 3 33 | readiness: *probes 34 | securityContext: 35 | allowPrivilegeEscalation: false 36 | readOnlyRootFilesystem: true 37 | capabilities: { drop: ["ALL"] } 38 | resources: 39 | requests: 40 | cpu: 10m 41 | limits: 42 | memory: 256Mi 43 | defaultPodOptions: 44 | securityContext: 45 | runAsNonRoot: true 46 | runAsUser: 65534 47 | runAsGroup: 65534 48 | service: 49 | app: 50 | controller: cloudflare-tunnel 51 | ports: 52 | http: 53 | port: *port 54 | serviceMonitor: 55 | app: 56 | serviceName: cloudflare-tunnel 57 | endpoints: 58 | - port: http 59 | persistence: 60 | config-file: 61 | type: configMap 62 | name: cloudflare-tunnel-configmap 63 | globalMounts: 64 | - path: /etc/cloudflared/config.yaml 65 | subPath: config.yaml 66 | readOnly: true 67 | -------------------------------------------------------------------------------- /.taskfiles/template/resources/kubeconform.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -euo pipefail 4 | 5 | KUBERNETES_DIR=$1 6 | 7 | [[ -z "${KUBERNETES_DIR}" ]] && echo "Kubernetes location not specified" && exit 1 8 | 9 | kustomize_args=( 10 | "--load-restrictor=LoadRestrictionsNone" 11 | "--enable-helm" 12 | "--enable-alpha-plugins" 13 | "--enable-exec" 14 | ) 15 | kustomize_config="kustomization.yaml" 16 | kubeconform_args=( 17 | "-strict" 18 | "-ignore-missing-schemas" 19 | "-skip" 20 | "Gateway,HTTPRoute,Secret" 21 | "-schema-location" 22 | "default" 23 | "-schema-location" 24 | "https://kubernetes-schemas.pages.dev/{{.Group}}/{{.ResourceKind}}_{{.ResourceAPIVersion}}.json" 25 | "-verbose" 26 | ) 27 | 28 | echo "=== Validating standalone manifests in ${KUBERNETES_DIR}/argo ===" 29 | find "${KUBERNETES_DIR}/argo" -maxdepth 1 -type f -name '*.yaml' -print0 | while IFS= read -r -d $'\0' file; 30 | do 31 | kubeconform "${kubeconform_args[@]}" "${file}" 32 | if [[ ${PIPESTATUS[0]} != 0 ]]; then 33 | exit 1 34 | fi 35 | done 36 | 37 | echo "=== Validating kustomizations in ${KUBERNETES_DIR}/argo ===" 38 | find "${KUBERNETES_DIR}/argo" -type f -name $kustomize_config -print0 | while IFS= read -r -d $'\0' file; 39 | do 40 | echo "=== Validating kustomizations in ${file/%$kustomize_config} ===" 41 | kustomize build "${file/%$kustomize_config}" "${kustomize_args[@]}" | kubeconform "${kubeconform_args[@]}" 42 | if [[ ${PIPESTATUS[0]} != 0 ]]; then 43 | exit 1 44 | fi 45 | done 46 | 47 | echo "=== Validating kustomizations in ${KUBERNETES_DIR}/apps ===" 48 | find "${KUBERNETES_DIR}/apps" -type f -name $kustomize_config -print0 | while IFS= read -r -d $'\0' file; 49 | do 50 | echo "=== Validating kustomizations in ${file/%$kustomize_config} ===" 51 | kustomize build "${file/%$kustomize_config}" "${kustomize_args[@]}" | kubeconform "${kubeconform_args[@]}" 52 | if [[ ${PIPESTATUS[0]} != 0 ]]; then 53 | exit 1 54 | fi 55 | done 56 | -------------------------------------------------------------------------------- /templates/config/kubernetes/apps/kube-system/cilium/values.yaml.j2: -------------------------------------------------------------------------------- 1 | --- 2 | autoDirectNodeRoutes: true 3 | bpf: 4 | masquerade: true 5 | # Ref: https://github.com/siderolabs/talos/issues/10002 6 | hostLegacyRouting: true 7 | #% if cilium_bgp_enabled %# 8 | bgpControlPlane: 9 | enabled: true 10 | #% endif %# 11 | cni: 12 | # Required for pairing with Multus CNI 13 | exclusive: false 14 | cgroup: 15 | automount: 16 | enabled: false 17 | hostRoot: /sys/fs/cgroup 18 | # NOTE: devices might need to be set if you have more than one active NIC on your hosts 19 | # devices: eno+ eth+ 20 | dashboards: 21 | enabled: true 22 | endpointRoutes: 23 | enabled: true 24 | envoy: 25 | rollOutPods: true 26 | prometheus: 27 | serviceMonitor: 28 | enabled: true 29 | gatewayAPI: 30 | enabled: true 31 | hubble: 32 | enabled: false 33 | ipam: 34 | mode: kubernetes 35 | ipv4NativeRoutingCIDR: "#{ cluster_pod_cidr }#" 36 | k8sServiceHost: 127.0.0.1 37 | k8sServicePort: 7445 38 | kubeProxyReplacement: true 39 | kubeProxyReplacementHealthzBindAddr: 0.0.0.0:10256 40 | l2announcements: 41 | enabled: true 42 | loadBalancer: 43 | algorithm: maglev 44 | mode: "#{ cilium_loadbalancer_mode }#" 45 | localRedirectPolicy: true 46 | operator: 47 | dashboards: 48 | enabled: true 49 | prometheus: 50 | enabled: true 51 | serviceMonitor: 52 | enabled: true 53 | replicas: 1 54 | rollOutPods: true 55 | prometheus: 56 | enabled: true 57 | serviceMonitor: 58 | enabled: true 59 | trustCRDsExist: true 60 | rollOutCiliumPods: true 61 | routingMode: native 62 | securityContext: 63 | capabilities: 64 | ciliumAgent: 65 | - CHOWN 66 | - KILL 67 | - NET_ADMIN 68 | - NET_RAW 69 | - IPC_LOCK 70 | - SYS_ADMIN 71 | - SYS_RESOURCE 72 | - PERFMON 73 | - BPF 74 | - DAC_OVERRIDE 75 | - FOWNER 76 | - SETGID 77 | - SETUID 78 | cleanCiliumState: 79 | - NET_ADMIN 80 | - SYS_ADMIN 81 | - SYS_RESOURCE 82 | socketLB: 83 | hostNamespaceOnly: true 84 | -------------------------------------------------------------------------------- /.github/workflows/e2e.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | name: "e2e" 3 | 4 | on: 5 | workflow_dispatch: 6 | pull_request: 7 | branches: ["main"] 8 | paths-ignore: 9 | - kubernetes/** 10 | 11 | concurrency: 12 | group: ${{ github.workflow }}-${{ github.event.number || github.ref }} 13 | cancel-in-progress: true 14 | 15 | jobs: 16 | configure: 17 | if: ${{ github.repository == 'ajaykumar4/cluster-template' }} 18 | name: configure 19 | runs-on: ubuntu-latest 20 | permissions: 21 | contents: write 22 | strategy: 23 | fail-fast: false 24 | matrix: 25 | config-files: 26 | - public 27 | - private 28 | steps: 29 | - name: Checkout 30 | uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 31 | 32 | - name: Setup mise 33 | uses: jdx/mise-action@146a28175021df8ca24f8ee1828cc2a60f980bd5 # v3.5.1 34 | env: 35 | GITHUB_TOKEN: "${{ secrets.GITHUB_TOKEN }}" 36 | with: 37 | cache: false 38 | 39 | - name: Run init task 40 | run: task init 41 | 42 | - name: Prepare files 43 | run: | 44 | cp ./.github/tests/${{ matrix.config-files }}.yaml cluster.yaml 45 | cp ./.github/tests/nodes.yaml nodes.yaml 46 | echo '{"AccountTag":"fake","TunnelSecret":"fake","TunnelID":"fake"}' > cloudflare-tunnel.json 47 | touch kubeconfig 48 | 49 | - name: Run configure task 50 | run: task configure --yes 51 | 52 | - name: Run generate talconfig task 53 | run: | 54 | FILENAME=talos/talsecret.sops.yaml 55 | talhelper gensecret | sops --filename-override $FILENAME --encrypt /dev/stdin > $FILENAME 56 | task talos:generate-config 57 | 58 | - name: Dry run bootstrap talos task 59 | run: task bootstrap:talos --dry 60 | 61 | - name: Dry run bootstrap apps task 62 | run: task bootstrap:apps --dry 63 | 64 | - name: Run reset task 65 | run: task template:reset --yes 66 | 67 | - name: Run cleanup task 68 | run: task template:tidy --yes 69 | -------------------------------------------------------------------------------- /.github/workflows/release.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | name: "Release" 3 | 4 | on: 5 | workflow_dispatch: 6 | schedule: 7 | - cron: "0 0 1 * *" # 1st of every month at midnight 8 | 9 | jobs: 10 | release: 11 | name: Release 12 | runs-on: ubuntu-latest 13 | permissions: 14 | contents: write 15 | steps: 16 | - name: Get Previous Release Tag and Determine Next Tag 17 | id: determine-next-tag 18 | uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 19 | with: 20 | github-token: "${{ secrets.GITHUB_TOKEN }}" 21 | result-encoding: string 22 | script: | 23 | const { data: releases } = await github.rest.repos.listReleases({ 24 | owner: context.repo.owner, 25 | repo: context.repo.repo, 26 | per_page: 1, 27 | }); 28 | 29 | let previousTag = "0.0.0"; // Default if no previous release exists 30 | if (releases.length > 0) { 31 | previousTag = releases[0].tag_name; 32 | } 33 | 34 | const [previousMajor, previousMinor, previousPatch] = previousTag.split('.').map(Number); 35 | const currentYear = new Date().getFullYear(); 36 | const currentMonth = new Date().getMonth() + 1; // Months are 0-indexed in JavaScript 37 | 38 | const nextMajorMinor = `${currentYear}.${currentMonth}`; 39 | let nextPatch; 40 | 41 | if (`${previousMajor}.${previousMinor}` === nextMajorMinor) { 42 | console.log("Month release already exists for the year. Incrementing patch number by 1."); 43 | nextPatch = previousPatch + 1; 44 | } else { 45 | console.log("Month release does not exist for the year. Starting with patch number 0."); 46 | nextPatch = 0; 47 | } 48 | 49 | return `${nextMajorMinor}.${nextPatch}`; 50 | 51 | - name: Create Release 52 | uses: ncipollo/release-action@b7eabc95ff50cbeeedec83973935c8f306dfcd0b # v1.20.0 53 | with: 54 | generateReleaseNotes: true 55 | tag: "${{ steps.determine-next-tag.outputs.result }}" 56 | token: "${{ secrets.GITHUB_TOKEN }}" 57 | -------------------------------------------------------------------------------- /.taskfiles/talos/Taskfile.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | version: '3' 3 | 4 | tasks: 5 | 6 | generate-config: 7 | desc: Generate Talos configuration 8 | dir: '{{.TALOS_DIR}}' 9 | cmd: talhelper genconfig 10 | preconditions: 11 | - test -f {{.TALOS_DIR}}/talconfig.yaml 12 | - test -f {{.ROOT_DIR}}/.sops.yaml 13 | - test -f {{.SOPS_AGE_KEY_FILE}} 14 | - which talhelper 15 | 16 | apply-node: 17 | desc: Apply Talos config to a node [IP=required] 18 | dir: '{{.TALOS_DIR}}' 19 | cmd: talhelper gencommand apply --node {{.IP}} --extra-flags '--mode={{.MODE}}' | bash 20 | vars: 21 | MODE: '{{.MODE | default "auto"}}' 22 | requires: 23 | vars: [IP] 24 | preconditions: 25 | - talosctl --nodes {{.IP}} get machineconfig 26 | - talosctl config info 27 | - test -f {{.TALOSCONFIG}} 28 | - which talhelper talosctl yq 29 | 30 | upgrade-node: 31 | desc: Upgrade Talos on a single node [IP=required] 32 | dir: '{{.TALOS_DIR}}' 33 | cmd: talhelper gencommand upgrade --node {{.IP}} --extra-flags "--image='{{.TALOS_IMAGE}}:{{.TALOS_VERSION}}' --timeout=10m" | bash 34 | vars: 35 | TALOS_IMAGE: 36 | sh: yq '.nodes[] | select(.ipAddress == "{{.IP}}") | .talosImageURL' {{.TALOS_DIR}}/talconfig.yaml 37 | TALOS_VERSION: 38 | sh: yq '.talosVersion' {{.TALOS_DIR}}/talenv.yaml 39 | requires: 40 | vars: [IP] 41 | preconditions: 42 | - talosctl --nodes {{.IP}} get machineconfig 43 | - talosctl config info 44 | - test -f {{.TALOSCONFIG}} 45 | - which kubectl talhelper talosctl yq 46 | 47 | upgrade-k8s: 48 | desc: Upgrade Kubernetes 49 | dir: '{{.TALOS_DIR}}' 50 | cmd: talhelper gencommand upgrade-k8s --extra-flags "--to '{{.KUBERNETES_VERSION}}'" | bash 51 | vars: 52 | KUBERNETES_VERSION: 53 | sh: yq '.kubernetesVersion' {{.TALOS_DIR}}/talenv.yaml 54 | preconditions: 55 | - talosctl config info 56 | - test -f {{.TALOSCONFIG}} 57 | - which talhelper talosctl yq 58 | 59 | reset: 60 | desc: Resets nodes back to maintenance mode 61 | dir: '{{.TALOS_DIR}}' 62 | prompt: This will destroy your cluster and reset the nodes back to maintenance mode... continue? 63 | cmd: talhelper gencommand reset --extra-flags="--reboot {{- if eq .CLI_FORCE false }} --system-labels-to-wipe STATE --system-labels-to-wipe EPHEMERAL{{ end }} --graceful=false --wait=false" | bash 64 | preconditions: 65 | - which talhelper 66 | -------------------------------------------------------------------------------- /scripts/lib/common.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -Eeuo pipefail 3 | 4 | # Log messages with different levels 5 | function log() { 6 | local level="${1:-info}" 7 | shift 8 | 9 | # Define log levels with their priorities 10 | local -A level_priority=( 11 | [debug]=1 12 | [info]=2 13 | [warn]=3 14 | [error]=4 15 | ) 16 | 17 | # Get the current log level's priority 18 | local current_priority=${level_priority[$level]:-2} # Default to "info" priority 19 | 20 | # Get the configured log level from the environment, default to "info" 21 | local configured_level=${LOG_LEVEL:-info} 22 | local configured_priority=${level_priority[$configured_level]:-2} 23 | 24 | # Skip log messages below the configured log level 25 | if ((current_priority < configured_priority)); then 26 | return 27 | fi 28 | 29 | # Define log colors 30 | local -A colors=( 31 | [debug]="\033[1m\033[38;5;63m" # Blue 32 | [info]="\033[1m\033[38;5;87m" # Cyan 33 | [warn]="\033[1m\033[38;5;192m" # Yellow 34 | [error]="\033[1m\033[38;5;198m" # Red 35 | ) 36 | 37 | # Fallback to "info" if the color for the given level is not defined 38 | local color="${colors[$level]:-${colors[info]}}" 39 | local msg="$1" 40 | shift 41 | 42 | # Prepare additional data 43 | local data= 44 | if [[ $# -gt 0 ]]; then 45 | for item in "$@"; do 46 | if [[ "${item}" == *=* ]]; then 47 | data+="\033[1m\033[38;5;236m${item%%=*}=\033[0m\"${item#*=}\" " 48 | else 49 | data+="${item} " 50 | fi 51 | done 52 | fi 53 | 54 | # Determine output stream based on log level 55 | local output_stream="/dev/stdout" 56 | if [[ "$level" == "error" ]]; then 57 | output_stream="/dev/stderr" 58 | fi 59 | 60 | # Print the log message 61 | printf "%s %b%s%b %s %b\n" "$(date -u +"%Y-%m-%dT%H:%M:%SZ")" \ 62 | "${color}" "${level^^}" "\033[0m" "${msg}" "${data}" >"${output_stream}" 63 | 64 | # Exit if the log level is error 65 | if [[ "$level" == "error" ]]; then 66 | exit 1 67 | fi 68 | } 69 | 70 | # Check if required environment variables are set 71 | function check_env() { 72 | local envs=("${@}") 73 | local missing=() 74 | local values=() 75 | 76 | for env in "${envs[@]}"; do 77 | if [[ -z "${!env-}" ]]; then 78 | missing+=("${env}") 79 | else 80 | values+=("${env}=${!env}") 81 | fi 82 | done 83 | 84 | if [ ${#missing[@]} -ne 0 ]; then 85 | log error "Missing required env variables" "envs=${missing[*]}" 86 | fi 87 | 88 | log debug "Env variables are set" "envs=${values[*]}" 89 | } 90 | 91 | # Check if required CLI tools are installed 92 | function check_cli() { 93 | local deps=("${@}") 94 | local missing=() 95 | 96 | for dep in "${deps[@]}"; do 97 | if ! command -v "${dep}" &>/dev/null; then 98 | missing+=("${dep}") 99 | fi 100 | done 101 | 102 | if [ ${#missing[@]} -ne 0 ]; then 103 | log error "Missing required deps" "deps=${missing[*]}" 104 | fi 105 | 106 | log debug "Deps are installed" "deps=${deps[*]}" 107 | } 108 | -------------------------------------------------------------------------------- /cluster.sample.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | # -- The network CIDR for the nodes. 3 | # (REQUIRED) / (e.g. 192.168.1.0/24) 4 | node_cidr: "" 5 | 6 | # -- DNS servers to use for the cluster. 7 | # (OPTIONAL) / (DEFAULT: ["1.1.1.1", "1.0.0.1"]) / (Cloudflare DNS) 8 | # node_dns_servers: [] 9 | 10 | # -- NTP servers to use for the cluster. 11 | # (OPTIONAL) / (DEFAULT: ["162.159.200.1", "162.159.200.123"]) / (Cloudflare NTP) 12 | # node_ntp_servers: [] 13 | 14 | # -- The default gateway for the nodes. 15 | # (OPTIONAL) / (DEFAULT: the first IP in the node_cidr) 16 | # node_default_gateway: "" 17 | 18 | # -- Attach a vlan tag to the Talos nodes. Not needed if ports on your switch are tagged or you are not using VLANs. 19 | # (OPTIONAL) / (REF: https://www.talos.dev/latest/advanced/advanced-networking/#vlans) 20 | # node_vlan_tag: "" 21 | 22 | # -- The IP address of the Kube API. 23 | # (REQUIRED) / (NOTE: Choose an unused IP in node_cidr) 24 | cluster_api_addr: "" 25 | 26 | # -- Additional SANs to add to the Kube API cert. This is useful if you want to call the Kube API by hostname rather than IP 27 | # (OPTIONAL) / (e.g. ["mycluster.example.com"]) 28 | # cluster_api_tls_sans: [] 29 | 30 | # -- The pod CIDR for the cluster, this must NOT overlap with any existing networks and should be a /16 (64K IPs). 31 | # (OPTIONAL) / (DEFAULT: "10.42.0.0/16") 32 | # cluster_pod_cidr: "" 33 | 34 | # -- The service CIDR for the cluster, this must NOT overlap with any existing networks and should be a /16 (64K IPs). 35 | # (OPTIONAL) / (DEFAULT: "10.43.0.0/16") 36 | # cluster_svc_cidr: "" 37 | 38 | # -- The Load balancer IP for k8s_gateway, this provides DNS to all your gateways when split DNS is configured on your internal DNS server (Dnsmasq, Pi-hole, etc) 39 | # (REQUIRED) / (NOTE: Choose an unused IP in node_cidr) 40 | cluster_dns_gateway_addr: "" 41 | 42 | # -- The Load balancer IP for the internal gateway 43 | # (REQUIRED) / (NOTE: Choose an unused IP in node_cidr) 44 | cluster_gateway_addr: "" 45 | 46 | # -- GitHub repository 47 | # (REQUIRED) / (e.g. "onedr0p/cluster-template") 48 | repository_name: "" 49 | 50 | # -- GitHub repository branch 51 | # (OPTIONAL) / (DEFAULT: "main") 52 | # repository_branch: "" 53 | 54 | # -- Repository visibility (public or private) 55 | # (OPTIONAL) / (DEFAULT: "public") / (NOTE: See the README for information when set private) 56 | # repository_visibility: "" 57 | 58 | # -- Domain you wish to use from your Cloudflare account 59 | # (REQUIRED) / (e.g. "example.com") 60 | cloudflare_domain: "" 61 | 62 | # -- API Token for Cloudflare with the 'Zone:DNS:Edit' and 'Account:Cloudflare Tunnel:Read' permissions 63 | # (REQUIRED) (NOTE: See the README for information on creating this) 64 | cloudflare_token: "" 65 | 66 | # -- The Load balancer IP for the external gateway 67 | # (REQUIRED) / (NOTE: Choose an unused IP in node_cidr) 68 | cloudflare_gateway_addr: "" 69 | 70 | # -- The load balancer mode for cilium. 71 | # (OPTIONAL) / (DEFAULT: "dsr") / (NOTE: accepted values are 'dsr' or 'snat') / (REF: https://docs.cilium.io/en/stable/network/kubernetes/kubeproxy-free/) 72 | # cilium_loadbalancer_mode: "" 73 | 74 | # -- The IP address of the BGP router, to keep things simple, node network will be used for BGP peering. 75 | # (OPTIONAL) / (e.g. "192.168.1.1") / (REF: https://docs.cilium.io/en/latest/network/bgp-control-plane/bgp-control-plane/) 76 | # cilium_bgp_router_addr: "" 77 | 78 | # -- The BGP router ASN 79 | # (OPTIONAL) / (e.g. "64513") 80 | # cilium_bgp_router_asn: "" 81 | 82 | # -- The BGP node ASN 83 | # (OPTIONAL) / (e.g. "64514") 84 | # cilium_bgp_node_asn: "" 85 | 86 | # -- Argo CD Admin password to login 87 | # (REQUIRED) / (NOTE: `htpasswd -nbBC 10 "" $ARGO_PWD | tr -d ':\n' | sed 's/$2y/$2a/'`) 88 | argo_password: "" 89 | -------------------------------------------------------------------------------- /templates/config/kubernetes/apps/argo-system/argo-cd/values.yaml.j2: -------------------------------------------------------------------------------- 1 | fullnameOverride: "argocd" 2 | crds: 3 | install: true 4 | # -- Keep CRDs on chart uninstall 5 | keep: false 6 | configs: 7 | params: 8 | server.insecure: true 9 | controller.diff.server.side: true 10 | cm: 11 | statusbadge.enabled: true 12 | kustomize.buildOptions: "--enable-alpha-plugins --enable-exec" 13 | helm.valuesFileSchemes: >- 14 | secrets+gpg-import, secrets+gpg-import-kubernetes, 15 | secrets+age-import, secrets+age-import-kubernetes, 16 | secrets, secrets+literal, 17 | https 18 | resource.exclusions: | 19 | - apiGroups: 20 | - cilium.io 21 | kinds: 22 | - CiliumIdentity 23 | clusters: 24 | - "*" 25 | controller: 26 | replicas: 2 27 | metrics: 28 | enabled: true 29 | applicationLabels: 30 | enabled: true 31 | serviceMonitor: &service-monitor 32 | enabled: true 33 | additionalLabels: 34 | release: kube-prometheus-stack 35 | dex: 36 | replicas: 2 37 | metrics: 38 | enabled: true 39 | serviceMonitor: *service-monitor 40 | redis: 41 | metrics: 42 | enabled: true 43 | serviceMonitor: *service-monitor 44 | server: 45 | replicas: 2 46 | allowAnyNamespace: true 47 | metrics: 48 | enabled: true 49 | serviceMonitor: *service-monitor 50 | notifications: 51 | replicas: 2 52 | metrics: 53 | enabled: true 54 | serviceMonitor: *service-monitor 55 | repoServer: 56 | replicas: 2 57 | serviceAccount: 58 | create: true 59 | name: argocd-repo-server 60 | rbac: 61 | - apiGroups: 62 | - "" 63 | resources: 64 | - secrets 65 | verbs: 66 | - get 67 | env: 68 | - name: HELM_PLUGINS 69 | value: /custom-tools/helm-plugins/ 70 | - name: HELM_SECRETS_CURL_PATH 71 | value: /custom-tools/curl 72 | - name: HELM_SECRETS_SOPS_PATH 73 | value: /custom-tools/sops 74 | - name: HELM_SECRETS_VALS_PATH 75 | value: /custom-tools/vals 76 | - name: HELM_SECRETS_KUBECTL_PATH 77 | value: /custom-tools/kubectl 78 | - name: HELM_SECRETS_BACKEND 79 | value: sops 80 | # https://github.com/jkroepke/helm-secrets/wiki/Security-in-shared-environments 81 | - name: HELM_SECRETS_VALUES_ALLOW_SYMLINKS 82 | value: "false" 83 | - name: HELM_SECRETS_VALUES_ALLOW_ABSOLUTE_PATH 84 | value: "true" 85 | - name: HELM_SECRETS_VALUES_ALLOW_PATH_TRAVERSAL 86 | value: "false" 87 | - name: HELM_SECRETS_WRAPPER_ENABLED 88 | value: "true" 89 | - name: HELM_SECRETS_DECRYPT_SECRETS_IN_TMP_DIR 90 | value: "true" 91 | - name: HELM_SECRETS_HELM_PATH 92 | value: /usr/local/bin/helm 93 | - name: SOPS_AGE_KEY_FILE # For age 94 | value: /helm-secrets-private-keys/key.txt 95 | volumes: 96 | - name: custom-tools 97 | emptyDir: {} 98 | # kubectl create secret generic helm-secrets-private-keys --from-file=key.asc=assets/gpg/private2.gpg 99 | - name: helm-secrets-private-keys 100 | secret: 101 | secretName: helm-secrets-private-keys 102 | volumeMounts: 103 | - mountPath: /custom-tools 104 | name: custom-tools 105 | - mountPath: /usr/local/sbin/helm 106 | subPath: helm 107 | name: custom-tools 108 | - mountPath: /usr/local/bin/kustomize 109 | name: custom-tools 110 | subPath: kustomize 111 | - mountPath: /usr/local/bin/ksops 112 | name: custom-tools 113 | subPath: ksops 114 | - mountPath: /helm-secrets-private-keys/ 115 | name: helm-secrets-private-keys 116 | initContainers: 117 | - name: gitops-tools 118 | image: ghcr.io/ajaykumar4/gitops-tools:2025.11.0 119 | imagePullPolicy: Always 120 | command: [sh, -ec] 121 | args: 122 | - | 123 | mkdir -p /custom-tools/ 124 | cp -rf /gitops-tools/* /custom-tools 125 | chmod +x /custom-tools/* 126 | volumeMounts: 127 | - mountPath: /custom-tools 128 | name: custom-tools 129 | -------------------------------------------------------------------------------- /templates/config/talos/talconfig.yaml.j2: -------------------------------------------------------------------------------- 1 | --- 2 | clusterName: kubernetes 3 | 4 | talosVersion: "${talosVersion}" 5 | kubernetesVersion: "${kubernetesVersion}" 6 | 7 | endpoint: https://#{ cluster_api_addr }#:6443 8 | additionalApiServerCertSans: &sans 9 | - "127.0.0.1" 10 | - "#{ cluster_api_addr }#" 11 | #% for item in cluster_api_tls_sans %# 12 | - "#{ item }#" 13 | #% endfor %# 14 | additionalMachineCertSans: *sans 15 | 16 | clusterPodNets: ["#{ cluster_pod_cidr }#"] 17 | clusterSvcNets: ["#{ cluster_svc_cidr }#"] 18 | 19 | # Disable built-in CNI to use Cilium 20 | cniConfig: 21 | name: none 22 | 23 | nodes: 24 | #% for item in nodes %# 25 | - hostname: "#{ item.name }#" 26 | ipAddress: "#{ item.address }#" 27 | #% if item.disk.startswith('/') %# 28 | installDisk: "#{ item.disk }#" 29 | #% else %# 30 | installDiskSelector: 31 | serial: "#{ item.disk }#" 32 | #% endif %# 33 | machineSpec: 34 | secureboot: #{ (true if item.secureboot else false) | string | lower }# 35 | talosImageURL: factory.talos.dev/installer#{ "-secureboot" if item.secureboot | default(false, true) }#/#{ item.schematic_id }# 36 | controlPlane: #{ (item.controller) | string | lower }# 37 | networkInterfaces: 38 | - deviceSelector: 39 | hardwareAddr: "#{ item.mac_addr | lower }#" 40 | #% if node_vlan_tag %# 41 | vlans: 42 | - vlanId: #{ node_vlan_tag }# 43 | addresses: 44 | - "#{ item.address }#/#{ node_cidr.split('/') | last }#" 45 | mtu: #{ item.mtu | default(1500, true) }# 46 | routes: 47 | - network: "0.0.0.0/0" 48 | gateway: "#{ node_default_gateway }#" 49 | #% if item.controller %# 50 | vip: 51 | ip: "#{ cluster_api_addr }#" 52 | #% endif %# 53 | #% else %# 54 | dhcp: false 55 | addresses: 56 | - "#{ item.address }#/#{ node_cidr.split('/') | last }#" 57 | routes: 58 | - network: "0.0.0.0/0" 59 | gateway: "#{ node_default_gateway }#" 60 | mtu: #{ item.mtu | default(1500, true) }# 61 | #% if item.controller %# 62 | vip: 63 | ip: "#{ cluster_api_addr }#" 64 | #% endif %# 65 | #% endif %# 66 | #% if talos_patches('%s' % (item.name)) | length == 0 %# 67 | #% if item.encrypt_disk | default(false, true) %# 68 | patches: 69 | - # Encrypt system disk with TPM 70 | |- 71 | machine: 72 | systemDiskEncryption: 73 | state: 74 | provider: luks2 75 | keys: 76 | - slot: 0 77 | tpm: {} 78 | ephemeral: 79 | provider: luks2 80 | keys: 81 | - slot: 0 82 | tpm: {} 83 | #% endif %# 84 | #% else %# 85 | #% for file in talos_patches('%s' % (item.name)) %# 86 | #% if loop.index == 1 %# 87 | patches: 88 | #% if item.encrypt_disk | default(false, true) %# 89 | - |- 90 | machine: 91 | systemDiskEncryption: 92 | state: 93 | provider: luks2 94 | keys: 95 | - slot: 0 96 | tpm: {} 97 | ephemeral: 98 | provider: luks2 99 | keys: 100 | - slot: 0 101 | tpm: {} 102 | #% endif %# 103 | #% endif %# 104 | - "@./patches/#{ item.name }#/#{ file | basename }#" 105 | #% endfor %# 106 | #% endif %# 107 | #% endfor %# 108 | 109 | #% for file in talos_patches('global') %# 110 | #% if loop.index == 1 %# 111 | # Global patches 112 | patches: 113 | #% endif %# 114 | - "@./patches/global/#{ file | basename }#" 115 | #% endfor %# 116 | 117 | #% for file in talos_patches('controller') %# 118 | #% if loop.index == 1 %# 119 | # Controller patches 120 | controlPlane: 121 | patches: 122 | #% endif %# 123 | - "@./patches/controller/#{ file | basename }#" 124 | #% endfor %# 125 | 126 | #% if (nodes | selectattr('controller', 'equalto', False) | list | length) and (talos_patches('worker') | length) %# 127 | #% for file in talos_patches('worker') %# 128 | #% if loop.index == 1 %# 129 | # Worker patches 130 | worker: 131 | patches: 132 | #% endif %# 133 | - "@./patches/worker/#{ file | basename }#" 134 | #% endfor %# 135 | #% endif %# 136 | -------------------------------------------------------------------------------- /templates/config/kubernetes/apps/network/envoy-gateway/config/envoy.sops.yaml.j2: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: gateway.envoyproxy.io/v1alpha1 3 | kind: EnvoyProxy 4 | metadata: 5 | name: envoy 6 | spec: 7 | logging: 8 | level: 9 | default: info 10 | provider: 11 | type: Kubernetes 12 | kubernetes: 13 | envoyDeployment: 14 | replicas: 2 15 | container: 16 | resources: 17 | requests: 18 | cpu: 100m 19 | limits: 20 | memory: 1Gi 21 | envoyService: 22 | externalTrafficPolicy: Cluster 23 | shutdown: 24 | drainTimeout: 180s 25 | telemetry: 26 | metrics: 27 | prometheus: 28 | compression: 29 | type: Gzip 30 | --- 31 | apiVersion: gateway.networking.k8s.io/v1 32 | kind: GatewayClass 33 | metadata: 34 | name: envoy 35 | spec: 36 | controllerName: gateway.envoyproxy.io/gatewayclass-controller 37 | parametersRef: 38 | group: gateway.envoyproxy.io 39 | kind: EnvoyProxy 40 | name: envoy 41 | namespace: network 42 | --- 43 | apiVersion: gateway.networking.k8s.io/v1 44 | kind: Gateway 45 | metadata: 46 | name: envoy-external 47 | annotations: 48 | external-dns.alpha.kubernetes.io/target: "external.#{ cloudflare_domain }#" 49 | spec: 50 | gatewayClassName: envoy 51 | infrastructure: 52 | annotations: 53 | external-dns.alpha.kubernetes.io/hostname: "external.#{ cloudflare_domain }#" 54 | lbipam.cilium.io/ips: "#{ cloudflare_gateway_addr }#" 55 | listeners: 56 | - name: http 57 | protocol: HTTP 58 | port: 80 59 | allowedRoutes: 60 | namespaces: 61 | from: Same 62 | - name: https 63 | protocol: HTTPS 64 | port: 443 65 | allowedRoutes: 66 | namespaces: 67 | from: All 68 | tls: 69 | certificateRefs: 70 | - kind: Secret 71 | name: "#{ cloudflare_domain.replace('.', '-') }#-production-tls" 72 | --- 73 | apiVersion: gateway.networking.k8s.io/v1 74 | kind: Gateway 75 | metadata: 76 | name: envoy-internal 77 | annotations: 78 | external-dns.alpha.kubernetes.io/target: "internal.#{ cloudflare_domain }#" 79 | spec: 80 | gatewayClassName: envoy 81 | infrastructure: 82 | annotations: 83 | external-dns.alpha.kubernetes.io/hostname: "internal.#{ cloudflare_domain }#" 84 | lbipam.cilium.io/ips: "#{ cluster_gateway_addr }#" 85 | listeners: 86 | - name: http 87 | protocol: HTTP 88 | port: 80 89 | allowedRoutes: 90 | namespaces: 91 | from: Same 92 | - name: https 93 | protocol: HTTPS 94 | port: 443 95 | allowedRoutes: 96 | namespaces: 97 | from: All 98 | tls: 99 | certificateRefs: 100 | - kind: Secret 101 | name: "#{ cloudflare_domain.replace('.', '-') }#-production-tls" 102 | --- 103 | apiVersion: gateway.envoyproxy.io/v1alpha1 104 | kind: BackendTrafficPolicy 105 | metadata: 106 | name: envoy 107 | spec: 108 | compression: 109 | - type: Brotli 110 | - type: Gzip 111 | connection: 112 | bufferLimit: 8Mi 113 | targetSelectors: 114 | - group: gateway.networking.k8s.io 115 | kind: Gateway 116 | tcpKeepalive: {} 117 | timeout: 118 | http: 119 | requestTimeout: 0s 120 | --- 121 | apiVersion: gateway.envoyproxy.io/v1alpha1 122 | kind: ClientTrafficPolicy 123 | metadata: 124 | name: envoy 125 | spec: 126 | clientIPDetection: 127 | xForwardedFor: 128 | numTrustedHops: 1 129 | connection: 130 | bufferLimit: 4Mi 131 | maxAcceptPerSocketEvent: 0 132 | http2: 133 | initialStreamWindowSize: 512Ki 134 | initialConnectionWindowSize: 8Mi 135 | onInvalidMessage: TerminateStream 136 | http3: {} 137 | targetSelectors: 138 | - group: gateway.networking.k8s.io 139 | kind: Gateway 140 | tcpKeepalive: {} 141 | timeout: 142 | http: 143 | requestReceivedTimeout: 0s 144 | tls: 145 | minVersion: "1.2" 146 | alpnProtocols: 147 | - h2 148 | - http/1.1 149 | --- 150 | apiVersion: gateway.networking.k8s.io/v1 151 | kind: HTTPRoute 152 | metadata: 153 | name: https-redirect 154 | annotations: 155 | external-dns.alpha.kubernetes.io/controller: none 156 | spec: 157 | parentRefs: 158 | - name: envoy-external 159 | namespace: network 160 | sectionName: http 161 | - name: envoy-internal 162 | namespace: network 163 | sectionName: http 164 | rules: 165 | - filters: 166 | - type: RequestRedirect 167 | requestRedirect: 168 | scheme: https 169 | statusCode: 301 170 | -------------------------------------------------------------------------------- /.renovaterc.json5: -------------------------------------------------------------------------------- 1 | { 2 | $schema: "https://docs.renovatebot.com/renovate-schema.json", 3 | extends: [ 4 | "config:recommended", 5 | "docker:enableMajor", 6 | "helpers:pinGitHubActionDigests", 7 | ":automergeBranch", 8 | ":dependencyDashboard", 9 | ":disableRateLimiting", 10 | ":semanticCommits", 11 | ], 12 | dependencyDashboard: true, 13 | dependencyDashboardTitle: "Renovate Dashboard 🤖", 14 | schedule: ["every weekend"], 15 | ignorePaths: ["**/*.sops.*"], 16 | argocd: { 17 | managerFilePatterns: ["(^|/)kubernetes/.+\\.ya?ml(?:\\.j2)?$"], 18 | }, 19 | helmfile: { 20 | managerFilePatterns: [ 21 | "/(^|/)helmfile\\.ya?ml?(?:\\.j2)?$/", 22 | "/(^|/)helmfile\\.d/.+\\.ya?ml?(?:\\.j2)?$/", 23 | ], 24 | }, 25 | "helm-values": { 26 | "managerFilePatterns": ["/(^|/)kubernetes/.+\\/values\\.ya?ml(?:\\.j2)?$/"] 27 | }, 28 | kubernetes: { 29 | managerFilePatterns: ["(^|/)kubernetes/.+\\.ya?ml(?:\\.j2)?$"], 30 | }, 31 | kustomize: { 32 | managerFilePatterns: ["/^kustomization\\.ya?ml(?:\\.j2)?$/"], 33 | }, 34 | packageRules: [ 35 | { 36 | description: "Override Helmfile Dependency Name", 37 | matchDatasources: ["docker"], 38 | matchManagers: ["helmfile"], 39 | overrideDepName: "{{packageName}}", 40 | }, 41 | { 42 | description: "Argo Operator Group", 43 | groupName: "argocd", 44 | matchDatasources: ["docker"], 45 | matchPackageNames: ["/argocd/"], 46 | group: { 47 | commitMessageTopic: "{{{groupName}}} group", 48 | }, 49 | minimumGroupSize: 2, 50 | }, 51 | { 52 | description: "Auto-merge GitHub Actions", 53 | matchManagers: ["github-actions"], 54 | automerge: true, 55 | automergeType: "branch", 56 | matchUpdateTypes: ["minor", "patch", "digest"], 57 | minimumReleaseAge: "3 days", 58 | ignoreTests: true, 59 | }, 60 | { 61 | description: "Auto-merge Mise Tools", 62 | matchManagers: ["mise"], 63 | automerge: true, 64 | automergeType: "branch", 65 | matchUpdateTypes: ["minor", "patch"], 66 | ignoreTests: true, 67 | }, 68 | { 69 | matchUpdateTypes: ["major"], 70 | semanticCommitType: "feat", 71 | commitMessagePrefix: "{{semanticCommitType}}({{semanticCommitScope}})!:", 72 | commitMessageExtra: "( {{currentVersion}} ➔ {{newVersion}} )", 73 | }, 74 | { 75 | matchUpdateTypes: ["minor"], 76 | semanticCommitType: "feat", 77 | commitMessageExtra: "( {{currentVersion}} ➔ {{newVersion}} )", 78 | }, 79 | { 80 | matchUpdateTypes: ["patch"], 81 | semanticCommitType: "fix", 82 | commitMessageExtra: "( {{currentVersion}} ➔ {{newVersion}} )", 83 | }, 84 | { 85 | matchUpdateTypes: ["digest"], 86 | semanticCommitType: "chore", 87 | commitMessageExtra: "( {{currentDigestShort}} ➔ {{newDigestShort}} )", 88 | }, 89 | { 90 | matchDatasources: ["docker"], 91 | semanticCommitScope: "container", 92 | commitMessageTopic: "image {{depName}}", 93 | }, 94 | { 95 | matchDatasources: ["helm"], 96 | semanticCommitScope: "helm", 97 | commitMessageTopic: "chart {{depName}}", 98 | }, 99 | { 100 | matchManagers: ["github-actions"], 101 | semanticCommitType: "ci", 102 | semanticCommitScope: "github-action", 103 | commitMessageTopic: "action {{depName}}", 104 | }, 105 | { 106 | matchDatasources: ["github-releases"], 107 | semanticCommitScope: "github-release", 108 | commitMessageTopic: "release {{depName}}", 109 | }, 110 | { 111 | matchManagers: ["mise"], 112 | semanticCommitScope: "mise", 113 | commitMessageTopic: "tool {{depName}}", 114 | }, 115 | { 116 | matchUpdateTypes: ["major"], 117 | labels: ["type/major"], 118 | }, 119 | { 120 | matchUpdateTypes: ["minor"], 121 | labels: ["type/minor"], 122 | }, 123 | { 124 | matchUpdateTypes: ["patch"], 125 | labels: ["type/patch"], 126 | }, 127 | { 128 | matchDatasources: ["docker"], 129 | addLabels: ["renovate/container"], 130 | }, 131 | { 132 | matchDatasources: ["helm"], 133 | addLabels: ["renovate/helm"], 134 | }, 135 | { 136 | matchManagers: ["github-actions"], 137 | addLabels: ["renovate/github-action"], 138 | }, 139 | { 140 | matchDatasources: ["github-releases"], 141 | addLabels: ["renovate/github-release"], 142 | }, 143 | ], 144 | customManagers: [ 145 | { 146 | description: "Process annotated dependencies", 147 | customType: "regex", 148 | managerFilePatterns: [ 149 | "/(^|/).+\\.env(?:\\.j2)?$/", 150 | "/(^|/).+\\.sh(?:\\.j2)?$/", 151 | "/(^|/).+\\.ya?ml(?:\\.j2)?$/", 152 | ], 153 | matchStrings: [ 154 | // # renovate: datasource=github-releases depName=k3s-io/k3s 155 | // k3s_release_version: &version v1.29.0+k3s1 156 | // # renovate: datasource=helm depName=cilium repository=https://helm.cilium.io 157 | // version: 1.15.1 158 | // # renovate: datasource=docker depName=ghcr.io/siderolabs/kubelet 159 | // KUBERNETES_VERSION=v1.31.1 160 | "datasource=(?\\S+) depName=(?\\S+)( repository=(?\\S+))?\\n.+(:\\s|=)(&\\S+\\s)?(?\\S+)", 161 | // # renovate: datasource=docker depName=ghcr.io/prometheus-operator/prometheus-operator 162 | // https://raw.githubusercontent.com/prometheus-operator/prometheus-operator/v0.80.0/example/prometheus-operator-crd/monitoring.coreos.com_alertmanagerconfigs.yaml 163 | "datasource=(?\\S+) depName=(?\\S+)\\n.+/(?(v|\\d)[^/]+)", 164 | ], 165 | datasourceTemplate: "{{#if datasource}}{{{datasource}}}{{else}}github-releases{{/if}}", 166 | }, 167 | ], 168 | } 169 | -------------------------------------------------------------------------------- /scripts/bootstrap-apps.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -Eeuo pipefail 3 | 4 | source "$(dirname "${0}")/lib/common.sh" 5 | 6 | export LOG_LEVEL="debug" 7 | export ROOT_DIR="$(git rev-parse --show-toplevel)" 8 | 9 | # Talos requires the nodes to be 'Ready=False' before applying resources 10 | function wait_for_nodes() { 11 | log debug "Waiting for nodes to be available" 12 | 13 | # Skip waiting if all nodes are 'Ready=True' 14 | if kubectl wait nodes --for=condition=Ready=True --all --timeout=10s &>/dev/null; then 15 | log info "Nodes are available and ready, skipping wait for nodes" 16 | return 17 | fi 18 | 19 | # Wait for all nodes to be 'Ready=False' 20 | until kubectl wait nodes --for=condition=Ready=False --all --timeout=10s &>/dev/null; do 21 | log info "Nodes are not available, waiting for nodes to be available. Retrying in 10 seconds..." 22 | sleep 10 23 | done 24 | } 25 | 26 | # Namespaces to be applied before the SOPS secrets are installed 27 | function apply_namespaces() { 28 | log debug "Applying namespaces" 29 | 30 | local -r apps_dir="${ROOT_DIR}/kubernetes/apps" 31 | 32 | if [[ ! -d "${apps_dir}" ]]; then 33 | log error "Directory does not exist" "directory=${apps_dir}" 34 | fi 35 | 36 | for app in "${apps_dir}"/*/; do 37 | namespace=$(basename "${app}") 38 | 39 | # Check if the namespace resources are up-to-date 40 | if kubectl get namespace "${namespace}" &>/dev/null; then 41 | log info "Namespace resource is up-to-date" "resource=${namespace}" 42 | continue 43 | fi 44 | 45 | # Apply the namespace resources 46 | if kubectl create namespace "${namespace}" --dry-run=client --output=yaml \ 47 | | kubectl apply --server-side --filename - &>/dev/null; 48 | then 49 | log info "Namespace resource applied" "resource=${namespace}" 50 | else 51 | log error "Failed to apply namespace resource" "resource=${namespace}" 52 | fi 53 | done 54 | } 55 | 56 | # SOPS secrets to be applied before the helmfile charts are installed 57 | function apply_sops_secrets() { 58 | log debug "Applying secrets" 59 | 60 | local -r secrets=( 61 | "${ROOT_DIR}/kubernetes/components/common/helm-secrets-private-keys.sops.yaml" 62 | ) 63 | 64 | for secret in "${secrets[@]}"; do 65 | if [ ! -f "${secret}" ]; then 66 | log warn "File does not exist" "file=${secret}" 67 | continue 68 | fi 69 | 70 | # Check if the secret resources are up-to-date 71 | if sops exec-file "${secret}" "kubectl --namespace argo-system diff --filename {}" &>/dev/null; then 72 | log info "Secret resource is up-to-date" "resource=$(basename "${secret}" ".sops.yaml")" 73 | continue 74 | fi 75 | 76 | # Apply secret resources 77 | if sops exec-file "${secret}" "kubectl --namespace argo-system apply --server-side --filename {}" &>/dev/null; then 78 | log info "Secret resource applied successfully" "resource=$(basename "${secret}" ".sops.yaml")" 79 | else 80 | log error "Failed to apply secret resource" "resource=$(basename "${secret}" ".sops.yaml")" 81 | fi 82 | done 83 | } 84 | 85 | # CRDs to be applied before the helmfile charts are installed 86 | function apply_crds() { 87 | log debug "Applying CRDs" 88 | 89 | local -r helmfile_file="${ROOT_DIR}/bootstrap/helmfile.d/00-crds.yaml" 90 | 91 | if [[ ! -f "${helmfile_file}" ]]; then 92 | log fatal "File does not exist" "file" "${helmfile_file}" 93 | fi 94 | 95 | if ! crds=$(helmfile --file "${helmfile_file}" template --quiet) || [[ -z "${crds}" ]]; then 96 | log fatal "Failed to render CRDs from Helmfile" "file" "${helmfile_file}" 97 | fi 98 | 99 | if echo "${crds}" | kubectl diff --filename - &>/dev/null; then 100 | log info "CRDs are up-to-date" 101 | return 102 | fi 103 | 104 | if ! echo "${crds}" | kubectl apply --server-side --filename - &>/dev/null; then 105 | log fatal "Failed to apply crds from Helmfile" "file" "${helmfile_file}" 106 | fi 107 | 108 | log info "CRDs applied successfully" 109 | } 110 | 111 | # Sync Helm releases 112 | function sync_helm_releases() { 113 | log debug "Syncing Helm releases" 114 | 115 | local -r helmfile_file="${ROOT_DIR}/bootstrap/helmfile.d/01-apps.yaml" 116 | 117 | if [[ ! -f "${helmfile_file}" ]]; then 118 | log error "File does not exist" "file=${helmfile_file}" 119 | fi 120 | 121 | if ! helmfile --file "${helmfile_file}" sync --hide-notes; then 122 | log error "Failed to sync Helm releases" 123 | fi 124 | 125 | log info "Helm releases synced successfully" 126 | } 127 | 128 | # Sync Argo Applications 129 | function sync_argo_apps() { 130 | log debug "Sync Argo Applications" 131 | 132 | local -r bootstrappingmaps=( 133 | "${ROOT_DIR}/kubernetes/components/common/apps.yaml" 134 | "${ROOT_DIR}/kubernetes/components/common/repositories.yaml" 135 | "${ROOT_DIR}/kubernetes/components/common/settings.yaml" 136 | ) 137 | 138 | for bootstrappingmap in "${bootstrappingmaps[@]}"; do 139 | if [ ! -f "${bootstrappingmap}" ]; then 140 | log warn "File does not exist" file "${bootstrappingmap}" 141 | continue 142 | fi 143 | 144 | # Check if the bootstrappingmap resources are up-to-date 145 | if kubectl --namespace argo-system diff --filename "${bootstrappingmap}" &>/dev/null; then 146 | log info "bootstrappingmap resource is up-to-date" "resource=$(basename "${bootstrappingmap}" ".yaml")" 147 | continue 148 | fi 149 | 150 | # Apply bootstrappingmap resources 151 | if kubectl --namespace argo-system apply --server-side --filename "${bootstrappingmap}" &>/dev/null; then 152 | log info "bootstrappingmap resource applied successfully" "resource=$(basename "${bootstrappingmap}" ".yaml")" 153 | else 154 | log error "Failed to apply bootstrappingmap resource" "resource=$(basename "${bootstrappingmap}" ".yaml")" 155 | fi 156 | done 157 | } 158 | 159 | function main() { 160 | check_env KUBECONFIG TALOSCONFIG 161 | check_cli helmfile kubectl kustomize sops talhelper yq 162 | 163 | # Apply resources and Helm releases 164 | wait_for_nodes 165 | apply_namespaces 166 | apply_sops_secrets 167 | apply_crds 168 | sync_helm_releases 169 | sync_argo_apps 170 | 171 | log info "Congrats! The cluster is bootstrapped and Argo is syncing the Git repository" 172 | } 173 | 174 | main "$@" 175 | -------------------------------------------------------------------------------- /.taskfiles/template/Taskfile.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | version: '3' 3 | 4 | vars: 5 | MAKEJINJA_CONFIG_FILE: '{{.ROOT_DIR}}/makejinja.toml' 6 | TEMPLATE_DIR: '{{.ROOT_DIR}}/templates' 7 | TEMPLATE_RESOURCES_DIR: '{{.ROOT_DIR}}/.taskfiles/template/resources' 8 | TEMPLATE_CONFIG_FILE: '{{.ROOT_DIR}}/cluster.yaml' 9 | TEMPLATE_NODE_CONFIG_FILE: '{{.ROOT_DIR}}/nodes.yaml' 10 | 11 | tasks: 12 | 13 | :init: 14 | desc: Initialize configuration files 15 | cmds: 16 | - task: generate-template-config 17 | - task: generate-age-key 18 | - task: generate-deploy-key 19 | - task: generate-push-token 20 | 21 | generate-template-config: 22 | internal: true 23 | cmds: 24 | - mv {{.TEMPLATE_CONFIG_FILE | replace ".yaml" ".sample.yaml"}} {{.TEMPLATE_CONFIG_FILE}} 25 | - mv {{.TEMPLATE_NODE_CONFIG_FILE | replace ".yaml" ".sample.yaml"}} {{.TEMPLATE_NODE_CONFIG_FILE}} 26 | status: 27 | - test -f {{.TEMPLATE_CONFIG_FILE}} 28 | - test -f {{.TEMPLATE_NODE_CONFIG_FILE}} 29 | 30 | generate-age-key: 31 | internal: true 32 | cmd: age-keygen --output {{.SOPS_AGE_KEY_FILE}} 33 | status: 34 | - test -f {{.SOPS_AGE_KEY_FILE}} 35 | preconditions: 36 | - which age-keygen 37 | 38 | generate-deploy-key: 39 | internal: true 40 | cmd: ssh-keygen -t ed25519 -C "deploy-key" -f {{.ROOT_DIR}}/github-deploy.key -q -P "" 41 | status: 42 | - test -f {{.ROOT_DIR}}/github-deploy.key 43 | preconditions: 44 | - which ssh-keygen 45 | 46 | generate-push-token: 47 | internal: true 48 | cmd: python -c "import secrets; print(secrets.token_hex(16))" > {{.ROOT_DIR}}/github-push-token.txt 49 | status: 50 | - test -f {{.ROOT_DIR}}/github-push-token.txt 51 | 52 | :configure: 53 | desc: Render and validate configuration files 54 | prompt: Any conflicting files in the kubernetes directory will be overwritten... continue? 55 | cmds: 56 | - task: validate-schemas 57 | - task: render-configs 58 | - task: encrypt-secrets 59 | - task: validate-kubernetes-config 60 | - task: validate-talos-config 61 | preconditions: 62 | - msg: An existing Age key interferes with the age key in this repository, rename or delete ~/.config/sops/age/keys.txt 63 | sh: '! test -f ~/.config/sops/age/keys.txt' 64 | - msg: File cluster.yaml not found, did you run `task init`? 65 | sh: test -f {{.TEMPLATE_CONFIG_FILE}} 66 | - msg: File nodes.yaml not found, did you run `task init`? 67 | sh: test -f {{.TEMPLATE_NODE_CONFIG_FILE}} 68 | - msg: File cloudflare-tunnel.json not found, see the README for information on creating it. 69 | sh: test -f {{.ROOT_DIR}}/cloudflare-tunnel.json 70 | 71 | validate-schemas: 72 | internal: true 73 | cmds: 74 | - cue vet {{.TEMPLATE_CONFIG_FILE}} {{.TEMPLATE_RESOURCES_DIR}}/cluster.schema.cue 75 | - cue vet {{.TEMPLATE_NODE_CONFIG_FILE}} {{.TEMPLATE_RESOURCES_DIR}}/nodes.schema.cue 76 | preconditions: 77 | - test -f {{.TEMPLATE_RESOURCES_DIR}}/cluster.schema.cue 78 | - test -f {{.TEMPLATE_RESOURCES_DIR}}/nodes.schema.cue 79 | - which cue 80 | 81 | render-configs: 82 | internal: true 83 | cmd: makejinja 84 | env: 85 | PYTHONDONTWRITEBYTECODE: '1' 86 | preconditions: 87 | - test -f {{.TEMPLATE_DIR}}/scripts/plugin.py 88 | - test -f {{.MAKEJINJA_CONFIG_FILE}} 89 | - which makejinja 90 | 91 | encrypt-secrets: 92 | internal: true 93 | cmds: 94 | - for: { var: SECRET_FILES } 95 | cmd: | 96 | if [ $(sops filestatus "{{.ITEM}}" | jq ".encrypted") == "false" ]; then 97 | sops --encrypt --in-place "{{.ITEM}}" 98 | fi 99 | vars: 100 | SECRET_FILES: 101 | sh: find "{{.BOOTSTRAP_DIR}}" "{{.KUBERNETES_DIR}}" "{{.TALOS_DIR}}" -type f -name "*.sops.*" -print 102 | preconditions: 103 | - test -f {{.SOPS_AGE_KEY_FILE}} 104 | - test -f {{.ROOT_DIR}}/.sops.yaml 105 | - which jq sops 106 | 107 | validate-kubernetes-config: 108 | internal: true 109 | cmd: bash {{.TEMPLATE_RESOURCES_DIR}}/kubeconform.sh {{.KUBERNETES_DIR}} 110 | preconditions: 111 | - test -f {{.TEMPLATE_RESOURCES_DIR}}/kubeconform.sh 112 | - which kubeconform 113 | 114 | validate-talos-config: 115 | internal: true 116 | dir: '{{.TALOS_DIR}}' 117 | cmd: talhelper validate talconfig {{.TALOS_DIR}}/talconfig.yaml 118 | preconditions: 119 | - test -f {{.TALOS_DIR}}/talconfig.yaml 120 | - which talhelper 121 | 122 | debug: 123 | desc: Gather common resources in your cluster 124 | cmds: 125 | - for: 126 | matrix: 127 | RESOURCE: [certificates, certificaterequests, gitrepositories, helmrepositories, helmreleases, httproutes, kustomizations, nodes, pods] 128 | cmd: kubectl get --all-namespaces {{.ITEM.RESOURCE}} 129 | preconditions: 130 | - test -f {{.KUBECONFIG}} 131 | - which kubectl 132 | 133 | tidy: 134 | desc: Archive template related files and directories 135 | prompt: All files and directories related to the templating process will be archived... continue? 136 | cmds: 137 | - mkdir -p {{.TIDY_FOLDER}} 138 | - rm -rf {{.ROOT_DIR}}/.github/tests 139 | - rm -rf {{.ROOT_DIR}}/.github/workflows/e2e.yaml 140 | - rm -rf {{.ROOT_DIR}}/.github/workflows/mise.yaml 141 | - rm -rf {{.ROOT_DIR}}/.github/workflows/release.yaml 142 | - | 143 | {{.SED}} -i 's/(..\.j2)\?//g' {{.ROOT_DIR}}/.renovaterc.json5 144 | - mv {{.TEMPLATE_DIR}} {{.TIDY_FOLDER}}/templates 145 | - mv {{.MAKEJINJA_CONFIG_FILE}} {{.TIDY_FOLDER}}/makejinja.toml 146 | - mv {{.TEMPLATE_CONFIG_FILE}} {{.TIDY_FOLDER}}/cluster.yaml 147 | - mv {{.TEMPLATE_NODE_CONFIG_FILE}} {{.TIDY_FOLDER}}/nodes.yaml 148 | - | 149 | {{.SED}} -i '/template:/d' {{.ROOT_DIR}}/Taskfile.yaml 150 | - mv {{.ROOT_DIR}}/.taskfiles/template {{.TIDY_FOLDER}}/.taskfiles/ 151 | vars: 152 | TIDY_FOLDER: '{{.PRIVATE_DIR}}/{{now | unixEpoch}}' 153 | SED: 154 | sh: which gsed || which sed 155 | preconditions: 156 | - msg: Unsupported sed version, run `brew install gsed` to upgrade 157 | sh: '{{if eq OS "darwin"}}test -f /opt/homebrew/bin/gsed || test -f /usr/local/bin/gsed{{end}}' 158 | - test -d {{.ROOT_DIR}}/.taskfiles/template 159 | - test -d {{.TEMPLATE_DIR}} 160 | - test -f {{.MAKEJINJA_CONFIG_FILE}} 161 | - test -f {{.ROOT_DIR}}/.renovaterc.json5 162 | 163 | reset: 164 | desc: Remove templated files and directories 165 | prompt: Remove all templated files and directories... continue? 166 | cmds: 167 | - rm -rf {{.BOOTSTRAP_DIR}} 168 | - rm -rf {{.KUBERNETES_DIR}} 169 | - rm -rf {{.TALOS_DIR}} 170 | - rm -rf {{.ROOT_DIR}}/.sops.yaml 171 | -------------------------------------------------------------------------------- /templates/scripts/plugin.py: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | from typing import Any 3 | from datetime import datetime 4 | 5 | import base64 6 | import ipaddress 7 | import makejinja 8 | import re 9 | import json 10 | 11 | 12 | # Return the filename of a path without the j2 extension 13 | def basename(value: str) -> str: 14 | return Path(value).stem 15 | 16 | 17 | # Return the nth host in a CIDR range 18 | def nthhost(value: str, query: int) -> str: 19 | try: 20 | network = ipaddress.ip_network(value, strict=False) 21 | if 0 <= query < network.num_addresses: 22 | return str(network[query]) 23 | except ValueError: 24 | pass 25 | return False 26 | 27 | 28 | # Return the age public or private key from age.key 29 | def age_key(key_type: str, file_path: str = 'age.key') -> str: 30 | try: 31 | with open(file_path, 'r') as file: 32 | file_content = file.read().strip() 33 | if key_type == 'public': 34 | key_match = re.search(r"# public key: (age1[\w]+)", file_content) 35 | if not key_match: 36 | raise ValueError("Could not find public key in the age key file.") 37 | return key_match.group(1) 38 | elif key_type == 'private': 39 | key_match = re.search(r"(AGE-SECRET-KEY-[\w]+)", file_content) 40 | if not key_match: 41 | raise ValueError("Could not find private key in the age key file.") 42 | return key_match.group(1) 43 | else: 44 | raise ValueError("Invalid key type. Use 'public' or 'private'.") 45 | except FileNotFoundError: 46 | raise FileNotFoundError(f"File not found: {file_path}") 47 | except Exception as e: 48 | raise RuntimeError(f"Unexpected error while processing {file_path}: {e}") 49 | 50 | 51 | # Return cloudflare tunnel fields from cloudflare-tunnel.json 52 | def cloudflare_tunnel_id(file_path: str = 'cloudflare-tunnel.json') -> str: 53 | try: 54 | with open(file_path, 'r') as file: 55 | data = json.load(file) 56 | tunnel_id = data.get("TunnelID") 57 | if tunnel_id is None: 58 | raise KeyError(f"Missing 'TunnelID' key in {file_path}") 59 | return tunnel_id 60 | 61 | except FileNotFoundError: 62 | raise FileNotFoundError(f"File not found: {file_path}") 63 | except json.JSONDecodeError: 64 | raise ValueError(f"Could not decode JSON file: {file_path}") 65 | except KeyError as e: 66 | raise KeyError(f"Error in JSON structure: {e}") 67 | except Exception as e: 68 | raise RuntimeError(f"Unexpected error while processing {file_path}: {e}") 69 | 70 | 71 | # Return cloudflare tunnel fields from cloudflare-tunnel.json in TUNNEL_TOKEN format 72 | def cloudflare_tunnel_secret(file_path: str = 'cloudflare-tunnel.json') -> str: 73 | try: 74 | with open(file_path, 'r') as file: 75 | data = json.load(file) 76 | transformed_data = { 77 | "a": data["AccountTag"], 78 | "t": data["TunnelID"], 79 | "s": data["TunnelSecret"] 80 | } 81 | json_string = json.dumps(transformed_data, separators=(',', ':')) 82 | return base64.b64encode(json_string.encode('utf-8')).decode('utf-8') 83 | 84 | except FileNotFoundError: 85 | raise FileNotFoundError(f"File not found: {file_path}") 86 | except json.JSONDecodeError: 87 | raise ValueError(f"Could not decode JSON file: {file_path}") 88 | except KeyError as e: 89 | raise KeyError(f"Missing key in JSON file {file_path}: {e}") 90 | except Exception as e: 91 | raise RuntimeError(f"Unexpected error while processing {file_path}: {e}") 92 | 93 | 94 | # Return the GitHub deploy key from github-deploy.key 95 | def github_deploy_key(file_path: str = 'github-deploy.key') -> str: 96 | try: 97 | with open(file_path, 'r') as file: 98 | return file.read().strip() 99 | except FileNotFoundError: 100 | raise FileNotFoundError(f"File not found: {file_path}") 101 | except Exception as e: 102 | raise RuntimeError(f"Unexpected error while reading {file_path}: {e}") 103 | 104 | 105 | # Return the Argo / GitHub push token from github-push-token.txt 106 | def github_push_token(file_path: str = 'github-push-token.txt') -> str: 107 | try: 108 | with open(file_path, 'r') as file: 109 | return file.read().strip() 110 | except FileNotFoundError: 111 | raise FileNotFoundError(f"File not found: {file_path}") 112 | except Exception as e: 113 | raise RuntimeError(f"Unexpected error while reading {file_path}: {e}") 114 | 115 | 116 | # Return a list of files in the talos patches directory 117 | def talos_patches(value: str) -> list[str]: 118 | path = Path(f'templates/config/talos/patches/{value}') 119 | if not path.is_dir(): 120 | return [] 121 | return [str(f) for f in sorted(path.glob('*.yaml.j2')) if f.is_file()] 122 | 123 | 124 | # Return current datetime 125 | def current_datetime() -> str: 126 | return datetime.now().strftime('%Y-%m-%dT%H:%M:%S%Z') 127 | 128 | 129 | class Plugin(makejinja.plugin.Plugin): 130 | def __init__(self, data: dict[str, Any]): 131 | self._data = data 132 | 133 | 134 | def data(self) -> makejinja.plugin.Data: 135 | data = self._data 136 | 137 | # Set default values for optional fields 138 | data.setdefault('node_default_gateway', nthhost(data.get('node_cidr'), 1)) 139 | data.setdefault('node_dns_servers', ['1.1.1.1', '1.0.0.1']) 140 | data.setdefault('node_ntp_servers', ['162.159.200.1', '162.159.200.123']) 141 | data.setdefault('cluster_pod_cidr', '10.42.0.0/16') 142 | data.setdefault('cluster_svc_cidr', '10.43.0.0/16') 143 | data.setdefault('repository_branch', 'main') 144 | data.setdefault('repository_visibility', 'public') 145 | data.setdefault('cilium_loadbalancer_mode', 'dsr') 146 | 147 | # If all BGP keys are set, enable BGP 148 | bgp_keys = ['cilium_bgp_router_addr', 'cilium_bgp_router_asn', 'cilium_bgp_node_asn'] 149 | bgp_enabled = all(data.get(key) for key in bgp_keys) 150 | data.setdefault('cilium_bgp_enabled', bgp_enabled) 151 | 152 | # If there is more than one node, enable spegel 153 | spegel_enabled = len(data.get('nodes')) > 1 154 | data.setdefault('spegel_enabled', spegel_enabled) 155 | 156 | return data 157 | 158 | 159 | def filters(self) -> makejinja.plugin.Filters: 160 | return [ 161 | basename, 162 | nthhost 163 | ] 164 | 165 | 166 | def functions(self) -> makejinja.plugin.Functions: 167 | return [ 168 | age_key, 169 | cloudflare_tunnel_id, 170 | cloudflare_tunnel_secret, 171 | github_deploy_key, 172 | github_push_token, 173 | talos_patches, 174 | current_datetime 175 | ] 176 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ⛵ Cluster Template 2 | 3 | Welcome to my template designed for deploying a single Kubernetes cluster. Whether you're setting up a cluster at home on bare-metal or virtual machines (VMs), this project aims to simplify the process and make Kubernetes more accessible. This template is inspired by onedr0p [cluster-template](https://github.com/onedr0p/cluster-template) repository, providing a practical starting point for anyone interested in managing their own Kubernetes environment. 4 | 5 | At its core, this project leverages [makejinja](https://github.com/mirkolenz/makejinja), a powerful tool for rendering templates. By reading configuration files—such as [cluster.yaml](./cluster.sample.yaml) and [nodes.yaml](./nodes.sample.yaml)—Makejinja generates the necessary configurations to deploy a Kubernetes cluster with the following features: 6 | 7 | - Easy configuration through YAML files. 8 | - Compatibility with home setups, whether on physical hardware or VMs. 9 | - A modular and extensible approach to cluster deployment and management. 10 | 11 | With this approach, you'll gain a solid foundation to build and manage your Kubernetes cluster efficiently. 12 | 13 | ## ✨ Features 14 | 15 | A Kubernetes cluster deployed with [Talos Linux](https://github.com/siderolabs/talos) and an opinionated implementation of [Argo](https://github.com/argoproj/argo-cd) using [GitHub](https://github.com/) as the Git provider, [sops](https://github.com/getsops/sops) to manage secrets and [cloudflared](https://github.com/cloudflare/cloudflared) to access applications external to your local network. 16 | 17 | - **Required:** Some knowledge of [Containers](https://opencontainers.org/), [YAML](https://noyaml.com/), [Git](https://git-scm.com/), and a **Cloudflare account** with a **domain**. 18 | - **Included components:** [argo](https://github.com/argoproj/argo-cd), [cilium](https://github.com/cilium/cilium), [cert-manager](https://github.com/cert-manager/cert-manager), [spegel](https://github.com/spegel-org/spegel), [reloader](https://github.com/stakater/Reloader), [envoy-gateway](https://github.com/envoyproxy/gateway), [external-dns](https://github.com/kubernetes-sigs/external-dns) and [cloudflared](https://github.com/cloudflare/cloudflared). 19 | 20 | **Other features include:** 21 | 22 | - Dev env managed w/ [mise](https://mise.jdx.dev/) 23 | - Workflow automation w/ [GitHub Actions](https://github.com/features/actions) 24 | - Dependency automation w/ [Renovate](https://www.mend.io/renovate) 25 | 26 | Does this sound cool to you? If so, continue to read on! 👇 27 | 28 | ## 🚀 Let's Go! 29 | 30 | There are **5 stages** outlined below for completing this project, make sure you follow the stages in order. 31 | 32 | ### Stage 1: Machine Preparation 33 | 34 | > [!IMPORTANT] 35 | > If you have **3 or more nodes** it is recommended to make 3 of them controller nodes for a highly available control plane. This project configures **all nodes** to be able to run workloads. **Worker nodes** are therefore **optional**. 36 | > 37 | > **Minimum system requirements** 38 | > | Role | Cores | Memory | System Disk | 39 | > |---------|----------|---------------|---------------------------| 40 | > | Control/Worker | 4 | 16GB | 256GB SSD/NVMe | 41 | 42 | 1. Head over to the [Talos Linux Image Factory](https://factory.talos.dev) and follow the instructions. Be sure to only choose the **bare-minimum system extensions** as some might require additional configuration and prevent Talos from booting without it. You can always add system extensions after Talos is installed and working. 43 | 44 | 2. This will eventually lead you to download a Talos Linux ISO (or for SBCs a RAW) image. Make sure to note the **schematic ID** you will need this later on. 45 | 46 | 3. Flash the Talos ISO or RAW image to a USB drive and boot from it on your nodes. 47 | 48 | 4. Verify with `nmap` that your nodes are available on the network. (Replace `192.168.1.0/24` with the network your nodes are on.) 49 | 50 | ```sh 51 | nmap -Pn -n -p 50000 192.168.1.0/24 -vv | grep 'Discovered' 52 | ``` 53 | 54 | ### Stage 2: Local Workstation 55 | 56 | > [!TIP] 57 | > It is recommended to set the visibility of your repository to `Public` so you can easily request help if you get stuck. 58 | 59 | 1. Create a new repository by clicking the green `Use this template` button at the top of this page, then clone the new repo you just created and `cd` into it. Alternatively you can us the [GitHub CLI](https://cli.github.com/) ... 60 | 61 | ```sh 62 | export REPONAME="home-ops" 63 | gh repo create $REPONAME --template ajaykumar4/cluster-template --disable-wiki --public --clone && cd $REPONAME 64 | ``` 65 | 66 | 2. **Install** the [Mise CLI](https://mise.jdx.dev/getting-started.html#installing-mise-cli) on your workstation. 67 | 68 | 3. **Activate** Mise in your shell by following the [activation guide](https://mise.jdx.dev/getting-started.html#activate-mise). 69 | 70 | 4. Use `mise` to install the **required** CLI tools: 71 | 72 | ```sh 73 | mise trust 74 | pip install pipx 75 | mise install 76 | mise run deps 77 | ``` 78 | 79 | 📍 _**Having trouble installing the tools?** Try unsetting the `GITHUB_TOKEN` env var and then run these commands again_ 80 | 81 | 📍 _**Having trouble compiling Python?** Try running `mise settings python.compile=0` and then run these commands again_ 82 | 83 | 5. Logout of GitHub Container Registry (GHCR) as this may cause authorization problems when using the public registry: 84 | 85 | ```sh 86 | docker logout ghcr.io 87 | helm registry logout ghcr.io 88 | ``` 89 | 90 | ### Stage 3: Cloudflare configuration 91 | 92 | > [!WARNING] 93 | > If any of the commands fail with `command not found` or `unknown command` it means `mise` is either not install or configured incorrectly. 94 | 95 | 1. Create a Cloudflare API token for use with cloudflared and external-dns by reviewing the official [documentation](https://developers.cloudflare.com/fundamentals/api/get-started/create-token/) and following the instructions below. 96 | 97 | - Click the blue `Use template` button for the `Edit zone DNS` template. 98 | - Name your token `kubernetes` 99 | - Under `Permissions`, click `+ Add More` and add permissions `Zone - DNS - Edit` and `Account - Cloudflare Tunnel - Read` 100 | - Limit the permissions to a specific account and/or zone resources and then click `Continue to Summary` and then `Create Token`. 101 | - **Save this token somewhere safe**, you will need it later on. 102 | 103 | 2. Create the Cloudflare Tunnel: 104 | 105 | ```sh 106 | cloudflared tunnel login 107 | cloudflared tunnel create --credentials-file cloudflare-tunnel.json kubernetes 108 | ``` 109 | 110 | ### Stage 4: Cluster configuration 111 | 112 | 1. Generate the config files from the sample files: 113 | 114 | ```sh 115 | task init 116 | ``` 117 | 118 | 2. Fill out `cluster.yaml` and `nodes.yaml` configuration files using the comments in those file as a guide. 119 | 120 | 3. Template out the kubernetes and talos configuration files, if any issues come up be sure to read the error and adjust your config files accordingly. 121 | 122 | ```sh 123 | task configure 124 | ``` 125 | 126 | 4. Push your changes to git: 127 | 128 | 📍 _**Verify** all the `./kubernetes/**/*.sops.*` files are **encrypted** with SOPS_ 129 | 130 | ```sh 131 | git add -A 132 | git commit -m "chore: initial commit :rocket:" 133 | git push 134 | ``` 135 | 136 | > [!TIP] 137 | > Using a **private repository**? Make sure to paste the public key from `github-deploy.key.pub` into the deploy keys section of your GitHub repository settings. This will make sure Argo has read/write access to your repository. 138 | 139 | ### Stage 5: Bootstrap Talos, Kubernetes, and Argo 140 | 141 | > [!WARNING] 142 | > It might take a while for the cluster to be setup (10+ minutes is normal). During which time you will see a variety of error messages like: "couldn't get current server API group list," "error: no matching resources found", etc. 'Ready' will remain "False" as no CNI is deployed yet. **This is a normal.** If this step gets interrupted, e.g. by pressing Ctrl + C, you likely will need to [reset the cluster](#-reset) before trying again 143 | 144 | 1. Install Talos: 145 | 146 | ```sh 147 | task bootstrap:talos 148 | ``` 149 | 150 | 2. Push your changes to git: 151 | 152 | ```sh 153 | git add -A 154 | git commit -m "chore: add talhelper encrypted secret :lock:" 155 | git push 156 | ``` 157 | 158 | 3. Install cilium, coredns, spegel, argo and sync the cluster to the repository state: 159 | 160 | ```sh 161 | task bootstrap:apps 162 | ``` 163 | 164 | 4. Watch the rollout of your cluster happen: 165 | 166 | ```sh 167 | kubectl get pods --all-namespaces --watch 168 | ``` 169 | 170 | ## 📣 Post installation 171 | 172 | ### ✅ Verifications 173 | 174 | 1. Check the status of Cilium: 175 | 176 | ```sh 177 | cilium status 178 | ``` 179 | 180 | 2. Check the status of Argo and if the Argo resources are up-to-date and in a ready state: 181 | 182 | 📍 _Run `task reconcile` to force Argo to sync your Git repository state_ 183 | 184 | ```sh 185 | argocd login argo.${cloudflare_domain} --username admin --password ${argo_password} --insecure 186 | argocd cluster list 187 | argocd repo list --output wide 188 | argocd app list -A --output wide 189 | ``` 190 | 191 | 3. Check TCP connectivity to both the internal and external gateways: 192 | 193 | 📍 _The variables are only placeholders, replace them with your actual values_ 194 | 195 | ```sh 196 | nmap -Pn -n -p 443 ${cluster_gateway_addr} ${cloudflare_gateway_addr} -vv 197 | ``` 198 | 199 | 4. Check you can resolve DNS for `echo`, this should resolve to `${cloudflare_gateway_addr}`: 200 | 201 | 📍 _The variables are only placeholders, replace them with your actual values_ 202 | 203 | ```sh 204 | dig @${cluster_dns_gateway_addr} echo.${cloudflare_domain} 205 | ``` 206 | 207 | 5. Check the status of your wildcard `Certificate`: 208 | 209 | ```sh 210 | kubectl -n cert-manager describe certificates 211 | ``` 212 | ### 🌐 Public DNS 213 | 214 | > [!TIP] 215 | > Use the `envoy-external` gateway on `HTTPRoutes` to make applications public to the internet. These are also accessible on your private network once you set up split DNS. 216 | 217 | The `external-dns` application created in the `network` namespace will handle creating public DNS records. By default, `echo` and the `argo` are the only subdomains reachable from the public internet. In order to make additional applications public you must **set the correct gateway** like in the HelmRelease for `echo`. 218 | 219 | ### 🏠 Home DNS 220 | 221 | > [!TIP] 222 | > Use the `envoy-internal` gateway on `HTTPRoutes` to make applications private to your network. If you're having trouble with internal DNS resolution check out [this](https://github.com/onedr0p/cluster-template/discussions/719) GitHub discussion. 223 | 224 | `k8s_gateway` will provide DNS resolution to external Kubernetes resources (i.e. points of entry to the cluster) from any device that uses your home DNS server. For this to work, your home DNS server must be configured to forward DNS queries for `${cloudflare_domain}` to `${cluster_dns_gateway_addr}` instead of the upstream DNS server(s) it normally uses. This is a form of **split DNS** (aka split-horizon DNS / conditional forwarding). 225 | 226 | _... Nothing working? That is expected, this is DNS after all!_ 227 | 228 | ### 🪝 Github Webhook 229 | 230 | By default Argo will periodically check your git repository for changes. In-order to have Argo reconcile on `git push` you must configure Github to send `push` events to Argo. 231 | 232 | 1. Piece together the full URL with the webhook path appended: 233 | 234 | ```text 235 | https://argo.${cloudflare_domain}/api/webhook 236 | ``` 237 | 238 | 3. Navigate to the settings of your repository on Github, under "Settings/Webhooks" press the "Add webhook" button. Fill in the webhook URL and your token from `github-push-token.txt`, Content type: `application/json`, Events: Choose Just the push event, and save. 239 | 240 | ## 💥 Reset 241 | 242 | > [!CAUTION] 243 | > **Resetting** the cluster **multiple times in a short period of time** could lead to being **rate limited by DockerHub or Let's Encrypt**. 244 | 245 | There might be a situation where you want to destroy your Kubernetes cluster. The following command will reset your nodes back to maintenance mode. 246 | 247 | ```sh 248 | task talos:reset 249 | ``` 250 | 251 | ## 🛠️ Talos and Kubernetes Maintenance 252 | 253 | ### ⚙️ Updating Talos node configuration 254 | 255 | > [!TIP] 256 | > Ensure you have updated `talconfig.yaml` and any patches with your updated configuration. In some cases you **not only need to apply the configuration but also upgrade talos** to apply new configuration. 257 | 258 | ```sh 259 | # (Re)generate the Talos config 260 | task talos:generate-config 261 | # Apply the config to the node 262 | task talos:apply-node IP=? MODE=? 263 | # e.g. task talos:apply-node IP=10.10.10.10 MODE=auto 264 | ``` 265 | 266 | ### ⬆️ Updating Talos and Kubernetes versions 267 | 268 | > [!TIP] 269 | > Ensure the `talosVersion` and `kubernetesVersion` in `talenv.yaml` are up-to-date with the version you wish to upgrade to. 270 | 271 | ```sh 272 | # Upgrade node to a newer Talos version 273 | task talos:upgrade-node IP=? 274 | # e.g. task talos:upgrade-node IP=10.10.10.10 275 | ``` 276 | 277 | ```sh 278 | # Upgrade cluster to a newer Kubernetes version 279 | task talos:upgrade-k8s 280 | # e.g. task talos:upgrade-k8s 281 | ``` 282 | 283 | ## 🤖 Renovate 284 | 285 | [Renovate](https://www.mend.io/renovate) is a tool that automates dependency management. It is designed to scan your repository around the clock and open PRs for out-of-date dependencies it finds. Common dependencies it can discover are Helm charts, container images, GitHub Actions and more! In most cases merging a PR will cause Argo to apply the update to your cluster. 286 | 287 | To enable Renovate, click the 'Configure' button over at their [Github app page](https://github.com/apps/renovate) and select your repository. Renovate creates a "Dependency Dashboard" as an issue in your repository, giving an overview of the status of all updates. The dashboard has interactive checkboxes that let you do things like advance scheduling or reattempt update PRs you closed without merging. 288 | 289 | The base Renovate configuration in your repository can be viewed at [.renovaterc.json5](.renovaterc.json5). By default it is scheduled to be active with PRs every weekend, but you can [change the schedule to anything you want](https://docs.renovatebot.com/presets-schedule), or remove it if you want Renovate to open PRs immediately. 290 | 291 | ## 🐛 Debugging 292 | 293 | Below is a general guide on trying to debug an issue with an resource or application. For example, if a workload/resource is not showing up or a pod has started but in a `CrashLoopBackOff` or `Pending` state. These steps do not include a way to fix the problem as the problem could be one of many different things. 294 | 295 | 1. Check if the Argo resources are up-to-date and in a ready state: 296 | 297 | 📍 _Run `task reconcile` to force Argo to sync your Git repository state_ 298 | 299 | ```sh 300 | argocd repo list --output wide 301 | argocd app list -A --output wide 302 | ``` 303 | 304 | 2. Do you see the pod of the workload you are debugging: 305 | 306 | ```sh 307 | kubectl -n get pods -o wide 308 | ``` 309 | 310 | 3. Check the logs of the pod if its there: 311 | 312 | ```sh 313 | kubectl -n logs -f 314 | ``` 315 | 316 | 4. If a resource exists try to describe it to see what problems it might have: 317 | 318 | ```sh 319 | kubectl -n describe 320 | ``` 321 | 322 | 5. Check the namespace events: 323 | 324 | ```sh 325 | kubectl -n get events --sort-by='.metadata.creationTimestamp' 326 | ``` 327 | 328 | Resolving problems that you have could take some tweaking of your YAML manifests in order to get things working, other times it could be a external factor like permissions on a NFS server. If you are unable to figure out your problem see the support sections below. 329 | 330 | ## 🧹 Tidy up 331 | 332 | Once your cluster is fully configured and you no longer need to run `task configure`, it's a good idea to clean up the repository by removing the [templates](./templates) directory and any files related to the templating process. This will help eliminate unnecessary clutter from the upstream template repository and resolve any "duplicate registry" warnings from Renovate. 333 | 334 | 1. Tidy up your repository: 335 | 336 | ```sh 337 | task template:tidy 338 | ``` 339 | 340 | 2. Push your changes to git: 341 | 342 | ```sh 343 | git add -A 344 | git commit -m "chore: tidy up :broom:" 345 | git push 346 | ``` 347 | 348 | ## ❔ What's next 349 | 350 | There's a lot to absorb here, especially if you're new to these tools. Take some time to familiarize yourself with the tooling and understand how all the components interconnect. Dive into the documentation of the various tools included — they are a valuable resource. This shouldn't be a production environment yet, so embrace the freedom to experiment. Move fast, break things intentionally, and challenge yourself to fix them. 351 | 352 | Below are some optional considerations you may want to explore. 353 | 354 | ### DNS 355 | 356 | The template uses [k8s_gateway](https://github.com/ori-edge/k8s_gateway) to provide DNS for your applications, consider exploring [external-dns](https://github.com/kubernetes-sigs/external-dns) as an alternative. 357 | 358 | External-DNS offers broad support for various DNS providers, including but not limited to: 359 | 360 | - [Pi-hole](https://github.com/kubernetes-sigs/external-dns/blob/master/docs/tutorials/pihole.md) 361 | - [UniFi](https://github.com/kashalls/external-dns-unifi-webhook) 362 | - [Adguard Home](https://github.com/muhlba91/external-dns-provider-adguard) 363 | - [Bind](https://github.com/kubernetes-sigs/external-dns/blob/master/docs/tutorials/rfc2136.md) 364 | 365 | This flexibility allows you to integrate seamlessly with a range of DNS solutions to suit your environment and offload DNS from your cluster to your router, or external device. 366 | 367 | ### Secrets 368 | 369 | SOPs is an excellent tool for managing secrets in a GitOps workflow. However, it can become cumbersome when rotating secrets or maintaining a single source of truth for secret items. 370 | 371 | For a more streamlined approach to those issues, consider [External Secrets](https://external-secrets.io/latest/). This tool allows you to move away from SOPs and leverage an external provider for managing your secrets. External Secrets supports a wide range of providers, from cloud-based solutions to self-hosted options. 372 | 373 | ### Storage 374 | 375 | If your workloads require persistent storage with features like replication or connectivity to NFS, SMB, or iSCSI servers, there are several projects worth exploring: 376 | 377 | - [rook-ceph](https://github.com/rook/rook) 378 | - [longhorn](https://github.com/longhorn/longhorn) 379 | - [openebs](https://github.com/openebs/openebs) 380 | - [democratic-csi](https://github.com/democratic-csi/democratic-csi) 381 | - [csi-driver-nfs](https://github.com/kubernetes-csi/csi-driver-nfs) 382 | - [csi-driver-smb](https://github.com/kubernetes-csi/csi-driver-smb) 383 | - [synology-csi](https://github.com/SynologyOpenSource/synology-csi) 384 | 385 | These tools offer a variety of solutions to meet your persistent storage needs, whether you’re using cloud-native or self-hosted infrastructures. 386 | 387 | ### Community Repositories 388 | 389 | Community member [@whazor](https://github.com/whazor) created [Kubesearch](https://kubesearch.dev) to allow searching Argo Helm Releases across Github and Gitlab repositories with the `kubesearch` topic. 390 | 391 | ## 🙋 Support 392 | 393 | ### Community 394 | 395 | - Make a post in this repository's Github [Discussions](https://github.com/ajaykumar4/cluster-template/discussions). 396 | - Start a thread in the `#support` or `#cluster-template` channels in the [Home Operations](https://discord.gg/home-operations) Discord server. 397 | 398 | ## 🙌 Related Projects 399 | 400 | If this repo is too hot to handle or too cold to hold check out these following projects. 401 | 402 | - [onedr0p/cluster-template](https://github.com/onedr0p/cluster-template) - _A template for deploying a Talos Kubernetes cluster including Flux for GitOps_ 403 | - [khuedoan/homelab](https://github.com/khuedoan/homelab) - _Fully automated homelab from empty disk to running services with a single command._ 404 | - [mitchross/k3s-argocd-starter](https://github.com/mitchross/k3s-argocd-starter) - starter kit for k3s, argocd 405 | - [ricsanfre/pi-cluster](https://github.com/ricsanfre/pi-cluster) - _Pi Kubernetes Cluster. Homelab kubernetes cluster automated with Ansible and FluxCD_ 406 | - [techno-tim/k3s-ansible](https://github.com/techno-tim/k3s-ansible) - _The easiest way to bootstrap a self-hosted High Availability Kubernetes cluster. A fully automated HA k3s etcd install with kube-vip, MetalLB, and more. Build. Destroy. Repeat._ 407 | 408 | ## ⭐ Stargazers 409 | 410 | 421 | 422 | ## 🤝 Thanks 423 | 424 | Big shout out to [onedr0p](https://github.com/onedr0p) 425 | --------------------------------------------------------------------------------