first commit

This commit is contained in:
2025-10-02 19:04:38 +08:00
commit d34f04427a
5 changed files with 479 additions and 0 deletions

23
.env Normal file
View File

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

26
.env.example Normal file
View File

@@ -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 并按需修改变量。

279
README.md Normal file
View File

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

69
docker-compose.yml Normal file
View File

@@ -0,0 +1,69 @@
version: "3.9"
services:
# 1) 本机 Consul agentclient
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 旁车:把服务注册到"云端" Consulserver
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

82
registrar.sh Executable file
View File

@@ -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 tagsConsulCatalog
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 <<EOF
{"Name":"http","HTTP":"http://${SERVICE_ADDR}:${SERVICE_PORT}${CHECK_PATH}","Interval":"${CHECK_INTERVAL}","Timeout":"${CHECK_TIMEOUT}","DeregisterCriticalServiceAfter":"${DEREG_AFTER}"}
EOF
)
else
CHECK_JSON=$(cat <<EOF
{"Name":"tcp","TCP":"${SERVICE_ADDR}:${SERVICE_PORT}","Interval":"${CHECK_INTERVAL}","Timeout":"${CHECK_TIMEOUT}","DeregisterCriticalServiceAfter":"${DEREG_AFTER}"}
EOF
)
fi
# 写 service 定义并注册到"云端" Consul Server
cat > /tmp/svc.json <<EOF
{"service":{"id":"${ID}","name":"${SERVICE_NAME}","address":"${SERVICE_ADDR}","port":${SERVICE_PORT},"tags":${TAGS_JSON},"checks":[${CHECK_JSON}]}}
EOF
echo "[registrar] register ${ID} -\u003e ${CONSUL}"
consul services register -http-addr="$CONSUL" /tmp/svc.json
term() {
echo "[registrar] deregister ${ID}"
consul services deregister -http-addr="$CONSUL" /tmp/svc.json || true
exit 0
}
trap term TERM INT
tail -f /dev/null