
Windmill SSH 远程执行指南用#ssh指令与 userland wrapper 在跳板机上运行脚本【免费下载链接】windmillOpen-source developer platform to power your entire infra and turn scripts into webhooks, workflows and UIs. Fastest workflow engine (13x vs Airflow). Open-source alternative to Retool and Temporal.项目地址: https://gitcode.com/GitHub_Trending/wi/windmill导读本文基于 Windmill 仓库中的 examples/usecase/ssh-execution-wrapper 示例讲解如何在 Windmill 无法部署 Worker、但能通过 SSH 访问的远程主机跳板机/工具节点上执行脚本。你将掌握两种实现路径企业版的一等公民特性#ssh指令全对等执行体验以及无需任何许可的 userland wrapperssh_exec.sh/ssh_exec.py并理解其资源类型设计、主机密钥固定机制、退出码传播等底层细节从而为自己的隔离环境执行需求做出正确的技术选型。重要前提对于几乎所有在隔离/分段环境中运行代码的需求Windmill 官方推荐的首选方案是agent worker而非本文的 SSH 路径。SSH wrapper 只适用于只能通过 SSH 到达跳板机、且脚本简单自包含的窄场景详见下文 何时用哪种方案。方案总览一条共享的ssh_target资源两种执行路径仓库中 examples/usecase/ssh-execution-wrapper 目录提供了一套完整的示例包含三个文件文件用途ssh_target.resource-type.json资源类型定义host、port、user、private_keysecret、host_pubkey、accept_unknown_host。两种方案共用ssh_exec.shuserland wrapper 的 Windmillbash脚本版本ssh_exec.pyuserland wrapper 的 Windmillpython脚本版本含解释器分发表两种方案共用同一个ssh_target资源类型区别在于执行方式#ssh指令推荐企业版写一个普通的 bash 脚本仅在首行加一行#ssh resource_path。Worker 会把执行重路由到远程主机且保持全对等体验类型化的位置参数传入、结构化结果返回、日志实时流式输出、任务可取消、远程退出码决定任务成败。这是一等公民的后端特性参见下文#ssh指令。userland wrapper无需许可一个可复用的 Windmill 脚本ssh_exec.sh/ssh_exec.py由你调用它并把远程代码作为字符串参数传入。无需后端改动、无需 license但会失去编辑器体验和结构化结果。在无法运行企业版镜像时使用参见下文 userland wrapper。何时用哪种方案agent worker 优先Windmill 的官方决策建议非常明确——默认使用 agent workerAgent worker一个轻量级 Worker运行在目标环境内部仅通过出站 HTTP使用jwt_agent_*token回连 Windmill server。无需入站端口、无需数据库访问。它保留了 Windmill Worker 的全部能力自动依赖管理、nsjail 沙箱、S3 二进制缓存、原生 secrets、全语言支持、无每次任务的连接开销。只要能在目标环境里跑一个进程就用 agent worker。Worker-group tagsWorker 组标签当你能在目标环境中放置一个完整 Worker、并希望把特定脚本路由过去时这是正确的工具。本文的 SSH wrapper仅在同时满足以下两个条件时使用——你只能通过 SSH 到达跳板机/工具节点无法在那里放置任何 Worker 或 agent 进程脚本简单且自包含不依赖 Windmill 托管的依赖。ssh_target资源类型字段与安全语义两种方案共享的资源类型 schema 定义在 ssh_target.resource-type.json 中字段如下字段类型必填默认值说明hoststring✅—SSH 跳板/工具节点的主机名或 IPportinteger—22SSH 端口userstring✅—SSH 登录用户private_keystring✅—PEM 编码的私钥用于认证。在 schema 中标记为password: true即作为 secret 存储host_pubkeystring—服务端主机公钥行用于 known_hosts 固定如ssh-ed25519 AAAAC3Nz...可通过ssh-keyscan -t ed25519 host获取。设置后强制StrictHostKeyCheckingyes为空时除非显式设置accept_unknown_host: true否则拒绝执行accept_unknown_hostboolean—false允许在未固定host_pubkey时连接采用首次信任StrictHostKeyCheckingaccept-new即 TOFU。对 MITM 不安全仅供开发使用关键安全设计点私钥永远以 secret 存储password: true不会进入日志、不会出现在任务参数明文里。主机密钥固定host-key pinning是默认安全基线一旦设置了host_pubkeywrapper 会将其写入任务局部的known_hosts并强制StrictHostKeyCheckingyes。若host_pubkey为空wrapper 会拒绝运行除非资源显式设置accept_unknown_host: true此时降级为较弱的 TOFUaccept-new并打印告警——仅限开发环境。非默认端口使用 known_hosts 的[host]:port形式例如[your.jump.host]:2222 ssh-ed25519 AAAAC3Nz...。#ssh指令Enterprise开启一次性的 Superadmin 设置该特性是企业版门控enterprise-gated且默认关闭。需要以 superadmin 身份在 Superadmin settings 中开启ssh_execution_enabled实例设置且要求有效的企业版 license。该设置项在源码中定义于 backend/windmill-common/src/global_settings.rs// Enables the #ssh resource directive that reroutes bash execution to a // remote host over SSH (enterprise feature). Off by default. See // windmill-worker/src/ssh_executor_ee.rs. pub const SSH_EXECUTION_SETTING: str ssh_execution_enabled;在开源非企业版本中对应的执行函数是一个明确报错的 stub位于 backend/windmill-worker/src/ssh_executor_oss.rs 的handle_ssh_bash_job会返回错误 SSH execution (#ssh) is an enterprise feature. Use the enterprise image, or the userland SSH wrapper in examples/usecase/ssh-execution-wrapper/.——这也从源码层面印证了文档对两种路径的划分。使用一行指令重路由执行创建好ssh_target资源见下文 Setup后写一个 bash 脚本在引导注释行放置指令#ssh f/infra/jump_node # ^ reroutes this script to run on the host described by the # ssh_target resource at f/infra/jump_node Service$1 # typed positional args work as usual systemctl is-active $Service echo {\service\: \$Service\, \checked\: true} # last stdout line result该脚本在远程主机上的运行方式与本地 bash 脚本完全一致参数来自运行表单结果收集方式相同result.jsonresult.out 最后一行 stdout日志实时流式输出远程非零退出码会使任务失败。唯一改变的是执行位置。从后端实现看bash 执行器在 backend/windmill-worker/src/bash_executor.rs 中通过BashAnnotations::ssh_target(content)提取该指令进而调用handle_ssh_bash_job走 SSH 执行分支。动态目标#ssh $arg_name除了硬编码资源路径指令还可以引用一个任务参数在调用时提供目标——用于从运行表单挑选主机或在 flow 的 forloop 中遍历多台主机#ssh $jump_host target$1 # jump_hosts position: always received as an empty string df -h这里有几点需要特别注意该参数必须是ssh_target资源的路径字符串带或不带$res:前缀均可内联的ssh_target对象会被拒绝因此目标总是经由 runner 的资源权限解析调用者只能把执行路由到其有权限读取的资源所描述的主机。目标参数本身会以空字符串转发给远程脚本其解析值内嵌私钥绝不能出现在远程命令行上它的位置会被保留以保证其它$1..$n对齐不错位。语义差异动态目标由runner决定代码在哪里执行受资源权限约束硬编码路径则由脚本作者固定执行位置。对等边界Parity boundary远程端只收到脚本主体 位置参数。Windmill 运行时不会被转发——BASE_INTERNAL_URL、wmill客户端、保留的WM_*变量在远程均不可用因此脚本内的 Windmill API 回调不会工作。与 wrapper 相同的取舍远程无依赖管理、无 nsjail 沙箱、无 S3 缓存、每次任务有 SSH 连接开销。v1 仅支持 bash。userland wrapper当无法使用企业版镜像时wrapper 是完整的替代路径。它接收一个ssh_target资源、一个script_content字符串和一个language然后执行四个步骤将私钥写入0600权限的临时文件并生成任务局部的known_hosts建立单条SSH 连接不带 TTY将脚本主体通过远端 stdin 流式传入——远端一个小型 bootstrap 用mktemp创建文件、trap在EXIT时删除它、用正确的解释器运行它并以脚本的退出码退出实时流式回传 stdout/stderr并传播远端退出码使远端脚本失败即 Windmill 任务失败。执行架构Windmill worker Remote jump node ┌────────────────────┐ ┌─────────────────────────────┐ │ ssh_exec.sh │ ssh (no -t) │ sh -c bootstrap │ │ key → 0600 tmp │ ───────────────▶ │ f$(mktemp) │ │ known_hosts pin │ body on stdin │ trap rm -f $f EXIT │ │ printf body | ssh │ ────────────────▶│ cat $f │ │ │ ◀─────────────── │ interp $f (live logs) │ │ exit ssh rc │ remote rc │ exit $? │ └────────────────────┘ └─────────────────────────────┘源码级关键设计这些细节决定成败ssh_exec.sh 与 ssh_exec.py 中的每个设计点都是刻意的改编时值得保留退出码传播ssh host cmd返回的是远端退出码。bash 版通过${PIPESTATUS[1]}读取并重新exitpython 版在非零时抛异常。远端脚本失败 → Windmill 任务失败。无 TTY从不传-t/-tt。TTY 会把 stdout 和 stderr 合并破坏日志捕获。只有需要交互式远程提示如sudo询问密码时才启用-tt。实时无缓冲日志python 使用python3 -u对管道传输时会缓冲的话痨型 bash 脚本可把远端解释器包上stdbuf -oL修改分发表如interpstdbuf -oL bash。远端清理在失败时依然生效trap rm -f $f EXIT设置在远端流式 bootstrap 内部因此即使脚本出错临时文件也会被删除。README 的测试部分确认了远端与本地临时文件清理均被验证。主机密钥固定设置host_pubkey时固定进任务局部known_hosts并强制StrictHostKeyCheckingyes非默认端口用[host]:port形式为空时拒绝运行除非accept_unknown_host: trueTOFU生产环境必须固定。引号 heredoc远端 bootstrap 用REMOTE构建保证$f、$?、$TMPDIR在远端求值而不是在 worker 上被展开。Body 走 stdin脚本主体通过 stdin 流式传输绝不写入本地临时文件、绝不插值进命令行。--放在目标前OpenSSH 会把以-开头的 destination 解析为选项若不使用分隔符资源中被精心构造的user如-oProxyCommand...会在主机密钥校验前于 worker 上执行本地命令。改编时必须保留--。多次往返本 wrapper 只建立单条 SSH 连接。若扩展为多次ssh调用应添加-o ControlMasterauto -o ControlPersist60 -o ControlPathjob-local复用连接避免每次重新认证。Setup 设置步骤创建资源类型用 CLI 推送wmill resource-type push ssh_target.resource-type.json或在 UI 中Resources → Resource Types用相同 schema 重建。private_key标记为 secretpassword: truehost_pubkey可选。为跳板机创建ssh_target资源。用如下命令从服务器获取host_pubkeykeytype key部分注释可选ssh-keyscan -t ed25519 your.jump.host # → ssh-ed25519 AAAAC3Nz...创建脚本从ssh_exec.shbash或ssh_exec.pypython创建 Windmill 脚本并把第一个参数标记为ssh_target类型的资源。Usage 调用示例调用 wrapper传入目标、远端脚本主体与语言{ ssh_target: $res:u/me/my_jump_node, script_content: set -euo pipefail\ndf -h\nsystemctl is-active nginx, language: bash }{ ssh_target: $res:u/me/my_jump_node, script_content: import platform\nprint(platform.platform()), language: python }支持的language键bash、sh、python/python3、node/javascript、ruby、php、perl。任何其它值会作为原始远端解释器命令透传。远端主机必须已安装对应解释器及脚本所需的全部依赖参见取舍部分。源码中 bash 版的分发表位于 ssh_exec.sh 的case $language分支python 版为 ssh_exec.py 的INTERPRETERS字典其中python/python3统一映射为python3 -u以强制无缓冲输出。走 SSH 路径会失去什么无依赖管理远端主机必须已具备解释器以及脚本用到的每个库/工具。没有任何安装或锁定。无 nsjail 沙箱脚本以 SSH 用户的身份、以该用户的完整权限运行。跳板机会成为高价值目标——务必严格限定密钥与用户的权限范围。无 S3 / 二进制缓存没有共享的依赖或产物缓存。每次任务的 SSH 开销每次运行都付出连接 认证延迟只有多次往返时才能用 ControlMaster 缓解。远端无原生 Windmill 集成无资源/变量注入、无wmill客户端、无 flow 步骤上下文除你显式传入的外。该原型的局限性worker 上需要ssh客户端bash 版还需要jq。假定脚本自包含、非交互stdin 不会被转发给远端脚本stdin 承载脚本主体。未知language值会原样透传为远端解释器——务必让language由作者控制而非终端用户输入。测试情况README 记录了两种 wrapper 均针对本地sshd验证过成功路径、远端退出码传播bash${PIPESTATUS[1]}、python 抛异常、干净的 stdout/stderr 分离、python -u解释器分发、主机密钥固定对错误密钥的拒绝脚本完全不执行、TOFU 可选开启accept_unknown_host: true及其缺失时的拒绝以及远端和本地临时文件清理的确认。总结与选型建议本文给出了在 Windmill 无法放置 Worker 的跳板机上执行自包含脚本的完整方案优先评估 agent worker能力最全、开销最低能放完整 Worker 就用 worker-group tags 路由只有仅 SSH 可达 脚本自包含同时成立时才选用本示例的 SSH 路径。在此前提下企业版用户优先使用#ssh指令编辑体验与结构化结果俱全无法使用企业版镜像的部署则使用 ssh_exec.sh / ssh_exec.py 这份无许可的 userland wrapper并严格遵循其安全设计固定主机密钥、私钥以 secret 存储、language保持作者可控、保留--分隔符与远端trap清理。【免费下载链接】windmillOpen-source developer platform to power your entire infra and turn scripts into webhooks, workflows and UIs. Fastest workflow engine (13x vs Airflow). Open-source alternative to Retool and Temporal.项目地址: https://gitcode.com/GitHub_Trending/wi/windmill创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考