Files
ykxmz/_build/deploy_lightsail.py
xmz7891 0ad3f648c6 修上一轮清理留下的副作用:占位符把运维脚本弄连不上了
上一个提交把仓库里的服务器 IP 换成了占位符,但**那些脚本本身要用这个 IP
去连服务器** —— 换掉之后它们就跑不起来了。这个提交把取值方式理清楚。

== 取值统一走 _build/lssh.py ==

    YKXMZ_LS_HOST 环境变量
    或 _build/.lightsail_host(一行 IP,已 gitignore)
    都没有就报错,并告诉你怎么设

十个运维脚本改成 `from lssh import ... HOST as LS_HOST`,
公开仓库里一个具体地址都不留。

== 顺带修的两个自己造的 bug ==

  * 用正则往 `from lssh import ...` 后面追加东西时,
    追加到了行尾的 `# noqa: E402` **注释里面** ——
    十处导入全是废的,脚本一跑就 NameError。
    改成先剥掉注释、拼好导入、再把 noqa 加回去。

  * 同一个正则还把 `from lssh import HOST as LS_HOST` 加进了
    **lssh.py 自己**,造成自我导入。

教训:**正则批量改导入语句很危险**(注释、行尾、顺序都能坑到),
这种地方宁可逐个 edit。

== 为什么要重建产物 ==

docs/AFTER-INSTALL.txt 会被打进服务端 zip,而它上一轮改过
(去掉了写死的旧管理员密码)—— 不重打的话发出去的还是旧的。

重打后确认:三个产物里都没有旧密码、没有服务器 IP。

== 验证 ==

    全量回归退出码 0,41 个阶段全 PASS
    603 个 Python 文件语法通过
    ykxmz-agent / server / watch 三个产物扫描干净
    提交前泄漏闸门通过
2026-10-09 00:02:32 +08:00

