diff --git a/README.md b/README.md index 112880e..be9f707 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # Cloud Native AI Platform -A cost-bounded platform for a minimal AI summarization API, built with production-oriented patterns: multi-stage containers, Kubernetes probes, Helm packaging, Terraform foundations, and GitHub Actions publishing immutable images to GHCR. +A cost-bounded platform for a minimal AI summarization API, built with production-oriented patterns: multi-stage containers, Kubernetes probes, Helm packaging, Terraform foundations, GitHub Actions publishing immutable images to GHCR, and Argo CD GitOps on a Hetzner VPS. Monthly spend is capped at **$15**. See [docs/cost-budget.md](docs/cost-budget.md). @@ -10,45 +10,143 @@ Monthly spend is capped at **$15**. See [docs/cost-budget.md](docs/cost-budget.m |-------|--------|-----------| | API + local dev | Done | FastAPI (`/health`, `/ready`, `/metrics`, `POST /v1/summarize`), Compose, golden-path tests | | Container image | Done | Multi-stage Dockerfile, non-root runtime | -| Terraform (hobby) | Scaffolded | Hetzner modules + hobby env; not applied until a VPS is needed | +| Terraform (hobby) | Done | Hetzner CX22 VPS (`cnai-hobby`) + firewall; k3s via cloud-init | | Kubernetes (local) | Done | `kubernetes/base`, liveness/readiness probes. [Runbook](docs/runbooks/app-wont-start.md) | | Helm | Done | `helm/api` + Bitnami Postgres/Redis. [Runbook](docs/runbooks/helm.md) | | CI / registry | Done | GitHub Actions: test → build → push to GHCR on `main` | -| GitOps / cloud deploy | Next | Argo CD + VPS | +| GitOps / cloud deploy | Done | Argo CD on k3s; Application watches `helm/api` + `values-hobby.yaml` | -Operational notes: [docs/lessons-learned.md](docs/lessons-learned.md). +Operational notes: [docs/lessons-learned.md](docs/lessons-learned.md). GitOps details: [gitops/README.md](gitops/README.md). ## Architecture ``` Client → FastAPI (apps/api) → PostgreSQL └→ Redis + +GitHub (main) → GHCR image + └→ Argo CD (k3s on Hetzner) → Helm release in ai-platform ``` The API exposes standard health and metrics endpoints. LLM calls go through a single `summarize()` abstraction: `stub` in tests/CI, `deepseek` via OpenAI-compatible HTTP when configured. Liveness (`/health`) stays cheap; readiness (`/ready`) gates traffic until Postgres and Redis are reachable. +On the hobby VPS, desired state lives in Git. Argo CD renders the Helm chart and applies it in-cluster — you do not `helm upgrade` from the laptop for cloud deploys. + +## Live stack (hobby) + +Hetzner VPS `cnai-hobby` (Nuremberg) — ~$6.49/mo while it exists: + +![Hetzner hobby VPS running](assets/server_running.png) + +Argo CD managing the `api` Application (Healthy, path `helm/api`, namespace `ai-platform`): + +![Argo CD Applications dashboard](assets/argo.png) + ## Repository layout ``` apps/api/ FastAPI service, Dockerfile, tests kubernetes/base/ Raw Kustomize manifests -helm/api/ Application Helm chart +helm/api/ Application Helm chart (values-local + values-hobby) +gitops/ Argo CD Application manifests infrastructure/ Terraform modules + hobby environment .github/workflows/ CI pipeline docs/ Runbooks, architecture, cost budget +assets/ Screenshots referenced from this README ``` ## Deployment options -Pick **one** local stack. Do not run Compose and a cluster side by side. - | Goal | Path | |------|------| -| Fastest dev loop | Docker Compose (below) | +| Fastest local loop | Docker Compose (below) | | Learn raw K8s objects | `kubectl apply -k kubernetes/base` | -| Day-to-day cluster work | Helm + Bitnami. [Runbook](docs/runbooks/helm.md) | +| Day-to-day local cluster | Helm + Bitnami. [Runbook](docs/runbooks/helm.md) | +| Cloud (hobby VPS) | Terraform → k3s → Argo CD. [gitops/README.md](gitops/README.md) | + +Do not run Compose and a local cluster side by side. Cloud work uses the VPS kubeconfig (`~/.kube/hobby.yaml`), not Docker Desktop. + +## Pause, resume, and cost control + +The VPS is the only recurring bill (~$6–7/mo). GHCR, GitHub Actions free tier, Argo CD, and k3s are $0. Keep the monthly total under **$15**. -Cloud infrastructure (`infrastructure/terraform/environments/hobby`) stays offline until a VPS is required. +### Closing for the day (pick one) + +| Intent | Action | Still billed? | +|--------|--------|----------------| +| Short break (hours / overnight) | Hetzner console → power **OFF** on `cnai-hobby` | **Yes** — disk/server reservation still charges | +| Pause multi-day / keep spend flat | `terraform destroy` in `infrastructure/terraform/environments/hobby` | **No** — this is the real off switch | +| Pause > 7 days | Always `terraform destroy` (see [docs/cost-budget.md](docs/cost-budget.md)) | No | + +Powering off is convenient but **does not stop the meter**. Deleting the server (Terraform destroy or Hetzner Delete) does. + +Before destroy: nothing unique should live only on the box — Git + GHCR are source of truth. After destroy, note spend in [docs/cost-budget.md](docs/cost-budget.md). + +```bash +cd infrastructure/terraform/environments/hobby +export HCLOUD_TOKEN=... # Hetzner API token +terraform destroy +``` + +### Opening again (same machine, VPS still exists) + +1. Hetzner console → power **ON** if you powered off. +2. Confirm your public IP still matches the firewall allow-list in `main.tf` (`ssh_source_cidrs`). If your ISP changed it, update the CIDR and `terraform apply` before SSH will work. +3. Point kubectl at the hobby cluster and verify: + +```bash +export KUBECONFIG=~/.kube/hobby.yaml +kubectl get nodes +kubectl -n argocd get pods +kubectl -n argocd port-forward svc/argocd-server 8080:443 +# UI: https://localhost:8080 +``` + +### Fresh device → back to the current point + +You need: this repo, your SSH **private** key (same key Terraform registered), `HCLOUD_TOKEN`, a GitHub PAT with `read:packages`, and either (A) the existing VPS still running or (B) a willingness to recreate it. + +**A — VPS still running (cheaper resume)** + +```bash +git clone https://github.com/notsubash/cloud-native-AI-platform.git +cd cloud-native-AI-platform + +# Laptop only — do not run scp while SSH'd into the VPS +mkdir -p ~/.kube +scp -i ~/.ssh/id_ed25519 root@:/etc/rancher/k3s/k3s.yaml ~/.kube/hobby.yaml +# Edit hobby.yaml: replace 127.0.0.1 with + +export KUBECONFIG=~/.kube/hobby.yaml +kubectl get nodes + +# Argo UI +kubectl -n argocd port-forward svc/argocd-server 8080:443 +``` + +If you lack Terraform state on the new machine, manage the existing server from the Hetzner console (or copy `*.tfstate` from the old laptop). Do not `terraform apply` blindly — it may try to create a second billable server. + +**B — Recreate from zero (after destroy, or no state)** + +```bash +cd infrastructure/terraform/environments/hobby +cp terraform.tfvars.example terraform.tfvars # set ssh_public_key_path +export HCLOUD_TOKEN=... +# Update ssh_source_cidrs in main.tf to YOUR current public IP/32 +terraform init && terraform plan && terraform apply + +# Wait ~2–3 min for cloud-init/k3s, then copy kubeconfig (see A) +# Install Argo CD (server-side apply), create ghcr-pull secret, apply gitops/applications/api.yaml +# Full sequence: gitops/README.md +``` + +### Cost hygiene checklist + +- [ ] Before leaving for the day: power off **or** destroy (know which you chose). +- [ ] If paused > 7 days: destroy, don’t leave an idle ON server. +- [ ] After destroy: confirm Hetzner console shows **no** `cnai-hobby` server. +- [ ] Never commit `HCLOUD_TOKEN`, `terraform.tfvars`, `*.tfstate`, or the GHCR PAT. +- [ ] Firewall is IP-locked — a new network/café IP blocks SSH until you update `ssh_source_cidrs`. ## Local development (Compose) @@ -86,6 +184,12 @@ helm history api -n ai-platform Full sequence, DNS notes, and tear-down: [docs/runbooks/helm.md](docs/runbooks/helm.md). +## GitOps (hobby cloud) + +Desired state: [gitops/applications/api.yaml](gitops/applications/api.yaml) → chart `helm/api` with [helm/api/values-hobby.yaml](helm/api/values-hobby.yaml) (GHCR image + `ghcr-pull` secret). + +App changes go through Git + Argo Sync — not `helm upgrade` on the laptop. Bootstrap, sync, and drift notes: [gitops/README.md](gitops/README.md). + ## CI / container registry [`.github/workflows/ci.yml`](.github/workflows/ci.yml) runs on pull requests and pushes to `main`: @@ -105,7 +209,7 @@ gh auth token | docker login ghcr.io -u --password-stdin docker pull ghcr.io//cloud-native-ai-api:sha- ``` -PR builds validate the Dockerfile without publishing. Deploy wiring to GHCR images is handled by GitOps, not in CI. +PR builds validate the Dockerfile without publishing. Cloud deploys consume GHCR via Argo + `values-hobby.yaml`. ## Image build diff --git a/assets/argo.png b/assets/argo.png new file mode 100644 index 0000000..7daeed6 Binary files /dev/null and b/assets/argo.png differ diff --git a/assets/server_running.png b/assets/server_running.png new file mode 100644 index 0000000..e6da684 Binary files /dev/null and b/assets/server_running.png differ diff --git a/docs/lessons-learned.md b/docs/lessons-learned.md index e9393a3..d1e93ea 100644 --- a/docs/lessons-learned.md +++ b/docs/lessons-learned.md @@ -50,13 +50,13 @@ Do not run `make up` (Compose) alongside the K8s stack. Access the API with `kub Owned the API as `helm/api` (Chart.yaml, values, templates, helpers, NOTES) and left Postgres/Redis to Bitnami — own app charts, consume mature data-store charts. -`values-local.yaml` is the local knob file: `pullPolicy: Never`, stub LLM, tiny resources, and Bitnami Service DNS (`postgres-postgresql`, `redis-master`). Defaults in `values.yaml` stay closer to Phase 3 names; local overrides win with `-f`. +`values-local.yaml` is the local knob file: `pullPolicy: Never`, stub LLM, tiny resources, and Bitnami Service DNS (`postgres-postgresql`, `redis-master`). Defaults in `values.yaml` stay closer to the raw `kubernetes/base` names; local overrides win with `-f`. Proved the exit checklist: `helm upgrade ... --set image.tag=local-v2` moved the pod image; `helm rollback api 1` restored `cloud-native-ai-api:local` and wrote a new history revision (rollback is a new revision, not a rewind of the list). Service DNS still bites after switching charts: Bitnami release `postgres` → Service `postgres-postgresql`, Redis standalone → `redis-master`. Wrong host → `/ready` fails even when pods look fine. -Do not run Phase 3 raw `api`/`postgres`/`redis` next to Helm/Bitnami in the same namespace — two Deployments fight for the same mental model. Pick one path: today that path is Bitnami + `helm/api`. +Do not run raw `api`/`postgres`/`redis` manifests next to Helm/Bitnami in the same namespace — two Deployments fight for the same mental model. Pick one path: today that path is Bitnami + `helm/api` locally, and Argo + `values-hobby.yaml` on the VPS. `helm template` before `install` caught path typos (`./help/...`); `helm lint` + rendered YAML beat debugging CrashLoops from bad templates. @@ -72,4 +72,22 @@ GHCR auth is `GITHUB_TOKEN` + `packages: write` on the build job only — no PAT Buildx + `cache-from` / `cache-to type=gha,mode=max` stores builder layers in GitHub Actions cache — first run cold (~0% cached), later runs reuse pip/install layers. The `.dockerbuild` artifact is a Buildx record, not a published package. -CI produces images; it does not deploy. Helm values still point at local tags for laptop work — wiring `image.repository` to `ghcr.io//cloud-native-ai-api` and `pullPolicy: IfNotPresent` is Phase 6 (GitOps on the VPS), not something to bolt onto Actions with `kubectl apply`. +CI produces images; it does not deploy. Laptop Helm keeps local tags; cloud wiring (`ghcr.io/...`, `pullPolicy: IfNotPresent`, `imagePullSecrets`) lives in `values-hobby.yaml` and is applied by Argo — not bolted onto Actions with `kubectl apply`. + +## Hobby cloud & GitOps (Hetzner + k3s + Argo CD) + +Applied Terraform for real: Hetzner CX23 (`cnai-hobby`), firewall, cloud-init → single-node k3s. Billing starts at `apply`; the only reliable off switch is `terraform destroy` (console power-off still charges for the reserved server). + +Cloudflare provider still configures even when DNS is disabled — empty `provider "cloudflare" {}` demands a token and can pull a breaking provider major. For hobby without DNS: leave Cloudflare out of the apply path entirely (comment provider + DNS module) until you need records. + +Copied kubeconfig from the **laptop** with `scp root@:... ~/.kube/hobby.yaml`, then replaced `127.0.0.1` with the public IP. Running `scp` while already SSH’d into the VPS targets the server itself and fails (key-only auth, wrong destination). + +Argo install: prefer `kubectl apply --server-side` — client-side apply blows the last-applied annotation size limit on large CRDs (ApplicationSet). Core `Application` still works; finish install with server-side / `--force-conflicts` if needed. + +GitOps only sees what GitHub has. Local branch + uncommitted `values-hobby.yaml` → Argo errors like “unable to resolve revision” or missing values file. Commit, push, then Sync. + +`ghcr-pull` must be a docker-registry Secret whose password is a GitHub PAT (`read:packages`). An image tag like `sha-...` is not a password — that mistake yields `ImagePullBackOff` after a “healthy” Application. + +Kept sync **manual** at first: applying the Application CR registers desired state; pods appear after Sync. Automate prune/selfHeal later once the loop feels boring. + +Firewall `ssh_source_cidrs` is a single home IP `/32`. Café/VPN IP changes look like “SSH hang” — update Terraform and re-apply before debugging k3s. diff --git a/gitops/README.md b/gitops/README.md new file mode 100644 index 0000000..de8e2ce --- /dev/null +++ b/gitops/README.md @@ -0,0 +1,90 @@ +# GitOps (Argo CD) + +Desired state for the hobby cluster lives in Git. Argo CD watches this repo, renders the Helm chart, and applies it into `ai-platform`. You do not `helm upgrade` from your laptop for cloud deploys. + +``` +GitHub (branch in Application) → Argo CD → Helm (helm/api + values-hobby.yaml) → k3s +``` + +## Layout + +| Path | Role | +|------|------| +| `gitops/applications/api.yaml` | Argo `Application` CR — source repo/path, destination namespace, sync policy | +| `helm/api/` | Chart Argo renders | +| `helm/api/values-hobby.yaml` | Cloud values: GHCR image, `IfNotPresent`, `imagePullSecrets: ghcr-pull` | + +`root-app.yaml` is reserved for an optional app-of-apps bootstrap later; the working entry point today is `applications/api.yaml`. + +## Prerequisites (cluster already up) + +- Hobby VPS with k3s (Terraform + cloud-init) +- `KUBECONFIG` pointing at the VPS (e.g. `~/.kube/hobby.yaml` with `127.0.0.1` replaced by the public IP) +- Argo CD installed in namespace `argocd` +- Namespace `ai-platform` and docker-registry secret `ghcr-pull` (GitHub PAT with `read:packages` — not an image tag) + +## Bootstrap Argo CD (once per cluster) + +```bash +kubectl create namespace argocd +kubectl apply --server-side -n argocd \ + -f https://raw.githubusercontent.com/argoproj/argo-cd/stable/manifests/install.yaml +# If objects already exist and conflict: +# kubectl apply --server-side --force-conflicts -n argocd -f + +kubectl -n argocd get pods # wait until Running +kubectl -n argocd get secret argocd-initial-admin-secret \ + -o jsonpath='{.data.password}' | base64 -d; echo + +kubectl -n argocd port-forward svc/argocd-server 8080:443 +# UI: https://localhost:8080 (user: admin) +``` + +Use **server-side** apply: client-side `kubectl apply` can fail on large Argo CRDs (annotation size limit). + +## Register the Application + +Commit and **push** the chart path and values Argo should see. Argo clones GitHub — uncommitted or local-only branches are invisible. + +```bash +# Secret once per namespace (PAT, not an image digest) +kubectl create namespace ai-platform # if missing +kubectl -n ai-platform create secret docker-registry ghcr-pull \ + --docker-server=ghcr.io \ + --docker-username= \ + --docker-password='' \ + --docker-email= + +kubectl apply -f gitops/applications/api.yaml +kubectl -n argocd get application api +``` + +Sync is **manual** by default (safer while learning). In the UI: open `api` → **SYNC**, or: + +```bash +# after installing the argocd CLI, or use the UI Sync button +argocd app sync api +``` + +Automated sync / self-heal can be enabled later in `api.yaml` (`syncPolicy.automated`). + +## Day-to-day loop + +1. Change chart or `values-hobby.yaml` (prefer pinning `image.tag` to `sha-` from CI). +2. Commit + push to the revision in `api.yaml` (`targetRevision`). +3. Refresh / Sync in Argo. +4. Check: `kubectl -n ai-platform get pods` and port-forward `svc/api` if needed. + +## Common failures + +| Symptom | Likely cause | +|---------|----------------| +| `unable to resolve '' to a commit SHA` | Branch not pushed to GitHub | +| `values-hobby.yaml: no such file` | File not on the tracked revision / not committed | +| Sync OK but `ImagePullBackOff` | Bad `ghcr-pull` secret (need PAT with `read:packages`) | +| Application Healthy but empty namespace | Manual sync not run yet | +| SSH to VPS hangs | Your public IP changed; update `ssh_source_cidrs` in Terraform | + +## Cost note + +Argo and k3s are free. The Hetzner VPS is the bill. Power-off still charges; `terraform destroy` stops billing. See the pause/resume section in the root [README.md](../README.md). diff --git a/gitops/applications/api.yaml b/gitops/applications/api.yaml new file mode 100644 index 0000000..87b9783 --- /dev/null +++ b/gitops/applications/api.yaml @@ -0,0 +1,68 @@ +# ============================================================================= +# Argo CD Application — "desired state" for our API Helm chart +# ============================================================================= +# This is NOT a Kubernetes Deployment. It is an Argo CRD that says: +# "Watch this Git path, render Helm, apply into that namespace." +# +# After you commit this file, you either: +# A) kubectl apply -f gitops/applications/api.yaml (bootstrap once) +# B) or create it via Argo UI pointing at the same repo/path +# +# From then on, CHANGES TO THE APP go through Git + Argo sync — +# not through helm upgrade on your laptop. +# ============================================================================= + +apiVersion: argoproj.io/v1alpha1 +kind: Application +metadata: + name: api + namespace: argocd # Applications live in Argo's namespace by default + # finalizer: ensures resources are deleted when the Application is deleted + finalizers: + - resources-finalizer.argocd.argoproj.io +spec: + project: default + + # ------------------------------------------------------------------------- + # SOURCE — where desired state lives + # ------------------------------------------------------------------------- + source: + # Your GitHub repo (HTTPS). Private repo → register credentials in Argo. + repoURL: https://github.com/notsubash/cloud-native-AI-platform.git + targetRevision: main # branch/tag/commit Argo tracks + path: helm/api # chart directory + + # Tell Argo this path is a Helm chart + helm: + valueFiles: + - values-hobby.yaml # cloud values (GHCR image + pull secret) + # Optional: override tag without editing values file + # parameters: + # - name: image.tag + # value: sha-abc1234 + + + # ------------------------------------------------------------------------- + # DESTINATION — which cluster/namespace to apply into + # ------------------------------------------------------------------------- + destination: + server: https://kubernetes.default.svc # "in-cluster" (Argo talks to local API) + namespace: ai-platform + + # Create the namespace if missing + syncPolicy: + syncOptions: + - CreateNamespace=true + + # ----------------------------------------------------------------------- + # Start MANUAL (safer for learning). Click Sync in UI or: + # argocd app sync api + # + # Later, enable automated: + # automated: + # prune: true # delete resources removed from Git + # selfHeal: true # revert manual kubectl edits (drift drill) + # ----------------------------------------------------------------------- + # automated: + # prune: true + # selfHeal: true \ No newline at end of file diff --git a/gitops/root-app.yaml b/gitops/root-app.yaml new file mode 100644 index 0000000..e69de29 diff --git a/helm/api/values-hobby.yaml b/helm/api/values-hobby.yaml new file mode 100644 index 0000000..c3105fa --- /dev/null +++ b/helm/api/values-hobby.yaml @@ -0,0 +1,41 @@ +# ============================================================================= +# values-hobby.yaml — cloud VPS +# ============================================================================= +# Diff from values-local.yaml: +# - image comes from GHCR (not local Docker Desktop) +# - pullPolicy IfNotPresent (nodes pull once, then cache) +# - imagePullSecrets so private GHCR packages work +# ============================================================================= + +replicaCount: 1 + +image: + repository: ghcr.io/notsubash/cloud-native-ai-api + # Prefer immutable sha tag from CI, not "latest", for real GitOps + tag: latest # first sync; then pin to sha- once you verify + pullPolicy: IfNotPresent + +# Name of the Secret we create below (docker-registry type) +imagePullSecrets: + - name: ghcr-pull + +# Non-secret config — same pattern as local, in-cluster Service DNS +config: + LOG_LEVEL: "INFO" + LLM_MODE: "stub" + DATABASE_URL: "postgresql+psycopg://app:app@postgres:5432/app" + REDIS_URL: "redis://redis:6379/0" + +secret: + create: true + POSTGRES_USER: "app" + POSTGRES_PASSWORD: "app" # Todo: Sealed Secrets / SOPS — not plaintext forever + POSTGRES_DB: "app" + DEEPSEEK_API_KEY: "" + +resources: + requests: + cpu: 50m + memory: 128Mi + limits: + memory: 256Mi \ No newline at end of file diff --git a/infrastructure/terraform/environments/hobby/.terraform.lock.hcl b/infrastructure/terraform/environments/hobby/.terraform.lock.hcl index 281abff..101a289 100644 --- a/infrastructure/terraform/environments/hobby/.terraform.lock.hcl +++ b/infrastructure/terraform/environments/hobby/.terraform.lock.hcl @@ -1,45 +1,23 @@ # This file is maintained automatically by "terraform init". # Manual edits may be lost in future updates. -provider "registry.terraform.io/cloudflare/cloudflare" { - version = "4.52.8" - constraints = "~> 4.0" - hashes = [ - "h1:yieZ7NWRYQcLYHUevzDDrrmDkoQICM+3ZeX4i/mw9ME=", - "zh:08b305329a680a9213b2d8e642fbce7e4d97a524b1d2cef59e190ba9d678c477", - "zh:47975bd711ee18a46e589822171fa87474a552b332bfc8dea8fd1a64504eed8d", - "zh:5640d0d226bbafff3395542456c29feb942ca9c55ac01b4245a34c0590a33363", - "zh:5b0ad839fafba938c60a95d6b4a865843643591e97573ed2b9c4af3754064f74", - "zh:890df766e9b839623b1f0437355032a3c006226a6c200cd911e15ee1a9014e9f", - "zh:93a9bc1139f5c02a44fdbf51fb2ce0891e2a42033b66febb928ccf72e870408f", - "zh:977f75cdf365686aa16ae02dcc0fc1769bae6f86be6e165393756c940bfb4af0", - "zh:9afcda2660b3dc6ee6329acea532a719225bd1f6cf46f695feb2d77160847c1b", - "zh:9e3da67b1b05b03d1f0c18b8677edf3797815b5dd49629f10eb2f457dccbed30", - "zh:b8d7da230f5266367c1b6b1cf31aa39087e231a87d55b71bb9d5c854ca1fcfe6", - "zh:cc96e7cd7350456b7a11a53d1c76c73a542de8359f9750f637ff865e8e77be91", - "zh:da1f58d067def243047bb7178cb197e2b9c3a791a9eb380ad92b73290615ae29", - "zh:ed5f3e1f59a338bcdce0a2537a8a2f299cfe18c168626799b2a9c97af3d8d3cb", - "zh:f16cc31f73a58a26ffe2d93223eddb7c340623a4e2ef38c6091766fd611e3e11", - ] -} - provider "registry.terraform.io/hetznercloud/hcloud" { - version = "1.66.0" + version = "1.66.1" constraints = "~> 1.45" hashes = [ - "h1:+R3t/5wqCb2ics5bNZ4JgtfaAa08Z6uRzXzNtwovVEY=", - "zh:1286cee6fb63dbcb18f53077bbb5e5d132a4e4d9f006af4e8d8edfc08d6bcdc8", - "zh:204460dacc044bda019a4a18b398e094289500c36913c7c9457f432adf31b8b2", - "zh:214175d50773481cbeaf9c9004e4121a3a1c9686c79424ebdc8ff189dd057d3e", - "zh:22b17bceff61cc13ad04a399ba87521356a3a134d4687273727473ae9eccf5f1", - "zh:368867dac5525c411de7e38f2e27de0a71854d1750867322ff2b9321128c88fb", - "zh:5289b75f8370bdbc4c6051d55cf33d0b1bd25dc6d71bfbd39b360249a37f1501", - "zh:81cb676aa50c5777df8fc80d4e69c9012330ae751f5e6f12bf6074bfd2e7c496", - "zh:ab08aead10643b21aa6b51af562b50492e12b9dd0ab7dca27a05aa63209b7d66", - "zh:af25c210d0570cf61ef767b2545bf9f3fb909178135f0e5e14bec0c1c9d07a63", - "zh:bcad66f4830c97118fa793723e53f8a4d27ddd34ea969ff259408842c2238331", - "zh:ce3ed323d75ae905d975925fa98c7054a7514c81276a485fc37da8232b53e39f", - "zh:d481bc0ef0c87ab1969c17777f526b2f59f823432d676145134c41a6d29bd98e", - "zh:ea7ef88df2c3ca154d86238920636d52a3c9066c7467543d3fa45f1e52ec2f7b", + "h1:7CSl5SdPjP2VK96O/4rz4zem2WSljipRr2r2DQWDbRw=", + "zh:113070176eb4fb26a3758b3d1031bb904e34a74f7c4f99f90df2301ca4468a51", + "zh:1bfc988bdcd7c9422e09c262c736ad265a205684f0402fa4a83e63c0b08e09ea", + "zh:37d92b4cf0f344295b0d780aabbc1408f02db31141cd4408276455a458071e54", + "zh:386bd1207b3ed284b513294ddad9b9f59047a693c3aa375605f858f9d9758b58", + "zh:43d26f0a4f5a64bf0ade1c5a278b40153d4ae8c77508933b9c8bb7f7860dae6d", + "zh:6a1fe681a1706be87f0d03b749f1dc69c838d663c6ab08a00ae8d87be2e4425e", + "zh:7032556ae2a74b2e1b7d68add9b96158e4f7202ebb8fbd75e0e8a1581719df7b", + "zh:893296927373b4afa7ec3639abb1ebab3c1b6cb9cfe427877cfa7bf69a33480d", + "zh:8d58b340e21428a59cc27dc4f5a7fcd2035ee1aa9372c3932f7962647532fe0d", + "zh:93509170347bf097ca38e288a6e921811bb769bb2dfac3c339eb2032ae8454c1", + "zh:9c00443cb5ae2401089a62e223d7d741eedb04f6a73c357b6a7443b6ae71b0be", + "zh:9c356be5ce8cb7b1d83d5cf2276823242f106dbd0c43fe992055f0fa11290f95", + "zh:f619116583c7e47751e0baefffca216c83f9f42edf99325121b85ee075db78cc", ] } diff --git a/infrastructure/terraform/environments/hobby/cloud-init.yaml.tpl b/infrastructure/terraform/environments/hobby/cloud-init.yaml.tpl new file mode 100644 index 0000000..a40a9c7 --- /dev/null +++ b/infrastructure/terraform/environments/hobby/cloud-init.yaml.tpl @@ -0,0 +1,19 @@ +#cloud-config +# ============================================================================= +# Minimal cloud-init — install single-node k3s +# ============================================================================= +# After terraform apply: +# ssh root@ +# cat /etc/rancher/k3s/k3s.yaml # this is your kubeconfig +# On your laptop: replace 127.0.0.1 with the public IP. +# ============================================================================= + +package_update: true + +packages: + - curl + +runcmd: + # Install k3s as a single-node cluster (default Traefik + servicelb). + # DISABLE traefik later if you prefer nginx — for Phase 6, leave defaults. + - curl -sfL https://get.k3s.io | sh -s - --write-kubeconfig-mode 644 \ No newline at end of file diff --git a/infrastructure/terraform/environments/hobby/main.tf b/infrastructure/terraform/environments/hobby/main.tf index 0d58030..9388a81 100644 --- a/infrastructure/terraform/environments/hobby/main.tf +++ b/infrastructure/terraform/environments/hobby/main.tf @@ -1,3 +1,10 @@ +# ============================================================================= +# templatefile() reads the .tpl and substitutes ${...} variables. +# Here we keep the template simple (no vars) — empty map {}. +# +# MONEY MOMENT: the next `terraform apply` creates a billable Hetzner server. +# ========================================================================== + locals { ssh_public_key = trimspace(file(pathexpand(var.ssh_public_key_path))) } @@ -6,7 +13,7 @@ module "firewall" { source = "../../modules/firewall" name = "${var.server_name}-fw" - ssh_source_cidrs = ["0.0.0.0/0"] # TODO: replace with our IP/32 + ssh_source_cidrs = ["103.129.135.175/32"] } module "server" { @@ -18,21 +25,14 @@ module "server" { ssh_public_key = local.ssh_public_key firewall_id = module.firewall.id - - # TODO: switch to templatefile() once you create cloud-init.yaml.tpl - user_data = <<-EOT - #cloud-config - package_update: true - runcmd: - - echo "hobby VPS ready — k3s comes later" - EOT + user_data = templatefile("${path.module}/cloud-init.yaml.tpl", {}) } -module "dns" { - source = "../../modules/dns" - count = var.enable_dns ? 1 : 0 - - zone_name = var.domain - record_name = var.subdomain - ipv4_address = module.server.ipv4_address -} \ No newline at end of file +# module "dns" { +# source = "../../modules/dns" +# count = var.enable_dns ? 1 : 0 +# +# zone_name = var.domain +# record_name = var.subdomain +# ipv4_address = module.server.ipv4_address +#} \ No newline at end of file diff --git a/infrastructure/terraform/environments/hobby/outputs.tf b/infrastructure/terraform/environments/hobby/outputs.tf index a1db8fe..8cb59b9 100644 --- a/infrastructure/terraform/environments/hobby/outputs.tf +++ b/infrastructure/terraform/environments/hobby/outputs.tf @@ -10,5 +10,5 @@ output "ssh_command" { output "dns_name" { description = "FQDN if DNS enabled" - value = var.enable_dns ? module.dns[0].fqdn : null + value = null } \ No newline at end of file diff --git a/infrastructure/terraform/environments/hobby/providers.tf b/infrastructure/terraform/environments/hobby/providers.tf index 1d0d165..3dd8706 100644 --- a/infrastructure/terraform/environments/hobby/providers.tf +++ b/infrastructure/terraform/environments/hobby/providers.tf @@ -5,6 +5,6 @@ provider "hcloud"{ # token defaults to HCLOUD_TOKEN - so no need to hardcode } -provider "cloudflare" { +# provider "cloudflare" { # uses CLOUDFLARE_API_TOKEN when we enable the dns module -} \ No newline at end of file +# } \ No newline at end of file diff --git a/infrastructure/terraform/environments/hobby/versions.tf b/infrastructure/terraform/environments/hobby/versions.tf index 9e6a7f9..69b70eb 100644 --- a/infrastructure/terraform/environments/hobby/versions.tf +++ b/infrastructure/terraform/environments/hobby/versions.tf @@ -8,10 +8,10 @@ terraform { } # DNS can wait until we have a domain - cloudflare = { - source = "cloudflare/cloudflare" - version = "~> 4.0" - } + # cloudflare = { + # source = "cloudflare/cloudflare" + # version = "~> 4.0" + # } } # TODO: backend "s3" or terraform cloud when we destroy/recreate often