From 556eaa2fc1c2e10de26f3d5a2fc1a0f2079e9418 Mon Sep 17 00:00:00 2001 From: Sagnik Date: Mon, 13 Apr 2026 00:50:06 +0530 Subject: [PATCH] fix: Added Velocity Backend to Ingress Computer with Elastic IP --- app/dist/index.html | 2 +- app/src/lib/api.ts | 2 +- app/vite.config.ts | 11 +- .../TEAM_HANDOFF_2026-04-08.md | 74 +++++++++ .../desineuron-velocity-route-sync.service | 9 ++ .../desineuron-velocity-route-sync.timer | 10 ++ .../install_linux_velocity_route_sync.sh | 33 ++++ .../desineuron_ingress/sync_velocity_route.py | 142 ++++++++++++++++++ remote_bootstrap_20260401.sh | 4 +- 9 files changed, 280 insertions(+), 7 deletions(-) create mode 100644 infrastructure/desineuron_ingress/desineuron-velocity-route-sync.service create mode 100644 infrastructure/desineuron_ingress/desineuron-velocity-route-sync.timer create mode 100644 infrastructure/desineuron_ingress/install_linux_velocity_route_sync.sh create mode 100644 infrastructure/desineuron_ingress/sync_velocity_route.py diff --git a/app/dist/index.html b/app/dist/index.html index 08eed3df..979c2480 100644 --- a/app/dist/index.html +++ b/app/dist/index.html @@ -4,7 +4,7 @@ Velocity WebOS - + diff --git a/app/src/lib/api.ts b/app/src/lib/api.ts index 724431e5..b11340a8 100644 --- a/app/src/lib/api.ts +++ b/app/src/lib/api.ts @@ -1,5 +1,5 @@ const rawApiBase = import.meta.env.VITE_API_URL?.trim(); -const DEPLOYED_BACKEND_ORIGIN = 'https://54.152.236.10'; +const DEPLOYED_BACKEND_ORIGIN = 'https://api.desineuron.in'; function getBrowserOrigin() { if (typeof window !== 'undefined' && window.location?.origin) { diff --git a/app/vite.config.ts b/app/vite.config.ts index 41350ce7..c5dafa78 100644 --- a/app/vite.config.ts +++ b/app/vite.config.ts @@ -3,10 +3,15 @@ import react from "@vitejs/plugin-react" import { defineConfig } from "vite" import { inspectAttr } from 'kimi-plugin-inspect-react' +const backendProxyTarget = process.env.VITE_BACKEND_PROXY_TARGET?.trim() || "https://api.desineuron.in" + // https://vite.dev/config/ export default defineConfig({ base: './', plugins: [inspectAttr(), react()], + optimizeDeps: { + exclude: ['@mediapipe/tasks-vision'], + }, resolve: { alias: { "@": path.resolve(__dirname, "./src"), @@ -17,18 +22,18 @@ export default defineConfig({ port: 5173, proxy: { "/api": { - target: "https://54.152.236.10", + target: backendProxyTarget, changeOrigin: true, secure: false, ws: true, }, "/assets": { - target: "https://54.152.236.10", + target: backendProxyTarget, changeOrigin: true, secure: false, }, "/vault": { - target: "https://54.152.236.10", + target: backendProxyTarget, changeOrigin: true, secure: false, }, diff --git a/infrastructure/desineuron_ingress/TEAM_HANDOFF_2026-04-08.md b/infrastructure/desineuron_ingress/TEAM_HANDOFF_2026-04-08.md index 45d4376c..5ce4ea15 100644 --- a/infrastructure/desineuron_ingress/TEAM_HANDOFF_2026-04-08.md +++ b/infrastructure/desineuron_ingress/TEAM_HANDOFF_2026-04-08.md @@ -21,6 +21,7 @@ Date: 2026-04-08 15. Team Summary 16. Current Status Snapshot - 2026-04-12 17. Linux Ops Control Plane +18. Velocity Stable API Runbook ### Outcome @@ -589,3 +590,76 @@ Reference docs: - [README.md](/F:/Workin%20In%20Progress/DESINEURON/GITLAB/Project_Velocity/infrastructure/ops_control_plane/README.md) - [Desineuron Ops Control Plane Bibel.md](/F:/Workin%20In%20Progress/DESINEURON/GITLAB/Project_Velocity/.Agent%20Context/Bibels/Desineuron%20Ops%20Control%20Plane%20Bibel.md) + +### Velocity Stable API Runbook + +Problem: + +- the Velocity backend was still exposed through an ephemeral AWS instance IP +- frontend code was hardcoded to `https://54.152.236.10` +- EC2 stop/start changed the backend public IP and broke the app +- the stable ingress already existed, but Velocity had never been mapped through it + +Correct production pattern: + +- public API hostname: `api.desineuron.in` +- public edge: ingress `98.87.120.120` +- ingress route target: current private IP of the EC2 instance tagged `DesineuronRole=velocity-backend` +- Linux box runs the route-sync timer, just like the ComfyUI pattern +- backend stays private and should only accept `8000/8001` from ingress security group `sg-0721b8b48e12c531d` + +Repo artifacts added for this pattern: + +- [sync_velocity_route.py](/F:/Workin%20In%20Progress/DESINEURON/GITLAB/Project_Velocity/infrastructure/desineuron_ingress/sync_velocity_route.py) +- [desineuron-velocity-route-sync.service](/F:/Workin%20In%20Progress/DESINEURON/GITLAB/Project_Velocity/infrastructure/desineuron_ingress/desineuron-velocity-route-sync.service) +- [desineuron-velocity-route-sync.timer](/F:/Workin%20In%20Progress/DESINEURON/GITLAB/Project_Velocity/infrastructure/desineuron_ingress/desineuron-velocity-route-sync.timer) +- [install_linux_velocity_route_sync.sh](/F:/Workin%20In%20Progress/DESINEURON/GITLAB/Project_Velocity/infrastructure/desineuron_ingress/install_linux_velocity_route_sync.sh) + +Frontend changes expected by this pattern: + +- `app/src/lib/api.ts` now points production traffic to `https://api.desineuron.in` +- `app/vite.config.ts` uses `VITE_BACKEND_PROXY_TARGET` for local dev override +- Vite proxy errors are no longer tied to one stale EC2 IP + +Backend bootstrap note: + +- `remote_bootstrap_20260401.sh` now includes: + - `https://api.desineuron.in` + - `https://54.152.236.10` + - `https://18.212.122.77` + in `CORS_ORIGINS` + +Operator steps still required outside the repo: + +1. Tag the backend EC2 instance: + - key: `DesineuronRole` + - value: `velocity-backend` + +2. Add Cloudflare DNS: + - record: `api.desineuron.in` + - type: `A` + - value: `98.87.120.120` + - proxy: `DNS only` + +3. Bootstrap the first ingress route once: + - target host: current backend private IP + - target port: `8001` unless the backend listener is changed + +4. Lock down backend security group: + - revoke public `0.0.0.0/0` access to the backend app port + - allow backend app port only from ingress security group `sg-0721b8b48e12c531d` + +5. Update backend runtime env and restart: + - add `https://api.desineuron.in` to `CORS_ORIGINS` + - restart `velocity-backend.service` + +6. Install the Linux route sync timer: + - copy `infrastructure/desineuron_ingress/*velocity*` to Linux temporary staging + - run `install_linux_velocity_route_sync.sh` + +Expected result after the 6 steps: + +- frontend reaches `https://api.desineuron.in` +- ingress forwards to the current backend private IP +- backend public IP changes stop mattering +- Linux auto-heals route drift every 2 minutes and on boot diff --git a/infrastructure/desineuron_ingress/desineuron-velocity-route-sync.service b/infrastructure/desineuron_ingress/desineuron-velocity-route-sync.service new file mode 100644 index 00000000..755a54d7 --- /dev/null +++ b/infrastructure/desineuron_ingress/desineuron-velocity-route-sync.service @@ -0,0 +1,9 @@ +[Unit] +Description=Sync api.desineuron.in managed route to current Velocity backend private IP +After=network-online.target +Wants=network-online.target + +[Service] +Type=oneshot +EnvironmentFile=/etc/desineuron-velocity-route-sync.env +ExecStart=/opt/desineuron-velocity-route-sync/.venv/bin/python /usr/local/bin/sync_velocity_route.py diff --git a/infrastructure/desineuron_ingress/desineuron-velocity-route-sync.timer b/infrastructure/desineuron_ingress/desineuron-velocity-route-sync.timer new file mode 100644 index 00000000..50c7c2db --- /dev/null +++ b/infrastructure/desineuron_ingress/desineuron-velocity-route-sync.timer @@ -0,0 +1,10 @@ +[Unit] +Description=Run velocity route sync on boot and every 2 minutes + +[Timer] +OnBootSec=1min +OnUnitActiveSec=2min +Unit=desineuron-velocity-route-sync.service + +[Install] +WantedBy=timers.target diff --git a/infrastructure/desineuron_ingress/install_linux_velocity_route_sync.sh b/infrastructure/desineuron_ingress/install_linux_velocity_route_sync.sh new file mode 100644 index 00000000..f52f7048 --- /dev/null +++ b/infrastructure/desineuron_ingress/install_linux_velocity_route_sync.sh @@ -0,0 +1,33 @@ +#!/usr/bin/env bash +set -euo pipefail + +APP_ROOT=/opt/desineuron-velocity-route-sync +VENV_PATH="$APP_ROOT/.venv" +ENV_FILE=/etc/desineuron-velocity-route-sync.env +SCRIPT_PATH=/usr/local/bin/sync_velocity_route.py +SERVICE_FILE=/etc/systemd/system/desineuron-velocity-route-sync.service +TIMER_FILE=/etc/systemd/system/desineuron-velocity-route-sync.timer + +sudo mkdir -p "$APP_ROOT" /var/lib/desineuron-velocity-route-sync +python3 -m venv "$VENV_PATH" +"$VENV_PATH/bin/pip" install --upgrade pip boto3 + +sudo install -m 0755 /tmp/desineuron_ingress/sync_velocity_route.py "$SCRIPT_PATH" +sudo install -m 0644 /tmp/desineuron_ingress/desineuron-velocity-route-sync.service "$SERVICE_FILE" +sudo install -m 0644 /tmp/desineuron_ingress/desineuron-velocity-route-sync.timer "$TIMER_FILE" + +sudo tee "$ENV_FILE" >/dev/null < dict[str, str]: + data: dict[str, str] = {} + if not path.exists(): + return data + for line in path.read_text(encoding="utf-8").splitlines(): + line = line.strip() + if not line or line.startswith("#") or "=" not in line: + continue + key, value = line.split("=", 1) + data[key.strip()] = value.strip() + return data + + +def env(name: str, default: str = "") -> str: + return os.environ.get(name, default) + + +def resolve_target_instance(ec2) -> dict | None: + explicit_instance_id = env("VELOCITY_INSTANCE_ID") + if explicit_instance_id: + reservations = ec2.describe_instances(InstanceIds=[explicit_instance_id])["Reservations"] + for reservation in reservations: + for instance in reservation["Instances"]: + if instance["State"]["Name"] == "running": + return instance + return None + + tag_key = env("VELOCITY_INSTANCE_TAG_KEY", "DesineuronRole") + tag_value = env("VELOCITY_INSTANCE_TAG_VALUE", "velocity-backend") + filters = [ + {"Name": "instance-state-name", "Values": ["running"]}, + {"Name": f"tag:{tag_key}", "Values": [tag_value]}, + ] + reservations = ec2.describe_instances(Filters=filters)["Reservations"] + instances = [instance for reservation in reservations for instance in reservation["Instances"]] + if not instances: + return None + instances.sort(key=lambda row: row["LaunchTime"], reverse=True) + return instances[0] + + +def upsert_route(hostname: str, private_ip: str, port: int) -> subprocess.CompletedProcess[str]: + ingress_host = env("INGRESS_SSH_HOST") + ingress_user = env("INGRESS_SSH_USER", "ec2-user") + ingress_port = env("INGRESS_SSH_PORT", "22") + ingress_key = env("INGRESS_SSH_KEY_PATH") + helper = env("INGRESS_ROUTE_HELPER", "/usr/local/bin/manage_desineuron_routes.py") + payload = json.dumps( + { + "hostname": hostname, + "scheme": "http", + "target_host": private_ip, + "target_port": port, + } + ) + command = ( + f"sudo {helper} upsert '{payload}'" + " && sudo caddy validate --config /etc/caddy/Caddyfile" + " && sudo systemctl reload caddy" + ) + return subprocess.run( + [ + "ssh", + "-o", + "StrictHostKeyChecking=no", + "-o", + "UserKnownHostsFile=/dev/null", + "-i", + ingress_key, + "-p", + ingress_port, + f"{ingress_user}@{ingress_host}", + command, + ], + capture_output=True, + text=True, + check=False, + ) + + +def main() -> int: + ops_env = load_env_file(Path(env("OPS_ENV_FILE", "/opt/desineuron-ops-control-plane/.env"))) + for key in ["AWS_ACCESS_KEY_ID", "AWS_SECRET_ACCESS_KEY", "AWS_DEFAULT_REGION"]: + if key not in os.environ and key in ops_env: + os.environ[key] = ops_env[key] + os.environ.setdefault("AWS_DEFAULT_REGION", ops_env.get("OPS_DEFAULT_REGION", "us-east-1")) + os.environ.setdefault("INGRESS_SSH_HOST", ops_env.get("OPS_INGRESS_SSH_HOST", "")) + os.environ.setdefault("INGRESS_SSH_USER", ops_env.get("OPS_INGRESS_SSH_USER", "ec2-user")) + os.environ.setdefault("INGRESS_SSH_PORT", ops_env.get("OPS_INGRESS_SSH_PORT", "22")) + normalized_key_path = ops_env.get("OPS_SSH_KEY_PATH", "/opt/desineuron-ops-control-plane/state/desineuron-l4-node.pem") + if normalized_key_path.startswith("/app/state/"): + normalized_key_path = normalized_key_path.replace("/app/state/", "/opt/desineuron-ops-control-plane/state/") + os.environ.setdefault("INGRESS_SSH_KEY_PATH", normalized_key_path) + os.environ.setdefault("INGRESS_ROUTE_HELPER", ops_env.get("OPS_INGRESS_ROUTE_HELPER", "/usr/local/bin/manage_desineuron_routes.py")) + + region = os.environ["AWS_DEFAULT_REGION"] + hostname = env("VELOCITY_ROUTE_HOSTNAME", "api.desineuron.in") + port = int(env("VELOCITY_ROUTE_PORT", "8001")) + state_file = Path(env("VELOCITY_ROUTE_STATE_FILE", "/var/lib/desineuron-velocity-route-sync/current_target.txt")) + + ec2 = boto3.client("ec2", region_name=region) + instance = resolve_target_instance(ec2) + if not instance: + print("No running velocity-backend target instance found", file=sys.stderr) + return 1 + + private_ip = instance.get("PrivateIpAddress") + if not private_ip: + print("Target instance has no private IP", file=sys.stderr) + return 1 + + current = state_file.read_text(encoding="utf-8").strip() if state_file.exists() else "" + if current == private_ip: + print(json.dumps({"status": "noop", "hostname": hostname, "target_host": private_ip})) + return 0 + + result = upsert_route(hostname, private_ip, port) + if result.returncode != 0: + print(result.stdout) + print(result.stderr, file=sys.stderr) + return result.returncode + + state_file.parent.mkdir(parents=True, exist_ok=True) + state_file.write_text(private_ip, encoding="utf-8") + print(json.dumps({"status": "updated", "hostname": hostname, "target_host": private_ip})) + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/remote_bootstrap_20260401.sh b/remote_bootstrap_20260401.sh index a2ec4e30..ec98797b 100644 --- a/remote_bootstrap_20260401.sh +++ b/remote_bootstrap_20260401.sh @@ -34,7 +34,7 @@ VELOCITY_DB_NAME=velocity VELOCITY_DB_USER=velocity_app VELOCITY_DB_PASSWORD=${DB_PASSWORD} VELOCITY_JWT_SECRET=${JWT_SECRET} -CORS_ORIGINS=http://localhost:5173,https://18.212.122.77 +CORS_ORIGINS=http://localhost:5173,https://api.desineuron.in,https://54.152.236.10,https://18.212.122.77 VELOCITY_ASSET_DIR=/opt/dlami/nvme/assets OLLAMA_BASE_URL=http://127.0.0.1:11434 NEMOCLAW_BASE_URL=http://127.0.0.1:8080 @@ -112,4 +112,4 @@ sudo systemctl daemon-reload sudo systemctl enable velocity-backend.service sudo systemctl restart velocity-backend.service sudo nginx -t -sudo systemctl restart nginx \ No newline at end of file +sudo systemctl restart nginx -- 2.49.1