From c6eed359e1571e238cbae46ff955384ed64d32cc Mon Sep 17 00:00:00 2001 From: Ian Keane Date: Tue, 18 Aug 2026 10:00:53 -0400 Subject: [PATCH] Restructure and variablize config --- .gitignore | 2 ++ Makefile | 25 ++++++++++++-- README.md | 42 ++++++++++++++++++++++-- charts/argocd/values.yaml | 2 +- charts/cert-manager/cluster-issuers.yaml | 4 +-- charts/fluent-bit/values.yaml | 4 +-- manifests/cluster/argocd.yaml | 1 + manifests/cluster/cert-manager.yaml | 17 ++++++---- manifests/cluster/fluent-bit.yaml | 1 + terraform/argocd.tf | 6 ++-- terraform/logging.tf | 14 ++++---- terraform/outputs.tf | 2 +- terraform/variables.tf | 20 +++++++++-- values.yaml | 10 ++++++ 14 files changed, 122 insertions(+), 28 deletions(-) create mode 100644 values.yaml diff --git a/.gitignore b/.gitignore index d05beac..382eebc 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,5 @@ worker.yaml talosconfig terraform/.terraform/ terraform/.terraform.lock.hcl +terraform.tfstate +terraform/terraform.tfvars diff --git a/Makefile b/Makefile index 05ba3c6..434f5ec 100644 --- a/Makefile +++ b/Makefile @@ -1,6 +1,6 @@ -.PHONY: init plan apply destroy kubeconfig talosconfig argocd-password bootstrap decrypt post-apply fluentbit-secret +.PHONY: init plan apply destroy clean kubeconfig talosconfig argocd-password bootstrap decrypt post-apply fluentbit-secret -SECRET=dumpnet +SECRET ?= dumpnet TF_DIR=terraform SOPS_FILES=controlplane.yaml worker.yaml talosconfig @@ -12,11 +12,29 @@ plan: cd $(TF_DIR) && terraform plan apply: + cd $(TF_DIR) && terraform apply -target=talos_cluster_kubeconfig.this cd $(TF_DIR) && terraform apply destroy: cd $(TF_DIR) && terraform destroy +# Full clean slate - run before destroy when recreating from scratch +# Removes resources that block terraform from recreating cleanly +clean: + @echo "==> Force-deleting Secrets Manager secret..." + -aws secretsmanager delete-secret --secret-id $(SECRET) --force-delete-without-recovery 2>/dev/null || true + @echo "==> Emptying S3 buckets..." + -aws s3 rm s3://dumpnet-logs --recursive 2>/dev/null || true + -aws s3 rm s3://dumpnet-athena-results --recursive 2>/dev/null || true + @echo "==> Clearing stale kubeconfig contexts..." + -kubectl config delete-context admin@dumpnet 2>/dev/null || true + -kubectl config delete-cluster dumpnet 2>/dev/null || true + -kubectl config delete-user admin@dumpnet 2>/dev/null || true + @echo "==> Clearing stale talosctl contexts..." + -talosctl config remove dumpnet 2>/dev/null || true + -talosctl config remove dumpnet-1 2>/dev/null || true + @echo "==> Done. Run 'make destroy' then 'make apply' to rebuild from scratch." + # Bootstrap ArgoCD app of apps (run once after cluster is up) bootstrap: kubectl apply -f apps/apps.yaml @@ -32,6 +50,9 @@ talosconfig: talosctl config context dumpnet kubeconfig: + -kubectl config delete-context admin@dumpnet 2>/dev/null || true + -kubectl config delete-cluster dumpnet 2>/dev/null || true + -kubectl config delete-user admin@dumpnet 2>/dev/null || true aws secretsmanager get-secret-value --secret-id $(SECRET) \ --query SecretString --output text | python3 -c \ "import sys,json; print(json.load(sys.stdin)['cluster']['kubeconfig'])" \ diff --git a/README.md b/README.md index d1ff5a0..ed24049 100644 --- a/README.md +++ b/README.md @@ -23,7 +23,7 @@ GitOps cluster management for dumpnet — a single-node Talos/Kubernetes cluster Sensitive files (`talosconfig`, `controlplane.yaml`, `worker.yaml`) are **gitignored** and never stored in the repo in any form. They are managed as follows: -- **Cluster credentials** (kubeconfig, talosconfig, ArgoCD password) → stored in AWS Secrets Manager at `dumpnet/cluster` +- **Cluster credentials** (kubeconfig, talosconfig, ArgoCD password) → stored in AWS Secrets Manager at `dumpnet` - **Talos machine configs** → encrypted with SOPS + age, stored outside the repo - **Age private key** → lives at `~/.age/key.txt` — **back this up securely** @@ -57,6 +57,13 @@ cp terraform/terraform.tfvars.example terraform/terraform.tfvars make apply ``` +> **Note:** `make apply` runs two Terraform passes internally. The first pass +> (`-target=talos_cluster_kubeconfig.this`) brings up the EC2 instance, bootstraps +> the cluster, and retrieves the kubeconfig. The second pass then uses that live +> kubeconfig to provision Kubernetes resources (namespaces, ArgoCD Helm release). +> This two-phase approach is necessary because the Helm and Kubernetes Terraform +> providers need a reachable cluster to initialize. + This will: - Allocate an EIP - Create a dedicated subnet and security group @@ -90,11 +97,23 @@ make argocd-password Then log in at https://argocd.dumpnet.chat — ArgoCD will finish deploying ingress-nginx and metrics-server automatically. +## Rebuilding from Scratch + +To fully tear down and recreate the cluster: + +```bash +make clean # force-delete secret, empty S3, clear stale kube/talos contexts +make destroy # tear down all terraform resources +make apply # recreate everything +make post-apply # wait for cluster ready, bootstrap ArgoCD +make bootstrap # apply App of Apps +``` + ## Day-to-Day - **Add a new app**: add a manifest to `manifests/` and values to `charts/` — ArgoCD picks it up on next sync - **Add a DNS record**: add the subdomain to `dns_records` in `terraform/terraform.tfvars` and run `make apply` -- **Cluster access**: `make kubeconfig` or `make talosconfig` (pulls from Secrets Manager) +- **Cluster access**: `make kubeconfig` or `make talosconfig` (pulls from Secrets Manager, safe to re-run — automatically clears any stale context from a previous cluster before merging) - **Emergency talosctl access**: `make decrypt` to get configs in `/tmp` ## Makefile Reference @@ -104,9 +123,10 @@ Then log in at https://argocd.dumpnet.chat — ArgoCD will finish deploying ingr | `make apply` | Create/update cluster infrastructure | | `make plan` | Preview infrastructure changes | | `make destroy` | Tear down everything | +| `make clean` | Pre-destroy cleanup (force-delete secret, empty S3 buckets, clear stale kube/talos contexts) | | `make post-apply` | One-time bootstrap after fresh cluster creation | | `make bootstrap` | Apply App of Apps only | -| `make kubeconfig` | Pull kubeconfig from Secrets Manager | +| `make kubeconfig` | Pull kubeconfig from Secrets Manager (clears stale context first) | | `make talosconfig` | Pull talosconfig from Secrets Manager | | `make argocd-password` | Print ArgoCD admin password | | `make decrypt` | Decrypt Talos configs to /tmp | @@ -141,6 +161,22 @@ dumpnet-argo/ └── outputs.tf ``` +## Forking / Multiple Environments + +Global per-cluster config lives in **`values.yaml`** at the repo root: + +```yaml +clusterName: dumpnet +domain: dumpnet.chat +repoURL: https://forge.keane.sh/ian/dumpnet-argo.git +certEmail: dumpnetcerts@keane.sh +awsRegion: us-east-1 +``` + +This file is passed as the first `valueFiles` entry to every Helm chart, so `domain`, `certEmail`, `clusterName`, and `awsRegion` are available as `{{ .Values.* }}` in all chart values. Terraform variables in `terraform/variables.tf` mirror these same settings for the infrastructure side. + +**The one thing that can't be templated** is `repoURL` in the ArgoCD `Application` manifests themselves (under `apps/` and `manifests/`). These are plain YAML consumed by ArgoCD before any Helm rendering happens — there's no way to interpolate them without a [Config Management Plugin](https://argo-cd.readthedocs.io/en/stable/operator-manual/config-management-plugins/). When forking this repo, do a global find/replace on `forge.keane.sh/ian/dumpnet-argo` with your own repo URL. + ## Notes - The control plane taint is disabled via `allowSchedulingOnControlPlanes: true` in the Talos machine config — no manual taint removal needed diff --git a/charts/argocd/values.yaml b/charts/argocd/values.yaml index cbdc4d9..79d5bc8 100644 --- a/charts/argocd/values.yaml +++ b/charts/argocd/values.yaml @@ -5,7 +5,7 @@ server: ingress: enabled: true ingressClassName: nginx - hostname: argocd.dumpnet.chat + hostname: argocd.{{ .Values.domain }} tls: true annotations: cert-manager.io/cluster-issuer: letsencrypt-prod diff --git a/charts/cert-manager/cluster-issuers.yaml b/charts/cert-manager/cluster-issuers.yaml index b20dff1..fc8bd23 100644 --- a/charts/cert-manager/cluster-issuers.yaml +++ b/charts/cert-manager/cluster-issuers.yaml @@ -5,7 +5,7 @@ metadata: spec: acme: server: https://acme-v02.api.letsencrypt.org/directory - email: dumpnetcerts@keane.sh + email: {{ .Values.certEmail }} privateKeySecretRef: name: letsencrypt-prod solvers: @@ -20,7 +20,7 @@ metadata: spec: acme: server: https://acme-staging-v02.api.letsencrypt.org/directory - email: dumpnetcerts@keane.sh + email: {{ .Values.certEmail }} privateKeySecretRef: name: letsencrypt-staging solvers: diff --git a/charts/fluent-bit/values.yaml b/charts/fluent-bit/values.yaml index 67a2916..a6fdb53 100644 --- a/charts/fluent-bit/values.yaml +++ b/charts/fluent-bit/values.yaml @@ -47,8 +47,8 @@ config: [OUTPUT] Name s3 Match kube.* - bucket dumpnet-logs - region us-east-1 + bucket {{ .Values.clusterName }}-logs + region {{ .Values.awsRegion }} s3_key_format /logs/%Y/%m/%d/$TAG[4].%H%M%S.gz total_file_size 100M upload_timeout 10m diff --git a/manifests/cluster/argocd.yaml b/manifests/cluster/argocd.yaml index 5bd64ae..0e89561 100644 --- a/manifests/cluster/argocd.yaml +++ b/manifests/cluster/argocd.yaml @@ -14,6 +14,7 @@ spec: targetRevision: "*" helm: valueFiles: + - $values/values.yaml - $values/charts/argocd/values.yaml destination: server: https://kubernetes.default.svc diff --git a/manifests/cluster/cert-manager.yaml b/manifests/cluster/cert-manager.yaml index f0263b5..4f6e6b7 100644 --- a/manifests/cluster/cert-manager.yaml +++ b/manifests/cluster/cert-manager.yaml @@ -32,12 +32,17 @@ metadata: namespace: argocd spec: project: default - source: - repoURL: https://forge.keane.sh/ian/dumpnet-argo.git - targetRevision: HEAD - path: charts/cert-manager - directory: - include: "cluster-issuers.yaml" + sources: + - repoURL: https://forge.keane.sh/ian/dumpnet-argo.git + targetRevision: HEAD + ref: values + - repoURL: https://forge.keane.sh/ian/dumpnet-argo.git + targetRevision: HEAD + path: charts/cert-manager + helm: + valueFiles: + - $values/values.yaml + releaseName: cert-manager-issuers destination: server: https://kubernetes.default.svc namespace: cert-manager diff --git a/manifests/cluster/fluent-bit.yaml b/manifests/cluster/fluent-bit.yaml index e8128ee..2867276 100644 --- a/manifests/cluster/fluent-bit.yaml +++ b/manifests/cluster/fluent-bit.yaml @@ -14,6 +14,7 @@ spec: targetRevision: "*" helm: valueFiles: + - $values/values.yaml - $values/charts/fluent-bit/values.yaml destination: server: https://kubernetes.default.svc diff --git a/terraform/argocd.tf b/terraform/argocd.tf index 47106a9..7aed63f 100644 --- a/terraform/argocd.tf +++ b/terraform/argocd.tf @@ -5,10 +5,10 @@ resource "random_password" "argocd_admin" { override_special = "!#$%&*()-_=+[]{}?" } -# Single secrets manager secret for all dumpnet secrets +# Single secrets manager secret for all cluster secrets resource "aws_secretsmanager_secret" "dumpnet" { - name = "dumpnet" - description = "All dumpnet secrets" + name = var.cluster_name + description = "All ${var.cluster_name} secrets" } resource "aws_secretsmanager_secret_version" "dumpnet" { diff --git a/terraform/logging.tf b/terraform/logging.tf index bdafea2..28f6ed3 100644 --- a/terraform/logging.tf +++ b/terraform/logging.tf @@ -1,6 +1,6 @@ # S3 bucket for logs resource "aws_s3_bucket" "logs" { - bucket = "dumpnet-logs" + bucket = "${var.cluster_name}-logs" } resource "aws_s3_bucket_lifecycle_configuration" "logs" { @@ -10,7 +10,9 @@ resource "aws_s3_bucket_lifecycle_configuration" "logs" { id = "expire-old-logs" status = "Enabled" - filter {} + filter { + prefix = "" + } transition { days = 30 @@ -34,16 +36,16 @@ resource "aws_s3_bucket_server_side_encryption_configuration" "logs" { # Athena database + workgroup resource "aws_athena_database" "logs" { - name = "dumpnet_logs" + name = "${replace(var.cluster_name, "-", "_")}_logs" bucket = aws_s3_bucket.athena_results.bucket } resource "aws_s3_bucket" "athena_results" { - bucket = "dumpnet-athena-results" + bucket = "${var.cluster_name}-athena-results" } resource "aws_athena_workgroup" "logs" { - name = "dumpnet-logs" + name = "${var.cluster_name}-logs" configuration { result_configuration { @@ -58,7 +60,7 @@ resource "aws_athena_workgroup" "logs" { # IAM user for fluent-bit to write to S3 resource "aws_iam_user" "fluentbit" { - name = "dumpnet-fluentbit" + name = "${var.cluster_name}-fluentbit" } resource "aws_iam_access_key" "fluentbit" { diff --git a/terraform/outputs.tf b/terraform/outputs.tf index 442aada..edc33a7 100644 --- a/terraform/outputs.tf +++ b/terraform/outputs.tf @@ -3,5 +3,5 @@ output "public_ip" { } output "argocd_password_secret" { - value = "aws secretsmanager get-secret-value --secret-id dumpnet/cluster --query SecretString --output text | python3 -m json.tool" + value = "aws secretsmanager get-secret-value --secret-id ${var.cluster_name} --query SecretString --output text | python3 -m json.tool" } diff --git a/terraform/variables.tf b/terraform/variables.tf index fe39596..2b44935 100644 --- a/terraform/variables.tf +++ b/terraform/variables.tf @@ -3,7 +3,8 @@ variable "talos_version" { } variable "cluster_name" { - default = "dumpnet" + description = "Name used for cluster resources, S3 buckets, secrets, etc." + default = "dumpnet" } variable "instance_type" { @@ -19,8 +20,13 @@ variable "ami_id" { default = "ami-000f1b0ad9d8ceafd" } +variable "aws_region" { + description = "AWS region" + default = "us-east-1" +} + variable "hosted_zone_id" { - description = "Route53 hosted zone ID for dumpnet.chat" + description = "Route53 hosted zone ID" } variable "domain" { @@ -33,3 +39,13 @@ variable "dns_records" { type = list(string) default = ["argocd"] } + +variable "cert_manager_email" { + description = "Email for Let's Encrypt certificate notifications" + default = "dumpnetcerts@keane.sh" +} + +variable "state_bucket" { + description = "S3 bucket for Terraform remote state (set in backend config, documented here for reference)" + default = "iankeane-tfstate" +} diff --git a/values.yaml b/values.yaml new file mode 100644 index 0000000..4b36b53 --- /dev/null +++ b/values.yaml @@ -0,0 +1,10 @@ +# Global cluster configuration +# Override any of these in terraform.tfvars or by forking the repo. +# NOTE: repoURL in apps/apps.yaml and manifests/*.yaml must be updated manually — +# ArgoCD Application spec fields cannot be templated via Helm values. + +clusterName: dumpnet +domain: dumpnet.chat +repoURL: https://forge.keane.sh/ian/dumpnet-argo.git +certEmail: dumpnetcerts@keane.sh +awsRegion: us-east-1