diff --git a/k8s/railiance-production/00-namespace.yaml b/k8s/railiance-production/00-namespace.yaml new file mode 100644 index 0000000..aa823a7 --- /dev/null +++ b/k8s/railiance-production/00-namespace.yaml @@ -0,0 +1,9 @@ +apiVersion: v1 +kind: Namespace +metadata: + name: core-hub + labels: + app.kubernetes.io/name: core-hub + app.kubernetes.io/part-of: core-hub + environment: production + railiance.io/postgres-client: apps-pg diff --git a/k8s/railiance-production/20-runtime.yaml b/k8s/railiance-production/20-runtime.yaml new file mode 100644 index 0000000..f19a822 --- /dev/null +++ b/k8s/railiance-production/20-runtime.yaml @@ -0,0 +1,113 @@ +apiVersion: v1 +kind: ConfigMap +metadata: + name: core-hub-prod-runtime + namespace: core-hub + labels: + app.kubernetes.io/name: core-hub + app.kubernetes.io/part-of: core-hub + environment: production +data: + CORE_HUB_ENV: production + CORE_HUB_AUTO_CREATE_TABLES: "0" +--- +apiVersion: batch/v1 +kind: Job +metadata: + name: core-hub-prod-migrate + namespace: core-hub + labels: + app.kubernetes.io/name: core-hub + app.kubernetes.io/component: migration + app.kubernetes.io/part-of: core-hub + environment: production +spec: + backoffLimit: 1 + template: + metadata: + labels: + app.kubernetes.io/name: core-hub + app.kubernetes.io/component: migration + app.kubernetes.io/part-of: core-hub + environment: production + spec: + restartPolicy: Never + containers: + - name: migrate + image: gitea.coulomb.social/coulomb/core-hub:production-placeholder + imagePullPolicy: IfNotPresent + command: ["alembic", "upgrade", "head"] + envFrom: + - configMapRef: + name: core-hub-prod-runtime + - secretRef: + name: core-hub-prod-env +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: core-hub-api + namespace: core-hub + labels: + app.kubernetes.io/name: core-hub + app.kubernetes.io/component: api + app.kubernetes.io/part-of: core-hub + environment: production +spec: + replicas: 1 + selector: + matchLabels: + app.kubernetes.io/name: core-hub + app.kubernetes.io/component: api + template: + metadata: + labels: + app.kubernetes.io/name: core-hub + app.kubernetes.io/component: api + app.kubernetes.io/part-of: core-hub + environment: production + spec: + containers: + - name: api + image: gitea.coulomb.social/coulomb/core-hub:production-placeholder + imagePullPolicy: IfNotPresent + ports: + - name: http + containerPort: 8010 + envFrom: + - configMapRef: + name: core-hub-prod-runtime + - secretRef: + name: core-hub-prod-env + readinessProbe: + httpGet: + path: /readyz + port: http + initialDelaySeconds: 5 + periodSeconds: 10 + livenessProbe: + httpGet: + path: /healthz + port: http + initialDelaySeconds: 15 + periodSeconds: 20 +--- +apiVersion: v1 +kind: Service +metadata: + name: core-hub-api + namespace: core-hub + labels: + app.kubernetes.io/name: core-hub + app.kubernetes.io/component: api + app.kubernetes.io/part-of: core-hub + environment: production +spec: + type: ClusterIP + selector: + app.kubernetes.io/name: core-hub + app.kubernetes.io/component: api + ports: + - name: http + port: 8010 + targetPort: http diff --git a/k8s/railiance-production/30-ingress.yaml b/k8s/railiance-production/30-ingress.yaml new file mode 100644 index 0000000..454e299 --- /dev/null +++ b/k8s/railiance-production/30-ingress.yaml @@ -0,0 +1,30 @@ +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: core-hub + namespace: core-hub + annotations: + cert-manager.io/cluster-issuer: letsencrypt-prod + traefik.ingress.kubernetes.io/router.entrypoints: websecure + traefik.ingress.kubernetes.io/router.tls: "true" + labels: + app.kubernetes.io/name: core-hub + app.kubernetes.io/part-of: core-hub + environment: production +spec: + ingressClassName: traefik + rules: + - host: hub.coulomb.social + http: + paths: + - backend: + service: + name: core-hub-api + port: + number: 8010 + path: / + pathType: Prefix + tls: + - hosts: + - hub.coulomb.social + secretName: core-hub-tls diff --git a/k8s/railiance-production/README.md b/k8s/railiance-production/README.md new file mode 100644 index 0000000..8e51392 --- /dev/null +++ b/k8s/railiance-production/README.md @@ -0,0 +1,47 @@ +# Core Hub production deployment (hub.coulomb.social cutover) + +`30-ingress.yaml` is the cutover flip — do NOT apply it while the inter-hub +ingress still claims `hub.coulomb.social`. + +## Sequence + +```bash +# 1. Image (operator: registry push) +IMAGE_REPOSITORY=gitea.coulomb.social/coulomb/core-hub IMAGE_TAG= make container-build +docker push gitea.coulomb.social/coulomb/core-hub: + +# 2. Prod DB + secret (operator: shared-DB DDL) +# role core_hub / db core_hub on apps-pg; secret core-hub-prod-env with +# CORE_HUB_DATABASE_URL (postgresql+asyncpg://...apps-pg-rw.databases...:5432/core_hub) +# and CORE_HUB_API_TOKEN (generated) + +# 3. Deploy (no traffic yet) +kubectl apply -f k8s/railiance-production/00-namespace.yaml +kubectl apply -f k8s/railiance-production/20-runtime.yaml +kubectl -n core-hub set image job/core-hub-prod-migrate migrate=gitea.coulomb.social/coulomb/core-hub: +kubectl -n core-hub set image deploy/core-hub-api api=gitea.coulomb.social/coulomb/core-hub: +kubectl -n core-hub wait --for=condition=complete job/core-hub-prod-migrate --timeout=180s +kubectl -n core-hub rollout status deploy/core-hub-api --timeout=180s + +# 4. Final data import (fresh Inter-Hub export at cutover time) +kubectl exec -i -n databases net-kingdom-pg-1 -- psql -U postgres -d interhub -tA \ + < scripts/migration/inter_hub_export.sql > final-bundle.json +# validate, then import in-cluster (see scripts/migration/README.md) + +# 5. THE FLIP (operator approval point) +kubectl -n inter-hub delete ingress inter-hub +kubectl apply -f k8s/railiance-production/30-ingress.yaml +# cert-manager issues core-hub-tls for hub.coulomb.social (~1 min) + +# 6. Verify +curl -s https://hub.coulomb.social/healthz # 200 +curl -s https://hub.coulomb.social/api/v2/widget-types | head -c 200 +curl -s -o /dev/null -w '%{http_code}' https://hub.coulomb.social/api/v2/hubs # 401 = hardened contract + +## Rollback (any time; Inter-Hub deployment stays running) +kubectl delete -f k8s/railiance-production/30-ingress.yaml +kubectl -n inter-hub apply -f # or helm upgrade inter-hub +``` + +Keep the Inter-Hub deployment/service running untouched until stabilization +passes (CORE-WP-0005-T04); only the ingress moves. diff --git a/k8s/railiance-production/rollback-inter-hub-ingress.yaml b/k8s/railiance-production/rollback-inter-hub-ingress.yaml new file mode 100644 index 0000000..b1d7628 --- /dev/null +++ b/k8s/railiance-production/rollback-inter-hub-ingress.yaml @@ -0,0 +1,39 @@ +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + annotations: + cert-manager.io/cluster-issuer: letsencrypt-prod + meta.helm.sh/release-name: inter-hub + meta.helm.sh/release-namespace: inter-hub + traefik.ingress.kubernetes.io/router.entrypoints: websecure + traefik.ingress.kubernetes.io/router.tls: "true" + creationTimestamp: "2026-06-14T12:49:01Z" + generation: 1 + labels: + app: inter-hub + app.kubernetes.io/managed-by: Helm + name: inter-hub + namespace: inter-hub + resourceVersion: "14479609" + uid: 2f1d1557-76be-445c-b77b-e03e7b0c3935 +spec: + ingressClassName: traefik + rules: + - host: hub.coulomb.social + http: + paths: + - backend: + service: + name: inter-hub + port: + number: 8000 + path: / + pathType: Prefix + tls: + - hosts: + - hub.coulomb.social + secretName: inter-hub-tls +status: + loadBalancer: + ingress: + - ip: 92.205.130.254