从零搭建 AtomOps 全栈运维平台:4 台华为云 ECS 实战部署与踩坑实录
本文属于「码动四季·开源同行」秋季征稿 —— 技术经验体系化沉淀赛道。
记录开源项目 AtomOps 从开发到 4 服务器生产部署的完整实战,包含架构设计、执行引擎实现、安全策略和 6 个真实踩坑案例。
项目背景
AtomOps 是一个面向中小企业的全栈运维管理平台,托管在 AtomGit(GitCode)上。它不是又一个"Wraps SSH 的 Web UI"——核心目标是用工程化方式解决三个痛点:
- 脚本批量执行:在数十台主机上并行跑 Shell/Python 脚本,实时收集结果
- 作业编排:多步骤作业按顺序执行,每步可指定不同脚本和目标主机
- 定时运维: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! EOF2.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>五、最棘手的问题:登录后闪退
这个问题花了最长时间排查。
现象:用户登录成功,页面跳转到首页后立即闪退回登录页。
排查过程:
- ✅ 登录 API 返回 200,token 正常
- ✅ 前端
localStorage已设置登录标志 - ❌ 跳转后 API 请求全部返回 401
- 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 |
| Grafana | http://1.94.202.193:3000 | ✅ 302 |
| Prometheus | http://1.94.202.193:9090 | ✅ 302 |
七、总结与反思
做对了什么
- 职责分离:4 台服务器各司其职,不混部服务
- 内网通信:MySQL/Redis 不暴露公网,安全组最小权限
- 白名单 SSH:指纹校验防中间人攻击,比 AutoAddPolicy 安全
- 配置驱动前端:菜单配置生成 CRUD 页面,新增模块只需加配置
- 本地构建前端:避免服务器环境不一致
踩了哪些坑
| 坑 | 根因 | 教训 |
|---|---|---|
| 登录闪退 | Securecookie + HTTP | HTTP 部署必须secure=False |
| 作业状态错误 | 硬编码 key 访问 | 用.get()防 KeyError |
| 定时任务找不到脚本 | target_id语义冲突 | 字段不要复用 |
| SSH 拒绝连接 | known_hosts 未注册 | 部署后自动注册指纹 |
| Grafana 安装失败 | GPG key 问题 | 直接用 .deb 包 |
| Nginx 404 | f-string 花括号 | 配置文件不走 f-string |
给开源新手的建议
- 先跑通再优化:第一版用最简单的方案,跑通了再迭代
- 部署文档要趁热写:部署完立刻记录,过两天就忘细节了
- 踩坑是最好的老师:每个坑都值得记录,比成功经验更有价值
- 安全要前置考虑:cookie secure、SSH 指纹、CSRF 防护,不要等上线再补
项目仓库:https://gitcode.com/cpyaxjq/AtomOps
部署文档:仓库docs/ECS_4SERVER_DEPLOYMENT.md包含完整部署步骤和运维手册
本文为 AtomGit「码动四季·开源同行」秋季征稿投稿,属于技术经验体系化沉淀赛道。如果觉得有帮助,欢迎到仓库点个 Star ⭐