chore: add go native ecs deploy script
This commit is contained in:
@@ -173,21 +173,24 @@ python3 tools/go_native_prod_smoke.py --date 2026-07-02 --timeout 8
|
|||||||
|
|
||||||
## 部署命令
|
## 部署命令
|
||||||
|
|
||||||
在开发机或 CI 构建 Linux amd64:
|
推荐使用仓库脚本发布完整 Go 原生 release。脚本会在本机/CI 构建 Linux amd64 四个二进制,打包上传到应用 ECS,切换 `/opt/lingniu-go-native/current`,逐个重启四个 systemd 服务,并在发布后运行聚合生产验收:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
cd go/vehicle-gateway
|
python3 tools/go_native_deploy.py --date 2026-07-02
|
||||||
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -trimpath -ldflags='-s -w' -o /tmp/lingniu-go-native/gateway ./cmd/gateway
|
|
||||||
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -trimpath -ldflags='-s -w' -o /tmp/lingniu-go-native/history-writer ./cmd/history-writer
|
|
||||||
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -trimpath -ldflags='-s -w' -o /tmp/lingniu-go-native/stat-writer ./cmd/stat-writer
|
|
||||||
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -trimpath -ldflags='-s -w' -o /tmp/lingniu-go-native/realtime-api ./cmd/realtime-api
|
|
||||||
```
|
```
|
||||||
|
|
||||||
上传到 ECS 新 release 目录后切换:
|
脚本不会保存 SSH 或数据库密钥;认证仍使用当前操作环境可用的 SSH 凭据。需要跳过发布后验收时可显式追加 `--skip-acceptance`,但生产发布默认应保留验收。
|
||||||
|
|
||||||
|
脚本执行的发布动作等价于:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -trimpath -ldflags='-s -w' ./cmd/gateway
|
||||||
|
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -trimpath -ldflags='-s -w' ./cmd/history-writer
|
||||||
|
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -trimpath -ldflags='-s -w' ./cmd/stat-writer
|
||||||
|
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -trimpath -ldflags='-s -w' ./cmd/realtime-api
|
||||||
|
scp release.tar.gz root@115.29.187.205:/tmp/
|
||||||
|
ln -sfn /opt/lingniu-go-native/releases/<git-short-sha> /opt/lingniu-go-native/current
|
||||||
systemctl daemon-reload
|
systemctl daemon-reload
|
||||||
systemctl enable lingniu-go-gateway lingniu-go-history-writer lingniu-go-stat-writer lingniu-go-realtime-api
|
|
||||||
systemctl restart lingniu-go-gateway lingniu-go-history-writer lingniu-go-stat-writer lingniu-go-realtime-api
|
systemctl restart lingniu-go-gateway lingniu-go-history-writer lingniu-go-stat-writer lingniu-go-realtime-api
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
196
tools/go_native_deploy.py
Normal file
196
tools/go_native_deploy.py
Normal file
@@ -0,0 +1,196 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Build and deploy the Go native vehicle gateway release to ECS."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import os
|
||||||
|
import pathlib
|
||||||
|
import shutil
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
import tarfile
|
||||||
|
import tempfile
|
||||||
|
from dataclasses import dataclass
|
||||||
|
|
||||||
|
|
||||||
|
DEFAULT_APP_HOST = "115.29.187.205"
|
||||||
|
DEFAULT_KAFKA_HOST = "114.55.58.251"
|
||||||
|
DEFAULT_USER = "root"
|
||||||
|
DEFAULT_RELEASE_ROOT = "/opt/lingniu-go-native"
|
||||||
|
DEFAULT_GO_DIR = "go/vehicle-gateway"
|
||||||
|
BINARIES = [
|
||||||
|
("gateway", "./cmd/gateway"),
|
||||||
|
("history-writer", "./cmd/history-writer"),
|
||||||
|
("stat-writer", "./cmd/stat-writer"),
|
||||||
|
("realtime-api", "./cmd/realtime-api"),
|
||||||
|
]
|
||||||
|
SERVICES = [
|
||||||
|
"lingniu-go-gateway",
|
||||||
|
"lingniu-go-history-writer",
|
||||||
|
"lingniu-go-stat-writer",
|
||||||
|
"lingniu-go-realtime-api",
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class BuildCommand:
|
||||||
|
output_name: str
|
||||||
|
argv: list[str]
|
||||||
|
env: list[str]
|
||||||
|
cwd: str
|
||||||
|
|
||||||
|
|
||||||
|
def git_short_sha(cwd: str) -> str:
|
||||||
|
return run_capture(["git", "rev-parse", "--short=7", "HEAD"], cwd=cwd).strip()
|
||||||
|
|
||||||
|
|
||||||
|
def build_commands(go_dir: str, output_dir: str) -> list[BuildCommand]:
|
||||||
|
commands: list[BuildCommand] = []
|
||||||
|
env = ["CGO_ENABLED=0", "GOOS=linux", "GOARCH=amd64"]
|
||||||
|
for output_name, package in BINARIES:
|
||||||
|
commands.append(BuildCommand(
|
||||||
|
output_name=output_name,
|
||||||
|
argv=[
|
||||||
|
"go",
|
||||||
|
"build",
|
||||||
|
"-trimpath",
|
||||||
|
"-ldflags=-s -w",
|
||||||
|
"-o",
|
||||||
|
str(pathlib.Path(output_dir) / output_name),
|
||||||
|
package,
|
||||||
|
],
|
||||||
|
env=env,
|
||||||
|
cwd=go_dir,
|
||||||
|
))
|
||||||
|
return commands
|
||||||
|
|
||||||
|
|
||||||
|
def build_release(go_dir: str, output_dir: str) -> None:
|
||||||
|
for command in build_commands(go_dir, output_dir):
|
||||||
|
env = os.environ.copy()
|
||||||
|
for item in command.env:
|
||||||
|
key, value = item.split("=", 1)
|
||||||
|
env[key] = value
|
||||||
|
run(command.argv, cwd=command.cwd, env=env)
|
||||||
|
|
||||||
|
|
||||||
|
def create_archive(source_dir: str, archive_path: str) -> None:
|
||||||
|
with tarfile.open(archive_path, "w:gz") as archive:
|
||||||
|
for binary, _ in BINARIES:
|
||||||
|
archive.add(pathlib.Path(source_dir) / binary, arcname=binary)
|
||||||
|
|
||||||
|
|
||||||
|
def remote_switch_command(release: str, release_root: str, remote_archive: str) -> str:
|
||||||
|
if not safe_release_name(release):
|
||||||
|
raise ValueError("release must contain only letters, numbers, dot, underscore, or dash")
|
||||||
|
release_dir = release_root.rstrip("/") + "/releases/" + release
|
||||||
|
root = release_root.rstrip("/")
|
||||||
|
commands = [
|
||||||
|
"set -euo pipefail",
|
||||||
|
"mkdir -p " + shell_quote(release_dir),
|
||||||
|
"tar -xzf " + shell_quote(remote_archive) + " -C " + shell_quote(release_dir),
|
||||||
|
"chmod +x " + shell_quote(release_dir) + "/*",
|
||||||
|
"ln -sfn " + shell_quote(release_dir) + " " + shell_quote(root + "/current"),
|
||||||
|
"systemctl daemon-reload",
|
||||||
|
]
|
||||||
|
commands.extend("systemctl restart " + service for service in SERVICES)
|
||||||
|
commands.append("rm -f " + shell_quote(remote_archive))
|
||||||
|
return " && ".join(commands)
|
||||||
|
|
||||||
|
|
||||||
|
def acceptance_command(app_host: str, kafka_host: str, date: str, timeout: float) -> list[str]:
|
||||||
|
command = [
|
||||||
|
"python3",
|
||||||
|
"tools/go_prod_acceptance.py",
|
||||||
|
"--app-host",
|
||||||
|
app_host,
|
||||||
|
"--kafka-host",
|
||||||
|
kafka_host,
|
||||||
|
"--timeout",
|
||||||
|
str(int(timeout) if timeout == int(timeout) else timeout),
|
||||||
|
]
|
||||||
|
if date:
|
||||||
|
command.extend(["--date", date])
|
||||||
|
return command
|
||||||
|
|
||||||
|
|
||||||
|
def deploy(args: argparse.Namespace) -> None:
|
||||||
|
repo = pathlib.Path(args.repo).resolve()
|
||||||
|
go_dir = str(repo / args.go_dir)
|
||||||
|
release = args.release or git_short_sha(str(repo))
|
||||||
|
with tempfile.TemporaryDirectory(prefix="lingniu-go-native-") as tmp:
|
||||||
|
output_dir = pathlib.Path(tmp) / "out"
|
||||||
|
output_dir.mkdir()
|
||||||
|
archive_path = pathlib.Path(tmp) / ("lingniu-go-native-" + release + ".tar.gz")
|
||||||
|
build_release(go_dir, str(output_dir))
|
||||||
|
create_archive(str(output_dir), str(archive_path))
|
||||||
|
remote_archive = "/tmp/" + archive_path.name
|
||||||
|
target = ssh_target(args.user, args.app_host)
|
||||||
|
run(["scp", "-o", "StrictHostKeyChecking=no", "-o", "UserKnownHostsFile=/dev/null", str(archive_path), target + ":" + remote_archive])
|
||||||
|
run([
|
||||||
|
"ssh",
|
||||||
|
"-o",
|
||||||
|
"StrictHostKeyChecking=no",
|
||||||
|
"-o",
|
||||||
|
"UserKnownHostsFile=/dev/null",
|
||||||
|
target,
|
||||||
|
remote_switch_command(release, args.release_root, remote_archive),
|
||||||
|
])
|
||||||
|
if not args.skip_acceptance:
|
||||||
|
run(acceptance_command(args.app_host, args.kafka_host, args.date, args.timeout), cwd=str(repo))
|
||||||
|
|
||||||
|
|
||||||
|
def ssh_target(user: str, host: str) -> str:
|
||||||
|
return user + "@" + host if user else host
|
||||||
|
|
||||||
|
|
||||||
|
def shell_quote(value: str) -> str:
|
||||||
|
return "'" + value.replace("'", "'\"'\"'") + "'"
|
||||||
|
|
||||||
|
|
||||||
|
def safe_release_name(value: str) -> bool:
|
||||||
|
value = value.strip()
|
||||||
|
if not value:
|
||||||
|
return False
|
||||||
|
return all(r.isalnum() or r in "._-" for r in value)
|
||||||
|
|
||||||
|
|
||||||
|
def run(argv: list[str], cwd: str | None = None, env: dict[str, str] | None = None) -> None:
|
||||||
|
subprocess.run(argv, cwd=cwd, env=env, check=True)
|
||||||
|
|
||||||
|
|
||||||
|
def run_capture(argv: list[str], cwd: str | None = None) -> str:
|
||||||
|
return subprocess.run(argv, cwd=cwd, check=True, stdout=subprocess.PIPE, text=True).stdout
|
||||||
|
|
||||||
|
|
||||||
|
def parse_args(argv: list[str]) -> argparse.Namespace:
|
||||||
|
parser = argparse.ArgumentParser(description=__doc__)
|
||||||
|
parser.add_argument("--repo", default=".")
|
||||||
|
parser.add_argument("--go-dir", default=DEFAULT_GO_DIR)
|
||||||
|
parser.add_argument("--app-host", default=DEFAULT_APP_HOST)
|
||||||
|
parser.add_argument("--kafka-host", default=DEFAULT_KAFKA_HOST)
|
||||||
|
parser.add_argument("--user", default=DEFAULT_USER)
|
||||||
|
parser.add_argument("--release-root", default=DEFAULT_RELEASE_ROOT)
|
||||||
|
parser.add_argument("--release", default="")
|
||||||
|
parser.add_argument("--date", default="")
|
||||||
|
parser.add_argument("--timeout", type=float, default=20.0)
|
||||||
|
parser.add_argument("--skip-acceptance", action="store_true")
|
||||||
|
return parser.parse_args(argv)
|
||||||
|
|
||||||
|
|
||||||
|
def main(argv: list[str]) -> int:
|
||||||
|
args = parse_args(argv)
|
||||||
|
try:
|
||||||
|
deploy(args)
|
||||||
|
except subprocess.CalledProcessError as exc:
|
||||||
|
print("command failed: " + " ".join(exc.cmd), file=sys.stderr)
|
||||||
|
return exc.returncode or 1
|
||||||
|
except (OSError, shutil.Error) as exc:
|
||||||
|
print("deploy failed: " + str(exc), file=sys.stderr)
|
||||||
|
return 1
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
raise SystemExit(main(sys.argv[1:]))
|
||||||
50
tools/test_go_native_deploy.py
Normal file
50
tools/test_go_native_deploy.py
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
import unittest
|
||||||
|
|
||||||
|
from tools import go_native_deploy as deploy
|
||||||
|
|
||||||
|
|
||||||
|
class GoNativeDeployTest(unittest.TestCase):
|
||||||
|
def test_build_commands_cover_four_production_binaries(self):
|
||||||
|
commands = deploy.build_commands("/repo/go/vehicle-gateway", "/tmp/out")
|
||||||
|
|
||||||
|
self.assertEqual([command.output_name for command in commands], [
|
||||||
|
"gateway",
|
||||||
|
"history-writer",
|
||||||
|
"stat-writer",
|
||||||
|
"realtime-api",
|
||||||
|
])
|
||||||
|
for command in commands:
|
||||||
|
self.assertEqual(command.argv[:3], ["go", "build", "-trimpath"])
|
||||||
|
self.assertIn("GOOS=linux", command.env)
|
||||||
|
self.assertIn("GOARCH=amd64", command.env)
|
||||||
|
self.assertIn("CGO_ENABLED=0", command.env)
|
||||||
|
|
||||||
|
def test_remote_switch_command_creates_release_and_restarts_all_services(self):
|
||||||
|
command = deploy.remote_switch_command("409f55b", "/opt/lingniu-go-native", "/tmp/lingniu-go-native-409f55b.tar.gz")
|
||||||
|
|
||||||
|
self.assertIn("/opt/lingniu-go-native/releases/409f55b", command)
|
||||||
|
self.assertIn("ln -sfn", command)
|
||||||
|
self.assertIn("systemctl restart lingniu-go-gateway", command)
|
||||||
|
self.assertIn("systemctl restart lingniu-go-history-writer", command)
|
||||||
|
self.assertIn("systemctl restart lingniu-go-stat-writer", command)
|
||||||
|
self.assertIn("systemctl restart lingniu-go-realtime-api", command)
|
||||||
|
|
||||||
|
def test_acceptance_command_uses_deployed_hosts_and_date(self):
|
||||||
|
command = deploy.acceptance_command(
|
||||||
|
app_host="115.29.187.205",
|
||||||
|
kafka_host="114.55.58.251",
|
||||||
|
date="2026-07-02",
|
||||||
|
timeout=20,
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(command[:2], ["python3", "tools/go_prod_acceptance.py"])
|
||||||
|
self.assertIn("--app-host", command)
|
||||||
|
self.assertIn("115.29.187.205", command)
|
||||||
|
self.assertIn("--kafka-host", command)
|
||||||
|
self.assertIn("114.55.58.251", command)
|
||||||
|
self.assertIn("--date", command)
|
||||||
|
self.assertIn("2026-07-02", command)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
unittest.main()
|
||||||
Reference in New Issue
Block a user