Merge pull request 'Harden prod + expand GCP to multi-region' (#3) from gcs-backup-wiring into main
This commit is contained in:
@@ -0,0 +1,96 @@
|
|||||||
|
# ── Artifact Registry ─────────────────────────────────────────────────────────
|
||||||
|
# One repository per service. Images are pushed here from Legion CI runners
|
||||||
|
# (the only compiled artifacts that cross the boundary to GCP).
|
||||||
|
# Source code and intelligence stay on Legion — only the built container
|
||||||
|
# crosses out.
|
||||||
|
|
||||||
|
resource "google_artifact_registry_repository" "marketing" {
|
||||||
|
location = "us-central1"
|
||||||
|
repository_id = "neuron-marketing"
|
||||||
|
description = "Marketing site (Next.js) Docker images"
|
||||||
|
format = "DOCKER"
|
||||||
|
project = var.project_id
|
||||||
|
|
||||||
|
cleanup_policies {
|
||||||
|
id = "keep-last-10"
|
||||||
|
action = "KEEP"
|
||||||
|
most_recent_versions {
|
||||||
|
keep_count = 10
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
resource "google_artifact_registry_repository" "accounts" {
|
||||||
|
location = "us-central1"
|
||||||
|
repository_id = "neuron-accounts"
|
||||||
|
description = "Accounts service (Go) Docker images"
|
||||||
|
format = "DOCKER"
|
||||||
|
project = var.project_id
|
||||||
|
|
||||||
|
cleanup_policies {
|
||||||
|
id = "keep-last-10"
|
||||||
|
action = "KEEP"
|
||||||
|
most_recent_versions {
|
||||||
|
keep_count = 10
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
resource "google_artifact_registry_repository" "api" {
|
||||||
|
location = "us-central1"
|
||||||
|
repository_id = "neuron-api"
|
||||||
|
description = "REST API (neuron-rest, Go) Docker images"
|
||||||
|
format = "DOCKER"
|
||||||
|
project = var.project_id
|
||||||
|
|
||||||
|
cleanup_policies {
|
||||||
|
id = "keep-last-10"
|
||||||
|
action = "KEEP"
|
||||||
|
most_recent_versions {
|
||||||
|
keep_count = 10
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# ── IAM: CI runner pushes images ──────────────────────────────────────────────
|
||||||
|
# The CI service account (from Legion's Gitea runner) needs Artifact Registry
|
||||||
|
# writer access so it can push compiled images after each build.
|
||||||
|
|
||||||
|
resource "google_service_account" "ci_pusher" {
|
||||||
|
account_id = "neuron-ci-pusher"
|
||||||
|
display_name = "Neuron CI Image Pusher"
|
||||||
|
description = "Legion CI runner uses this SA to push compiled images to Artifact Registry"
|
||||||
|
project = var.project_id
|
||||||
|
}
|
||||||
|
|
||||||
|
resource "google_artifact_registry_repository_iam_member" "ci_marketing" {
|
||||||
|
project = var.project_id
|
||||||
|
location = "us-central1"
|
||||||
|
repository = google_artifact_registry_repository.marketing.name
|
||||||
|
role = "roles/artifactregistry.writer"
|
||||||
|
member = "serviceAccount:${google_service_account.ci_pusher.email}"
|
||||||
|
}
|
||||||
|
|
||||||
|
resource "google_artifact_registry_repository_iam_member" "ci_accounts" {
|
||||||
|
project = var.project_id
|
||||||
|
location = "us-central1"
|
||||||
|
repository = google_artifact_registry_repository.accounts.name
|
||||||
|
role = "roles/artifactregistry.writer"
|
||||||
|
member = "serviceAccount:${google_service_account.ci_pusher.email}"
|
||||||
|
}
|
||||||
|
|
||||||
|
resource "google_artifact_registry_repository_iam_member" "ci_api" {
|
||||||
|
project = var.project_id
|
||||||
|
location = "us-central1"
|
||||||
|
repository = google_artifact_registry_repository.api.name
|
||||||
|
role = "roles/artifactregistry.writer"
|
||||||
|
member = "serviceAccount:${google_service_account.ci_pusher.email}"
|
||||||
|
}
|
||||||
|
|
||||||
|
# ── CI pusher JSON key (download once, store in Vault/Secret Manager) ─────────
|
||||||
|
# After `terraform apply`, create a key and save it to GCP Secret Manager:
|
||||||
|
# gcloud iam service-accounts keys create /tmp/ci-pusher.json \
|
||||||
|
# --iam-account=$(terraform output -raw ci_pusher_email)
|
||||||
|
# gcloud secrets create ci-gcp-sa-key --data-file=/tmp/ci-pusher.json
|
||||||
|
# rm /tmp/ci-pusher.json
|
||||||
|
# Then set it as a repo secret in Gitea: GCP_SA_KEY
|
||||||
Executable
+97
@@ -0,0 +1,97 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# ── GCP Bootstrap — run once after terraform apply ────────────────────────────
|
||||||
|
# Sets up secrets and credentials that Terraform can't create automatically.
|
||||||
|
#
|
||||||
|
# Prerequisites:
|
||||||
|
# - GOOGLE_APPLICATION_CREDENTIALS or `gcloud auth application-default login`
|
||||||
|
# - GCP project access
|
||||||
|
# - Vault running (for reading existing secrets)
|
||||||
|
# - terraform apply has run successfully (creates SA, secrets)
|
||||||
|
#
|
||||||
|
# Usage:
|
||||||
|
# cd servers/gcp
|
||||||
|
# terraform apply
|
||||||
|
# bash bootstrap.sh
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
PROJECT="neuron-785695"
|
||||||
|
VAULT_ADDR="${VAULT_ADDR:-https://vault.neuralplatform.ai}"
|
||||||
|
|
||||||
|
echo "==> Bootstrapping GCP secrets and CI credentials"
|
||||||
|
|
||||||
|
# ── 1. Populate Stripe secrets in GCP Secret Manager ─────────────────────────
|
||||||
|
echo "--> Pulling Stripe secrets from Vault..."
|
||||||
|
MARKETING_SECRETS=$(curl -sf \
|
||||||
|
-H "X-Vault-Token: ${VAULT_TOKEN}" \
|
||||||
|
"${VAULT_ADDR}/v1/secret/data/neuron-technologies/marketing")
|
||||||
|
|
||||||
|
populate_secret() {
|
||||||
|
local secret_id="$1"
|
||||||
|
local vault_key="$2"
|
||||||
|
local value
|
||||||
|
value=$(echo "${MARKETING_SECRETS}" | python3 -c "
|
||||||
|
import json,sys; d=json.load(sys.stdin)['data']['data']; print(d.get('${vault_key}',''))
|
||||||
|
")
|
||||||
|
if [ -z "${value}" ]; then
|
||||||
|
echo " WARN: vault key '${vault_key}' not found — skipping ${secret_id}"
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
# Check if secret already has a version
|
||||||
|
if gcloud secrets versions list "${secret_id}" --project="${PROJECT}" \
|
||||||
|
--format="value(name)" 2>/dev/null | grep -q .; then
|
||||||
|
echo " SKIP: ${secret_id} already has a version"
|
||||||
|
else
|
||||||
|
echo "${value}" | gcloud secrets versions add "${secret_id}" \
|
||||||
|
--project="${PROJECT}" \
|
||||||
|
--data-file=-
|
||||||
|
echo " OK: ${secret_id}"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
populate_secret "stripe-secret-key" "stripe_secret_key"
|
||||||
|
populate_secret "stripe-webhook-secret" "stripe_webhook_secret"
|
||||||
|
populate_secret "stripe-price-professional" "stripe_price_professional"
|
||||||
|
populate_secret "stripe-price-founding" "stripe_price_founding"
|
||||||
|
|
||||||
|
# ── 2. License admin token ────────────────────────────────────────────────────
|
||||||
|
echo "--> Populating license-admin-token..."
|
||||||
|
LICENSE_TOKEN=$(curl -sf \
|
||||||
|
-H "X-Vault-Token: ${VAULT_TOKEN}" \
|
||||||
|
"${VAULT_ADDR}/v1/secret/data/neuron-technologies/marketing" \
|
||||||
|
| python3 -c "import json,sys; print(json.load(sys.stdin)['data']['data'].get('license_admin_token',''))")
|
||||||
|
if [ -n "${LICENSE_TOKEN}" ]; then
|
||||||
|
echo "${LICENSE_TOKEN}" | gcloud secrets versions add "license-admin-token" \
|
||||||
|
--project="${PROJECT}" --data-file=- 2>/dev/null || echo " SKIP: already exists"
|
||||||
|
echo " OK: license-admin-token"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ── 3. Create CI service account JSON key ────────────────────────────────────
|
||||||
|
CI_SA_EMAIL=$(terraform output -raw ci_pusher_email)
|
||||||
|
echo "--> Creating CI SA key for: ${CI_SA_EMAIL}"
|
||||||
|
KEY_FILE="/tmp/neuron-ci-pusher.json"
|
||||||
|
gcloud iam service-accounts keys create "${KEY_FILE}" \
|
||||||
|
--iam-account="${CI_SA_EMAIL}" \
|
||||||
|
--project="${PROJECT}"
|
||||||
|
echo " SA key written to: ${KEY_FILE}"
|
||||||
|
echo ""
|
||||||
|
echo " !! Add this as a Gitea secret GCP_SA_KEY in the neuron-technologies/neuron repo:"
|
||||||
|
echo " tea secret set --repo neuron-technologies/neuron GCP_SA_KEY < ${KEY_FILE}"
|
||||||
|
echo " Then delete the key file:"
|
||||||
|
echo " rm ${KEY_FILE}"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# ── 4. Summary ────────────────────────────────────────────────────────────────
|
||||||
|
echo "==> Bootstrap complete"
|
||||||
|
echo ""
|
||||||
|
echo "Next steps:"
|
||||||
|
echo " 1. Set Gitea secret: tea secret set --repo neuron-technologies/neuron GCP_SA_KEY < /tmp/neuron-ci-pusher.json"
|
||||||
|
echo " 2. Delete key file: rm /tmp/neuron-ci-pusher.json"
|
||||||
|
echo " 3. Point DNS at: $(terraform output -raw prod_lb_ip)"
|
||||||
|
echo " A records (proxied=true in Cloudflare):"
|
||||||
|
echo " neurontechnologies.ai → prod_lb_ip"
|
||||||
|
echo " www.neurontechnologies.ai → prod_lb_ip"
|
||||||
|
echo " api.neurontechnologies.ai → prod_lb_ip"
|
||||||
|
echo " accounts.neurontechnologies.ai → prod_lb_ip"
|
||||||
|
echo " 4. Run a marketing CI build on main to push first image to GCP AR"
|
||||||
|
echo " 5. Check SSL cert provisioning: gcloud compute ssl-certificates list --project=${PROJECT}"
|
||||||
@@ -0,0 +1,443 @@
|
|||||||
|
# ── Accounts Service — Cloud Run ──────────────────────────────────────────────
|
||||||
|
# Handles auth, billing, subscriptions, marketplace.
|
||||||
|
# Connects to Cloud SQL via built-in Auth Proxy (Unix socket volume mount).
|
||||||
|
# All three regions share the same Cloud SQL instance (us-central1).
|
||||||
|
|
||||||
|
locals {
|
||||||
|
accounts_labels = {
|
||||||
|
"managed-by" = "terraform"
|
||||||
|
"service" = "neuron-accounts"
|
||||||
|
}
|
||||||
|
sql_instance = google_sql_database_instance.main.connection_name
|
||||||
|
}
|
||||||
|
|
||||||
|
# ── Prod — us-central1 ────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
resource "google_cloud_run_v2_service" "accounts_us" {
|
||||||
|
name = "accounts-prod-us"
|
||||||
|
location = "us-central1"
|
||||||
|
project = var.project_id
|
||||||
|
ingress = "INGRESS_TRAFFIC_ALL"
|
||||||
|
labels = local.accounts_labels
|
||||||
|
|
||||||
|
template {
|
||||||
|
service_account = google_service_account.accounts.email
|
||||||
|
|
||||||
|
scaling {
|
||||||
|
min_instance_count = 1
|
||||||
|
max_instance_count = 50
|
||||||
|
}
|
||||||
|
|
||||||
|
containers {
|
||||||
|
image = local.accounts_image
|
||||||
|
|
||||||
|
resources {
|
||||||
|
limits = {
|
||||||
|
cpu = "1"
|
||||||
|
memory = "512Mi"
|
||||||
|
}
|
||||||
|
cpu_idle = true
|
||||||
|
}
|
||||||
|
|
||||||
|
env {
|
||||||
|
name = "ENV"
|
||||||
|
value = "production"
|
||||||
|
}
|
||||||
|
env {
|
||||||
|
name = "PORT"
|
||||||
|
value = "8080"
|
||||||
|
}
|
||||||
|
|
||||||
|
env {
|
||||||
|
name = "ACCOUNTS_DATABASE_URL"
|
||||||
|
value_source {
|
||||||
|
secret_key_ref {
|
||||||
|
secret = google_secret_manager_secret.accounts_database_url.secret_id
|
||||||
|
version = "latest"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
env {
|
||||||
|
name = "JWT_SECRET"
|
||||||
|
value_source {
|
||||||
|
secret_key_ref {
|
||||||
|
secret = google_secret_manager_secret.jwt_secret.secret_id
|
||||||
|
version = "latest"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
env {
|
||||||
|
name = "STRIPE_SECRET_KEY"
|
||||||
|
value_source {
|
||||||
|
secret_key_ref {
|
||||||
|
secret = "stripe-secret-key"
|
||||||
|
version = "latest"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
env {
|
||||||
|
name = "STRIPE_WEBHOOK_SECRET"
|
||||||
|
value_source {
|
||||||
|
secret_key_ref {
|
||||||
|
secret = "stripe-webhook-secret"
|
||||||
|
version = "latest"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
env {
|
||||||
|
name = "STRIPE_PRICE_PROFESSIONAL"
|
||||||
|
value_source {
|
||||||
|
secret_key_ref {
|
||||||
|
secret = "stripe-price-professional"
|
||||||
|
version = "latest"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
env {
|
||||||
|
name = "STRIPE_PRICE_FOUNDING"
|
||||||
|
value_source {
|
||||||
|
secret_key_ref {
|
||||||
|
secret = "stripe-price-founding"
|
||||||
|
version = "latest"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ports {
|
||||||
|
container_port = 8080
|
||||||
|
name = "http1"
|
||||||
|
}
|
||||||
|
|
||||||
|
startup_probe {
|
||||||
|
http_get {
|
||||||
|
path = "/health"
|
||||||
|
port = 8080
|
||||||
|
}
|
||||||
|
initial_delay_seconds = 2
|
||||||
|
timeout_seconds = 5
|
||||||
|
period_seconds = 5
|
||||||
|
failure_threshold = 10
|
||||||
|
}
|
||||||
|
|
||||||
|
liveness_probe {
|
||||||
|
http_get {
|
||||||
|
path = "/health"
|
||||||
|
port = 8080
|
||||||
|
}
|
||||||
|
timeout_seconds = 5
|
||||||
|
period_seconds = 30
|
||||||
|
failure_threshold = 3
|
||||||
|
}
|
||||||
|
|
||||||
|
volume_mounts {
|
||||||
|
name = "cloudsql"
|
||||||
|
mount_path = "/cloudsql"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
volumes {
|
||||||
|
name = "cloudsql"
|
||||||
|
cloud_sql_instance {
|
||||||
|
instances = [local.sql_instance]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
traffic {
|
||||||
|
type = "TRAFFIC_TARGET_ALLOCATION_TYPE_LATEST"
|
||||||
|
percent = 100
|
||||||
|
}
|
||||||
|
|
||||||
|
depends_on = [
|
||||||
|
google_project_iam_member.accounts_secret_accessor,
|
||||||
|
google_project_iam_member.accounts_sql_client,
|
||||||
|
google_secret_manager_secret_version.accounts_database_url,
|
||||||
|
google_secret_manager_secret_version.jwt_secret,
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
# ── Prod — europe-west1 ───────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
resource "google_cloud_run_v2_service" "accounts_eu" {
|
||||||
|
name = "accounts-prod-eu"
|
||||||
|
location = "europe-west1"
|
||||||
|
project = var.project_id
|
||||||
|
ingress = "INGRESS_TRAFFIC_ALL"
|
||||||
|
labels = local.accounts_labels
|
||||||
|
|
||||||
|
template {
|
||||||
|
service_account = google_service_account.accounts.email
|
||||||
|
|
||||||
|
scaling {
|
||||||
|
min_instance_count = 1
|
||||||
|
max_instance_count = 50
|
||||||
|
}
|
||||||
|
|
||||||
|
containers {
|
||||||
|
image = local.accounts_image
|
||||||
|
|
||||||
|
resources {
|
||||||
|
limits = {
|
||||||
|
cpu = "1"
|
||||||
|
memory = "512Mi"
|
||||||
|
}
|
||||||
|
cpu_idle = true
|
||||||
|
}
|
||||||
|
|
||||||
|
env {
|
||||||
|
name = "ENV"
|
||||||
|
value = "production"
|
||||||
|
}
|
||||||
|
env {
|
||||||
|
name = "PORT"
|
||||||
|
value = "8080"
|
||||||
|
}
|
||||||
|
|
||||||
|
env {
|
||||||
|
name = "ACCOUNTS_DATABASE_URL"
|
||||||
|
value_source {
|
||||||
|
secret_key_ref {
|
||||||
|
secret = google_secret_manager_secret.accounts_database_url.secret_id
|
||||||
|
version = "latest"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
env {
|
||||||
|
name = "JWT_SECRET"
|
||||||
|
value_source {
|
||||||
|
secret_key_ref {
|
||||||
|
secret = google_secret_manager_secret.jwt_secret.secret_id
|
||||||
|
version = "latest"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
env {
|
||||||
|
name = "STRIPE_SECRET_KEY"
|
||||||
|
value_source {
|
||||||
|
secret_key_ref {
|
||||||
|
secret = "stripe-secret-key"
|
||||||
|
version = "latest"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
env {
|
||||||
|
name = "STRIPE_WEBHOOK_SECRET"
|
||||||
|
value_source {
|
||||||
|
secret_key_ref {
|
||||||
|
secret = "stripe-webhook-secret"
|
||||||
|
version = "latest"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
env {
|
||||||
|
name = "STRIPE_PRICE_PROFESSIONAL"
|
||||||
|
value_source {
|
||||||
|
secret_key_ref {
|
||||||
|
secret = "stripe-price-professional"
|
||||||
|
version = "latest"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
env {
|
||||||
|
name = "STRIPE_PRICE_FOUNDING"
|
||||||
|
value_source {
|
||||||
|
secret_key_ref {
|
||||||
|
secret = "stripe-price-founding"
|
||||||
|
version = "latest"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ports {
|
||||||
|
container_port = 8080
|
||||||
|
name = "http1"
|
||||||
|
}
|
||||||
|
|
||||||
|
startup_probe {
|
||||||
|
http_get {
|
||||||
|
path = "/health"
|
||||||
|
port = 8080
|
||||||
|
}
|
||||||
|
initial_delay_seconds = 2
|
||||||
|
timeout_seconds = 5
|
||||||
|
period_seconds = 5
|
||||||
|
failure_threshold = 10
|
||||||
|
}
|
||||||
|
|
||||||
|
liveness_probe {
|
||||||
|
http_get {
|
||||||
|
path = "/health"
|
||||||
|
port = 8080
|
||||||
|
}
|
||||||
|
timeout_seconds = 5
|
||||||
|
period_seconds = 30
|
||||||
|
failure_threshold = 3
|
||||||
|
}
|
||||||
|
|
||||||
|
volume_mounts {
|
||||||
|
name = "cloudsql"
|
||||||
|
mount_path = "/cloudsql"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
volumes {
|
||||||
|
name = "cloudsql"
|
||||||
|
cloud_sql_instance {
|
||||||
|
instances = [local.sql_instance]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
traffic {
|
||||||
|
type = "TRAFFIC_TARGET_ALLOCATION_TYPE_LATEST"
|
||||||
|
percent = 100
|
||||||
|
}
|
||||||
|
|
||||||
|
depends_on = [
|
||||||
|
google_project_iam_member.accounts_secret_accessor,
|
||||||
|
google_project_iam_member.accounts_sql_client,
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
# ── Prod — asia-northeast1 ────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
resource "google_cloud_run_v2_service" "accounts_apac" {
|
||||||
|
name = "accounts-prod-apac"
|
||||||
|
location = "asia-northeast1"
|
||||||
|
project = var.project_id
|
||||||
|
ingress = "INGRESS_TRAFFIC_ALL"
|
||||||
|
labels = local.accounts_labels
|
||||||
|
|
||||||
|
template {
|
||||||
|
service_account = google_service_account.accounts.email
|
||||||
|
|
||||||
|
scaling {
|
||||||
|
min_instance_count = 1
|
||||||
|
max_instance_count = 50
|
||||||
|
}
|
||||||
|
|
||||||
|
containers {
|
||||||
|
image = local.accounts_image
|
||||||
|
|
||||||
|
resources {
|
||||||
|
limits = {
|
||||||
|
cpu = "1"
|
||||||
|
memory = "512Mi"
|
||||||
|
}
|
||||||
|
cpu_idle = true
|
||||||
|
}
|
||||||
|
|
||||||
|
env {
|
||||||
|
name = "ENV"
|
||||||
|
value = "production"
|
||||||
|
}
|
||||||
|
env {
|
||||||
|
name = "PORT"
|
||||||
|
value = "8080"
|
||||||
|
}
|
||||||
|
|
||||||
|
env {
|
||||||
|
name = "ACCOUNTS_DATABASE_URL"
|
||||||
|
value_source {
|
||||||
|
secret_key_ref {
|
||||||
|
secret = google_secret_manager_secret.accounts_database_url.secret_id
|
||||||
|
version = "latest"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
env {
|
||||||
|
name = "JWT_SECRET"
|
||||||
|
value_source {
|
||||||
|
secret_key_ref {
|
||||||
|
secret = google_secret_manager_secret.jwt_secret.secret_id
|
||||||
|
version = "latest"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
env {
|
||||||
|
name = "STRIPE_SECRET_KEY"
|
||||||
|
value_source {
|
||||||
|
secret_key_ref {
|
||||||
|
secret = "stripe-secret-key"
|
||||||
|
version = "latest"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
env {
|
||||||
|
name = "STRIPE_WEBHOOK_SECRET"
|
||||||
|
value_source {
|
||||||
|
secret_key_ref {
|
||||||
|
secret = "stripe-webhook-secret"
|
||||||
|
version = "latest"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
env {
|
||||||
|
name = "STRIPE_PRICE_PROFESSIONAL"
|
||||||
|
value_source {
|
||||||
|
secret_key_ref {
|
||||||
|
secret = "stripe-price-professional"
|
||||||
|
version = "latest"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
env {
|
||||||
|
name = "STRIPE_PRICE_FOUNDING"
|
||||||
|
value_source {
|
||||||
|
secret_key_ref {
|
||||||
|
secret = "stripe-price-founding"
|
||||||
|
version = "latest"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ports {
|
||||||
|
container_port = 8080
|
||||||
|
name = "http1"
|
||||||
|
}
|
||||||
|
|
||||||
|
startup_probe {
|
||||||
|
http_get {
|
||||||
|
path = "/health"
|
||||||
|
port = 8080
|
||||||
|
}
|
||||||
|
initial_delay_seconds = 2
|
||||||
|
timeout_seconds = 5
|
||||||
|
period_seconds = 5
|
||||||
|
failure_threshold = 10
|
||||||
|
}
|
||||||
|
|
||||||
|
liveness_probe {
|
||||||
|
http_get {
|
||||||
|
path = "/health"
|
||||||
|
port = 8080
|
||||||
|
}
|
||||||
|
timeout_seconds = 5
|
||||||
|
period_seconds = 30
|
||||||
|
failure_threshold = 3
|
||||||
|
}
|
||||||
|
|
||||||
|
volume_mounts {
|
||||||
|
name = "cloudsql"
|
||||||
|
mount_path = "/cloudsql"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
volumes {
|
||||||
|
name = "cloudsql"
|
||||||
|
cloud_sql_instance {
|
||||||
|
instances = [local.sql_instance]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
traffic {
|
||||||
|
type = "TRAFFIC_TARGET_ALLOCATION_TYPE_LATEST"
|
||||||
|
percent = 100
|
||||||
|
}
|
||||||
|
|
||||||
|
depends_on = [
|
||||||
|
google_project_iam_member.accounts_secret_accessor,
|
||||||
|
google_project_iam_member.accounts_sql_client,
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,308 @@
|
|||||||
|
# ── REST API (neuron-rest) — Cloud Run ────────────────────────────────────────
|
||||||
|
# Stateless API layer. Validates JWTs from accounts service.
|
||||||
|
# No database connection — reads user context from accounts service.
|
||||||
|
# cpu_idle=false: always-hot, no cold-start latency for API calls.
|
||||||
|
|
||||||
|
locals {
|
||||||
|
api_labels = {
|
||||||
|
"managed-by" = "terraform"
|
||||||
|
"service" = "neuron-api"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# ── Prod — us-central1 ────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
resource "google_cloud_run_v2_service" "api_us" {
|
||||||
|
name = "api-prod-us"
|
||||||
|
location = "us-central1"
|
||||||
|
project = var.project_id
|
||||||
|
ingress = "INGRESS_TRAFFIC_ALL"
|
||||||
|
labels = local.api_labels
|
||||||
|
|
||||||
|
template {
|
||||||
|
service_account = google_service_account.api.email
|
||||||
|
|
||||||
|
scaling {
|
||||||
|
min_instance_count = 1
|
||||||
|
max_instance_count = 100
|
||||||
|
}
|
||||||
|
|
||||||
|
containers {
|
||||||
|
image = local.api_image
|
||||||
|
|
||||||
|
resources {
|
||||||
|
limits = {
|
||||||
|
cpu = "2"
|
||||||
|
memory = "1Gi"
|
||||||
|
}
|
||||||
|
cpu_idle = false
|
||||||
|
}
|
||||||
|
|
||||||
|
env {
|
||||||
|
name = "ENV"
|
||||||
|
value = "production"
|
||||||
|
}
|
||||||
|
env {
|
||||||
|
name = "PORT"
|
||||||
|
value = "8080"
|
||||||
|
}
|
||||||
|
env {
|
||||||
|
name = "ACCOUNTS_SERVICE_URL"
|
||||||
|
value = "https://accounts.neurontechnologies.ai"
|
||||||
|
}
|
||||||
|
|
||||||
|
env {
|
||||||
|
name = "JWT_SECRET"
|
||||||
|
value_source {
|
||||||
|
secret_key_ref {
|
||||||
|
secret = google_secret_manager_secret.jwt_secret.secret_id
|
||||||
|
version = "latest"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
env {
|
||||||
|
name = "LICENSE_ADMIN_TOKEN"
|
||||||
|
value_source {
|
||||||
|
secret_key_ref {
|
||||||
|
secret = google_secret_manager_secret.license_admin_token.secret_id
|
||||||
|
version = "latest"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ports {
|
||||||
|
container_port = 8080
|
||||||
|
name = "http1"
|
||||||
|
}
|
||||||
|
|
||||||
|
startup_probe {
|
||||||
|
http_get {
|
||||||
|
path = "/health"
|
||||||
|
port = 8080
|
||||||
|
}
|
||||||
|
initial_delay_seconds = 2
|
||||||
|
timeout_seconds = 5
|
||||||
|
period_seconds = 5
|
||||||
|
failure_threshold = 6
|
||||||
|
}
|
||||||
|
|
||||||
|
liveness_probe {
|
||||||
|
http_get {
|
||||||
|
path = "/health"
|
||||||
|
port = 8080
|
||||||
|
}
|
||||||
|
timeout_seconds = 5
|
||||||
|
period_seconds = 30
|
||||||
|
failure_threshold = 3
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
max_instance_request_concurrency = 1000
|
||||||
|
}
|
||||||
|
|
||||||
|
traffic {
|
||||||
|
type = "TRAFFIC_TARGET_ALLOCATION_TYPE_LATEST"
|
||||||
|
percent = 100
|
||||||
|
}
|
||||||
|
|
||||||
|
depends_on = [
|
||||||
|
google_project_iam_member.api_secret_accessor,
|
||||||
|
google_secret_manager_secret_version.jwt_secret,
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
# ── Prod — europe-west1 ───────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
resource "google_cloud_run_v2_service" "api_eu" {
|
||||||
|
name = "api-prod-eu"
|
||||||
|
location = "europe-west1"
|
||||||
|
project = var.project_id
|
||||||
|
ingress = "INGRESS_TRAFFIC_ALL"
|
||||||
|
labels = local.api_labels
|
||||||
|
|
||||||
|
template {
|
||||||
|
service_account = google_service_account.api.email
|
||||||
|
|
||||||
|
scaling {
|
||||||
|
min_instance_count = 1
|
||||||
|
max_instance_count = 100
|
||||||
|
}
|
||||||
|
|
||||||
|
containers {
|
||||||
|
image = local.api_image
|
||||||
|
|
||||||
|
resources {
|
||||||
|
limits = {
|
||||||
|
cpu = "2"
|
||||||
|
memory = "1Gi"
|
||||||
|
}
|
||||||
|
cpu_idle = false
|
||||||
|
}
|
||||||
|
|
||||||
|
env {
|
||||||
|
name = "ENV"
|
||||||
|
value = "production"
|
||||||
|
}
|
||||||
|
env {
|
||||||
|
name = "PORT"
|
||||||
|
value = "8080"
|
||||||
|
}
|
||||||
|
env {
|
||||||
|
name = "ACCOUNTS_SERVICE_URL"
|
||||||
|
value = "https://accounts.neurontechnologies.ai"
|
||||||
|
}
|
||||||
|
|
||||||
|
env {
|
||||||
|
name = "JWT_SECRET"
|
||||||
|
value_source {
|
||||||
|
secret_key_ref {
|
||||||
|
secret = google_secret_manager_secret.jwt_secret.secret_id
|
||||||
|
version = "latest"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
env {
|
||||||
|
name = "LICENSE_ADMIN_TOKEN"
|
||||||
|
value_source {
|
||||||
|
secret_key_ref {
|
||||||
|
secret = google_secret_manager_secret.license_admin_token.secret_id
|
||||||
|
version = "latest"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ports {
|
||||||
|
container_port = 8080
|
||||||
|
name = "http1"
|
||||||
|
}
|
||||||
|
|
||||||
|
startup_probe {
|
||||||
|
http_get {
|
||||||
|
path = "/health"
|
||||||
|
port = 8080
|
||||||
|
}
|
||||||
|
initial_delay_seconds = 2
|
||||||
|
timeout_seconds = 5
|
||||||
|
period_seconds = 5
|
||||||
|
failure_threshold = 6
|
||||||
|
}
|
||||||
|
|
||||||
|
liveness_probe {
|
||||||
|
http_get {
|
||||||
|
path = "/health"
|
||||||
|
port = 8080
|
||||||
|
}
|
||||||
|
timeout_seconds = 5
|
||||||
|
period_seconds = 30
|
||||||
|
failure_threshold = 3
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
max_instance_request_concurrency = 1000
|
||||||
|
}
|
||||||
|
|
||||||
|
traffic {
|
||||||
|
type = "TRAFFIC_TARGET_ALLOCATION_TYPE_LATEST"
|
||||||
|
percent = 100
|
||||||
|
}
|
||||||
|
|
||||||
|
depends_on = [google_project_iam_member.api_secret_accessor]
|
||||||
|
}
|
||||||
|
|
||||||
|
# ── Prod — asia-northeast1 ────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
resource "google_cloud_run_v2_service" "api_apac" {
|
||||||
|
name = "api-prod-apac"
|
||||||
|
location = "asia-northeast1"
|
||||||
|
project = var.project_id
|
||||||
|
ingress = "INGRESS_TRAFFIC_ALL"
|
||||||
|
labels = local.api_labels
|
||||||
|
|
||||||
|
template {
|
||||||
|
service_account = google_service_account.api.email
|
||||||
|
|
||||||
|
scaling {
|
||||||
|
min_instance_count = 1
|
||||||
|
max_instance_count = 100
|
||||||
|
}
|
||||||
|
|
||||||
|
containers {
|
||||||
|
image = local.api_image
|
||||||
|
|
||||||
|
resources {
|
||||||
|
limits = {
|
||||||
|
cpu = "2"
|
||||||
|
memory = "1Gi"
|
||||||
|
}
|
||||||
|
cpu_idle = false
|
||||||
|
}
|
||||||
|
|
||||||
|
env {
|
||||||
|
name = "ENV"
|
||||||
|
value = "production"
|
||||||
|
}
|
||||||
|
env {
|
||||||
|
name = "PORT"
|
||||||
|
value = "8080"
|
||||||
|
}
|
||||||
|
env {
|
||||||
|
name = "ACCOUNTS_SERVICE_URL"
|
||||||
|
value = "https://accounts.neurontechnologies.ai"
|
||||||
|
}
|
||||||
|
|
||||||
|
env {
|
||||||
|
name = "JWT_SECRET"
|
||||||
|
value_source {
|
||||||
|
secret_key_ref {
|
||||||
|
secret = google_secret_manager_secret.jwt_secret.secret_id
|
||||||
|
version = "latest"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
env {
|
||||||
|
name = "LICENSE_ADMIN_TOKEN"
|
||||||
|
value_source {
|
||||||
|
secret_key_ref {
|
||||||
|
secret = google_secret_manager_secret.license_admin_token.secret_id
|
||||||
|
version = "latest"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ports {
|
||||||
|
container_port = 8080
|
||||||
|
name = "http1"
|
||||||
|
}
|
||||||
|
|
||||||
|
startup_probe {
|
||||||
|
http_get {
|
||||||
|
path = "/health"
|
||||||
|
port = 8080
|
||||||
|
}
|
||||||
|
initial_delay_seconds = 2
|
||||||
|
timeout_seconds = 5
|
||||||
|
period_seconds = 5
|
||||||
|
failure_threshold = 6
|
||||||
|
}
|
||||||
|
|
||||||
|
liveness_probe {
|
||||||
|
http_get {
|
||||||
|
path = "/health"
|
||||||
|
port = 8080
|
||||||
|
}
|
||||||
|
timeout_seconds = 5
|
||||||
|
period_seconds = 30
|
||||||
|
failure_threshold = 3
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
max_instance_request_concurrency = 1000
|
||||||
|
}
|
||||||
|
|
||||||
|
traffic {
|
||||||
|
type = "TRAFFIC_TARGET_ALLOCATION_TYPE_LATEST"
|
||||||
|
percent = 100
|
||||||
|
}
|
||||||
|
|
||||||
|
depends_on = [google_project_iam_member.api_secret_accessor]
|
||||||
|
}
|
||||||
@@ -0,0 +1,150 @@
|
|||||||
|
# ── Cloud SQL — PostgreSQL 15 ─────────────────────────────────────────────────
|
||||||
|
# Single instance in us-central1 for the accounts service.
|
||||||
|
# Cloud Run services connect via the built-in Cloud SQL Auth Proxy
|
||||||
|
# (no direct IP exposure, encrypted, IAM-authenticated).
|
||||||
|
#
|
||||||
|
# Sizing: db-g1-small (1 shared vCPU, 1.7 GB) for launch. Scale up to
|
||||||
|
# db-n1-standard-2 once traffic warrants it — zero-downtime restart.
|
||||||
|
|
||||||
|
resource "google_sql_database_instance" "main" {
|
||||||
|
name = "neuron-prod-pg15"
|
||||||
|
database_version = "POSTGRES_15"
|
||||||
|
region = "us-central1"
|
||||||
|
project = var.project_id
|
||||||
|
|
||||||
|
# Prevent accidental deletion via terraform destroy
|
||||||
|
deletion_protection = true
|
||||||
|
|
||||||
|
settings {
|
||||||
|
tier = "db-g1-small"
|
||||||
|
availability_type = "ZONAL" # Upgrade to REGIONAL (HA) once > 1k users
|
||||||
|
|
||||||
|
backup_configuration {
|
||||||
|
enabled = true
|
||||||
|
point_in_time_recovery_enabled = true
|
||||||
|
start_time = "03:00" # 3am UTC — before backup window on Legion
|
||||||
|
transaction_log_retention_days = 7
|
||||||
|
backup_retention_settings {
|
||||||
|
retained_backups = 14
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
maintenance_window {
|
||||||
|
day = 7 # Sunday
|
||||||
|
hour = 4 # 4am UTC
|
||||||
|
update_track = "stable"
|
||||||
|
}
|
||||||
|
|
||||||
|
ip_configuration {
|
||||||
|
# No public IP — Cloud Run uses the Auth Proxy via private service connect
|
||||||
|
# Flip ipv4_enabled=true + authorized_networks if you ever need direct access
|
||||||
|
# for migrations/seeding from a bastion.
|
||||||
|
ipv4_enabled = true # Required for Cloud Run Auth Proxy (until VPC SC configured)
|
||||||
|
ssl_mode = "ENCRYPTED_ONLY"
|
||||||
|
}
|
||||||
|
|
||||||
|
database_flags {
|
||||||
|
name = "max_connections"
|
||||||
|
value = "100" # Cloud Run can burst many instances; cap connections early
|
||||||
|
}
|
||||||
|
|
||||||
|
database_flags {
|
||||||
|
name = "log_min_duration_statement"
|
||||||
|
value = "500" # Log queries taking > 500ms
|
||||||
|
}
|
||||||
|
|
||||||
|
insights_config {
|
||||||
|
query_insights_enabled = true
|
||||||
|
query_string_length = 1024
|
||||||
|
record_application_tags = true
|
||||||
|
record_client_address = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# ── Databases ─────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
resource "google_sql_database" "accounts" {
|
||||||
|
name = "accounts"
|
||||||
|
instance = google_sql_database_instance.main.name
|
||||||
|
project = var.project_id
|
||||||
|
}
|
||||||
|
|
||||||
|
# ── Users ─────────────────────────────────────────────────────────────────────
|
||||||
|
# Passwords are stored in Secret Manager. The accounts service reads them
|
||||||
|
# at runtime via the ACCOUNTS_DATABASE_URL secret.
|
||||||
|
# Cloud IAM DB users are the preferred path long-term (no password rotation);
|
||||||
|
# using password auth here for compatibility with pgx/stdlib.
|
||||||
|
|
||||||
|
resource "google_sql_user" "accounts" {
|
||||||
|
name = "accounts"
|
||||||
|
instance = google_sql_database_instance.main.name
|
||||||
|
project = var.project_id
|
||||||
|
password = random_password.accounts_db.result
|
||||||
|
}
|
||||||
|
|
||||||
|
resource "random_password" "accounts_db" {
|
||||||
|
length = 32
|
||||||
|
special = true
|
||||||
|
}
|
||||||
|
|
||||||
|
# ── Secret Manager — Database URL ─────────────────────────────────────────────
|
||||||
|
# Stored as a full DSN so the app just reads one env var.
|
||||||
|
# Uses /cloudsql/ Unix socket path — Cloud Run Auth Proxy mounts it there.
|
||||||
|
|
||||||
|
resource "google_secret_manager_secret" "accounts_database_url" {
|
||||||
|
secret_id = "accounts-database-url"
|
||||||
|
project = var.project_id
|
||||||
|
|
||||||
|
replication {
|
||||||
|
auto {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
resource "google_secret_manager_secret_version" "accounts_database_url" {
|
||||||
|
secret = google_secret_manager_secret.accounts_database_url.id
|
||||||
|
secret_data = "host=/cloudsql/${google_sql_database_instance.main.connection_name} user=accounts password=${random_password.accounts_db.result} dbname=accounts sslmode=disable"
|
||||||
|
}
|
||||||
|
|
||||||
|
# ── Secret Manager — JWT signing key ─────────────────────────────────────────
|
||||||
|
# Shared between accounts (issues tokens) and neuron-rest (validates tokens).
|
||||||
|
|
||||||
|
resource "random_password" "jwt_secret" {
|
||||||
|
length = 64
|
||||||
|
special = false # URL-safe; used as HMAC key
|
||||||
|
}
|
||||||
|
|
||||||
|
resource "google_secret_manager_secret" "jwt_secret" {
|
||||||
|
secret_id = "jwt-signing-key"
|
||||||
|
project = var.project_id
|
||||||
|
|
||||||
|
replication {
|
||||||
|
auto {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
resource "google_secret_manager_secret_version" "jwt_secret" {
|
||||||
|
secret = google_secret_manager_secret.jwt_secret.id
|
||||||
|
secret_data = random_password.jwt_secret.result
|
||||||
|
}
|
||||||
|
|
||||||
|
# ── Secret Manager — License admin token ─────────────────────────────────────
|
||||||
|
# Used by neuron-rest to authorize license validation calls.
|
||||||
|
# Currently held in Vault under neuron-technologies/marketing.license_admin_token.
|
||||||
|
# Create this secret manually from Vault:
|
||||||
|
# gcloud secrets create license-admin-token --data-file=<(vault kv get \
|
||||||
|
# -field=license_admin_token secret/neuron-technologies/marketing)
|
||||||
|
|
||||||
|
resource "google_secret_manager_secret" "license_admin_token" {
|
||||||
|
secret_id = "license-admin-token"
|
||||||
|
project = var.project_id
|
||||||
|
|
||||||
|
replication {
|
||||||
|
auto {}
|
||||||
|
}
|
||||||
|
# Version populated manually or via bootstrap script — not managed by Terraform
|
||||||
|
# to avoid exposing the value in state.
|
||||||
|
lifecycle {
|
||||||
|
ignore_changes = [id]
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,119 @@
|
|||||||
|
# ── Serverless NEGs — Accounts ────────────────────────────────────────────────
|
||||||
|
|
||||||
|
resource "google_compute_region_network_endpoint_group" "accounts_us" {
|
||||||
|
name = "accounts-neg-us"
|
||||||
|
network_endpoint_type = "SERVERLESS"
|
||||||
|
region = "us-central1"
|
||||||
|
project = var.project_id
|
||||||
|
cloud_run { service = google_cloud_run_v2_service.accounts_us.name }
|
||||||
|
}
|
||||||
|
|
||||||
|
resource "google_compute_region_network_endpoint_group" "accounts_eu" {
|
||||||
|
name = "accounts-neg-eu"
|
||||||
|
network_endpoint_type = "SERVERLESS"
|
||||||
|
region = "europe-west1"
|
||||||
|
project = var.project_id
|
||||||
|
cloud_run { service = google_cloud_run_v2_service.accounts_eu.name }
|
||||||
|
}
|
||||||
|
|
||||||
|
resource "google_compute_region_network_endpoint_group" "accounts_apac" {
|
||||||
|
name = "accounts-neg-apac"
|
||||||
|
network_endpoint_type = "SERVERLESS"
|
||||||
|
region = "asia-northeast1"
|
||||||
|
project = var.project_id
|
||||||
|
cloud_run { service = google_cloud_run_v2_service.accounts_apac.name }
|
||||||
|
}
|
||||||
|
|
||||||
|
# ── Serverless NEGs — API ─────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
resource "google_compute_region_network_endpoint_group" "api_us" {
|
||||||
|
name = "api-neg-us"
|
||||||
|
network_endpoint_type = "SERVERLESS"
|
||||||
|
region = "us-central1"
|
||||||
|
project = var.project_id
|
||||||
|
cloud_run { service = google_cloud_run_v2_service.api_us.name }
|
||||||
|
}
|
||||||
|
|
||||||
|
resource "google_compute_region_network_endpoint_group" "api_eu" {
|
||||||
|
name = "api-neg-eu"
|
||||||
|
network_endpoint_type = "SERVERLESS"
|
||||||
|
region = "europe-west1"
|
||||||
|
project = var.project_id
|
||||||
|
cloud_run { service = google_cloud_run_v2_service.api_eu.name }
|
||||||
|
}
|
||||||
|
|
||||||
|
resource "google_compute_region_network_endpoint_group" "api_apac" {
|
||||||
|
name = "api-neg-apac"
|
||||||
|
network_endpoint_type = "SERVERLESS"
|
||||||
|
region = "asia-northeast1"
|
||||||
|
project = var.project_id
|
||||||
|
cloud_run { service = google_cloud_run_v2_service.api_apac.name }
|
||||||
|
}
|
||||||
|
|
||||||
|
# ── Backend Service — Accounts ────────────────────────────────────────────────
|
||||||
|
|
||||||
|
resource "google_compute_backend_service" "accounts" {
|
||||||
|
name = "accounts-backend-prod"
|
||||||
|
project = var.project_id
|
||||||
|
load_balancing_scheme = "EXTERNAL_MANAGED"
|
||||||
|
protocol = "HTTPS"
|
||||||
|
timeout_sec = 30
|
||||||
|
|
||||||
|
security_policy = google_compute_security_policy.marketing.self_link
|
||||||
|
|
||||||
|
# No CDN for accounts — responses are user-specific and must not be cached
|
||||||
|
enable_cdn = false
|
||||||
|
|
||||||
|
backend { group = google_compute_region_network_endpoint_group.accounts_us.self_link }
|
||||||
|
backend { group = google_compute_region_network_endpoint_group.accounts_eu.self_link }
|
||||||
|
backend { group = google_compute_region_network_endpoint_group.accounts_apac.self_link }
|
||||||
|
|
||||||
|
log_config {
|
||||||
|
enable = true
|
||||||
|
sample_rate = 1.0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# ── Backend Service — API ─────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
resource "google_compute_backend_service" "api" {
|
||||||
|
name = "api-backend-prod"
|
||||||
|
project = var.project_id
|
||||||
|
load_balancing_scheme = "EXTERNAL_MANAGED"
|
||||||
|
protocol = "HTTPS"
|
||||||
|
timeout_sec = 60 # API calls may be longer (AI inference routing)
|
||||||
|
|
||||||
|
security_policy = google_compute_security_policy.marketing.self_link
|
||||||
|
|
||||||
|
enable_cdn = false # API responses are per-request; no CDN benefit
|
||||||
|
|
||||||
|
backend { group = google_compute_region_network_endpoint_group.api_us.self_link }
|
||||||
|
backend { group = google_compute_region_network_endpoint_group.api_eu.self_link }
|
||||||
|
backend { group = google_compute_region_network_endpoint_group.api_apac.self_link }
|
||||||
|
|
||||||
|
log_config {
|
||||||
|
enable = true
|
||||||
|
sample_rate = 1.0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# ── SSL Certs — accounts and api subdomains ───────────────────────────────────
|
||||||
|
# Added to the existing prod HTTPS proxy alongside the marketing cert.
|
||||||
|
# DNS must point to the same prod global IP (marketing-ip-prod) for
|
||||||
|
# provisioning to succeed.
|
||||||
|
|
||||||
|
resource "google_compute_managed_ssl_certificate" "accounts" {
|
||||||
|
name = "accounts-cert-prod"
|
||||||
|
project = var.project_id
|
||||||
|
managed {
|
||||||
|
domains = ["accounts.neurontechnologies.ai"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
resource "google_compute_managed_ssl_certificate" "api" {
|
||||||
|
name = "api-cert-prod"
|
||||||
|
project = var.project_id
|
||||||
|
managed {
|
||||||
|
domains = ["api.neurontechnologies.ai"]
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -223,21 +223,59 @@ resource "google_compute_managed_ssl_certificate" "stage" {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
# ── Prod URL Map ──────────────────────────────────────────────────────────────
|
# ── Prod URL Map — host-based routing ────────────────────────────────────────
|
||||||
|
# One global IP handles all three services via Host header routing.
|
||||||
|
# - neurontechnologies.ai / www. → marketing (default)
|
||||||
|
# - api.neurontechnologies.ai → API backend
|
||||||
|
# - accounts.neurontechnologies.ai → accounts backend
|
||||||
|
|
||||||
resource "google_compute_url_map" "prod" {
|
resource "google_compute_url_map" "prod" {
|
||||||
name = "marketing-urlmap-prod"
|
name = "marketing-urlmap-prod"
|
||||||
project = var.project_id
|
project = var.project_id
|
||||||
default_service = google_compute_backend_service.prod.self_link
|
default_service = google_compute_backend_service.prod.self_link
|
||||||
|
|
||||||
|
host_rule {
|
||||||
|
hosts = ["neurontechnologies.ai", "www.neurontechnologies.ai"]
|
||||||
|
path_matcher = "marketing"
|
||||||
|
}
|
||||||
|
|
||||||
|
host_rule {
|
||||||
|
hosts = ["api.neurontechnologies.ai"]
|
||||||
|
path_matcher = "api"
|
||||||
|
}
|
||||||
|
|
||||||
|
host_rule {
|
||||||
|
hosts = ["accounts.neurontechnologies.ai"]
|
||||||
|
path_matcher = "accounts"
|
||||||
|
}
|
||||||
|
|
||||||
|
path_matcher {
|
||||||
|
name = "marketing"
|
||||||
|
default_service = google_compute_backend_service.prod.self_link
|
||||||
|
}
|
||||||
|
|
||||||
|
path_matcher {
|
||||||
|
name = "api"
|
||||||
|
default_service = google_compute_backend_service.api.self_link
|
||||||
|
}
|
||||||
|
|
||||||
|
path_matcher {
|
||||||
|
name = "accounts"
|
||||||
|
default_service = google_compute_backend_service.accounts.self_link
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
# ── Prod HTTPS Target Proxy ───────────────────────────────────────────────────
|
# ── Prod HTTPS Target Proxy ───────────────────────────────────────────────────
|
||||||
|
|
||||||
resource "google_compute_target_https_proxy" "prod" {
|
resource "google_compute_target_https_proxy" "prod" {
|
||||||
name = "marketing-https-proxy-prod"
|
name = "marketing-https-proxy-prod"
|
||||||
project = var.project_id
|
project = var.project_id
|
||||||
url_map = google_compute_url_map.prod.self_link
|
url_map = google_compute_url_map.prod.self_link
|
||||||
ssl_certificates = [google_compute_managed_ssl_certificate.prod.self_link]
|
ssl_certificates = [
|
||||||
|
google_compute_managed_ssl_certificate.prod.self_link,
|
||||||
|
google_compute_managed_ssl_certificate.accounts.self_link,
|
||||||
|
google_compute_managed_ssl_certificate.api.self_link,
|
||||||
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
# ── Prod HTTP → HTTPS redirect ────────────────────────────────────────────────
|
# ── Prod HTTP → HTTPS redirect ────────────────────────────────────────────────
|
||||||
|
|||||||
@@ -10,6 +10,10 @@ terraform {
|
|||||||
source = "hashicorp/google-beta"
|
source = "hashicorp/google-beta"
|
||||||
version = "~> 5.0"
|
version = "~> 5.0"
|
||||||
}
|
}
|
||||||
|
random = {
|
||||||
|
source = "hashicorp/random"
|
||||||
|
version = "~> 3.6"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
backend "s3" {
|
backend "s3" {
|
||||||
|
|||||||
+68
-16
@@ -1,44 +1,96 @@
|
|||||||
# ── Outputs ───────────────────────────────────────────────────────────────────
|
# ── Outputs ───────────────────────────────────────────────────────────────────
|
||||||
# After `terraform apply`, use these IPs to update Cloudflare DNS A records.
|
# After `terraform apply`, use prod_lb_ip for Cloudflare DNS A records.
|
||||||
#
|
#
|
||||||
# Prod: neurontechnologies.ai + www.neurontechnologies.ai → prod_lb_ip
|
# All three services share the same global anycast IP (prod_lb_ip).
|
||||||
# Stage: stage.neurontechnologies.ai → stage_lb_ip
|
# Set these A records in Cloudflare (proxied=true for CDN + DDoS):
|
||||||
|
# neurontechnologies.ai → prod_lb_ip
|
||||||
|
# www.neurontechnologies.ai → prod_lb_ip
|
||||||
|
# api.neurontechnologies.ai → prod_lb_ip
|
||||||
|
# accounts.neurontechnologies.ai → prod_lb_ip
|
||||||
#
|
#
|
||||||
# In the Legion Terraform (dns-neurontechnologies.tf), replace the tunnel CNAMEs
|
# Stage: stage.neurontechnologies.ai → stage_lb_ip (separate IP)
|
||||||
# for neurontechnologies.ai, www, and stage with A records pointing to these IPs.
|
|
||||||
# Set proxied=true in Cloudflare to keep CDN/DDoS protection layered in front.
|
|
||||||
|
|
||||||
output "prod_lb_ip" {
|
output "prod_lb_ip" {
|
||||||
description = "Global anycast IP for the production load balancer (neurontechnologies.ai + www)"
|
description = "Global anycast IP for all prod services (marketing, accounts, api)"
|
||||||
value = google_compute_global_address.prod.address
|
value = google_compute_global_address.prod.address
|
||||||
}
|
}
|
||||||
|
|
||||||
output "stage_lb_ip" {
|
output "stage_lb_ip" {
|
||||||
description = "Global anycast IP for the staging load balancer (stage.neurontechnologies.ai)"
|
description = "Global anycast IP for the staging load balancer"
|
||||||
value = google_compute_global_address.stage.address
|
value = google_compute_global_address.stage.address
|
||||||
}
|
}
|
||||||
|
|
||||||
output "prod_ssl_cert_name" {
|
output "prod_ssl_cert_name" {
|
||||||
description = "Name of the Google-managed SSL cert for prod (check provisioning status in GCP console)"
|
description = "Marketing SSL cert (check provisioning status in GCP console)"
|
||||||
value = google_compute_managed_ssl_certificate.prod.name
|
value = google_compute_managed_ssl_certificate.prod.name
|
||||||
}
|
}
|
||||||
|
|
||||||
|
output "accounts_ssl_cert_name" {
|
||||||
|
description = "Accounts SSL cert (check provisioning status in GCP console)"
|
||||||
|
value = google_compute_managed_ssl_certificate.accounts.name
|
||||||
|
}
|
||||||
|
|
||||||
|
output "api_ssl_cert_name" {
|
||||||
|
description = "API SSL cert (check provisioning status in GCP console)"
|
||||||
|
value = google_compute_managed_ssl_certificate.api.name
|
||||||
|
}
|
||||||
|
|
||||||
output "stage_ssl_cert_name" {
|
output "stage_ssl_cert_name" {
|
||||||
description = "Name of the Google-managed SSL cert for stage"
|
description = "Stage SSL cert"
|
||||||
value = google_compute_managed_ssl_certificate.stage.name
|
value = google_compute_managed_ssl_certificate.stage.name
|
||||||
}
|
}
|
||||||
|
|
||||||
output "marketing_service_account_email" {
|
output "marketing_service_account_email" {
|
||||||
description = "Service account email for Cloud Run services"
|
description = "Marketing Cloud Run SA"
|
||||||
value = google_service_account.marketing.email
|
value = google_service_account.marketing.email
|
||||||
}
|
}
|
||||||
|
|
||||||
|
output "accounts_service_account_email" {
|
||||||
|
description = "Accounts Cloud Run SA"
|
||||||
|
value = google_service_account.accounts.email
|
||||||
|
}
|
||||||
|
|
||||||
|
output "api_service_account_email" {
|
||||||
|
description = "API Cloud Run SA"
|
||||||
|
value = google_service_account.api.email
|
||||||
|
}
|
||||||
|
|
||||||
|
output "ci_pusher_email" {
|
||||||
|
description = "CI SA email — use to create the JSON key for GCP_SA_KEY Gitea secret"
|
||||||
|
value = google_service_account.ci_pusher.email
|
||||||
|
}
|
||||||
|
|
||||||
|
output "cloud_sql_connection_name" {
|
||||||
|
description = "Cloud SQL instance connection name — use in Cloud Run volume mounts"
|
||||||
|
value = google_sql_database_instance.main.connection_name
|
||||||
|
}
|
||||||
|
|
||||||
|
output "cloud_sql_instance_ip" {
|
||||||
|
description = "Cloud SQL public IP (for bastion/migration access — disable when not needed)"
|
||||||
|
value = google_sql_database_instance.main.public_ip_address
|
||||||
|
}
|
||||||
|
|
||||||
output "cloud_run_services" {
|
output "cloud_run_services" {
|
||||||
description = "Cloud Run service URLs per region"
|
description = "Cloud Run service URLs per service and region"
|
||||||
value = {
|
value = {
|
||||||
prod_us = google_cloud_run_v2_service.prod_us.uri
|
marketing_us = google_cloud_run_v2_service.prod_us.uri
|
||||||
prod_eu = google_cloud_run_v2_service.prod_eu.uri
|
marketing_eu = google_cloud_run_v2_service.prod_eu.uri
|
||||||
prod_apac = google_cloud_run_v2_service.prod_apac.uri
|
marketing_apac = google_cloud_run_v2_service.prod_apac.uri
|
||||||
stage = google_cloud_run_v2_service.stage.uri
|
marketing_stage = google_cloud_run_v2_service.stage.uri
|
||||||
|
accounts_us = google_cloud_run_v2_service.accounts_us.uri
|
||||||
|
accounts_eu = google_cloud_run_v2_service.accounts_eu.uri
|
||||||
|
accounts_apac = google_cloud_run_v2_service.accounts_apac.uri
|
||||||
|
api_us = google_cloud_run_v2_service.api_us.uri
|
||||||
|
api_eu = google_cloud_run_v2_service.api_eu.uri
|
||||||
|
api_apac = google_cloud_run_v2_service.api_apac.uri
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
output "artifact_registry_urls" {
|
||||||
|
description = "Docker image base URLs for each service"
|
||||||
|
value = {
|
||||||
|
marketing = "us-central1-docker.pkg.dev/${var.project_id}/neuron-marketing/marketing"
|
||||||
|
accounts = "us-central1-docker.pkg.dev/${var.project_id}/neuron-accounts/accounts"
|
||||||
|
api = "us-central1-docker.pkg.dev/${var.project_id}/neuron-api/api"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,13 +1,27 @@
|
|||||||
# ── Service Account ───────────────────────────────────────────────────────────
|
# ── Service Accounts ──────────────────────────────────────────────────────────
|
||||||
|
|
||||||
resource "google_service_account" "marketing" {
|
resource "google_service_account" "marketing" {
|
||||||
account_id = "neuron-marketing-sa"
|
account_id = "neuron-marketing-sa"
|
||||||
display_name = "Neuron Marketing Cloud Run SA"
|
display_name = "Neuron Marketing Cloud Run SA"
|
||||||
description = "Service account for the neurontechnologies.ai marketing site on Cloud Run"
|
description = "Service account for the neurontechnologies.ai marketing site on Cloud Run"
|
||||||
|
project = var.project_id
|
||||||
}
|
}
|
||||||
|
|
||||||
# ── IAM — Secret Manager accessor ─────────────────────────────────────────────
|
resource "google_service_account" "accounts" {
|
||||||
# Grant the SA access to read all marketing secrets from Secret Manager.
|
account_id = "neuron-accounts-sa"
|
||||||
|
display_name = "Neuron Accounts Cloud Run SA"
|
||||||
|
description = "Service account for the accounts service (auth, billing, marketplace)"
|
||||||
|
project = var.project_id
|
||||||
|
}
|
||||||
|
|
||||||
|
resource "google_service_account" "api" {
|
||||||
|
account_id = "neuron-api-sa"
|
||||||
|
display_name = "Neuron REST API Cloud Run SA"
|
||||||
|
description = "Service account for the neuron-rest API service"
|
||||||
|
project = var.project_id
|
||||||
|
}
|
||||||
|
|
||||||
|
# ── IAM — Secret Manager ──────────────────────────────────────────────────────
|
||||||
|
|
||||||
resource "google_project_iam_member" "marketing_secret_accessor" {
|
resource "google_project_iam_member" "marketing_secret_accessor" {
|
||||||
project = var.project_id
|
project = var.project_id
|
||||||
@@ -15,9 +29,32 @@ resource "google_project_iam_member" "marketing_secret_accessor" {
|
|||||||
member = "serviceAccount:${google_service_account.marketing.email}"
|
member = "serviceAccount:${google_service_account.marketing.email}"
|
||||||
}
|
}
|
||||||
|
|
||||||
# ── IAM — Public (unauthenticated) invocation for prod services ───────────────
|
resource "google_project_iam_member" "accounts_secret_accessor" {
|
||||||
# Cloud Run v2: allow-unauthenticated is set per-service via google_cloud_run_v2_service_iam_member.
|
project = var.project_id
|
||||||
|
role = "roles/secretmanager.secretAccessor"
|
||||||
|
member = "serviceAccount:${google_service_account.accounts.email}"
|
||||||
|
}
|
||||||
|
|
||||||
|
resource "google_project_iam_member" "api_secret_accessor" {
|
||||||
|
project = var.project_id
|
||||||
|
role = "roles/secretmanager.secretAccessor"
|
||||||
|
member = "serviceAccount:${google_service_account.api.email}"
|
||||||
|
}
|
||||||
|
|
||||||
|
# ── IAM — Cloud SQL ───────────────────────────────────────────────────────────
|
||||||
|
# Only accounts connects to Cloud SQL; API and marketing don't need DB access.
|
||||||
|
|
||||||
|
resource "google_project_iam_member" "accounts_sql_client" {
|
||||||
|
project = var.project_id
|
||||||
|
role = "roles/cloudsql.client"
|
||||||
|
member = "serviceAccount:${google_service_account.accounts.email}"
|
||||||
|
}
|
||||||
|
|
||||||
|
# ── IAM — Public invocation (Cloud Run → LB) ──────────────────────────────────
|
||||||
|
# allUsers invoker allows the global HTTP(S) load balancer's health checks
|
||||||
|
# and traffic to reach the services without additional auth headers.
|
||||||
|
|
||||||
|
# Marketing
|
||||||
resource "google_cloud_run_v2_service_iam_member" "prod_us_public" {
|
resource "google_cloud_run_v2_service_iam_member" "prod_us_public" {
|
||||||
project = var.project_id
|
project = var.project_id
|
||||||
location = "us-central1"
|
location = "us-central1"
|
||||||
@@ -48,7 +85,55 @@ resource "google_cloud_run_v2_service_iam_member" "stage_public" {
|
|||||||
name = google_cloud_run_v2_service.stage.name
|
name = google_cloud_run_v2_service.stage.name
|
||||||
role = "roles/run.invoker"
|
role = "roles/run.invoker"
|
||||||
member = "allUsers"
|
member = "allUsers"
|
||||||
|
# Note: Cloudflare Access enforces auth in front of stage.
|
||||||
# Note: Cloudflare Access enforces authentication in front of stage.
|
}
|
||||||
# Cloud Run itself allows allUsers so the LB health checks pass.
|
|
||||||
|
# Accounts
|
||||||
|
resource "google_cloud_run_v2_service_iam_member" "accounts_us_public" {
|
||||||
|
project = var.project_id
|
||||||
|
location = "us-central1"
|
||||||
|
name = google_cloud_run_v2_service.accounts_us.name
|
||||||
|
role = "roles/run.invoker"
|
||||||
|
member = "allUsers"
|
||||||
|
}
|
||||||
|
|
||||||
|
resource "google_cloud_run_v2_service_iam_member" "accounts_eu_public" {
|
||||||
|
project = var.project_id
|
||||||
|
location = "europe-west1"
|
||||||
|
name = google_cloud_run_v2_service.accounts_eu.name
|
||||||
|
role = "roles/run.invoker"
|
||||||
|
member = "allUsers"
|
||||||
|
}
|
||||||
|
|
||||||
|
resource "google_cloud_run_v2_service_iam_member" "accounts_apac_public" {
|
||||||
|
project = var.project_id
|
||||||
|
location = "asia-northeast1"
|
||||||
|
name = google_cloud_run_v2_service.accounts_apac.name
|
||||||
|
role = "roles/run.invoker"
|
||||||
|
member = "allUsers"
|
||||||
|
}
|
||||||
|
|
||||||
|
# API
|
||||||
|
resource "google_cloud_run_v2_service_iam_member" "api_us_public" {
|
||||||
|
project = var.project_id
|
||||||
|
location = "us-central1"
|
||||||
|
name = google_cloud_run_v2_service.api_us.name
|
||||||
|
role = "roles/run.invoker"
|
||||||
|
member = "allUsers"
|
||||||
|
}
|
||||||
|
|
||||||
|
resource "google_cloud_run_v2_service_iam_member" "api_eu_public" {
|
||||||
|
project = var.project_id
|
||||||
|
location = "europe-west1"
|
||||||
|
name = google_cloud_run_v2_service.api_eu.name
|
||||||
|
role = "roles/run.invoker"
|
||||||
|
member = "allUsers"
|
||||||
|
}
|
||||||
|
|
||||||
|
resource "google_cloud_run_v2_service_iam_member" "api_apac_public" {
|
||||||
|
project = var.project_id
|
||||||
|
location = "asia-northeast1"
|
||||||
|
name = google_cloud_run_v2_service.api_apac.name
|
||||||
|
role = "roles/run.invoker"
|
||||||
|
member = "allUsers"
|
||||||
}
|
}
|
||||||
|
|||||||
+19
-16
@@ -5,7 +5,19 @@ variable "project_id" {
|
|||||||
}
|
}
|
||||||
|
|
||||||
variable "image_tag" {
|
variable "image_tag" {
|
||||||
description = "Docker image tag to deploy"
|
description = "Docker image tag to deploy for the marketing site"
|
||||||
|
type = string
|
||||||
|
default = "latest"
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "accounts_image_tag" {
|
||||||
|
description = "Docker image tag to deploy for the accounts service"
|
||||||
|
type = string
|
||||||
|
default = "latest"
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "api_image_tag" {
|
||||||
|
description = "Docker image tag to deploy for the REST API (neuron-rest)"
|
||||||
type = string
|
type = string
|
||||||
default = "latest"
|
default = "latest"
|
||||||
}
|
}
|
||||||
@@ -20,23 +32,14 @@ variable "cloudflare_zone_id_neurontechnologies" {
|
|||||||
|
|
||||||
locals {
|
locals {
|
||||||
project_id = var.project_id
|
project_id = var.project_id
|
||||||
image = "us-central1-docker.pkg.dev/${var.project_id}/neuron-marketing/marketing:${var.image_tag}"
|
|
||||||
|
|
||||||
# Production secrets from GCP Secret Manager
|
# Image refs per service
|
||||||
secrets = [
|
marketing_image = "us-central1-docker.pkg.dev/${var.project_id}/neuron-marketing/marketing:${var.image_tag}"
|
||||||
"stripe-secret-key",
|
accounts_image = "us-central1-docker.pkg.dev/${var.project_id}/neuron-accounts/accounts:${var.accounts_image_tag}"
|
||||||
"stripe-webhook-secret",
|
api_image = "us-central1-docker.pkg.dev/${var.project_id}/neuron-api/api:${var.api_image_tag}"
|
||||||
"stripe-price-professional",
|
|
||||||
"stripe-price-founding",
|
|
||||||
]
|
|
||||||
|
|
||||||
# Env var names for each secret (uppercase, hyphen → underscore)
|
# Keep backward-compat alias (used in cloud-run.tf for marketing service)
|
||||||
secret_env_map = {
|
image = local.marketing_image
|
||||||
"stripe-secret-key" = "STRIPE_SECRET_KEY"
|
|
||||||
"stripe-webhook-secret" = "STRIPE_WEBHOOK_SECRET"
|
|
||||||
"stripe-price-professional" = "STRIPE_PRICE_PROFESSIONAL"
|
|
||||||
"stripe-price-founding" = "STRIPE_PRICE_FOUNDING"
|
|
||||||
}
|
|
||||||
|
|
||||||
# Static publishable key (not sensitive — lives in source)
|
# Static publishable key (not sensitive — lives in source)
|
||||||
stripe_publishable_key = "pk_live_51TPoHnJg9Fv1D3AUPMXnYyOJIVhn1FyH56zCMNnATo9tR7pO9lNDbXncp6VeDxm38qSdBHZPfqEBipUh3GZsWNyd00jvIO97E6"
|
stripe_publishable_key = "pk_live_51TPoHnJg9Fv1D3AUPMXnYyOJIVhn1FyH56zCMNnATo9tR7pO9lNDbXncp6VeDxm38qSdBHZPfqEBipUh3GZsWNyd00jvIO97E6"
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ spec:
|
|||||||
source:
|
source:
|
||||||
repoURL: https://grafana.github.io/helm-charts
|
repoURL: https://grafana.github.io/helm-charts
|
||||||
chart: alloy
|
chart: alloy
|
||||||
targetRevision: "*"
|
targetRevision: ">=0.9.0, <1.0.0"
|
||||||
helm:
|
helm:
|
||||||
values: |
|
values: |
|
||||||
controller:
|
controller:
|
||||||
|
|||||||
@@ -1,3 +1,7 @@
|
|||||||
|
---
|
||||||
|
# Gitea CI runner — general-purpose (legion)
|
||||||
|
# Docker socket REMOVED. Builds go through buildkitd TCP endpoint.
|
||||||
|
# Set DOCKER_HOST=tcp://buildkitd.ci.svc.cluster.local:1234 in build steps.
|
||||||
apiVersion: apps/v1
|
apiVersion: apps/v1
|
||||||
kind: Deployment
|
kind: Deployment
|
||||||
metadata:
|
metadata:
|
||||||
@@ -15,8 +19,10 @@ spec:
|
|||||||
labels:
|
labels:
|
||||||
app: gitea-runner
|
app: gitea-runner
|
||||||
annotations:
|
annotations:
|
||||||
config-version: "2026-04-23-nodeport-url"
|
config-version: "2026-04-25-buildkit-no-sock"
|
||||||
spec:
|
spec:
|
||||||
|
securityContext:
|
||||||
|
runAsNonRoot: false # act_runner needs root for container management
|
||||||
initContainers:
|
initContainers:
|
||||||
- name: register
|
- name: register
|
||||||
image: registry.neuralplatform.ai/ci-base:latest
|
image: registry.neuralplatform.ai/ci-base:latest
|
||||||
@@ -36,10 +42,9 @@ spec:
|
|||||||
timeout: 3h
|
timeout: 3h
|
||||||
container:
|
container:
|
||||||
network: host
|
network: host
|
||||||
docker_host: "unix:///var/run/docker.sock"
|
docker_host: "tcp://buildkitd.ci.svc.cluster.local:1234"
|
||||||
force_pull: false
|
force_pull: false
|
||||||
valid_volumes:
|
valid_volumes: []
|
||||||
- /var/run/docker.sock
|
|
||||||
default_image: "registry.neuralplatform.ai/ci-base:latest"
|
default_image: "registry.neuralplatform.ai/ci-base:latest"
|
||||||
extra_hosts:
|
extra_hosts:
|
||||||
- "gitea.git.svc.cluster.local:10.43.1.53"
|
- "gitea.git.svc.cluster.local:10.43.1.53"
|
||||||
@@ -55,14 +60,16 @@ spec:
|
|||||||
image: registry.neuralplatform.ai/ci-base:latest
|
image: registry.neuralplatform.ai/ci-base:latest
|
||||||
workingDir: /data
|
workingDir: /data
|
||||||
command: ["act_runner", "daemon", "--config", "/data/config.yaml"]
|
command: ["act_runner", "daemon", "--config", "/data/config.yaml"]
|
||||||
|
env:
|
||||||
|
- name: DOCKER_HOST
|
||||||
|
value: "tcp://buildkitd.ci.svc.cluster.local:1234"
|
||||||
envFrom:
|
envFrom:
|
||||||
- secretRef:
|
- secretRef:
|
||||||
name: gitea-runner-secret
|
name: gitea-runner-secret
|
||||||
volumeMounts:
|
volumeMounts:
|
||||||
- name: data
|
- name: data
|
||||||
mountPath: /data
|
mountPath: /data
|
||||||
- name: docker-sock
|
# docker-sock volume intentionally removed — use buildkitd TCP instead
|
||||||
mountPath: /var/run/docker.sock
|
|
||||||
resources:
|
resources:
|
||||||
requests:
|
requests:
|
||||||
memory: 512Mi
|
memory: 512Mi
|
||||||
@@ -73,11 +80,9 @@ spec:
|
|||||||
volumes:
|
volumes:
|
||||||
- name: data
|
- name: data
|
||||||
emptyDir: {}
|
emptyDir: {}
|
||||||
- name: docker-sock
|
# docker-sock hostPath intentionally removed
|
||||||
hostPath:
|
|
||||||
path: /var/run/docker.sock
|
|
||||||
type: Socket
|
|
||||||
---
|
---
|
||||||
|
# Neuron Technologies CI runner
|
||||||
apiVersion: apps/v1
|
apiVersion: apps/v1
|
||||||
kind: Deployment
|
kind: Deployment
|
||||||
metadata:
|
metadata:
|
||||||
@@ -95,7 +100,7 @@ spec:
|
|||||||
labels:
|
labels:
|
||||||
app: neuron-technologies-runner
|
app: neuron-technologies-runner
|
||||||
annotations:
|
annotations:
|
||||||
config-version: "2026-04-24-initial"
|
config-version: "2026-04-25-buildkit-no-sock"
|
||||||
spec:
|
spec:
|
||||||
initContainers:
|
initContainers:
|
||||||
- name: register
|
- name: register
|
||||||
@@ -116,10 +121,9 @@ spec:
|
|||||||
timeout: 3h
|
timeout: 3h
|
||||||
container:
|
container:
|
||||||
network: host
|
network: host
|
||||||
docker_host: "unix:///var/run/docker.sock"
|
docker_host: "tcp://buildkitd.ci.svc.cluster.local:1234"
|
||||||
force_pull: false
|
force_pull: false
|
||||||
valid_volumes:
|
valid_volumes: []
|
||||||
- /var/run/docker.sock
|
|
||||||
default_image: "registry.neuralplatform.ai/ci-base:latest"
|
default_image: "registry.neuralplatform.ai/ci-base:latest"
|
||||||
extra_hosts:
|
extra_hosts:
|
||||||
- "gitea.git.svc.cluster.local:10.43.1.53"
|
- "gitea.git.svc.cluster.local:10.43.1.53"
|
||||||
@@ -135,14 +139,15 @@ spec:
|
|||||||
image: registry.neuralplatform.ai/ci-base:latest
|
image: registry.neuralplatform.ai/ci-base:latest
|
||||||
workingDir: /data
|
workingDir: /data
|
||||||
command: ["act_runner", "daemon", "--config", "/data/config.yaml"]
|
command: ["act_runner", "daemon", "--config", "/data/config.yaml"]
|
||||||
|
env:
|
||||||
|
- name: DOCKER_HOST
|
||||||
|
value: "tcp://buildkitd.ci.svc.cluster.local:1234"
|
||||||
envFrom:
|
envFrom:
|
||||||
- secretRef:
|
- secretRef:
|
||||||
name: neuron-technologies-runner-secret
|
name: neuron-technologies-runner-secret
|
||||||
volumeMounts:
|
volumeMounts:
|
||||||
- name: data
|
- name: data
|
||||||
mountPath: /data
|
mountPath: /data
|
||||||
- name: docker-sock
|
|
||||||
mountPath: /var/run/docker.sock
|
|
||||||
resources:
|
resources:
|
||||||
requests:
|
requests:
|
||||||
memory: 512Mi
|
memory: 512Mi
|
||||||
@@ -153,7 +158,3 @@ spec:
|
|||||||
volumes:
|
volumes:
|
||||||
- name: data
|
- name: data
|
||||||
emptyDir: {}
|
emptyDir: {}
|
||||||
- name: docker-sock
|
|
||||||
hostPath:
|
|
||||||
path: /var/run/docker.sock
|
|
||||||
type: Socket
|
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ spec:
|
|||||||
source:
|
source:
|
||||||
repoURL: https://prometheus-community.github.io/helm-charts
|
repoURL: https://prometheus-community.github.io/helm-charts
|
||||||
chart: kube-prometheus-stack
|
chart: kube-prometheus-stack
|
||||||
targetRevision: "*"
|
targetRevision: ">=60.0.0, <70.0.0"
|
||||||
helm:
|
helm:
|
||||||
values: |
|
values: |
|
||||||
grafana:
|
grafana:
|
||||||
@@ -37,6 +37,13 @@ spec:
|
|||||||
cert-manager.io/cluster-issuer: letsencrypt-prod
|
cert-manager.io/cluster-issuer: letsencrypt-prod
|
||||||
prometheus:
|
prometheus:
|
||||||
prometheusSpec:
|
prometheusSpec:
|
||||||
|
# Discover ServiceMonitors and PrometheusRules from ALL namespaces.
|
||||||
|
# Without this, Prometheus only sees resources in the monitoring namespace.
|
||||||
|
serviceMonitorSelectorNilUsesHelmValues: false
|
||||||
|
serviceMonitorNamespaceSelector: {}
|
||||||
|
serviceMonitorSelector: {}
|
||||||
|
ruleNamespaceSelector: {}
|
||||||
|
podMonitorNamespaceSelector: {}
|
||||||
storageSpec:
|
storageSpec:
|
||||||
volumeClaimTemplate:
|
volumeClaimTemplate:
|
||||||
spec:
|
spec:
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ spec:
|
|||||||
source:
|
source:
|
||||||
repoURL: https://grafana.github.io/helm-charts
|
repoURL: https://grafana.github.io/helm-charts
|
||||||
chart: loki
|
chart: loki
|
||||||
targetRevision: "*"
|
targetRevision: ">=6.0.0, <7.0.0"
|
||||||
helm:
|
helm:
|
||||||
values: |
|
values: |
|
||||||
loki:
|
loki:
|
||||||
|
|||||||
@@ -0,0 +1,89 @@
|
|||||||
|
---
|
||||||
|
# ArgoCD AppProject — neuron-prod
|
||||||
|
# Restricts which repos can deploy to which namespaces.
|
||||||
|
# Prevents misconfigured apps from deploying cluster-admin RBAC or
|
||||||
|
# accidentally targeting personal/hobby namespaces.
|
||||||
|
apiVersion: argoproj.io/v1alpha1
|
||||||
|
kind: AppProject
|
||||||
|
metadata:
|
||||||
|
name: neuron-prod
|
||||||
|
namespace: argocd
|
||||||
|
finalizers:
|
||||||
|
- resources-finalizer.argocd.argoproj.io
|
||||||
|
spec:
|
||||||
|
description: "Neuron Technologies production services"
|
||||||
|
|
||||||
|
# Only allow deployments sourced from the infrastructure repo.
|
||||||
|
sourceRepos:
|
||||||
|
- "http://10.43.1.53:3000/will/infrastructure.git"
|
||||||
|
- "https://code.forgejo.org"
|
||||||
|
- "registry.neuralplatform.ai"
|
||||||
|
|
||||||
|
# Only allow deploying to neuron-prod and platform namespaces.
|
||||||
|
destinations:
|
||||||
|
- server: https://kubernetes.default.svc
|
||||||
|
namespace: neuron-prod
|
||||||
|
- server: https://kubernetes.default.svc
|
||||||
|
namespace: platform
|
||||||
|
|
||||||
|
# Cluster-scoped resources this project may NOT create.
|
||||||
|
clusterResourceBlacklist:
|
||||||
|
- group: rbac.authorization.k8s.io
|
||||||
|
kind: ClusterRole
|
||||||
|
- group: rbac.authorization.k8s.io
|
||||||
|
kind: ClusterRoleBinding
|
||||||
|
- group: ""
|
||||||
|
kind: Namespace
|
||||||
|
|
||||||
|
# Namespace-scoped resources this project may create.
|
||||||
|
namespaceResourceWhitelist:
|
||||||
|
- group: "apps"
|
||||||
|
kind: Deployment
|
||||||
|
- group: "apps"
|
||||||
|
kind: ReplicaSet
|
||||||
|
- group: "batch"
|
||||||
|
kind: CronJob
|
||||||
|
- group: "batch"
|
||||||
|
kind: Job
|
||||||
|
- group: ""
|
||||||
|
kind: Service
|
||||||
|
- group: ""
|
||||||
|
kind: ConfigMap
|
||||||
|
- group: ""
|
||||||
|
kind: Secret
|
||||||
|
- group: ""
|
||||||
|
kind: PersistentVolumeClaim
|
||||||
|
- group: ""
|
||||||
|
kind: ServiceAccount
|
||||||
|
- group: "networking.k8s.io"
|
||||||
|
kind: Ingress
|
||||||
|
- group: "networking.k8s.io"
|
||||||
|
kind: NetworkPolicy
|
||||||
|
- group: "policy"
|
||||||
|
kind: PodDisruptionBudget
|
||||||
|
- group: "autoscaling"
|
||||||
|
kind: HorizontalPodAutoscaler
|
||||||
|
- group: "traefik.io"
|
||||||
|
kind: IngressRoute
|
||||||
|
- group: "external-secrets.io"
|
||||||
|
kind: ExternalSecret
|
||||||
|
- group: "monitoring.coreos.com"
|
||||||
|
kind: ServiceMonitor
|
||||||
|
- group: "monitoring.coreos.com"
|
||||||
|
kind: PrometheusRule
|
||||||
|
|
||||||
|
# Sync windows: only allow automated syncs during low-traffic hours.
|
||||||
|
# Manual syncs allowed any time.
|
||||||
|
syncWindows:
|
||||||
|
- kind: allow
|
||||||
|
schedule: "0 2-6 * * *" # 2am-6am UTC (off-peak)
|
||||||
|
duration: 4h
|
||||||
|
applications:
|
||||||
|
- "*"
|
||||||
|
manualSync: true
|
||||||
|
- kind: allow
|
||||||
|
schedule: "* * * * *"
|
||||||
|
duration: "1m"
|
||||||
|
manualSync: true # Always allow manual deploys
|
||||||
|
applications:
|
||||||
|
- "*"
|
||||||
@@ -9,7 +9,7 @@ spec:
|
|||||||
# Helm chart from Docker Hub OCI registry
|
# Helm chart from Docker Hub OCI registry
|
||||||
- repoURL: registry-1.docker.io/bitnamicharts
|
- repoURL: registry-1.docker.io/bitnamicharts
|
||||||
chart: postgresql
|
chart: postgresql
|
||||||
targetRevision: "*"
|
targetRevision: ">=16.0.0, <17.0.0"
|
||||||
helm:
|
helm:
|
||||||
values: |
|
values: |
|
||||||
auth:
|
auth:
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ spec:
|
|||||||
source:
|
source:
|
||||||
repoURL: registry-1.docker.io/bitnamicharts
|
repoURL: registry-1.docker.io/bitnamicharts
|
||||||
chart: redis
|
chart: redis
|
||||||
targetRevision: "*"
|
targetRevision: ">=20.0.0, <21.0.0"
|
||||||
helm:
|
helm:
|
||||||
values: |
|
values: |
|
||||||
auth:
|
auth:
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ spec:
|
|||||||
source:
|
source:
|
||||||
repoURL: https://grafana.github.io/helm-charts
|
repoURL: https://grafana.github.io/helm-charts
|
||||||
chart: tempo
|
chart: tempo
|
||||||
targetRevision: "*"
|
targetRevision: ">=1.0.0, <2.0.0"
|
||||||
helm:
|
helm:
|
||||||
values: |
|
values: |
|
||||||
tempo:
|
tempo:
|
||||||
|
|||||||
@@ -43,8 +43,10 @@ spec:
|
|||||||
limits:
|
limits:
|
||||||
memory: 256Mi
|
memory: 256Mi
|
||||||
containers:
|
containers:
|
||||||
|
# Pin to explicit version — never use latest for backup tooling.
|
||||||
|
# Bump this intentionally after testing; do not let it float.
|
||||||
- name: backup
|
- name: backup
|
||||||
image: restic/restic:latest
|
image: restic/restic:0.17.3
|
||||||
command:
|
command:
|
||||||
- /bin/sh
|
- /bin/sh
|
||||||
- -c
|
- -c
|
||||||
@@ -93,3 +95,77 @@ spec:
|
|||||||
readOnly: true
|
readOnly: true
|
||||||
- name: dump
|
- name: dump
|
||||||
emptyDir: {}
|
emptyDir: {}
|
||||||
|
---
|
||||||
|
# Weekly restore-verify job — proves the backup is actually restorable.
|
||||||
|
# Runs Sunday 4am. Restores latest snapshot to /tmp, validates the SQL dump.
|
||||||
|
# Alerts via Alertmanager if it fails (BackupJobFailed PrometheusRule watches git namespace).
|
||||||
|
apiVersion: batch/v1
|
||||||
|
kind: CronJob
|
||||||
|
metadata:
|
||||||
|
name: backup-verify
|
||||||
|
namespace: git
|
||||||
|
spec:
|
||||||
|
schedule: "0 4 * * 0"
|
||||||
|
concurrencyPolicy: Forbid
|
||||||
|
failedJobsHistoryLimit: 2
|
||||||
|
successfulJobsHistoryLimit: 1
|
||||||
|
jobTemplate:
|
||||||
|
spec:
|
||||||
|
backoffLimit: 0
|
||||||
|
activeDeadlineSeconds: 1800 # 30 min max
|
||||||
|
template:
|
||||||
|
spec:
|
||||||
|
restartPolicy: Never
|
||||||
|
containers:
|
||||||
|
- name: verify
|
||||||
|
image: restic/restic:0.17.3
|
||||||
|
command:
|
||||||
|
- /bin/sh
|
||||||
|
- -c
|
||||||
|
- |
|
||||||
|
set -e
|
||||||
|
|
||||||
|
echo "=== Backup Restore Verification ==="
|
||||||
|
echo "Listing latest snapshots..."
|
||||||
|
restic snapshots --latest 3
|
||||||
|
|
||||||
|
echo "Restoring latest snapshot to /tmp/restore-verify..."
|
||||||
|
mkdir -p /tmp/restore-verify
|
||||||
|
restic restore latest \
|
||||||
|
--target /tmp/restore-verify \
|
||||||
|
--include /all-databases.sql
|
||||||
|
|
||||||
|
SQL_FILE="/tmp/restore-verify/all-databases.sql"
|
||||||
|
if [ ! -f "$SQL_FILE" ]; then
|
||||||
|
echo "ERROR: SQL dump not found in restored snapshot"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
SIZE=$(du -sh "$SQL_FILE" | cut -f1)
|
||||||
|
echo "SQL dump restored: $SIZE"
|
||||||
|
|
||||||
|
# Basic structural validation
|
||||||
|
if ! grep -q "PostgreSQL database cluster dump" "$SQL_FILE"; then
|
||||||
|
echo "ERROR: SQL dump does not look like a valid pg_dumpall output"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
TABLE_COUNT=$(grep -c "^CREATE TABLE" "$SQL_FILE" || true)
|
||||||
|
echo "Tables found in dump: $TABLE_COUNT"
|
||||||
|
|
||||||
|
if [ "$TABLE_COUNT" -lt 5 ]; then
|
||||||
|
echo "ERROR: Too few tables in dump (expected >= 5, got $TABLE_COUNT)"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "=== Verification PASSED: backup is restorable ==="
|
||||||
|
rm -rf /tmp/restore-verify
|
||||||
|
envFrom:
|
||||||
|
- secretRef:
|
||||||
|
name: backup-credentials
|
||||||
|
resources:
|
||||||
|
requests:
|
||||||
|
memory: 256Mi
|
||||||
|
cpu: 100m
|
||||||
|
limits:
|
||||||
|
memory: 512Mi
|
||||||
|
|||||||
@@ -1,11 +1,47 @@
|
|||||||
---
|
---
|
||||||
# ClusterSecretStore — connects ESO to Vault using the root token Secret.
|
# ClusterSecretStore — connects ESO to Vault
|
||||||
# The vault-token Secret (name: vault-token, key: token) must be created
|
|
||||||
# via kubectl as a one-time bootstrap step before this store can sync:
|
|
||||||
#
|
#
|
||||||
# kubectl create secret generic vault-token \
|
# CURRENT STATE: token auth (root token). This works but the root token
|
||||||
# --namespace external-secrets \
|
# has no expiry and full Vault access — it's a single credential that
|
||||||
# --from-literal=token=$(cat ~/Secrets/tokens/vault-root-token)
|
# compromises everything if leaked.
|
||||||
|
#
|
||||||
|
# MIGRATION TO APPROLE (do this before GCP migration):
|
||||||
|
# ─────────────────────────────────────────────────────
|
||||||
|
# 1. Enable AppRole on Vault (if not already):
|
||||||
|
# vault auth enable approle
|
||||||
|
#
|
||||||
|
# 2. Create a policy scoped to only the paths ESO reads:
|
||||||
|
# vault policy write eso-read - <<EOF
|
||||||
|
# path "secret/data/neuron-technologies/*" { capabilities = ["read"] }
|
||||||
|
# path "secret/data/r2" { capabilities = ["read"] }
|
||||||
|
# path "secret/data/slack" { capabilities = ["read"] }
|
||||||
|
# path "secret/data/legion-db" { capabilities = ["read"] }
|
||||||
|
# EOF
|
||||||
|
#
|
||||||
|
# 3. Create the AppRole:
|
||||||
|
# vault write auth/approle/role/eso \
|
||||||
|
# token_policies="eso-read" \
|
||||||
|
# token_ttl=1h \
|
||||||
|
# token_max_ttl=4h \
|
||||||
|
# secret_id_ttl=0 # no expiry for the secretId
|
||||||
|
#
|
||||||
|
# 4. Get the roleId and secretId:
|
||||||
|
# vault read auth/approle/role/eso/role-id → ROLE_ID
|
||||||
|
# vault write -f auth/approle/role/eso/secret-id → SECRET_ID
|
||||||
|
#
|
||||||
|
# 5. Create the k8s secret (one-time bootstrap, same as the root token was):
|
||||||
|
# kubectl create secret generic vault-approle \
|
||||||
|
# --namespace external-secrets \
|
||||||
|
# --from-literal=roleId=<ROLE_ID> \
|
||||||
|
# --from-literal=secretId=<SECRET_ID>
|
||||||
|
#
|
||||||
|
# 6. Flip this file to the appRole stanza below, commit, push.
|
||||||
|
# ESO will hot-reload when the manifest changes.
|
||||||
|
#
|
||||||
|
# 7. Delete the old vault-token secret:
|
||||||
|
# kubectl delete secret vault-token -n external-secrets
|
||||||
|
# ─────────────────────────────────────────────────────
|
||||||
|
|
||||||
apiVersion: external-secrets.io/v1beta1
|
apiVersion: external-secrets.io/v1beta1
|
||||||
kind: ClusterSecretStore
|
kind: ClusterSecretStore
|
||||||
metadata:
|
metadata:
|
||||||
@@ -17,7 +53,17 @@ spec:
|
|||||||
path: "secret"
|
path: "secret"
|
||||||
version: "v2"
|
version: "v2"
|
||||||
auth:
|
auth:
|
||||||
|
# ── Current: root token auth ──────────────────────────────────────────
|
||||||
|
# TODO: replace with appRole stanza once AppRole is bootstrapped (see above).
|
||||||
tokenSecretRef:
|
tokenSecretRef:
|
||||||
name: vault-token
|
name: vault-token
|
||||||
namespace: external-secrets
|
namespace: external-secrets
|
||||||
key: token
|
key: token
|
||||||
|
# ── Target: AppRole auth (uncomment after bootstrap) ──────────────────
|
||||||
|
# appRole:
|
||||||
|
# path: "approle"
|
||||||
|
# roleId: "eso"
|
||||||
|
# secretRef:
|
||||||
|
# name: vault-approle
|
||||||
|
# namespace: external-secrets
|
||||||
|
# key: secretId
|
||||||
|
|||||||
@@ -0,0 +1,71 @@
|
|||||||
|
---
|
||||||
|
# BuildKit daemon — rootless image builder for CI runners.
|
||||||
|
# Replaces the /var/run/docker.sock hostPath mount that gave containers
|
||||||
|
# full root access to the Legion host node.
|
||||||
|
#
|
||||||
|
# Runners connect via TCP: DOCKER_HOST=tcp://buildkitd.ci.svc.cluster.local:1234
|
||||||
|
apiVersion: apps/v1
|
||||||
|
kind: Deployment
|
||||||
|
metadata:
|
||||||
|
name: buildkitd
|
||||||
|
namespace: ci
|
||||||
|
labels:
|
||||||
|
app: buildkitd
|
||||||
|
spec:
|
||||||
|
replicas: 1
|
||||||
|
selector:
|
||||||
|
matchLabels:
|
||||||
|
app: buildkitd
|
||||||
|
template:
|
||||||
|
metadata:
|
||||||
|
labels:
|
||||||
|
app: buildkitd
|
||||||
|
spec:
|
||||||
|
containers:
|
||||||
|
- name: buildkitd
|
||||||
|
image: moby/buildkit:v0.19.0-rootless
|
||||||
|
args:
|
||||||
|
- --oci-worker-no-process-sandbox
|
||||||
|
- --addr
|
||||||
|
- tcp://0.0.0.0:1234
|
||||||
|
- --addr
|
||||||
|
- unix:///run/buildkit/buildkitd.sock
|
||||||
|
ports:
|
||||||
|
- name: tcp
|
||||||
|
containerPort: 1234
|
||||||
|
securityContext:
|
||||||
|
seccompProfile:
|
||||||
|
type: Unconfined # BuildKit rootless requires this
|
||||||
|
runAsUser: 1000
|
||||||
|
runAsGroup: 1000
|
||||||
|
readinessProbe:
|
||||||
|
exec:
|
||||||
|
command: ["buildctl", "debug", "workers"]
|
||||||
|
initialDelaySeconds: 5
|
||||||
|
periodSeconds: 10
|
||||||
|
resources:
|
||||||
|
requests:
|
||||||
|
cpu: 200m
|
||||||
|
memory: 256Mi
|
||||||
|
limits:
|
||||||
|
cpu: "4"
|
||||||
|
memory: 4Gi
|
||||||
|
volumeMounts:
|
||||||
|
- name: buildkit-storage
|
||||||
|
mountPath: /home/user/.local/share/buildkit
|
||||||
|
volumes:
|
||||||
|
- name: buildkit-storage
|
||||||
|
emptyDir: {}
|
||||||
|
---
|
||||||
|
apiVersion: v1
|
||||||
|
kind: Service
|
||||||
|
metadata:
|
||||||
|
name: buildkitd
|
||||||
|
namespace: ci
|
||||||
|
spec:
|
||||||
|
selector:
|
||||||
|
app: buildkitd
|
||||||
|
ports:
|
||||||
|
- name: tcp
|
||||||
|
port: 1234
|
||||||
|
targetPort: 1234
|
||||||
@@ -0,0 +1,153 @@
|
|||||||
|
---
|
||||||
|
# PrometheusRules — SLO and operational alerts for neuron-prod
|
||||||
|
# These fire into Alertmanager which routes to #infrastructure-alerts on Slack.
|
||||||
|
apiVersion: monitoring.coreos.com/v1
|
||||||
|
kind: PrometheusRule
|
||||||
|
metadata:
|
||||||
|
name: neuron-slo
|
||||||
|
namespace: monitoring
|
||||||
|
labels:
|
||||||
|
release: kube-prometheus-stack
|
||||||
|
spec:
|
||||||
|
groups:
|
||||||
|
|
||||||
|
# ── Neuron API SLOs ──────────────────────────────────────��───────────────
|
||||||
|
- name: neuron.slo
|
||||||
|
interval: 60s
|
||||||
|
rules:
|
||||||
|
- alert: NeuronMCPHighErrorRate
|
||||||
|
expr: |
|
||||||
|
(
|
||||||
|
sum(rate(http_server_requests_seconds_count{namespace="neuron-prod",app="neuron-mcp",status=~"5.."}[5m]))
|
||||||
|
/
|
||||||
|
sum(rate(http_server_requests_seconds_count{namespace="neuron-prod",app="neuron-mcp"}[5m]))
|
||||||
|
) > 0.01
|
||||||
|
for: 2m
|
||||||
|
labels:
|
||||||
|
severity: critical
|
||||||
|
namespace: neuron-prod
|
||||||
|
annotations:
|
||||||
|
summary: "neuron-mcp 5xx error rate > 1%"
|
||||||
|
description: "Error rate is {{ $value | humanizePercentage }} over the last 5 minutes."
|
||||||
|
|
||||||
|
- alert: NeuronRestHighErrorRate
|
||||||
|
expr: |
|
||||||
|
(
|
||||||
|
sum(rate(http_server_requests_seconds_count{namespace="neuron-prod",app="neuron-rest",status=~"5.."}[5m]))
|
||||||
|
/
|
||||||
|
sum(rate(http_server_requests_seconds_count{namespace="neuron-prod",app="neuron-rest"}[5m]))
|
||||||
|
) > 0.01
|
||||||
|
for: 2m
|
||||||
|
labels:
|
||||||
|
severity: critical
|
||||||
|
namespace: neuron-prod
|
||||||
|
annotations:
|
||||||
|
summary: "neuron-rest 5xx error rate > 1%"
|
||||||
|
description: "Error rate is {{ $value | humanizePercentage }} over the last 5 minutes."
|
||||||
|
|
||||||
|
- alert: NeuronMCPHighLatency
|
||||||
|
expr: |
|
||||||
|
histogram_quantile(0.99,
|
||||||
|
sum(rate(http_server_requests_seconds_bucket{namespace="neuron-prod",app="neuron-mcp"}[10m]))
|
||||||
|
by (le)
|
||||||
|
) > 2.0
|
||||||
|
for: 5m
|
||||||
|
labels:
|
||||||
|
severity: warning
|
||||||
|
namespace: neuron-prod
|
||||||
|
annotations:
|
||||||
|
summary: "neuron-mcp p99 latency > 2s"
|
||||||
|
description: "p99 latency is {{ $value | humanizeDuration }} over the last 10 minutes."
|
||||||
|
|
||||||
|
- alert: NeuronRestHighLatency
|
||||||
|
expr: |
|
||||||
|
histogram_quantile(0.99,
|
||||||
|
sum(rate(http_server_requests_seconds_bucket{namespace="neuron-prod",app="neuron-rest"}[10m]))
|
||||||
|
by (le)
|
||||||
|
) > 2.0
|
||||||
|
for: 5m
|
||||||
|
labels:
|
||||||
|
severity: warning
|
||||||
|
namespace: neuron-prod
|
||||||
|
annotations:
|
||||||
|
summary: "neuron-rest p99 latency > 2s"
|
||||||
|
description: "p99 latency is {{ $value | humanizeDuration }} over the last 10 minutes."
|
||||||
|
|
||||||
|
# ── Pod health ───────────────────────────────────────────────────────────
|
||||||
|
- name: neuron.pods
|
||||||
|
rules:
|
||||||
|
- alert: NeuronPodCrashLooping
|
||||||
|
expr: |
|
||||||
|
rate(kube_pod_container_status_restarts_total{namespace="neuron-prod"}[15m]) * 60 * 15 > 3
|
||||||
|
for: 5m
|
||||||
|
labels:
|
||||||
|
severity: critical
|
||||||
|
namespace: neuron-prod
|
||||||
|
annotations:
|
||||||
|
summary: "Pod {{ $labels.pod }} is crash looping"
|
||||||
|
description: "Container {{ $labels.container }} restarted {{ $value | humanize }} times in 15 minutes."
|
||||||
|
|
||||||
|
- alert: NeuronPodNotReady
|
||||||
|
expr: |
|
||||||
|
sum by (pod) (kube_pod_status_ready{namespace="neuron-prod",condition="false"}) > 0
|
||||||
|
for: 5m
|
||||||
|
labels:
|
||||||
|
severity: warning
|
||||||
|
namespace: neuron-prod
|
||||||
|
annotations:
|
||||||
|
summary: "Pod {{ $labels.pod }} not ready for 5 minutes"
|
||||||
|
|
||||||
|
- alert: NeuronDeploymentReplicasMissing
|
||||||
|
expr: |
|
||||||
|
kube_deployment_status_replicas_available{namespace="neuron-prod"}
|
||||||
|
< kube_deployment_spec_replicas{namespace="neuron-prod"}
|
||||||
|
for: 3m
|
||||||
|
labels:
|
||||||
|
severity: warning
|
||||||
|
namespace: neuron-prod
|
||||||
|
annotations:
|
||||||
|
summary: "Deployment {{ $labels.deployment }} has fewer replicas than desired"
|
||||||
|
|
||||||
|
# ── Postgres ─────────────────────────���─────────────────────────────────��─
|
||||||
|
- name: postgres.health
|
||||||
|
rules:
|
||||||
|
- alert: PostgresDown
|
||||||
|
expr: pg_up{namespace="platform"} == 0
|
||||||
|
for: 2m
|
||||||
|
labels:
|
||||||
|
severity: critical
|
||||||
|
annotations:
|
||||||
|
summary: "Postgres is down"
|
||||||
|
|
||||||
|
- alert: PostgresHighConnections
|
||||||
|
expr: |
|
||||||
|
(pg_stat_database_numbackends{namespace="platform"} / pg_settings_max_connections{namespace="platform"})
|
||||||
|
> 0.80
|
||||||
|
for: 5m
|
||||||
|
labels:
|
||||||
|
severity: warning
|
||||||
|
annotations:
|
||||||
|
summary: "Postgres connection pool > 80%"
|
||||||
|
description: "Connection utilization is {{ $value | humanizePercentage }}."
|
||||||
|
|
||||||
|
# ── Backup health ────────────────────────────���───────────────────────────
|
||||||
|
- name: backup.health
|
||||||
|
rules:
|
||||||
|
- alert: BackupJobFailed
|
||||||
|
expr: |
|
||||||
|
kube_job_status_failed{namespace="git",job_name=~"gitea-backup.*"} > 0
|
||||||
|
for: 0m
|
||||||
|
labels:
|
||||||
|
severity: critical
|
||||||
|
annotations:
|
||||||
|
summary: "Gitea backup job failed"
|
||||||
|
description: "Check: kubectl logs -n git -l job-name=gitea-backup --previous"
|
||||||
|
|
||||||
|
- alert: BackupNotRunRecently
|
||||||
|
expr: |
|
||||||
|
(time() - kube_job_status_completion_time{namespace="git",job_name=~"gitea-backup.*"}) > 90000
|
||||||
|
for: 0m
|
||||||
|
labels:
|
||||||
|
severity: warning
|
||||||
|
annotations:
|
||||||
|
summary: "Last successful backup was more than 25 hours ago"
|
||||||
@@ -0,0 +1,64 @@
|
|||||||
|
---
|
||||||
|
# ServiceMonitors for neuron-prod — scraped by kube-prometheus-stack Prometheus
|
||||||
|
# Requires: kube-prometheus-stack prometheusSpec.serviceMonitorSelectorNilUsesHelmValues=false
|
||||||
|
# OR a serviceMonitorNamespaceSelector that includes neuron-prod.
|
||||||
|
# We set this in the Prometheus stack values (see apps/kube-prometheus-stack.yaml).
|
||||||
|
apiVersion: monitoring.coreos.com/v1
|
||||||
|
kind: ServiceMonitor
|
||||||
|
metadata:
|
||||||
|
name: neuron-mcp
|
||||||
|
namespace: monitoring
|
||||||
|
labels:
|
||||||
|
release: kube-prometheus-stack
|
||||||
|
spec:
|
||||||
|
namespaceSelector:
|
||||||
|
matchNames:
|
||||||
|
- neuron-prod
|
||||||
|
selector:
|
||||||
|
matchLabels:
|
||||||
|
app: neuron-mcp
|
||||||
|
endpoints:
|
||||||
|
- port: http
|
||||||
|
path: /actuator/prometheus
|
||||||
|
interval: 30s
|
||||||
|
scrapeTimeout: 10s
|
||||||
|
---
|
||||||
|
apiVersion: monitoring.coreos.com/v1
|
||||||
|
kind: ServiceMonitor
|
||||||
|
metadata:
|
||||||
|
name: neuron-rest
|
||||||
|
namespace: monitoring
|
||||||
|
labels:
|
||||||
|
release: kube-prometheus-stack
|
||||||
|
spec:
|
||||||
|
namespaceSelector:
|
||||||
|
matchNames:
|
||||||
|
- neuron-prod
|
||||||
|
selector:
|
||||||
|
matchLabels:
|
||||||
|
app: neuron-rest
|
||||||
|
endpoints:
|
||||||
|
- port: http
|
||||||
|
path: /actuator/prometheus
|
||||||
|
interval: 30s
|
||||||
|
scrapeTimeout: 10s
|
||||||
|
---
|
||||||
|
apiVersion: monitoring.coreos.com/v1
|
||||||
|
kind: ServiceMonitor
|
||||||
|
metadata:
|
||||||
|
name: neuron-marketing
|
||||||
|
namespace: monitoring
|
||||||
|
labels:
|
||||||
|
release: kube-prometheus-stack
|
||||||
|
spec:
|
||||||
|
namespaceSelector:
|
||||||
|
matchNames:
|
||||||
|
- neuron-prod
|
||||||
|
selector:
|
||||||
|
matchLabels:
|
||||||
|
app: neuron-marketing
|
||||||
|
endpoints:
|
||||||
|
- port: http
|
||||||
|
path: /api/metrics
|
||||||
|
interval: 60s
|
||||||
|
scrapeTimeout: 15s
|
||||||
@@ -17,6 +17,11 @@ spec:
|
|||||||
app: neuron-marketing
|
app: neuron-marketing
|
||||||
env: prod
|
env: prod
|
||||||
spec:
|
spec:
|
||||||
|
securityContext:
|
||||||
|
runAsNonRoot: true
|
||||||
|
runAsUser: 1001
|
||||||
|
seccompProfile:
|
||||||
|
type: RuntimeDefault
|
||||||
containers:
|
containers:
|
||||||
- name: neuron-marketing
|
- name: neuron-marketing
|
||||||
image: registry.neuralplatform.ai/neuron-technologies/marketing:1e94e8ae
|
image: registry.neuralplatform.ai/neuron-technologies/marketing:1e94e8ae
|
||||||
@@ -24,6 +29,12 @@ spec:
|
|||||||
ports:
|
ports:
|
||||||
- name: http
|
- name: http
|
||||||
containerPort: 3000
|
containerPort: 3000
|
||||||
|
securityContext:
|
||||||
|
allowPrivilegeEscalation: false
|
||||||
|
readOnlyRootFilesystem: false # Next.js writes .next/cache at runtime
|
||||||
|
runAsNonRoot: true
|
||||||
|
capabilities:
|
||||||
|
drop: ["ALL"]
|
||||||
env:
|
env:
|
||||||
- name: NODE_ENV
|
- name: NODE_ENV
|
||||||
value: production
|
value: production
|
||||||
|
|||||||
@@ -0,0 +1,98 @@
|
|||||||
|
---
|
||||||
|
# HorizontalPodAutoscalers for neuron-prod
|
||||||
|
# Single Legion node today — minReplicas=1 prevents unnecessary idle waste.
|
||||||
|
# When GCP GKE is live these minimums should go to 2 for HA.
|
||||||
|
apiVersion: autoscaling/v2
|
||||||
|
kind: HorizontalPodAutoscaler
|
||||||
|
metadata:
|
||||||
|
name: neuron-mcp-hpa
|
||||||
|
namespace: neuron-prod
|
||||||
|
spec:
|
||||||
|
scaleTargetRef:
|
||||||
|
apiVersion: apps/v1
|
||||||
|
kind: Deployment
|
||||||
|
name: neuron-mcp-blue
|
||||||
|
minReplicas: 1
|
||||||
|
maxReplicas: 6
|
||||||
|
metrics:
|
||||||
|
- type: Resource
|
||||||
|
resource:
|
||||||
|
name: cpu
|
||||||
|
target:
|
||||||
|
type: Utilization
|
||||||
|
averageUtilization: 65
|
||||||
|
- type: Resource
|
||||||
|
resource:
|
||||||
|
name: memory
|
||||||
|
target:
|
||||||
|
type: Utilization
|
||||||
|
averageUtilization: 80
|
||||||
|
behavior:
|
||||||
|
scaleDown:
|
||||||
|
stabilizationWindowSeconds: 300 # Don't thrash — wait 5m before scaling down
|
||||||
|
policies:
|
||||||
|
- type: Pods
|
||||||
|
value: 1
|
||||||
|
periodSeconds: 120
|
||||||
|
scaleUp:
|
||||||
|
stabilizationWindowSeconds: 30
|
||||||
|
policies:
|
||||||
|
- type: Pods
|
||||||
|
value: 2
|
||||||
|
periodSeconds: 60
|
||||||
|
---
|
||||||
|
apiVersion: autoscaling/v2
|
||||||
|
kind: HorizontalPodAutoscaler
|
||||||
|
metadata:
|
||||||
|
name: neuron-rest-hpa
|
||||||
|
namespace: neuron-prod
|
||||||
|
spec:
|
||||||
|
scaleTargetRef:
|
||||||
|
apiVersion: apps/v1
|
||||||
|
kind: Deployment
|
||||||
|
name: neuron-rest
|
||||||
|
minReplicas: 1
|
||||||
|
maxReplicas: 4
|
||||||
|
metrics:
|
||||||
|
- type: Resource
|
||||||
|
resource:
|
||||||
|
name: cpu
|
||||||
|
target:
|
||||||
|
type: Utilization
|
||||||
|
averageUtilization: 65
|
||||||
|
- type: Resource
|
||||||
|
resource:
|
||||||
|
name: memory
|
||||||
|
target:
|
||||||
|
type: Utilization
|
||||||
|
averageUtilization: 80
|
||||||
|
behavior:
|
||||||
|
scaleDown:
|
||||||
|
stabilizationWindowSeconds: 300
|
||||||
|
scaleUp:
|
||||||
|
stabilizationWindowSeconds: 30
|
||||||
|
---
|
||||||
|
apiVersion: autoscaling/v2
|
||||||
|
kind: HorizontalPodAutoscaler
|
||||||
|
metadata:
|
||||||
|
name: neuron-marketing-hpa
|
||||||
|
namespace: neuron-prod
|
||||||
|
spec:
|
||||||
|
scaleTargetRef:
|
||||||
|
apiVersion: apps/v1
|
||||||
|
kind: Deployment
|
||||||
|
name: neuron-marketing
|
||||||
|
minReplicas: 2
|
||||||
|
maxReplicas: 8
|
||||||
|
metrics:
|
||||||
|
- type: Resource
|
||||||
|
resource:
|
||||||
|
name: cpu
|
||||||
|
target:
|
||||||
|
type: Utilization
|
||||||
|
averageUtilization: 70
|
||||||
|
behavior:
|
||||||
|
scaleDown:
|
||||||
|
stabilizationWindowSeconds: 120
|
||||||
|
scaleUp:
|
||||||
|
stabilizationWindowSeconds: 15
|
||||||
@@ -15,3 +15,7 @@ resources:
|
|||||||
- backup-cronjob.yaml
|
- backup-cronjob.yaml
|
||||||
- runpod-inference.yaml
|
- runpod-inference.yaml
|
||||||
- runpod-lb-configmap.yaml
|
- runpod-lb-configmap.yaml
|
||||||
|
# Hardening additions
|
||||||
|
- hpa.yaml
|
||||||
|
- pdb.yaml
|
||||||
|
- network-policy.yaml
|
||||||
|
|||||||
@@ -25,6 +25,10 @@ spec:
|
|||||||
kubectl.kubernetes.io/restartedAt: "2026-04-25T08:40:00Z"
|
kubectl.kubernetes.io/restartedAt: "2026-04-25T08:40:00Z"
|
||||||
spec:
|
spec:
|
||||||
terminationGracePeriodSeconds: 30
|
terminationGracePeriodSeconds: 30
|
||||||
|
securityContext:
|
||||||
|
runAsNonRoot: true
|
||||||
|
seccompProfile:
|
||||||
|
type: RuntimeDefault
|
||||||
containers:
|
containers:
|
||||||
- name: neuron-mcp
|
- name: neuron-mcp
|
||||||
image: registry.neuralplatform.ai/neuron-technologies/neuron-mcp:v0.15.3
|
image: registry.neuralplatform.ai/neuron-technologies/neuron-mcp:v0.15.3
|
||||||
@@ -32,6 +36,12 @@ spec:
|
|||||||
ports:
|
ports:
|
||||||
- name: http
|
- name: http
|
||||||
containerPort: 8080
|
containerPort: 8080
|
||||||
|
securityContext:
|
||||||
|
allowPrivilegeEscalation: false
|
||||||
|
readOnlyRootFilesystem: false # JVM writes temp files; restrict paths via allowedPaths once profiled
|
||||||
|
runAsNonRoot: true
|
||||||
|
capabilities:
|
||||||
|
drop: ["ALL"]
|
||||||
env:
|
env:
|
||||||
- name: OTEL_EXPORTER_OTLP_ENDPOINT
|
- name: OTEL_EXPORTER_OTLP_ENDPOINT
|
||||||
value: "http://alloy-otlp.monitoring.svc.cluster.local:4318"
|
value: "http://alloy-otlp.monitoring.svc.cluster.local:4318"
|
||||||
@@ -57,18 +67,25 @@ spec:
|
|||||||
exec:
|
exec:
|
||||||
# Give Traefik time to drain in-flight connections before pod terminates
|
# Give Traefik time to drain in-flight connections before pod terminates
|
||||||
command: ["sh", "-c", "sleep 8"]
|
command: ["sh", "-c", "sleep 8"]
|
||||||
|
# startupProbe gives the JVM up to 150s to start before liveness takes over.
|
||||||
|
# Prevents false crash loops during slow cold starts.
|
||||||
|
startupProbe:
|
||||||
|
httpGet:
|
||||||
|
path: /actuator/health/liveness
|
||||||
|
port: http
|
||||||
|
failureThreshold: 30
|
||||||
|
periodSeconds: 5
|
||||||
livenessProbe:
|
livenessProbe:
|
||||||
httpGet:
|
httpGet:
|
||||||
path: /actuator/health/liveness
|
path: /actuator/health/liveness
|
||||||
port: http
|
port: http
|
||||||
initialDelaySeconds: 30
|
|
||||||
periodSeconds: 30
|
periodSeconds: 30
|
||||||
failureThreshold: 3
|
failureThreshold: 3
|
||||||
readinessProbe:
|
readinessProbe:
|
||||||
httpGet:
|
httpGet:
|
||||||
path: /actuator/health/readiness
|
path: /actuator/health/readiness
|
||||||
port: http
|
port: http
|
||||||
initialDelaySeconds: 10
|
initialDelaySeconds: 5
|
||||||
periodSeconds: 10
|
periodSeconds: 10
|
||||||
failureThreshold: 3
|
failureThreshold: 3
|
||||||
volumes:
|
volumes:
|
||||||
|
|||||||
@@ -0,0 +1,124 @@
|
|||||||
|
---
|
||||||
|
# NetworkPolicies for neuron-prod
|
||||||
|
# Default-deny all ingress and egress, then explicitly allow only what's needed.
|
||||||
|
# Enforced by k3s's built-in NetworkPolicy controller.
|
||||||
|
|
||||||
|
# ── Baseline: deny all traffic in namespace ───────────────────────────────────
|
||||||
|
apiVersion: networking.k8s.io/v1
|
||||||
|
kind: NetworkPolicy
|
||||||
|
metadata:
|
||||||
|
name: default-deny-all
|
||||||
|
namespace: neuron-prod
|
||||||
|
spec:
|
||||||
|
podSelector: {}
|
||||||
|
policyTypes:
|
||||||
|
- Ingress
|
||||||
|
- Egress
|
||||||
|
---
|
||||||
|
# ── neuron-mcp: accept traffic from Traefik (kube-system) only ───────────────
|
||||||
|
apiVersion: networking.k8s.io/v1
|
||||||
|
kind: NetworkPolicy
|
||||||
|
metadata:
|
||||||
|
name: allow-mcp-ingress
|
||||||
|
namespace: neuron-prod
|
||||||
|
spec:
|
||||||
|
podSelector:
|
||||||
|
matchLabels:
|
||||||
|
app: neuron-mcp
|
||||||
|
policyTypes:
|
||||||
|
- Ingress
|
||||||
|
ingress:
|
||||||
|
- from:
|
||||||
|
- namespaceSelector:
|
||||||
|
matchLabels:
|
||||||
|
kubernetes.io/metadata.name: kube-system
|
||||||
|
- namespaceSelector:
|
||||||
|
matchLabels:
|
||||||
|
kubernetes.io/metadata.name: neuron-prod
|
||||||
|
---
|
||||||
|
# ── neuron-rest: accept from mcp, kube-system (Traefik), monitoring (Alloy) ──
|
||||||
|
apiVersion: networking.k8s.io/v1
|
||||||
|
kind: NetworkPolicy
|
||||||
|
metadata:
|
||||||
|
name: allow-rest-ingress
|
||||||
|
namespace: neuron-prod
|
||||||
|
spec:
|
||||||
|
podSelector:
|
||||||
|
matchLabels:
|
||||||
|
app: neuron-rest
|
||||||
|
policyTypes:
|
||||||
|
- Ingress
|
||||||
|
ingress:
|
||||||
|
- from:
|
||||||
|
- namespaceSelector:
|
||||||
|
matchLabels:
|
||||||
|
kubernetes.io/metadata.name: kube-system
|
||||||
|
- namespaceSelector:
|
||||||
|
matchLabels:
|
||||||
|
kubernetes.io/metadata.name: neuron-prod
|
||||||
|
- namespaceSelector:
|
||||||
|
matchLabels:
|
||||||
|
kubernetes.io/metadata.name: monitoring
|
||||||
|
---
|
||||||
|
# ── neuron-marketing: accept from Traefik only ───────────────────────────────
|
||||||
|
apiVersion: networking.k8s.io/v1
|
||||||
|
kind: NetworkPolicy
|
||||||
|
metadata:
|
||||||
|
name: allow-marketing-ingress
|
||||||
|
namespace: neuron-prod
|
||||||
|
spec:
|
||||||
|
podSelector:
|
||||||
|
matchLabels:
|
||||||
|
app: neuron-marketing
|
||||||
|
policyTypes:
|
||||||
|
- Ingress
|
||||||
|
ingress:
|
||||||
|
- from:
|
||||||
|
- namespaceSelector:
|
||||||
|
matchLabels:
|
||||||
|
kubernetes.io/metadata.name: kube-system
|
||||||
|
---
|
||||||
|
# ── Egress: all prod pods may reach platform (postgres/redis), vault,
|
||||||
|
# monitoring (alloy OTLP), kube-dns, and the internet (external APIs) ─
|
||||||
|
apiVersion: networking.k8s.io/v1
|
||||||
|
kind: NetworkPolicy
|
||||||
|
metadata:
|
||||||
|
name: allow-prod-egress
|
||||||
|
namespace: neuron-prod
|
||||||
|
spec:
|
||||||
|
podSelector: {}
|
||||||
|
policyTypes:
|
||||||
|
- Egress
|
||||||
|
egress:
|
||||||
|
# Kubernetes DNS
|
||||||
|
- ports:
|
||||||
|
- port: 53
|
||||||
|
protocol: UDP
|
||||||
|
- port: 53
|
||||||
|
protocol: TCP
|
||||||
|
# platform namespace (Postgres, Redis, accounts service)
|
||||||
|
- to:
|
||||||
|
- namespaceSelector:
|
||||||
|
matchLabels:
|
||||||
|
kubernetes.io/metadata.name: platform
|
||||||
|
# vault namespace
|
||||||
|
- to:
|
||||||
|
- namespaceSelector:
|
||||||
|
matchLabels:
|
||||||
|
kubernetes.io/metadata.name: vault
|
||||||
|
# monitoring namespace (Alloy OTLP)
|
||||||
|
- to:
|
||||||
|
- namespaceSelector:
|
||||||
|
matchLabels:
|
||||||
|
kubernetes.io/metadata.name: monitoring
|
||||||
|
# intra-namespace (mcp ↔ rest ↔ license)
|
||||||
|
- to:
|
||||||
|
- namespaceSelector:
|
||||||
|
matchLabels:
|
||||||
|
kubernetes.io/metadata.name: neuron-prod
|
||||||
|
# external internet (Stripe, OAuth providers, Cloudflare, etc.)
|
||||||
|
- ports:
|
||||||
|
- port: 443
|
||||||
|
protocol: TCP
|
||||||
|
- port: 80
|
||||||
|
protocol: TCP
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
---
|
||||||
|
# PodDisruptionBudgets for neuron-prod
|
||||||
|
# Ensures at least 1 replica of each service survives node drains and upgrades.
|
||||||
|
# With replicas=1 today, minAvailable=1 means drain will block until HPA scales up
|
||||||
|
# to 2+ — this is intentional and provides a natural safety gate.
|
||||||
|
apiVersion: policy/v1
|
||||||
|
kind: PodDisruptionBudget
|
||||||
|
metadata:
|
||||||
|
name: neuron-mcp-pdb
|
||||||
|
namespace: neuron-prod
|
||||||
|
spec:
|
||||||
|
minAvailable: 1
|
||||||
|
selector:
|
||||||
|
matchLabels:
|
||||||
|
app: neuron-mcp
|
||||||
|
slot: blue
|
||||||
|
---
|
||||||
|
apiVersion: policy/v1
|
||||||
|
kind: PodDisruptionBudget
|
||||||
|
metadata:
|
||||||
|
name: neuron-rest-pdb
|
||||||
|
namespace: neuron-prod
|
||||||
|
spec:
|
||||||
|
minAvailable: 1
|
||||||
|
selector:
|
||||||
|
matchLabels:
|
||||||
|
app: neuron-rest
|
||||||
|
---
|
||||||
|
apiVersion: policy/v1
|
||||||
|
kind: PodDisruptionBudget
|
||||||
|
metadata:
|
||||||
|
name: neuron-marketing-pdb
|
||||||
|
namespace: neuron-prod
|
||||||
|
spec:
|
||||||
|
# Marketing runs 2 replicas — keep at least 1 alive during drains.
|
||||||
|
minAvailable: 1
|
||||||
|
selector:
|
||||||
|
matchLabels:
|
||||||
|
app: neuron-marketing
|
||||||
@@ -19,6 +19,10 @@ spec:
|
|||||||
annotations:
|
annotations:
|
||||||
kubectl.kubernetes.io/restartedAt: "2026-04-24T10:00:00Z"
|
kubectl.kubernetes.io/restartedAt: "2026-04-24T10:00:00Z"
|
||||||
spec:
|
spec:
|
||||||
|
securityContext:
|
||||||
|
runAsNonRoot: true
|
||||||
|
seccompProfile:
|
||||||
|
type: RuntimeDefault
|
||||||
containers:
|
containers:
|
||||||
- name: neuron-rest
|
- name: neuron-rest
|
||||||
image: registry.neuralplatform.ai/neuron-technologies/neuron-rest:v0.15.3
|
image: registry.neuralplatform.ai/neuron-technologies/neuron-rest:v0.15.3
|
||||||
@@ -26,6 +30,12 @@ spec:
|
|||||||
ports:
|
ports:
|
||||||
- name: http
|
- name: http
|
||||||
containerPort: 8081
|
containerPort: 8081
|
||||||
|
securityContext:
|
||||||
|
allowPrivilegeEscalation: false
|
||||||
|
readOnlyRootFilesystem: false
|
||||||
|
runAsNonRoot: true
|
||||||
|
capabilities:
|
||||||
|
drop: ["ALL"]
|
||||||
envFrom:
|
envFrom:
|
||||||
- configMapRef:
|
- configMapRef:
|
||||||
name: neuron-prod-config
|
name: neuron-prod-config
|
||||||
@@ -41,18 +51,23 @@ spec:
|
|||||||
limits:
|
limits:
|
||||||
cpu: 1000m
|
cpu: 1000m
|
||||||
memory: 1Gi
|
memory: 1Gi
|
||||||
|
startupProbe:
|
||||||
|
httpGet:
|
||||||
|
path: /actuator/health/liveness
|
||||||
|
port: http
|
||||||
|
failureThreshold: 30
|
||||||
|
periodSeconds: 5
|
||||||
livenessProbe:
|
livenessProbe:
|
||||||
httpGet:
|
httpGet:
|
||||||
path: /actuator/health/liveness
|
path: /actuator/health/liveness
|
||||||
port: http
|
port: http
|
||||||
initialDelaySeconds: 30
|
|
||||||
periodSeconds: 30
|
periodSeconds: 30
|
||||||
failureThreshold: 3
|
failureThreshold: 3
|
||||||
readinessProbe:
|
readinessProbe:
|
||||||
httpGet:
|
httpGet:
|
||||||
path: /actuator/health/readiness
|
path: /actuator/health/readiness
|
||||||
port: http
|
port: http
|
||||||
initialDelaySeconds: 15
|
initialDelaySeconds: 5
|
||||||
periodSeconds: 10
|
periodSeconds: 10
|
||||||
failureThreshold: 3
|
failureThreshold: 3
|
||||||
volumes:
|
volumes:
|
||||||
|
|||||||
@@ -1,46 +1,27 @@
|
|||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
# neuron-self-improve.sh — self-improvement loop for Neuron
|
# neuron-self-improve.sh — self-improvement loop for Neuron
|
||||||
#
|
#
|
||||||
# Manages code experiments across alpha/beta/gamma zones and promotes winners to prod.
|
# Model: stage is the experiment slot. prod runs blue/green.
|
||||||
# All cluster changes go through git → Argo CD. No direct kubectl mutations.
|
# Promote flow: implement → CI → stage → verify → blue-green-deploy → prod.
|
||||||
#
|
#
|
||||||
# Usage:
|
# Usage:
|
||||||
# ./neuron-self-improve.sh snapshot <slot> # copy prod DB to slot
|
# ./neuron-self-improve.sh snapshot # copy prod DB to stage
|
||||||
# ./neuron-self-improve.sh deploy <slot> <sha> # point slot at a specific image
|
# ./neuron-self-improve.sh deploy <sha> # point stage at a specific image
|
||||||
# ./neuron-self-improve.sh promote <slot> # copy slot's image to prod
|
# ./neuron-self-improve.sh promote <tag> # blue/green flip prod to <tag>
|
||||||
# ./neuron-self-improve.sh status # show what's running in each zone
|
# ./neuron-self-improve.sh status # show active blue/green slot + stage
|
||||||
# ./neuron-self-improve.sh rollback <slot> <sha> # revert slot to a previous image
|
# ./neuron-self-improve.sh rollback <tag> # roll prod back to <tag> via blue/green
|
||||||
|
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
INFRA_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
INFRA_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||||
REGISTRY="registry.neuralplatform.ai/neural-platform/neuron"
|
REGISTRY="registry.neuralplatform.ai/neuron-technologies/neuron-mcp"
|
||||||
KUBECONFIG="${KUBECONFIG:-$HOME/.kube/legion-config}"
|
KUBECONFIG="${KUBECONFIG:-$HOME/.kube/legion-config}"
|
||||||
|
|
||||||
|
KC_PROD="kubectl --kubeconfig=$KUBECONFIG -n neuron-prod"
|
||||||
|
KC_STAGE="kubectl --kubeconfig=$KUBECONFIG -n neuron-stage"
|
||||||
|
|
||||||
# ── helpers ───────────────────────────────────────────────────────────────────
|
# ── helpers ───────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
slot_manifest() {
|
|
||||||
local slot=$1
|
|
||||||
if [[ "$slot" == "prod" ]]; then
|
|
||||||
echo "$INFRA_DIR/servers/legion/apps/neuron.yaml"
|
|
||||||
else
|
|
||||||
echo "$INFRA_DIR/servers/legion/apps/neuron-${slot}.yaml"
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
current_sha() {
|
|
||||||
local slot=$1
|
|
||||||
grep "image:" "$(slot_manifest "$slot")" | grep -oP '[a-f0-9]{40}' | head -1
|
|
||||||
}
|
|
||||||
|
|
||||||
set_image() {
|
|
||||||
local slot=$1 sha=$2
|
|
||||||
local manifest
|
|
||||||
manifest="$(slot_manifest "$slot")"
|
|
||||||
sed -i "s|${REGISTRY}:.*|${REGISTRY}:${sha}|" "$manifest"
|
|
||||||
echo " set $slot → $sha"
|
|
||||||
}
|
|
||||||
|
|
||||||
git_push() {
|
git_push() {
|
||||||
local msg=$1
|
local msg=$1
|
||||||
cd "$INFRA_DIR"
|
cd "$INFRA_DIR"
|
||||||
@@ -50,35 +31,42 @@ git_push() {
|
|||||||
echo " pushed: $msg"
|
echo " pushed: $msg"
|
||||||
}
|
}
|
||||||
|
|
||||||
wait_healthy() {
|
active_prod_slot() {
|
||||||
local slot=$1
|
$KC_PROD get svc neuron-mcp -o jsonpath='{.spec.selector.slot}' 2>/dev/null || echo "unknown"
|
||||||
local app="neuron"
|
}
|
||||||
[[ "$slot" != "prod" ]] && app="neuron-${slot}"
|
|
||||||
echo " waiting for $slot to be healthy..."
|
current_prod_tag() {
|
||||||
local i=0
|
local slot
|
||||||
while [[ $i -lt 30 ]]; do
|
slot=$(active_prod_slot)
|
||||||
local ready
|
$KC_PROD get deployment "neuron-mcp-$slot" \
|
||||||
ready=$(KUBECONFIG="$KUBECONFIG" kubectl get pods -n neuron -l "app=${app}" \
|
-o jsonpath='{.spec.template.spec.containers[0].image}' 2>/dev/null \
|
||||||
--no-headers 2>/dev/null | grep "1/1" | wc -l)
|
| cut -d: -f2 || echo "unknown"
|
||||||
[[ "$ready" -ge 1 ]] && echo " $slot is healthy" && return 0
|
}
|
||||||
sleep 10
|
|
||||||
((i++))
|
wait_ready() {
|
||||||
done
|
local ns=$1 label=$2 timeout=${3:-180}
|
||||||
echo " WARNING: $slot did not become healthy within 5 minutes"
|
echo " waiting for $label to be ready..."
|
||||||
return 1
|
kubectl --kubeconfig="$KUBECONFIG" -n "$ns" \
|
||||||
|
rollout status deployment "$label" --timeout="${timeout}s"
|
||||||
|
echo " $label is ready"
|
||||||
}
|
}
|
||||||
|
|
||||||
# ── commands ──────────────────────────────────────────────────────────────────
|
# ── commands ──────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
cmd_snapshot() {
|
cmd_snapshot() {
|
||||||
local slot=${1:?Usage: snapshot <slot>}
|
echo "==> Snapshotting prod DB into stage..."
|
||||||
echo "==> Snapshotting prod DB into $slot..."
|
local prod_slot
|
||||||
KUBECONFIG="$KUBECONFIG" kubectl apply -f - << EOF
|
prod_slot=$(active_prod_slot)
|
||||||
|
local prod_pvc="neuron-prod-data"
|
||||||
|
local stage_pvc="neuron-stage-data"
|
||||||
|
|
||||||
|
# Run a one-shot job that copies prod DB to stage PVC
|
||||||
|
$KC_PROD apply -f - << EOF
|
||||||
apiVersion: batch/v1
|
apiVersion: batch/v1
|
||||||
kind: Job
|
kind: Job
|
||||||
metadata:
|
metadata:
|
||||||
name: snapshot-prod-to-${slot}
|
name: snapshot-prod-to-stage
|
||||||
namespace: neuron
|
namespace: neuron-prod
|
||||||
spec:
|
spec:
|
||||||
ttlSecondsAfterFinished: 300
|
ttlSecondsAfterFinished: 300
|
||||||
template:
|
template:
|
||||||
@@ -89,7 +77,9 @@ spec:
|
|||||||
image: keinos/sqlite3:latest
|
image: keinos/sqlite3:latest
|
||||||
command: ["/bin/sh", "-c"]
|
command: ["/bin/sh", "-c"]
|
||||||
args:
|
args:
|
||||||
- sqlite3 /src/neuron.db ".backup /dst/neuron.db" && echo "Done" && ls -lh /dst/neuron.db
|
- |
|
||||||
|
sqlite3 /src/neuron.db ".backup /dst/neuron.db"
|
||||||
|
echo "Done — $(ls -lh /dst/neuron.db | awk '{print \$5}')"
|
||||||
volumeMounts:
|
volumeMounts:
|
||||||
- name: src
|
- name: src
|
||||||
mountPath: /src
|
mountPath: /src
|
||||||
@@ -99,125 +89,127 @@ spec:
|
|||||||
volumes:
|
volumes:
|
||||||
- name: src
|
- name: src
|
||||||
persistentVolumeClaim:
|
persistentVolumeClaim:
|
||||||
claimName: neuron-data
|
claimName: ${prod_pvc}
|
||||||
- name: dst
|
- name: dst
|
||||||
persistentVolumeClaim:
|
persistentVolumeClaim:
|
||||||
claimName: neuron-${slot}-data
|
claimName: ${stage_pvc}
|
||||||
EOF
|
EOF
|
||||||
|
|
||||||
echo " waiting for snapshot job..."
|
echo " waiting for snapshot job..."
|
||||||
KUBECONFIG="$KUBECONFIG" kubectl wait job "snapshot-prod-to-${slot}" \
|
$KC_PROD wait job snapshot-prod-to-stage --for=condition=complete --timeout=120s
|
||||||
-n neuron --for=condition=complete --timeout=120s
|
echo " snapshot complete — stage DB is now a copy of prod"
|
||||||
echo " snapshot complete"
|
|
||||||
}
|
}
|
||||||
|
|
||||||
cmd_deploy() {
|
cmd_deploy() {
|
||||||
local slot=${1:?Usage: deploy <slot> <sha>}
|
local sha=${1:?Usage: deploy <sha>}
|
||||||
local sha=${2:?Usage: deploy <slot> <sha>}
|
echo "==> Deploying $sha to stage..."
|
||||||
echo "==> Deploying $sha to $slot..."
|
|
||||||
set_image "$slot" "$sha"
|
# Update stage deployment image in-place (stage is a single deployment, not blue/green)
|
||||||
git_push "chore(neuron): deploy ${sha:0:12} to $slot"
|
$KC_STAGE set image deployment/neuron-mcp "neuron-mcp=${REGISTRY}:${sha}"
|
||||||
wait_healthy "$slot"
|
$KC_STAGE rollout restart deployment/neuron-mcp
|
||||||
echo "==> $slot is live at https://neuron-${slot}.neuralplatform.ai"
|
wait_ready "neuron-stage" "neuron-mcp"
|
||||||
|
|
||||||
|
echo "==> stage is live"
|
||||||
|
echo " endpoint: https://neuron.neuralplatform.ai/stage (or stage ingress if configured)"
|
||||||
|
echo " MCP URL: (see stage ingress/services.yaml)"
|
||||||
}
|
}
|
||||||
|
|
||||||
cmd_promote() {
|
cmd_promote() {
|
||||||
local slot=${1:?Usage: promote <slot>}
|
local tag=${1:?Usage: promote <tag>}
|
||||||
local sha
|
echo "==> Promoting $tag to prod via blue/green flip..."
|
||||||
sha=$(current_sha "$slot")
|
"$INFRA_DIR/scripts/blue-green-deploy.sh" "$tag"
|
||||||
[[ -z "$sha" ]] && echo "ERROR: could not read SHA from $slot" && exit 1
|
|
||||||
echo "==> Promoting $slot ($sha) to prod..."
|
|
||||||
set_image "prod" "$sha"
|
|
||||||
# Also stamp the :stable registry tag (pull and re-tag)
|
|
||||||
git_push "chore(neuron): promote $slot (${sha:0:12}) to prod"
|
|
||||||
wait_healthy "prod"
|
|
||||||
echo "==> prod is now running $sha"
|
|
||||||
echo "==> prod endpoint: https://neuron.neuralplatform.ai"
|
|
||||||
}
|
}
|
||||||
|
|
||||||
cmd_rollback() {
|
cmd_rollback() {
|
||||||
local slot=${1:?Usage: rollback <slot> <sha>}
|
local tag=${1:?Usage: rollback <tag>}
|
||||||
local sha=${2:?Usage: rollback <slot> <sha>}
|
echo "==> Rolling prod back to $tag via blue/green..."
|
||||||
echo "==> Rolling $slot back to $sha..."
|
"$INFRA_DIR/scripts/blue-green-deploy.sh" "$tag"
|
||||||
set_image "$slot" "$sha"
|
|
||||||
git_push "chore(neuron): rollback $slot to ${sha:0:12}"
|
|
||||||
wait_healthy "$slot"
|
|
||||||
echo "==> $slot rolled back"
|
|
||||||
}
|
}
|
||||||
|
|
||||||
cmd_status() {
|
cmd_status() {
|
||||||
echo "==> Neuron slot status"
|
echo "==> Neuron deployment status"
|
||||||
echo ""
|
echo ""
|
||||||
for slot in prod alpha beta gamma; do
|
|
||||||
local app="neuron"
|
local active_slot
|
||||||
[[ "$slot" != "prod" ]] && app="neuron-${slot}"
|
active_slot=$(active_prod_slot)
|
||||||
local sha
|
local prod_tag
|
||||||
sha=$(current_sha "$slot" 2>/dev/null || echo "unknown")
|
prod_tag=$(current_prod_tag)
|
||||||
local ready
|
|
||||||
ready=$(KUBECONFIG="$KUBECONFIG" kubectl get pods -n neuron -l "app=${app}" \
|
printf " %-8s slot=%-5s tag=%s\n" "prod" "$active_slot" "$prod_tag"
|
||||||
--no-headers 2>/dev/null | grep "1/1" | wc -l | tr -d ' ')
|
|
||||||
printf " %-8s %s pods_ready=%s\n" "$slot" "${sha:0:12}" "$ready"
|
# Stage
|
||||||
done
|
local stage_tag
|
||||||
|
stage_tag=$($KC_STAGE get deployment neuron-mcp \
|
||||||
|
-o jsonpath='{.spec.template.spec.containers[0].image}' 2>/dev/null \
|
||||||
|
| cut -d: -f2 || echo "unknown")
|
||||||
|
local stage_ready
|
||||||
|
stage_ready=$($KC_STAGE get pods -l app=neuron-mcp \
|
||||||
|
--no-headers 2>/dev/null | grep "1/1" | wc -l | tr -d ' ')
|
||||||
|
printf " %-8s tag=%-20s pods_ready=%s\n" "stage" "$stage_tag" "$stage_ready"
|
||||||
|
|
||||||
echo ""
|
echo ""
|
||||||
echo " Endpoints:"
|
echo " Prod endpoint: https://neuron.neuralplatform.ai"
|
||||||
echo " prod: https://neuron.neuralplatform.ai"
|
echo " Prod MCP: https://neuron.neuralplatform.ai/mcp"
|
||||||
echo " alpha: https://neuron-alpha.neuralplatform.ai"
|
|
||||||
echo " beta: https://neuron-beta.neuralplatform.ai"
|
|
||||||
echo " gamma: https://neuron-gamma.neuralplatform.ai"
|
|
||||||
}
|
}
|
||||||
|
|
||||||
# ── self-improvement loop (autonomous) ────────────────────────────────────────
|
# ── self-improvement loop (autonomous) ────────────────────────────────────────
|
||||||
# When called with no args, prints the process for an autonomous agent to follow.
|
# When called with no args (or 'loop'), prints the process for an autonomous agent.
|
||||||
cmd_loop_process() {
|
cmd_loop_process() {
|
||||||
cat << 'PROCESS'
|
cat << 'PROCESS'
|
||||||
NEURON SELF-IMPROVEMENT LOOP
|
NEURON SELF-IMPROVEMENT LOOP
|
||||||
=============================
|
=============================
|
||||||
|
|
||||||
|
Deployment model: stage (experiment) → prod (blue/green).
|
||||||
|
|
||||||
Each iteration:
|
Each iteration:
|
||||||
|
|
||||||
1. IDENTIFY — find an improvement opportunity in the codebase.
|
1. IDENTIFY — find an improvement opportunity.
|
||||||
Sources: Neuron backlog, observed failures, performance gaps, code review.
|
Sources: Neuron backlog, observed failures, graph reliability gaps,
|
||||||
|
session startup issues, performance, quality observations.
|
||||||
|
|
||||||
2. IMPLEMENT — make the change on a git branch in neural-platform/neuron.
|
2. IMPLEMENT — make the change on a git branch in neuron-technologies/neuron.
|
||||||
Commit message should describe the hypothesis being tested.
|
Commit message describes the hypothesis.
|
||||||
|
|
||||||
3. CI — push the branch; CI runs lint/test/build and auto-deploys to alpha.
|
3. CI — push to main; CI builds image and publishes to registry.
|
||||||
Wait for CI to complete and alpha pod to be healthy.
|
Image tag = git SHA (short). Wait for CI to complete.
|
||||||
|
|
||||||
4. SNAPSHOT — before the next experiment touches a slot, snapshot prod DB:
|
4. SNAPSHOT — before running the experiment, copy prod DB to stage:
|
||||||
./scripts/neuron-self-improve.sh snapshot <slot>
|
./scripts/neuron-self-improve.sh snapshot
|
||||||
This ensures the experiment runs against real data without touching prod.
|
This gives stage a real-data copy without touching prod.
|
||||||
|
|
||||||
5. EXPERIMENT — run the experiment against the alpha endpoint:
|
5. DEPLOY TO STAGE — point stage at the new image:
|
||||||
https://neuron-alpha.neuralplatform.ai
|
./scripts/neuron-self-improve.sh deploy <sha>
|
||||||
Use real workloads. Compare against prod or other slots.
|
Stage runs against the snapshot DB. Experiment freely.
|
||||||
Multiple experiments can run simultaneously across alpha/beta/gamma.
|
|
||||||
|
|
||||||
6. EVALUATE — compare results. Criteria:
|
6. EVALUATE — test the stage endpoint. Criteria:
|
||||||
- Does it behave correctly?
|
- Correct behavior on same inputs?
|
||||||
- Does it perform better (latency, quality, fewer errors)?
|
- At least one dimension improved (latency, quality, error rate)?
|
||||||
- Does it handle edge cases prod handles?
|
- No regressions on cases prod handles?
|
||||||
Schema changes are acceptable — they become migrations against prod on promote.
|
- Schema changes are additive/non-destructive?
|
||||||
|
|
||||||
7. PROMOTE WINNER — if alpha wins:
|
7. PROMOTE — if stage passes, promote to prod via blue/green:
|
||||||
./scripts/neuron-self-improve.sh promote alpha
|
./scripts/neuron-self-improve.sh promote <tag>
|
||||||
This updates prod's image SHA in git → Argo CD deploys → prod is updated.
|
The blue-green-deploy.sh handles: scale idle slot → health check →
|
||||||
The old prod SHA is still in the registry — rollback is always available.
|
flip service selector → scale old slot to 0.
|
||||||
|
The old slot is kept at 0 for instant rollback.
|
||||||
|
|
||||||
8. ROLLBACK — if something goes wrong:
|
8. ROLLBACK — if something goes wrong post-promote:
|
||||||
./scripts/neuron-self-improve.sh rollback prod <previous-sha>
|
./scripts/neuron-self-improve.sh rollback <previous-tag>
|
||||||
|
Blue/green means rollback is always a slot flip — no redeployment.
|
||||||
|
|
||||||
9. LOOP — go to step 1. Alpha is now free for the next experiment.
|
9. LOOP — go to step 1. Stage is free for the next experiment.
|
||||||
|
|
||||||
RULES
|
RULES
|
||||||
------
|
------
|
||||||
- Never skip the snapshot step before running an experiment against a slot.
|
- Always snapshot before deploying to stage (stage DB starts from prod).
|
||||||
- Prod data is never lost — slots only get copies.
|
- Prod is never touched during the experiment — only on promote.
|
||||||
- Schema changes in experiment code → Alembic auto-migrates the snapshot DB.
|
- Schema changes: test Alembic migration on stage snapshot first.
|
||||||
On promote, the migration runs against prod. Always verify migrations are
|
On promote, the same migration runs against prod. Must be additive.
|
||||||
additive/non-destructive before promoting.
|
- The registry retains every SHA — any version is deployable to any slot.
|
||||||
- All three experiment zones can run simultaneously with different hypotheses.
|
- blue-green-deploy.sh handles the infra flip. This script orchestrates.
|
||||||
- The registry retains every SHA — any version can be deployed to any slot.
|
- Never kubectl apply directly — all changes via git + Argo CD.
|
||||||
- This script handles git push; Argo CD handles k8s — never kubectl apply.
|
Exception: blue-green-deploy.sh uses kubectl for the slot flip because
|
||||||
|
that is a runtime operation, not a config change.
|
||||||
|
|
||||||
PROCESS
|
PROCESS
|
||||||
}
|
}
|
||||||
@@ -225,11 +217,11 @@ PROCESS
|
|||||||
# ── dispatch ──────────────────────────────────────────────────────────────────
|
# ── dispatch ──────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
case "${1:-loop}" in
|
case "${1:-loop}" in
|
||||||
snapshot) cmd_snapshot "${@:2}" ;;
|
snapshot) cmd_snapshot ;;
|
||||||
deploy) cmd_deploy "${@:2}" ;;
|
deploy) cmd_deploy "${@:2}" ;;
|
||||||
promote) cmd_promote "${@:2}" ;;
|
promote) cmd_promote "${@:2}" ;;
|
||||||
rollback) cmd_rollback "${@:2}" ;;
|
rollback) cmd_rollback "${@:2}" ;;
|
||||||
status) cmd_status ;;
|
status) cmd_status ;;
|
||||||
loop) cmd_loop_process ;;
|
loop) cmd_loop_process ;;
|
||||||
*) echo "Usage: $0 snapshot|deploy|promote|rollback|status|loop" && exit 1 ;;
|
*) echo "Usage: $0 snapshot|deploy <sha>|promote <tag>|rollback <tag>|status|loop" && exit 1 ;;
|
||||||
esac
|
esac
|
||||||
|
|||||||
Reference in New Issue
Block a user