第一次提交
This commit is contained in:
48
router-proxy/README.md
Normal file
48
router-proxy/README.md
Normal file
@ -0,0 +1,48 @@
|
||||
# router-proxy
|
||||
|
||||
独立的实例入口反向代理:
|
||||
|
||||
- 对外暴露一个统一端口
|
||||
- 按 `Host` 头把专属 URL 转发到对应 `app-instance` 容器
|
||||
- 路由表从 `app-instance/runtime/registry/instances.json` 动态生成
|
||||
|
||||
## 文件
|
||||
|
||||
- `nginx.conf`
|
||||
- Nginx 主配置
|
||||
- `render-routes.py`
|
||||
- 从实例注册表生成 `runtime/conf.d/instances.conf`
|
||||
- `start-proxy.sh`
|
||||
- 启动路由代理容器
|
||||
- `reload-proxy.sh`
|
||||
- 重载路由配置
|
||||
|
||||
## 默认约定
|
||||
|
||||
- 容器名:`nano-router-proxy`
|
||||
- Docker network:`nano-instance-edge`
|
||||
- 对外端口:`8088`
|
||||
|
||||
## 启动
|
||||
|
||||
```bash
|
||||
cd /home/ivan/xuan/nano_project/router-proxy
|
||||
./start-proxy.sh
|
||||
```
|
||||
|
||||
## 重载
|
||||
|
||||
```bash
|
||||
cd /home/ivan/xuan/nano_project/router-proxy
|
||||
./reload-proxy.sh --start-if-missing
|
||||
```
|
||||
|
||||
## URL 约定
|
||||
|
||||
如果 deploy-control 侧使用默认配置,实例 URL 形如:
|
||||
|
||||
```text
|
||||
http://<instance-slug>.127.0.0.1.nip.io:8088
|
||||
```
|
||||
|
||||
只要本机或 DNS 能把该域名解析到代理所在机器,就会由该代理转发到目标实例容器。
|
||||
BIN
router-proxy/__pycache__/render-routes.cpython-310.pyc
Normal file
BIN
router-proxy/__pycache__/render-routes.cpython-310.pyc
Normal file
Binary file not shown.
23
router-proxy/nginx.conf
Normal file
23
router-proxy/nginx.conf
Normal file
@ -0,0 +1,23 @@
|
||||
worker_processes auto;
|
||||
|
||||
events {
|
||||
worker_connections 1024;
|
||||
}
|
||||
|
||||
http {
|
||||
include /etc/nginx/mime.types;
|
||||
default_type application/octet-stream;
|
||||
|
||||
sendfile on;
|
||||
tcp_nopush on;
|
||||
tcp_nodelay on;
|
||||
keepalive_timeout 65;
|
||||
client_max_body_size 100m;
|
||||
|
||||
map $http_upgrade $connection_upgrade {
|
||||
default upgrade;
|
||||
'' close;
|
||||
}
|
||||
|
||||
include /etc/nginx/conf.d/*.conf;
|
||||
}
|
||||
50
router-proxy/reload-proxy.sh
Executable file
50
router-proxy/reload-proxy.sh
Executable file
@ -0,0 +1,50 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
RENDER_SCRIPT="${SCRIPT_DIR}/render-routes.py"
|
||||
START_SCRIPT="${SCRIPT_DIR}/start-proxy.sh"
|
||||
|
||||
PROXY_CONTAINER_NAME="${PROXY_CONTAINER_NAME:-nano-router-proxy}"
|
||||
REGISTRY_PATH="${REGISTRY_PATH:-${SCRIPT_DIR}/../app-instance/runtime/registry/instances.json}"
|
||||
OUTPUT_PATH="${OUTPUT_PATH:-${SCRIPT_DIR}/runtime/conf.d/instances.conf}"
|
||||
START_IF_MISSING=0
|
||||
|
||||
usage() {
|
||||
cat <<'EOF'
|
||||
Usage:
|
||||
./reload-proxy.sh [--start-if-missing]
|
||||
EOF
|
||||
}
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--start-if-missing)
|
||||
START_IF_MISSING=1
|
||||
shift
|
||||
;;
|
||||
--help|-h)
|
||||
usage
|
||||
exit 0
|
||||
;;
|
||||
*)
|
||||
printf '[reload-proxy] unknown argument: %s\n' "$1" >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
python3 "$RENDER_SCRIPT" --registry "$REGISTRY_PATH" --output "$OUTPUT_PATH" >/dev/null
|
||||
|
||||
if ! docker container inspect "$PROXY_CONTAINER_NAME" >/dev/null 2>&1; then
|
||||
if [[ "$START_IF_MISSING" -eq 1 ]]; then
|
||||
"$START_SCRIPT"
|
||||
exit 0
|
||||
fi
|
||||
printf '[reload-proxy] proxy container not found: %s\n' "$PROXY_CONTAINER_NAME" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
docker exec "$PROXY_CONTAINER_NAME" nginx -t >/dev/null
|
||||
docker exec "$PROXY_CONTAINER_NAME" nginx -s reload >/dev/null
|
||||
printf 'container_name=%s\n' "$PROXY_CONTAINER_NAME"
|
||||
90
router-proxy/render-routes.py
Executable file
90
router-proxy/render-routes.py
Executable file
@ -0,0 +1,90 @@
|
||||
#!/usr/bin/env python3
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
|
||||
DEFAULT_REGISTRY = (
|
||||
Path(__file__).resolve().parents[1]
|
||||
/ "app-instance"
|
||||
/ "runtime"
|
||||
/ "registry"
|
||||
/ "instances.json"
|
||||
)
|
||||
DEFAULT_OUTPUT = Path(__file__).resolve().parent / "runtime" / "conf.d" / "instances.conf"
|
||||
|
||||
|
||||
def load_instances(path: Path) -> list[dict[str, Any]]:
|
||||
if not path.exists():
|
||||
return []
|
||||
try:
|
||||
data = json.loads(path.read_text(encoding="utf-8"))
|
||||
except json.JSONDecodeError:
|
||||
return []
|
||||
items = data.get("instances", [])
|
||||
return [item for item in items if isinstance(item, dict)]
|
||||
|
||||
|
||||
def render_server(instance_host: str, container_name: str, upstream_port: int) -> str:
|
||||
return f"""server {{
|
||||
listen 80;
|
||||
server_name {instance_host};
|
||||
|
||||
location / {{
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
proxy_set_header X-Forwarded-Host $host;
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection $connection_upgrade;
|
||||
proxy_read_timeout 3600s;
|
||||
proxy_send_timeout 3600s;
|
||||
proxy_pass http://{container_name}:{upstream_port};
|
||||
}}
|
||||
}}
|
||||
"""
|
||||
|
||||
|
||||
def render(instances: list[dict[str, Any]], upstream_port: int) -> str:
|
||||
blocks = [
|
||||
"server {\n"
|
||||
" listen 80 default_server;\n"
|
||||
" server_name _;\n"
|
||||
" return 404;\n"
|
||||
"}\n"
|
||||
]
|
||||
seen_hosts: set[str] = set()
|
||||
for item in sorted(instances, key=lambda value: str(value.get("instance_host", ""))):
|
||||
instance_host = str(item.get("instance_host", "") or "").strip()
|
||||
container_name = str(item.get("container_name", "") or "").strip()
|
||||
if not instance_host or not container_name or instance_host in seen_hosts:
|
||||
continue
|
||||
seen_hosts.add(instance_host)
|
||||
blocks.append(render_server(instance_host, container_name, upstream_port))
|
||||
return "\n".join(blocks)
|
||||
|
||||
|
||||
def main() -> int:
|
||||
parser = argparse.ArgumentParser(description="Render nginx routes for app instances.")
|
||||
parser.add_argument("--registry", default=str(DEFAULT_REGISTRY), help="Registry JSON path.")
|
||||
parser.add_argument("--output", default=str(DEFAULT_OUTPUT), help="Nginx output config path.")
|
||||
parser.add_argument("--upstream-port", default=8080, type=int, help="App container upstream port.")
|
||||
args = parser.parse_args()
|
||||
|
||||
registry_path = Path(args.registry).expanduser()
|
||||
output_path = Path(args.output).expanduser()
|
||||
output_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
contents = render(load_instances(registry_path), args.upstream_port)
|
||||
output_path.write_text(contents, encoding="utf-8")
|
||||
print(output_path)
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
24
router-proxy/runtime/conf.d/instances.conf
Normal file
24
router-proxy/runtime/conf.d/instances.conf
Normal file
@ -0,0 +1,24 @@
|
||||
server {
|
||||
listen 80 default_server;
|
||||
server_name _;
|
||||
return 404;
|
||||
}
|
||||
|
||||
server {
|
||||
listen 80;
|
||||
server_name deploy-smoke-001.127.0.0.1.nip.io;
|
||||
|
||||
location / {
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
proxy_set_header X-Forwarded-Host $host;
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection $connection_upgrade;
|
||||
proxy_read_timeout 3600s;
|
||||
proxy_send_timeout 3600s;
|
||||
proxy_pass http://app-instance-deploy-smoke-001:8080;
|
||||
}
|
||||
}
|
||||
67
router-proxy/start-proxy.sh
Executable file
67
router-proxy/start-proxy.sh
Executable file
@ -0,0 +1,67 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
RENDER_SCRIPT="${SCRIPT_DIR}/render-routes.py"
|
||||
|
||||
PROXY_IMAGE="${PROXY_IMAGE:-nginx:1.27-alpine}"
|
||||
PROXY_CONTAINER_NAME="${PROXY_CONTAINER_NAME:-nano-router-proxy}"
|
||||
PROXY_NETWORK_NAME="${PROXY_NETWORK_NAME:-nano-instance-edge}"
|
||||
PROXY_HTTP_PORT="${PROXY_HTTP_PORT:-8088}"
|
||||
REGISTRY_PATH="${REGISTRY_PATH:-${SCRIPT_DIR}/../app-instance/runtime/registry/instances.json}"
|
||||
OUTPUT_PATH="${OUTPUT_PATH:-${SCRIPT_DIR}/runtime/conf.d/instances.conf}"
|
||||
REPLACE=0
|
||||
|
||||
usage() {
|
||||
cat <<'EOF'
|
||||
Usage:
|
||||
./start-proxy.sh [--replace]
|
||||
EOF
|
||||
}
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--replace)
|
||||
REPLACE=1
|
||||
shift
|
||||
;;
|
||||
--help|-h)
|
||||
usage
|
||||
exit 0
|
||||
;;
|
||||
*)
|
||||
printf '[start-proxy] unknown argument: %s\n' "$1" >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
python3 "$RENDER_SCRIPT" --registry "$REGISTRY_PATH" --output "$OUTPUT_PATH" >/dev/null
|
||||
|
||||
if ! docker network inspect "$PROXY_NETWORK_NAME" >/dev/null 2>&1; then
|
||||
docker network create "$PROXY_NETWORK_NAME" >/dev/null
|
||||
fi
|
||||
|
||||
if docker container inspect "$PROXY_CONTAINER_NAME" >/dev/null 2>&1; then
|
||||
if [[ "$REPLACE" -eq 1 ]]; then
|
||||
docker rm -f "$PROXY_CONTAINER_NAME" >/dev/null
|
||||
else
|
||||
printf '[start-proxy] container already running: %s\n' "$PROXY_CONTAINER_NAME"
|
||||
exit 0
|
||||
fi
|
||||
fi
|
||||
|
||||
mkdir -p "${SCRIPT_DIR}/runtime/conf.d"
|
||||
|
||||
docker run -d \
|
||||
--name "$PROXY_CONTAINER_NAME" \
|
||||
--restart unless-stopped \
|
||||
--network "$PROXY_NETWORK_NAME" \
|
||||
-p "${PROXY_HTTP_PORT}:80" \
|
||||
-v "${SCRIPT_DIR}/nginx.conf:/etc/nginx/nginx.conf:ro" \
|
||||
-v "${SCRIPT_DIR}/runtime/conf.d:/etc/nginx/conf.d:ro" \
|
||||
"$PROXY_IMAGE" >/dev/null
|
||||
|
||||
printf 'container_name=%s\n' "$PROXY_CONTAINER_NAME"
|
||||
printf 'network_name=%s\n' "$PROXY_NETWORK_NAME"
|
||||
printf 'http_port=%s\n' "$PROXY_HTTP_PORT"
|
||||
Reference in New Issue
Block a user