Spells out the one-time secret generation that must NEVER be re-run. Documents local k3d setup and operations runbooks. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
135 lines
5.5 KiB
Markdown
135 lines
5.5 KiB
Markdown
# ezscale-website Helm Chart
|
|
|
|
Production deployment for the EZSCALE billing platform Laravel app.
|
|
|
|
## Environments
|
|
|
|
| Env | Values file | Notes |
|
|
|---|---|---|
|
|
| Local k3d | `values-local.yaml` | Self-contained: in-cluster MariaDB + Valkey. No ingress (use port-forward). |
|
|
| US prod | `values-us-prod.yaml` | Connects to existing `mariadb` CR in `ezscale` ns; per-app Valkey on Longhorn. |
|
|
|
|
## Prerequisites (us-prod)
|
|
|
|
The `ezscale` namespace must already have:
|
|
- mariadb-operator installed cluster-wide (provides `MariaDB`/`Database`/`User`/`Grant` CRDs)
|
|
- A `MariaDB` CR named `mariadb`
|
|
- cert-manager `ClusterIssuer` named `letsencrypt`
|
|
- Traefik with `cloudflarewarp` and `http-to-https` middlewares in `kube-system`
|
|
- Image-pull `Secret` named `gitea-registry`
|
|
|
|
These are all already provisioned per `infrastructure/kubernetes/`.
|
|
|
|
## First-time bootstrap (us-prod)
|
|
|
|
This step is run **once** per environment, never repeated. The Secret it creates lives forever — re-running it would invalidate every encrypted DB column and every issued OAuth token.
|
|
|
|
```bash
|
|
# 1. Generate APP_KEY locally (any Laravel checkout works)
|
|
APP_KEY=$(cd /tmp && composer create-project laravel/laravel _key_gen --quiet --no-install \
|
|
&& cd _key_gen && composer install --quiet \
|
|
&& php artisan key:generate --show)
|
|
|
|
# 2. Generate Passport keys
|
|
TMP_KEYS=$(mktemp -d)
|
|
openssl genrsa -out "$TMP_KEYS/oauth-private.key" 4096
|
|
openssl rsa -in "$TMP_KEYS/oauth-private.key" -pubout -out "$TMP_KEYS/oauth-public.key"
|
|
|
|
# 3. Gather production secrets. Read each one and store in shell vars.
|
|
DB_PASS=$(openssl rand -base64 32 | tr -d '/=+' | cut -c1-32)
|
|
read -sp "STRIPE_KEY (pk_live_...): " STRIPE_KEY; echo
|
|
read -sp "STRIPE_SECRET (sk_live_...): " STRIPE_SECRET; echo
|
|
read -sp "STRIPE_WEBHOOK_SECRET (whsec_...): " STRIPE_WEBHOOK; echo
|
|
read -sp "PAYPAL_CLIENT_ID: " PAYPAL_ID; echo
|
|
read -sp "PAYPAL_CLIENT_SECRET: " PAYPAL_SECRET; echo
|
|
read -sp "AWS_ACCESS_KEY_ID (Storj): " AWS_KEY; echo
|
|
read -sp "AWS_SECRET_ACCESS_KEY (Storj): " AWS_SECRET; echo
|
|
read -sp "MAIL_USERNAME (SMTP): " MAIL_USER; echo
|
|
read -sp "MAIL_PASSWORD (SMTP): " MAIL_PASS; echo
|
|
read -sp "VIRTFUSION_API_KEY: " VF_KEY; echo
|
|
read -sp "PTERODACTYL_APP_KEY: " PTERO_KEY; echo
|
|
|
|
# 4. Create the Secret in the ezscale namespace
|
|
kubectl create secret generic ezscale-website-secrets \
|
|
--namespace ezscale \
|
|
--from-literal=APP_KEY="$APP_KEY" \
|
|
--from-literal=DB_PASSWORD="$DB_PASS" \
|
|
--from-literal=STRIPE_KEY="$STRIPE_KEY" \
|
|
--from-literal=STRIPE_SECRET="$STRIPE_SECRET" \
|
|
--from-literal=STRIPE_WEBHOOK_SECRET="$STRIPE_WEBHOOK" \
|
|
--from-literal=PAYPAL_CLIENT_ID="$PAYPAL_ID" \
|
|
--from-literal=PAYPAL_CLIENT_SECRET="$PAYPAL_SECRET" \
|
|
--from-literal=AWS_ACCESS_KEY_ID="$AWS_KEY" \
|
|
--from-literal=AWS_SECRET_ACCESS_KEY="$AWS_SECRET" \
|
|
--from-literal=MAIL_USERNAME="$MAIL_USER" \
|
|
--from-literal=MAIL_PASSWORD="$MAIL_PASS" \
|
|
--from-literal=VIRTFUSION_API_KEY="$VF_KEY" \
|
|
--from-literal=PTERODACTYL_APP_KEY="$PTERO_KEY" \
|
|
--from-file=oauth-private.key="$TMP_KEYS/oauth-private.key" \
|
|
--from-file=oauth-public.key="$TMP_KEYS/oauth-public.key"
|
|
|
|
# 5. Wipe the local key copies
|
|
rm -rf /tmp/_key_gen "$TMP_KEYS"
|
|
```
|
|
|
|
After this, `helm install` and every subsequent `helm upgrade` reference the existing Secret without ever rewriting it.
|
|
|
|
## Deploy
|
|
|
|
```bash
|
|
helm upgrade --install ezscale-website ./helm/ezscale-website \
|
|
--namespace ezscale \
|
|
-f helm/ezscale-website/values-us-prod.yaml \
|
|
--set image.tag=v0.1.0
|
|
```
|
|
|
|
## Local dev cluster
|
|
|
|
```bash
|
|
# 1. Create cluster with Longhorn (or local-path) and the operator
|
|
k3d cluster create ezscale-local
|
|
helm repo add mariadb-operator https://helm.mariadb.com/mariadb-operator
|
|
helm install mariadb-operator -n mariadb-operator --create-namespace mariadb-operator/mariadb-operator
|
|
|
|
# 2. Build the image into the cluster
|
|
docker build --target app -t ezscale-website:app-local .
|
|
docker build --target horizon -t ezscale-website:horizon-local .
|
|
docker build --target scheduler -t ezscale-website:scheduler-local .
|
|
k3d image import ezscale-website:app-local ezscale-website:horizon-local ezscale-website:scheduler-local -c ezscale-local
|
|
|
|
# 3. Edit values-local.yaml — replace APP_KEY with `php artisan key:generate --show` output
|
|
|
|
# 4. Install
|
|
helm upgrade --install ezscale-website ./helm/ezscale-website \
|
|
--namespace ezscale --create-namespace \
|
|
-f helm/ezscale-website/values-local.yaml \
|
|
--set image.registry=docker.io \
|
|
--set image.repository=library/ezscale-website \
|
|
--set image.tag=local
|
|
|
|
# 5. Port-forward and visit http://localhost:8080
|
|
kubectl port-forward -n ezscale svc/ezscale-website 8080:80
|
|
```
|
|
|
|
## Operations
|
|
|
|
```bash
|
|
# Tail Horizon logs
|
|
kubectl logs -n ezscale -l app.kubernetes.io/component=horizon -f
|
|
|
|
# Run an artisan command in a one-off pod
|
|
kubectl run -n ezscale --rm -it --image=git.ezscale.cloud/ezscale/website:app-v0.1.0 \
|
|
--env-from='[{"configMapRef":{"name":"ezscale-website-env"}},{"secretRef":{"name":"ezscale-website-secrets"}}]' \
|
|
artisan-shell -- bash
|
|
|
|
# Force a Horizon rolling restart
|
|
kubectl rollout restart deployment -n ezscale ezscale-website-horizon
|
|
```
|
|
|
|
## What's NOT in this chart (intentionally)
|
|
|
|
- Cloudflare Zero Trust for the admin panel — layered on later.
|
|
- ExternalDNS records — DNS managed manually in Cloudflare Terraform.
|
|
- Backup CronJob for the application database — covered by the existing `mariadb` instance's backup CronJob in `infrastructure/kubernetes/ezscale/mysql/`.
|
|
- Backups for Storj uploads — Storj provides its own replication; per-bucket lifecycle policies live in the Storj console.
|