{"id":26276693,"url":"https://github.com/jsabo/local-k8s","last_synced_at":"2026-01-01T22:49:41.013Z","repository":{"id":253036591,"uuid":"836476120","full_name":"jsabo/local-k8s","owner":"jsabo","description":null,"archived":false,"fork":false,"pushed_at":"2024-08-22T22:38:58.000Z","size":17,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":1,"default_branch":"main","last_synced_at":"2024-08-23T00:08:43.776Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":null,"language":null,"has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"apache-2.0","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/jsabo.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2024-07-31T23:55:27.000Z","updated_at":"2024-08-22T22:39:01.000Z","dependencies_parsed_at":"2024-08-16T19:36:56.163Z","dependency_job_id":null,"html_url":"https://github.com/jsabo/local-k8s","commit_stats":null,"previous_names":["jsabo/local-k8s"],"tags_count":0,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/jsabo%2Flocal-k8s","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/jsabo%2Flocal-k8s/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/jsabo%2Flocal-k8s/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/jsabo%2Flocal-k8s/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/jsabo","download_url":"https://codeload.github.com/jsabo/local-k8s/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":243565365,"owners_count":20311705,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2025-03-14T11:19:03.543Z","updated_at":"2026-01-01T22:49:40.985Z","avatar_url":"https://github.com/jsabo.png","language":null,"funding_links":[],"categories":[],"sub_categories":[],"readme":"# local-k8s — kubeadm + Multipass demo cluster (WordPress + RBAC + CSR users + Dashboard + Ingress)\n\nThis repo builds a **reproducible local Kubernetes cluster** using **kubeadm** on **Multipass** VMs and deploys a **custom WordPress + MySQL Helm chart** with **persistent storage** and **minimum-privilege user access**.\n\nIt’s designed to be:\n- **Easy to run** on a laptop (single `task` command)\n- **Reproducible** (idempotent tasks, stamp files, local state in `./.state`)\n- **Security-focused** (namespace RBAC + CSR-based user credentials)\n- **Demo-friendly** (prints URLs, tokens, and `/etc/hosts` mapping)\n\n---\n\n## What this project includes\n\n### Cluster\n- 3-node Kubernetes cluster:\n  - 1 control-plane (`cp-0`)\n  - 2 workers (`worker-0`, `worker-1`)\n- Built with:\n  - **kubeadm**\n  - **containerd**\n  - **Calico** (via Tigera operator)\n- Local state output:\n  - `./.state/kubeconfig` (admin kubeconfig for cluster operations)\n  - `./.state/env` (helper to export `KUBECONFIG`)\n\n### Storage\n- **Rancher local-path-provisioner** for dynamic local persistent volumes\n  - Provides a default `StorageClass` (`local-path`) suitable for demos\n\n### Workloads\n- **Custom WordPress Helm chart** (`charts/custom-wordpress`)\n  - WordPress Deployment + Service\n  - MySQL Deployment + Service\n  - PVCs for WordPress and MySQL\n  - Secret with generated credentials (preserved across upgrades)\n- **Two user types** (namespace-scoped):\n  - **Deployer**: install/upgrade/uninstall the WordPress release in a namespace\n  - **Accessor**: read basics + port-forward to access WordPress\n- **Kubernetes Dashboard** (Helm install) with a read-only `view` token for convenience\n- **Ingress-NGINX (NodePort)** + Ingress rules:\n  - `/` → WordPress\n  - `/dashboard` → Dashboard\n  - `/api` → stub API service (currently `traefik/whoami`)\n\n---\n\n## Architecture overview\n\n### Components and flow\n\n1. Multipass launches 3 Ubuntu VMs using `cloud-init.tftpl`\n2. `kubeadm init` runs on `cp-0`\n3. Workers join using a token generated on `cp-0`\n4. Calico installs cluster networking\n5. local-path-provisioner installs default storage\n6. Namespaced RBAC + CSR user kubeconfigs are created\n7. WordPress is installed using the **deployer** kubeconfig\n8. Access is performed using:\n   - Ingress URLs (recommended for the demo), and/or\n   - Accessor kubeconfig + port-forward\n\n### Conceptual networking\n\n```\n\n```\n           (your laptop)\n                |\n   /etc/hosts: demo.local -\u003e cp-0 IP\n                |\n      NodePort 30080 / 30443 (Ingress-NGINX)\n                |\n +--------------+-------------------+\n |                                  |\n```\n\n\"/\" -\u003e WordPress Service           \"/api\" -\u003e goapi Service\n\"/dashboard\" -\u003e Dashboard Service   (stub whoami)\n\n````\n\n---\n\n## Security model (how access is controlled)\n\nThis repo intentionally demonstrates **minimum-privilege access**.\n\n### Authentication\nUser credentials are created via **Kubernetes CertificateSigningRequest (CSR)**:\n- A private key + CSR is generated locally\n- A CSR object is submitted to the cluster\n- The CSR is approved\n- A kubeconfig is generated embedding the user cert and cluster CA\n\nGenerated kubeconfigs live in:\n- `./.state/users/\u003cnamespace\u003e/deployer.kubeconfig`\n- `./.state/users/\u003cnamespace\u003e/accessor.kubeconfig`\n\n### Authorization (RBAC)\nRBAC is namespace-scoped in `./rbac`:\n- `wp-deployer` Role:\n  - create/update/delete common namespaced objects needed for Helm\n  - manage Deployments/ReplicaSets/Jobs/Ingresses/PVCs/Secrets/etc.\n- `wp-accessor` Role:\n  - read-only discovery (`get/list/watch`) for Pods/Services/Endpoints\n  - `create` on `pods/portforward` (required for port-forward)\n  - optional `get` on `pods/log` for troubleshooting\n\n\u003e Note: This keeps application access separate from cluster-admin operations. Cluster bootstrap and cluster-scoped components (Calico, storage class, ingress controller, dashboard install) are installed with the admin kubeconfig.\n\n---\n\n## Repository layout\n\n- `Taskfile.yaml` — automation entrypoint (`task demo:install`)\n- `cloud-init.tftpl` — provisions VMs (containerd + kubelet/kubeadm/kubectl)\n- `charts/custom-wordpress/` — custom Helm chart\n- `rbac/` — namespaced RBAC for deployer/accessor users\n- `./.state/` — generated state (kubeconfigs, rendered cloud-init, stamps)\n\n---\n\n## Prerequisites\n\nYou need these on your laptop:\n\n- `multipass`\n- `kubectl`\n- `helm`\n- `task` (Taskfile runner)\n- `envsubst` (from gettext; used to render cloud-init)\n\n---\n\n## Quickstart\n\n### 1) Install everything (cluster + apps)\n```bash\ntask demo:install\n````\n\nThis will:\n\n* create VMs\n* init kubeadm\n* install Calico\n* install local-path-provisioner\n* install Dashboard\n* install WordPress (using deployer credentials)\n* install ingress-nginx + rules\n* print URLs + dashboard token + `/etc/hosts` mapping\n\n### 2) Add the host mapping\n\nThe demo assumes you’ll resolve `demo.local` to the **cp-0 IP**.\n\nRun:\n\n```bash\ntask demo:info\n```\n\nIt prints a line like:\n\n```\n192.168.x.y demo.local\n```\n\nAdd that to `/etc/hosts` on your laptop.\n\n### 3) Browse\n\n* WordPress:  `http://demo.local:30080/`\n* Dashboard:  `https://demo.local:30443/dashboard`\n* API stub:   `http://demo.local:30080/api`\n\nThe Dashboard login token is printed by `task demo:info`.\n\n---\n\n## How to show least-privilege access during a demo\n\n### Admin kubeconfig (cluster operations)\n\n```bash\nexport KUBECONFIG=./.state/kubeconfig\nkubectl get nodes -o wide\n```\n\n### Deployer user (application lifecycle via Helm)\n\nThe WordPress release is installed using:\n\n* `./.state/users/\u003cnamespace\u003e/deployer.kubeconfig`\n\nExample:\n\n```bash\nexport KUBECONFIG=./.state/users/wordpress-demo/deployer.kubeconfig\nkubectl -n wordpress-demo get deploy,svc,pvc\n```\n\n### Accessor user (port-forward + basic reads)\n\nExample:\n\n```bash\nexport KUBECONFIG=./.state/users/wordpress-demo/accessor.kubeconfig\nkubectl -n wordpress-demo get svc\nkubectl -n wordpress-demo port-forward svc/custom-wordpress 8080:80\n# open http://127.0.0.1:8080\n```\n\n---\n\n## WordPress chart design\n\nThis repo includes a small, transparent Helm chart (`charts/custom-wordpress`) that deploys:\n\n* WordPress Deployment + Service\n* MySQL Deployment + Service\n* PVCs (WordPress + MySQL) when persistence is enabled\n* A Secret for DB credentials\n\n### Credentials behavior\n\n* By default, the chart **generates random passwords**\n* On upgrades, it **reuses existing Secret values** to avoid rotating credentials unexpectedly\n* You can supply your own Secret via `auth.existingSecret`\n\nTo print generated passwords:\n\n```bash\nexport KUBECONFIG=./.state/kubeconfig\nkubectl -n wordpress-demo get secret custom-wordpress-db \\\n  -o jsonpath='{.data.wordpress-password}' | base64 -d; echo\nkubectl -n wordpress-demo get secret custom-wordpress-db \\\n  -o jsonpath='{.data.mysql-root-password}' | base64 -d; echo\n```\n\n### Storage behavior\n\n* Uses the cluster default StorageClass unless overridden\n* The Taskfile installs **local-path-provisioner** and sets `local-path` as default\n* WordPress + MySQL each get a PVC (default 5Gi each)\n\n---\n\n## Ingress + Dashboard\n\n### Ingress\n\nIngress-NGINX is installed as a NodePort service:\n\n* HTTP: `30080`\n* HTTPS: `30443`\n\nIngress rules are created for:\n\n* WordPress: `/`\n* Dashboard: `/dashboard` (rewrites and uses HTTPS to backend)\n* API stub: `/api`\n\n### Dashboard access\n\nDashboard is installed via Helm, then a dedicated ServiceAccount is created and bound to the `view` ClusterRole for read-only access.\n\nGet a token:\n\n```bash\nexport KUBECONFIG=./.state/kubeconfig\nkubectl -n kubernetes-dashboard create token dashboard-viewer\n```\n\n\u003e Security note: `view` is cluster-wide read-only. It’s convenient for demos; for stricter environments, scope dashboard access more tightly.\n\n---\n\n## Common operations\n\n### List public tasks\n\n```bash\ntask\n```\n\n### Check cluster status\n\n```bash\ntask cluster:status\n```\n\n### Print demo URLs + token + /etc/hosts line\n\n```bash\ntask demo:info\n```\n\n### Uninstall demo apps (keep cluster)\n\n```bash\ntask demo:uninstall\n```\n\n### Delete everything (VMs + local state)\n\n```bash\ntask cluster:down\n```\n\n---\n\n## Troubleshooting\n\n### “demo.local does not resolve”\n\nRun:\n\n```bash\ntask demo:info\n```\n\nAdd the printed `cp-0 IP -\u003e demo.local` mapping to `/etc/hosts`.\n\n### Ingress works but the Dashboard path fails\n\nThe dashboard Ingress uses regex + rewrite and expects HTTPS to the backend service. If the dashboard Helm chart changes service names, the Taskfile detects the service dynamically, but if needed you can check:\n\n```bash\nexport KUBECONFIG=./.state/kubeconfig\nkubectl -n kubernetes-dashboard get svc\nkubectl -n kubernetes-dashboard get ingress\n```\n\n### Reset the CSR-generated kubeconfigs\n\n```bash\nFORCE=1 task -s wp:user:deployer\nFORCE=1 task -s wp:user:accessor\n```\n\n### Start from scratch\n\n```bash\ntask cluster:down\ntask demo:install\n```\n\n---\n\n## Tradeoffs and next steps\n\n### Tradeoffs in this approach\n\n* **CSR-based users** are easy to demonstrate but are operationally heavy at scale:\n\n  * manual approvals (or additional automation)\n  * certificate lifecycle/rotation and revocation complexity\n* **Local-path storage** is perfect for demos but not HA and not suitable for production\n* Cluster-scoped installs (CNI, ingress controller, dashboard) still require admin-level bootstrapping\n\n### Recommended next steps (if hardening beyond a demo)\n\n* Replace CSR users with **OIDC-based auth** (centralized identity, group claims)\n* Add automated policy enforcement (e.g., Pod Security admission, network policies)\n* Replace local-path storage with a proper CSI backend if moving beyond a single-machine demo\n* Replace the stub API service with a real Go service and least-privilege DB access\n\n---\n\n## License / usage\n\nThis repository is intended as a learning/demo environment. Use it freely and adapt as needed.\n\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fjsabo%2Flocal-k8s","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fjsabo%2Flocal-k8s","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fjsabo%2Flocal-k8s/lists"}