fix: Added Velocity Backend to Ingress Computer with Elastic IP (#21)

Co-authored-by: Sagnik <sagnik7896@gmail.com>
Reviewed-on: #21
This commit was merged in pull request #21.
This commit is contained in:
2026-04-13 00:51:39 +05:30
parent e241ff800c
commit bf950bc789
9 changed files with 280 additions and 7 deletions

2
app/dist/index.html vendored
View File

@@ -4,7 +4,7 @@
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Velocity WebOS</title> <title>Velocity WebOS</title>
<script type="module" crossorigin src="./assets/index-CJRJmEe7.js"></script> <script type="module" crossorigin src="./assets/index-BYTPd1oW.js"></script>
<link rel="stylesheet" crossorigin href="./assets/index-UA0RXBVG.css"> <link rel="stylesheet" crossorigin href="./assets/index-UA0RXBVG.css">
</head> </head>
<body> <body>

View File

@@ -1,5 +1,5 @@
const rawApiBase = import.meta.env.VITE_API_URL?.trim(); 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() { function getBrowserOrigin() {
if (typeof window !== 'undefined' && window.location?.origin) { if (typeof window !== 'undefined' && window.location?.origin) {

View File

@@ -3,10 +3,15 @@ import react from "@vitejs/plugin-react"
import { defineConfig } from "vite" import { defineConfig } from "vite"
import { inspectAttr } from 'kimi-plugin-inspect-react' import { inspectAttr } from 'kimi-plugin-inspect-react'
const backendProxyTarget = process.env.VITE_BACKEND_PROXY_TARGET?.trim() || "https://api.desineuron.in"
// https://vite.dev/config/ // https://vite.dev/config/
export default defineConfig({ export default defineConfig({
base: './', base: './',
plugins: [inspectAttr(), react()], plugins: [inspectAttr(), react()],
optimizeDeps: {
exclude: ['@mediapipe/tasks-vision'],
},
resolve: { resolve: {
alias: { alias: {
"@": path.resolve(__dirname, "./src"), "@": path.resolve(__dirname, "./src"),
@@ -17,18 +22,18 @@ export default defineConfig({
port: 5173, port: 5173,
proxy: { proxy: {
"/api": { "/api": {
target: "https://54.152.236.10", target: backendProxyTarget,
changeOrigin: true, changeOrigin: true,
secure: false, secure: false,
ws: true, ws: true,
}, },
"/assets": { "/assets": {
target: "https://54.152.236.10", target: backendProxyTarget,
changeOrigin: true, changeOrigin: true,
secure: false, secure: false,
}, },
"/vault": { "/vault": {
target: "https://54.152.236.10", target: backendProxyTarget,
changeOrigin: true, changeOrigin: true,
secure: false, secure: false,
}, },

View File

@@ -21,6 +21,7 @@ Date: 2026-04-08
15. Team Summary 15. Team Summary
16. Current Status Snapshot - 2026-04-12 16. Current Status Snapshot - 2026-04-12
17. Linux Ops Control Plane 17. Linux Ops Control Plane
18. Velocity Stable API Runbook
### Outcome ### Outcome
@@ -589,3 +590,76 @@ Reference docs:
- [README.md](/F:/Workin%20In%20Progress/DESINEURON/GITLAB/Project_Velocity/infrastructure/ops_control_plane/README.md) - [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) - [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

View File

@@ -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

View File

@@ -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

View File

@@ -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 <<EOF
OPS_ENV_FILE=/opt/desineuron-ops-control-plane/.env
VELOCITY_ROUTE_HOSTNAME=api.desineuron.in
VELOCITY_ROUTE_PORT=8001
VELOCITY_INSTANCE_TAG_KEY=DesineuronRole
VELOCITY_INSTANCE_TAG_VALUE=velocity-backend
VELOCITY_ROUTE_STATE_FILE=/var/lib/desineuron-velocity-route-sync/current_target.txt
INGRESS_SSH_KEY_PATH=/opt/desineuron-ops-control-plane/state/desineuron-l4-node.pem
EOF
sudo chmod 600 "$ENV_FILE"
sudo systemctl daemon-reload
sudo systemctl enable --now desineuron-velocity-route-sync.timer
sudo systemctl start desineuron-velocity-route-sync.service
sudo systemctl --no-pager --full status desineuron-velocity-route-sync.service desineuron-velocity-route-sync.timer

View File

@@ -0,0 +1,142 @@
#!/usr/bin/env python3
from __future__ import annotations
import json
import os
import subprocess
import sys
from pathlib import Path
import boto3
def load_env_file(path: Path) -> 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())

View File

@@ -34,7 +34,7 @@ VELOCITY_DB_NAME=velocity
VELOCITY_DB_USER=velocity_app VELOCITY_DB_USER=velocity_app
VELOCITY_DB_PASSWORD=${DB_PASSWORD} VELOCITY_DB_PASSWORD=${DB_PASSWORD}
VELOCITY_JWT_SECRET=${JWT_SECRET} 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 VELOCITY_ASSET_DIR=/opt/dlami/nvme/assets
OLLAMA_BASE_URL=http://127.0.0.1:11434 OLLAMA_BASE_URL=http://127.0.0.1:11434
NEMOCLAW_BASE_URL=http://127.0.0.1:8080 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 enable velocity-backend.service
sudo systemctl restart velocity-backend.service sudo systemctl restart velocity-backend.service
sudo nginx -t sudo nginx -t
sudo systemctl restart nginx sudo systemctl restart nginx