commit d34f04427a0a2ab8ed4810a4dccf3281cd97aa3d Author: lingyuzeng Date: Thu Oct 2 19:04:38 2025 +0800 first commit diff --git a/.env b/.env new file mode 100644 index 0000000..eaddbb6 --- /dev/null +++ b/.env @@ -0,0 +1,23 @@ +# === 本机(该内网节点)的 Tailscale IP(与宿主机共用)=== +LOCAL_TS_IP=100.64.0.10 + +# === 云端 Consul Server(阿里云那台的 TS IP)=== +CONSUL_SERVER_IP=100.64.0.1 +CONSUL_DC=dc1 + +# === 示例服务(你可以替换为任意容器或端口)=== +SERVICE_NAME=mypy +SERVICE_PORT=8229 +ROUTE_HOST=api.jmsu.top + +# === 健康检查 === +CHECK_TYPE=http # http | tcp +CHECK_PATH=/ +CHECK_INTERVAL=10s +CHECK_TIMEOUT=2s +DEREG_AFTER=1m + +# === Traefik 入口 & 证书解析器(与云端对应)=== +TRAEFIK_HTTP_ENTRYPOINT=websecure +TRAEFIK_TCP_ENTRYPOINT=tcp +TRAEFIK_CERT_RESOLVER=cf # 或 alidns diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..9079971 --- /dev/null +++ b/.env.example @@ -0,0 +1,26 @@ +# === 本机(该内网节点)的 Tailscale IP(与宿主机共用)=== +LOCAL_TS_IP=100.64.0.42 + +# === 云端 Consul Server(阿里云那台的 TS IP)=== +CONSUL_SERVER_IP=100.64.0.1 +CONSUL_DC=dc1 + +# === 示例服务(你可以替换为任意容器或端口)=== +SERVICE_NAME=mypy +SERVICE_PORT=8229 +ROUTE_HOST=api.jmsu.top + +# === 健康检查 === +CHECK_TYPE=http # http | tcp +CHECK_PATH=/ +CHECK_INTERVAL=10s +CHECK_TIMEOUT=2s +DEREG_AFTER=1m + +# === Traefik 入口 & 证书解析器(与云端对应)=== +TRAEFIK_HTTP_ENTRYPOINT=websecure +TRAEFIK_TCP_ENTRYPOINT=tcp +TRAEFIK_CERT_RESOLVER=cf # 或 alidns + + +使用时把该文件复制为 .env 并按需修改变量。 \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..6d7c0bc --- /dev/null +++ b/README.md @@ -0,0 +1,279 @@ +# Edge Node - Tailscale + Consul + Traefik 边缘节点方案 + +本项目提供了一个完整的边缘节点解决方案,通过 Tailscale 网络将内网服务安全地暴露到云端,使用 Consul 进行服务发现,Traefik 进行流量代理和 SSL 终端。 + +## 🏗️ 架构概览 + +``` +┌────────────────────────────────────────────────────────────────────┐ +│ 云端 (阿里云) │ +├─────────────────┬─────────────────┬─────────────────────────────────┤ +│ Traefik │ Consul Server │ 其他服务 │ +│ (代理入口) │ (服务发现) │ │ +└─────────────────┴─────────────────┴─────────────────────────────────┘ + │ + Tailscale 网络 (100.64.x.x) + │ +┌────────────────────────────────────────────────────────────────────┐ +│ 边缘节点 (内网) │ +├─────────────────┬─────────────────┬─────────────────────────────────┤ +│ Consul Agent │ 你的服务 │ Registrar (注册器) │ +│ (客户端) │ (API/应用) │ (自动注册到云端) │ +└─────────────────┴─────────────────┴─────────────────────────────────┘ +``` + +## 📁 项目结构 + +``` +edge-node/ +├── docker-compose.yml # 服务编排配置 +├── registrar.sh # 服务注册脚本 +├── .env.example # 环境变量模板 +└── README.md # 本文档 +``` + +## 🚀 快速开始 + +### 1. 环境准备 + +确保已安装: +- Docker & Docker Compose +- Tailscale 客户端 +- 有效的域名(用于 HTTPS) + +### 2. 配置 Tailscale + +在所有节点(云端和边缘节点)上安装并配置 Tailscale: + +```bash +# 安装 Tailscale +curl -fsSL https://tailscale.com/install.sh | sh + +# 启动并认证 +tailscale up + +# 查看 Tailscale IP +tailscale ip -4 +``` + +### 3. 配置环境变量 + +```bash +cd edge-node +cp .env.example .env +``` + +编辑 `.env` 文件,修改以下关键配置: + +```bash +# 边缘节点的 Tailscale IP +LOCAL_TS_IP=100.64.0.42 + +# 云端 Consul Server 的 Tailscale IP +CONSUL_SERVER_IP=100.64.0.1 + +# 要暴露的服务配置 +SERVICE_NAME=mypy +SERVICE_PORT=8229 +ROUTE_HOST=api.jmsu.top +``` + +### 4. 启动服务 + +```bash +docker compose up -d +``` + +### 5. 验证部署 + +- 检查服务状态:`docker compose ps` +- 查看 Consul 注册:`curl http://$CONSUL_SERVER_IP:8500/v1/agent/services` +- 访问 HTTPS:`https://api.jmsu.top` + +## ⚙️ 配置详解 + +### 环境变量说明 + +| 变量名 | 说明 | 示例 | +|-------|------|------| +| `LOCAL_TS_IP` | 边缘节点的 Tailscale IP | `100.64.0.42` | +| `CONSUL_SERVER_IP` | 云端 Consul Server IP | `100.64.0.1` | +| `CONSUL_DC` | 数据中心名称 | `dc1` | +| `SERVICE_NAME` | 服务名称(用于路由) | `mypy` | +| `SERVICE_PORT` | 服务端口 | `8229` | +| `ROUTE_HOST` | 暴露的域名 | `api.jmsu.top` | +| `CHECK_TYPE` | 健康检查类型 | `http` 或 `tcp` | +| `CHECK_PATH` | HTTP 检查路径 | `/` | +| `TRAEFIK_CERT_RESOLVER` | 证书解析器 | `cf` 或 `alidns` | + +### 服务组件 + +#### 1. Consul Agent(客户端) +- 连接到云端 Consul Server +- 提供本地服务发现 +- 使用 host 网络模式避免端口冲突 + +#### 2. API 服务 +- 示例 Python HTTP 服务 +- 仅绑定到 Tailscale IP(安全) +- 可替换为任何容器化服务 + +#### 3. Registrar(注册器) +- 自动将服务注册到云端 Consul +- 生成 Traefik 路由配置 +- 支持 HTTP/TCP 协议 +- 包含健康检查机制 + +## 🔧 高级配置 + +### TCP 服务配置 + +修改 `.env` 中的协议设置: + +```bash +SERVICE_PROTOCOL=tcp +ROUTE_HOST=tcp.jmsu.top +CHECK_TYPE=tcp +``` + +### 自定义健康检查 + +```bash +# HTTP 检查 +CHECK_TYPE=http +CHECK_PATH=/health +CHECK_INTERVAL=30s +CHECK_TIMEOUT=5s + +# TCP 检查 +CHECK_TYPE=tcp +CHECK_INTERVAL=10s +``` + +### 多服务部署 + +复制 `docker-compose.yml` 中的服务定义,修改: +- 服务名称 +- 端口配置 +- 环境变量 + +## 🔍 调试与监控 + +### 查看日志 + +```bash +# 查看所有服务日志 +docker compose logs -f + +# 查看特定服务 +docker compose logs -f registrar +``` + +### Consul 状态检查 + +```bash +# 检查 Consul Agent 状态 +curl http://localhost:8500/v1/agent/self + +# 检查服务注册 +consul catalog services + +# 检查健康状态 +consul health check service mypy +``` + +### 常见问题排查 + +1. **Consul 连接失败** + - 检查 Tailscale 网络连通性 + - 验证云端 Consul Server 状态 + - 确认防火墙端口开放 + +2. **服务注册失败** + - 检查环境变量配置 + - 验证服务端口监听状态 + - 查看 registrar 容器日志 + +3. **HTTPS 证书问题** + - 确认域名解析正确 + - 检查 Traefik 证书解析器配置 + - 验证 Cloudflare/API 密钥 + +## 🔒 安全建议 + +1. **网络隔离** + - 服务仅绑定 Tailscale IP + - 避免暴露到公网 + - 使用 Tailscale ACL 控制访问 + +2. **访问控制** + - 配置 Consul ACL + - 使用 Traefik 中间件 + - 启用 Tailscale 设备认证 + +3. **证书管理** + - 使用自动证书续期 + - 定期轮换 API 密钥 + - 监控证书有效期 + +## 🚀 扩展功能 + +### 自定义服务镜像 + +替换 `docker-compose.yml` 中的 `api` 服务: + +```yaml +api: + image: your-registry/your-app:latest + container_name: your-app + ports: + - "${LOCAL_TS_IP}:${SERVICE_PORT}:${SERVICE_PORT}" + environment: + - APP_ENV=production + depends_on: + consul-agent: + condition: service_healthy +``` + +### 环境变量管理 + +使用 Docker secrets 或外部配置管理: + +```bash +# 使用外部 env 文件 +docker compose --env-file .env.production up -d + +# 使用环境变量 +export LOCAL_TS_IP=100.64.0.42 +docker compose up -d +``` + +### 监控集成 + +添加 Prometheus 指标收集: + +```yaml +# 在 docker-compose.yml 中添加 +prometheus: + image: prom/prometheus + container_name: prometheus + ports: + - "${LOCAL_TS_IP}:9090:9090" + volumes: + - ./prometheus.yml:/etc/prometheus/prometheus.yml +``` + +## 📚 相关文档 + +- [Tailscale 文档](https://tailscale.com/kb/) +- [Consul 文档](https://www.consul.io/docs) +- [Traefik 文档](https://doc.traefik.io/traefik/) +- [Docker Compose 文档](https://docs.docker.com/compose/) + +## 🤝 贡献 + +欢迎提交 Issue 和 Pull Request! + +## 📄 许可证 + +MIT License - 详见 LICENSE 文件 \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..ed6409f --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,69 @@ +version: "3.9" + +services: + # 1) 本机 Consul agent(client) + consul-agent: + image: hashicorp/consul:1.21 + container_name: consul-agent + network_mode: "host" # 避免 8301/udp/lan gossip 的端口映射问题 + command: > + agent + -server=false + -client=0.0.0.0 + -bind=${LOCAL_TS_IP} + -advertise=${LOCAL_TS_IP} + -retry-join=${CONSUL_SERVER_IP} + -datacenter=${CONSUL_DC} + -data-dir=/consul/data + -leave-on-terminate + volumes: + - ./consul-data:/consul/data + healthcheck: + test: ["CMD-SHELL", "wget -qO- http://127.0.0.1:8500/v1/agent/self >/dev/null"] + interval: 3s + timeout: 2s + retries: 60 + restart: unless-stopped + + # 2) 你的服务(示例:Python http.server) + api: + image: python:3.12-slim + container_name: api + command: ["python", "-m", "http.server", "${SERVICE_PORT}"] + # 关键:只绑定到本机 Tailscale IP,避免暴露到 0.0.0.0 + ports: + - "${LOCAL_TS_IP}:${SERVICE_PORT}:${SERVICE_PORT}" + depends_on: + consul-agent: + condition: service_healthy + restart: unless-stopped + + # 3) registrar 旁车:把服务注册到"云端" Consul(server) + registrar: + image: hashicorp/consul:1.21 + container_name: registrar + network_mode: "host" + depends_on: + consul-agent: + condition: service_healthy + api: + condition: service_started + environment: + SERVICE_NAME: "${SERVICE_NAME}" + SERVICE_ADDR: "${LOCAL_TS_IP}" # 共用宿主 Tailscale IP + SERVICE_PORT: "${SERVICE_PORT}" + SERVICE_PROTOCOL: "http" # http | tcp + ROUTE_HOST: "${ROUTE_HOST}" # 要暴露的域名 + CONSUL_HTTP_ADDR: "http://${CONSUL_SERVER_IP}:8500" + CHECK_TYPE: "${CHECK_TYPE}" + CHECK_PATH: "${CHECK_PATH}" + CHECK_INTERVAL: "${CHECK_INTERVAL}" + CHECK_TIMEOUT: "${CHECK_TIMEOUT}" + DEREG_AFTER: "${DEREG_AFTER}" + TRAEFIK_HTTP_ENTRYPOINT: "${TRAEFIK_HTTP_ENTRYPOINT}" + TRAEFIK_TCP_ENTRYPOINT: "${TRAEFIK_TCP_ENTRYPOINT}" + TRAEFIK_CERT_RESOLVER: "${TRAEFIK_CERT_RESOLVER}" + volumes: + - ./registrar.sh:/registrar.sh:ro + entrypoint: ["/bin/sh","-lc","/registrar.sh"] + restart: unless-stopped \ No newline at end of file diff --git a/registrar.sh b/registrar.sh new file mode 100755 index 0000000..d43d8c4 --- /dev/null +++ b/registrar.sh @@ -0,0 +1,82 @@ +#!/bin/sh +set -eu + +: "${SERVICE_NAME:?need SERVICE_NAME}" +: "${SERVICE_ADDR:?need SERVICE_ADDR}" +: "${SERVICE_PORT:?need SERVICE_PORT}" +: "${ROUTE_HOST:?need ROUTE_HOST}" + +CONSUL="${CONSUL_HTTP_ADDR:?need CONSUL_HTTP_ADDR}" +SERVICE_PROTOCOL="${SERVICE_PROTOCOL:-http}" # http | tcp +CHECK_TYPE="${CHECK_TYPE:-tcp}" # http | tcp +CHECK_PATH="${CHECK_PATH:-/}" +CHECK_INTERVAL="${CHECK_INTERVAL:-10s}" +CHECK_TIMEOUT="${CHECK_TIMEOUT:-2s}" +DEREG_AFTER="${DEREG_AFTER:-1m}" +TRAEFIK_HTTP_ENTRYPOINT="${TRAEFIK_HTTP_ENTRYPOINT:-websecure}" +TRAEFIK_TCP_ENTRYPOINT="${TRAEFIK_TCP_ENTRYPOINT:-tcp}" +# TRAEFIK_CERT_RESOLVER="${TRAEFIK_CERT_RESOLVER:-cf}" + +echo "[registrar] consul: $CONSUL, service: $SERVICE_NAME@$SERVICE_ADDR:$SERVICE_PORT" + +# 等云端 Consul Server 可用 +for i in $(seq 1 90); do + if wget -qO- "$CONSUL/v1/status/leader" >/dev/null 2>&1; then + break + fi + sleep 1 +done + +ID="${SERVICE_NAME}-${SERVICE_ADDR}-${SERVICE_PORT}" + +# 组装 Traefik tags(ConsulCatalog) +TAGS="traefik.enable=true" +if [ "$SERVICE_PROTOCOL" = "http" ]; then + TAGS="$TAGS,traefik.http.routers.${SERVICE_NAME}.rule=Host(\`${ROUTE_HOST}\`)" + TAGS="$TAGS,traefik.http.routers.${SERVICE_NAME}.entrypoints=${TRAEFIK_HTTP_ENTRYPOINT}" + TAGS="$TAGS,traefik.http.routers.${SERVICE_NAME}.tls=true" + TAGS="$TAGS,traefik.http.services.${SERVICE_NAME}.loadbalancer.server.scheme=http" + TAGS="$TAGS,traefik.http.services.${SERVICE_NAME}.loadbalancer.server.port=${SERVICE_PORT}" + # 可选:应用云端 dynamic.yml 的中间件 + TAGS="$TAGS,traefik.http.routers.${SERVICE_NAME}.middlewares=gzip-all@file,security-headers@file" +elif [ "$SERVICE_PROTOCOL" = "tcp" ]; then + TAGS="$TAGS,traefik.tcp.routers.${SERVICE_NAME}.rule=HostSNI(\`${ROUTE_HOST}\`)" + TAGS="$TAGS,traefik.tcp.routers.${SERVICE_NAME}.entrypoints=${TRAEFIK_TCP_ENTRYPOINT}" + TAGS="$TAGS,traefik.tcp.services.${SERVICE_NAME}.loadbalancer.server.port=${SERVICE_PORT}" +else + echo "unsupported SERVICE_PROTOCOL=$SERVICE_PROTOCOL" >&2; exit 2 +fi + +# 转 JSON 数组(按逗号拆分) +to_json_array() { echo "$1" | awk -v RS=, 'NF{print "\""$0"\""}' | paste -sd, - | sed 's/^/[/' | sed 's/$/]/'; } +TAGS_JSON="$(to_json_array "$TAGS")" + +# 健康检查 JSON +if [ "$CHECK_TYPE" = "http" ]; then + CHECK_JSON=$(cat < /tmp/svc.json <