☰
从零搭建 AtomOps 全栈运维平台:4 台华为云 ECS 实战部署与踩坑实录
2026/10/3 7:21:29 网站建设 项目流程

从零搭建 AtomOps 全栈运维平台:4 台华为云 ECS 实战部署与踩坑实录

本文属于「码动四季·开源同行」秋季征稿 —— 技术经验体系化沉淀赛道。
记录开源项目 AtomOps 从开发到 4 服务器生产部署的完整实战,包含架构设计、执行引擎实现、安全策略和 6 个真实踩坑案例。

项目背景

AtomOps 是一个面向中小企业的全栈运维管理平台,托管在 AtomGit(GitCode)上。它不是又一个"Wraps SSH 的 Web UI"——核心目标是用工程化方式解决三个痛点:

  1. 脚本批量执行:在数十台主机上并行跑 Shell/Python 脚本,实时收集结果
  2. 作业编排:多步骤作业按顺序执行,每步可指定不同脚本和目标主机
  3. 定时运维:Cron 表达式定时触发,支持脚本/作业/巡检三种类型

技术选型不追求新潮,追求可维护:

层级技术选型理由
前端Vue 3 + Element Plus + Vite生态成熟,组件丰富
后端FastAPI + SQLAlchemy异步性能好,自动生成 API 文档
数据库MySQL 8.0 + Redis 7运维熟悉,稳定可靠
监控Prometheus + Grafana开源标配,社区活跃
部署Nginx + systemd裸机部署,不引入 K8s 复杂度

一、架构设计:4 服务器拓扑

4 台华为云 ECS(8vCPU/16GB/Ubuntu 24.04),同一 VPC 内网互通:

┌─────────────────────────────────────────┐ │ 华为云 VPC 内网 │ ┌──────────┐ │ ┌──────────┐ ┌──────────┐ │ │ 用户 │─────┼─▶│ ecs-0001 │─▶│ ecs-0002 │ │ │ 浏览器 │ HTTP│ │ Nginx │ │ MySQL │ │ └──────────┘ │ │ FastAPI │ │ Redis │ │ │ └────┬─────┘ └──────────┘ │ │ │ │ │ ┌────▼─────┐ ┌──────────┐ │ │ │ ecs-0003 │ │ ecs-0004 │ │ │ │ Prometheus│ │ Test │ │ │ │ Grafana │ │ Client │ │ │ └──────────┘ └──────────┘ │ └─────────────────────────────────────────┘

职责分离原则:数据库不放应用服务器,监控不放数据库服务器。这样任何一台挂了,其他服务仍能部分运作。内网通信避免公网暴露 MySQL/Redis 端口。


二、部署实战

2.1 数据库服务器(ecs-0002)

MySQL 8.0 + Redis 7,Ubuntu 24.04 apt 直装:

# MySQLapt-getinstall-ymysql-server mysql-uroot-e"ALTER USER 'root'@'localhost' IDENTIFIED WITH mysql_native_password BY 'MyPass!';"# 关键:监听 0.0.0.0 让内网其他机器能连cat>/etc/mysql/mysql.conf.d/zz-atomops.cnf<<'EOF' [mysqld] bind-address = 0.0.0.0 innodb_buffer_pool_size = 2G EOF# Redis 同理cat>/etc/redis/redis.conf<<'EOF' bind 0.0.0.0 requirepass RedisPass! EOF

2.2 应用服务器(ecs-0001)

后端 FastAPI 用 systemd 管理,Nginx 反代:

# /etc/systemd/system/atomops-backend.service [Service] ExecStart=/opt/AtomOps/backend/.venv/bin/uvicorn app.main:app \ --host 0.0.0.0 --port 8000 --workers 1 Restart=always KillMode=control-group

踩坑 #1:一开始用--workers 2,systemd 主进程追踪混乱,reload 时旧 worker 不退出。改为--workers 1后问题消失。单 worker 对中小规模足够,并发靠异步 I/O。

Nginx 配置三个 location:静态文件、API 反代、WebSocket(WebShell):