176 lines
6.1 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# -*- coding: utf-8 -*-
r'''把中转服务器部署到 AWS Lightsail 那台新机器上。
== 为什么要搬 ==
原来中转跑在一台"随身 WiFi"小机器上(Debian aarch64、460MB 内存、
走 4G),一直有一堆搞不定的问题:
* 线路对 Cloudflare 隧道的 **7844 端口间歇性阻断**
—— 443 通、7844 不通,隧道每 3~10 分钟就掉一次,公网跟着 530
(这个查了很久,cloudflared 自己的 precheck 才说清楚)
* 只有 460MB 内存,跑什么都得省着来
* 会掉电/硬挂(实测 18:42 一次非正常关机,19:58 才起来)
* 全靠 Cloudflare 隧道兜,隧道一抖网站就抖
新机器(Lightsail)**有公网 IP**,直接监听就行 ——
不需要隧道、不经过 Cloudflare 的 7844、内存是 4 倍、磁盘 59G。
上面那些问题**一次性全部消失**。
== 关键配置 ==
RELAY_PORT=13245 用户指定(防火墙那边他会开)
RELAY_BIND=0.0.0.0 有公网 IP,直接对外
13245 大于 1024,所以**不需要** CAP_NET_BIND_SERVICE,
用普通用户跑就行 —— 比听 80 干净。
== 为什么不用 root 跑 ==
relay 是个只读文件的 HTTP 服务,没有任何理由给它 root。
用 admin 用户跑,万一被打穿,损失也小得多。
(2026-10-08 之前那个仓库里还硬编码过管理员密码,
这种教训够一次就行了。)
'''
from __future__ import annotations
import sys
import tempfile
from pathlib import Path
ROOT = Path(__file__).resolve().parent.parent
sys.path.insert(0, str(ROOT / "_build"))
from lssh import run, upload, HOST as LS_HOST # noqa: E402
# 用户指定的端口
PORT = "13245"
REMOTE_DIR = "/opt/ykxmz-relay"
DATA_DIR = REMOTE_DIR + "/data" # relay.py 现在默认就用 data/ 了
# 服务端要传的文件(.py 是服务本体,.ico 是管理页图标)
FILES = ["relay.py", "admin.py", "landing.py", "showcase.py", "favicon.ico"]
UNIT = '''[Unit]
Description=ykxmz 中转服务器
After=network-online.target
Wants=network-online.target
[Service]
Type=simple
User=admin
Group=admin
WorkingDirectory=%(dir)s
# ⚠ 监听地址和端口都从这里来。
# 13245 是用户指定的(他会在 Lightsail 防火墙那边开)。
# BIND 用 0.0.0.0 是因为新机器**有公网 IP**,不再需要
# Cloudflare 隧道(原来那个隧道在这条线路上每 3~10 分钟掉一次,
# 是"网站容易挂"的主因之一)。
Environment=RELAY_PORT=%(port)s
Environment=RELAY_BIND=0.0.0.0
ExecStart=/usr/bin/python3 %(dir)s/relay.py
Restart=always
RestartSec=3
# 兜底:内存保护(这台有 2G,给 512M 足够且不会拖垮整机)
MemoryMax=512M
# 别让它写爆磁盘
LimitNOFILE=4096
StandardOutput=append:%(dir)s/relay.log
StandardError=append:%(dir)s/relay.log
[Install]
WantedBy=multi-user.target
'''
def main() -> int:
print("=" * 62)
print(" 部署到 Lightsail(LS_HOST),端口 %s" % PORT)
print("=" * 62)
print()
print(" ── 1. 建目录 ──")
out = run("sudo mkdir -p %s %s && sudo chown -R admin:admin %s && "
"echo OK" % (REMOTE_DIR, DATA_DIR, REMOTE_DIR),
timeout=120)
for ln in out[1].strip().splitlines():
print(" " + ln)
print()
print(" ── 2. 传服务端文件 ──")
for name in FILES:
local = ROOT / "relay" / name
if not local.is_file():
print(" [跳过] %s 不存在" % name)
continue
upload(str(local), "%s/%s" % (REMOTE_DIR, name))
# 产品截图(官网要用)
shots = ROOT / "relay" / "site_shots"
if shots.is_dir():
run("mkdir -p %s/site_shots" % REMOTE_DIR, timeout=60, quiet=True)
for f in sorted(shots.iterdir()):
if f.is_file():
upload(str(f), "%s/site_shots/%s" % (REMOTE_DIR, f.name))
else:
print(" [警告] 没有 relay/site_shots,官网截图会 404")
print()
print(" ── 3. 写 systemd unit ──")
unit = UNIT % {"dir": REMOTE_DIR, "port": PORT}
tmp = Path(tempfile.gettempdir()) / "ykxmz-relay.service"
tmp.write_text(unit, encoding="utf-8", newline="\n")
upload(str(tmp), "/tmp/ykxmz-relay.service")
out = run("sudo mv /tmp/ykxmz-relay.service /etc/systemd/system/ && "
"sudo systemctl daemon-reload && echo ' unit 已装'",
timeout=120)
for ln in out[1].strip().splitlines():
print(" " + ln)
print()
print(" ── 4. 语法检查 + 启动 ──")
out = run(
"cd %s && for f in relay.py admin.py landing.py showcase.py; do "
"python3 -c \"import ast,sys;ast.parse(open(sys.argv[1],"
"encoding='utf-8').read())\" $f || exit 1; done && echo PY_OK"
% REMOTE_DIR, timeout=120)
for ln in out[1].strip().splitlines():
print(" " + ln)
out = run("sudo systemctl enable ykxmz-relay >/dev/null 2>&1; "
"sudo systemctl restart ykxmz-relay; sleep 5; "
"systemctl is-active ykxmz-relay", timeout=180)
for ln in out[1].strip().splitlines():
print(" " + ln)
print()
print(" ── 5. 自测 ──")
out = run(
"echo \" 本机 health: $(curl -s -o /dev/null -w '%%{http_code}' "
"--max-time 8 http://127.0.0.1:%(p)s/v1/health)\"; "
"echo \" 监听: $(ss -tlnp 2>/dev/null | grep %(p)s | head -1)\"; "
"echo ' --- 启动日志 ---'; "
"tail -12 %(d)s/relay.log 2>/dev/null"
% {"p": PORT, "d": REMOTE_DIR}, timeout=120)
for ln in out[1].strip().splitlines():
print(" " + ln)
print()
print(" ── 6. 管理密码(登录后台用)──")
out = run("cat %s/data/admin_password.txt 2>/dev/null; echo; "
"cat %s/data/admin_key.txt 2>/dev/null" % (DATA_DIR, DATA_DIR),
timeout=60)
for ln in out[1].strip().splitlines():
print(" " + ln)
print()
print(" 提示:防火墙要先放行 TCP %s(用户来开)。" % PORT)
print(" 之后公网地址就是 http://LS_HOST:%s/" % PORT)
return 0
if __name__ == "__main__":
sys.exit(main())