location /api/ { proxy_pass http://127.0.0.1:8000; proxy_read_timeout 300s; # 脚本执行可能较慢 } location /api/coc/webshell/ws { proxy_pass http://127.0.0.1:8000; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection "upgrade"; proxy_read_timeout 3600s; # WebShell 长连接 }

2.3 监控服务器(ecs-0003)

Prometheus + Grafana + Alertmanager + node_exporter 全套 apt 安装。

踩坑 #2:Grafana 官方 apt 仓库 GPG key 在 Ubuntu 24.04 上验证失败。解决方案:直接下载.deb包安装,绕过 apt 仓库签名问题。

2.4 前端构建策略

不在服务器上npm run build——服务器 Node.js 版本可能不一致,构建可能失败。改为本地构建 + 上传 dist:

cdfrontend&&npmrun buildtarczf dist.tar.gz-Cdist.scpdist.tar.gz root@ecs-0001:/tmp/sshroot@ecs-0001"tar xzf /tmp/dist.tar.gz -C /opt/atomops-dist/"

三、核心执行引擎实现

这是项目最核心的部分,也是最容易出 bug 的地方。

3.1 脚本执行:并行 SSH

用ThreadPoolExecutor在多台主机上并行执行:

fromconcurrent.futuresimportThreadPoolExecutor,as_completed _exec_pool=ThreadPoolExecutor(max_workers=10)@router.post("/scripts/{script_id}/execute")asyncdefexecute_script(script_id:int,body:ExecuteRequest):script=db.get(CocScript,script_id)command=_build_command(script)futures={_exec_pool.submit(_run_on_host,hd,command):hdforhdintarget_hosts}results=[f.result()forfinas_completed(futures)]return{"results":results}

脚本类型处理——Python 脚本用 heredoc 传递多行内容:

def_build_command(script):ifscript.script_type=="python":returnf"python3 - <<'ATOMOPS_EOF'\n{script.content}\nATOMOPS_EOF"returnscript.content# shell 直接执行

3.2 作业执行:顺序步骤

作业按steps数组顺序执行,每步在所有目标主机上并行:

all_success=Trueforstepinjob.steps:script=db.get(CocScript,step["script_id"])futures={_pool.submit(_exec,hd,command):hdforhdinhost_dicts}step_results=[f.result()forfinas_completed(futures)]ifnotall(r.get("status")=="success"forrinstep_results):all_success=False

踩坑 #3:作业执行记录的status一直是failed,但实际执行都成功了。排查发现r["status"]用了硬编码 key 访问,个别结果 dict 缺少statuskey 导致 KeyError。改为r.get("status")后修复。

3.3 定时调度:APScheduler

defload_scheduled_tasks():tasks=db.query(CocScheduledTask).filter_by(enabled=True).all()fortaskintasks:scheduler.add_job(_run_scheduled_task,CronTrigger.from_crontab(task.cron_expr),args=[task.id])

踩坑 #4:定时任务触发报 “脚本不存在”。根因是target_id字段语义冲突——同时用于主机选择和脚本引用。解决方案:新增target_hosts字段(JSON 数组)专管主机选择,target_id专管脚本/作业引用。这个设计错误暴露了早期数据模型考虑不周。

3.4 SSH 安全策略

不使用简单的AutoAddPolicy(不安全),也不用RejectPolicy(部署时 known_hosts 未加载会全部拒绝)。采用白名单 + 后置指纹校验:

def_get_client(host):# 1. 白名单检查:主机指纹必须预先注册known={h["ip"]:h["fingerprint"]forhinlist_known_hosts()}ifhost["ip"]notinknown:raiseSSHException(f"主机{host['ip']}未注册")# 2. AutoAddPolicy 建立连接client.connect(**kwargs)# 3. 连接后校验实际指纹 vs 记录指纹fp=_get_fingerprint(client.get_transport())ifknown[host["ip"]]!=fp:raiseSSHException("指纹不匹配!可能中间人攻击")

踩坑 #5:新部署的服务器 IP 未注册到 known_hosts,所有脚本执行都报错。写了个自动注册脚本:通过公网 IP 获取指纹,用内网 IP 注册(因为后端通过内网 SSH)。


四、前端交互设计

4.1 统一列表页 + 执行对话框

所有 CRUD 页面复用ListPage.vue,通过菜单配置驱动:

// menu.js 配置驱动{path:'/coc/scripts',page:'list',endpoint:'/api/coc/scripts',columns:[{prop:'name',label:'脚本名称',type:'text'},{prop:'script_type',label:'类型',type:'select',options:[['Shell','shell'],['Python','python']]},{prop:'content',label:'内容',type:'textarea'},],actions:[{label:'执行',exec:'script'}]}

执行对话框支持多选主机、实时结果展示:

<el-select v-model="execHosts" multiple filterable> <el-option v-for="h in allHosts" :label="`${h.hostname} (${h.ip})`" /> </el-select>

4.2 作业步骤编辑器

不用原始 JSON 编辑,自定义type: 'steps'编辑器:

<div v-for="(step, i) in jobSteps"> <el-input v-model="step.name" placeholder="步骤名称" /> <el-select v-model="step.script_id" @focus="loadScripts"> <el-option v-for="s in scriptOptions" :label="s.name" :value="s.id" /> </el-select> </div>

五、最棘手的问题:登录后闪退

这个问题花了最长时间排查。

现象:用户登录成功,页面跳转到首页后立即闪退回登录页。

排查过程:

  1. ✅ 登录 API 返回 200,token 正常
  2. ✅ 前端localStorage已设置登录标志
  3. ❌ 跳转后 API 请求全部返回 401
  4. 401 拦截器清除 localStorage → 跳回登录页 → 闪退

根因:后端设置 auth cookie 时:

response.set_cookie(key="access_token",value=token,httponly=True,secure=settings.is_production,# 生产模式下 True!samesite="lax",)

生产模式下secure=True,但前端通过HTTP(非 HTTPS)访问。浏览器规范:拒绝存储 Secure cookie 在 HTTP 连接上。所以 cookie 根本没存进去,后续请求自然没有认证。

修复:

secure=False,# HTTP 部署时必须 False

教训:Securecookie 标志只适用于 HTTPS 部署。HTTP 环境下设置Secure=True会导致 cookie 被浏览器静默丢弃,没有任何错误提示,极难排查。


六、部署验证结果

端到端测试全部通过:

脚本执行:系统健康检查 × 2 台主机 → ✅ 2/2 success 定时任务(脚本):每日健康检查 × 4 台主机 → ✅ 4/4 success 定时任务(作业):部署前检查(2 步骤)× 4 台主机 → ✅ 全部成功 作业历史记录:#2 status=success result=共 2 步, 全部成功

外部访问验证:

服务地址状态
前端http://1.92.103.196✅ 200
Grafanahttp://1.94.202.193:3000✅ 302
Prometheushttp://1.94.202.193:9090✅ 302

七、总结与反思

做对了什么

  1. 职责分离:4 台服务器各司其职,不混部服务
  2. 内网通信:MySQL/Redis 不暴露公网,安全组最小权限
  3. 白名单 SSH:指纹校验防中间人攻击,比 AutoAddPolicy 安全
  4. 配置驱动前端:菜单配置生成 CRUD 页面,新增模块只需加配置
  5. 本地构建前端:避免服务器环境不一致

踩了哪些坑

坑根因教训
登录闪退Securecookie + HTTPHTTP 部署必须secure=False
作业状态错误硬编码 key 访问用.get()防 KeyError
定时任务找不到脚本target_id语义冲突字段不要复用
SSH 拒绝连接known_hosts 未注册部署后自动注册指纹
Grafana 安装失败GPG key 问题直接用 .deb 包
Nginx 404f-string 花括号配置文件不走 f-string

给开源新手的建议

  1. 先跑通再优化:第一版用最简单的方案,跑通了再迭代
  2. 部署文档要趁热写:部署完立刻记录,过两天就忘细节了
  3. 踩坑是最好的老师:每个坑都值得记录,比成功经验更有价值
  4. 安全要前置考虑:cookie secure、SSH 指纹、CSRF 防护,不要等上线再补

项目仓库:https://gitcode.com/cpyaxjq/AtomOps

部署文档:仓库docs/ECS_4SERVER_DEPLOYMENT.md包含完整部署步骤和运维手册

本文为 AtomGit「码动四季·开源同行」秋季征稿投稿,属于技术经验体系化沉淀赛道。如果觉得有帮助,欢迎到仓库点个 Star ⭐

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询