Windmill 实例配置即代码(IaC)完整指南:sync-config 与 Kubernetes Operator
【免费下载链接】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/deploy/instance-config-iac/目录中提供了完整的实例配置即代码(Infrastructure-as-Code)示例,允许通过声明式 YAML 文件统一管理实例级配置(全局设置 global settings 与 worker group 配置),并自动同步到数据库。本文以该目录下的 README.md 为主体,结合 docker-compose.yml、windmill-config.yaml、k8s-windmill-instance.yaml 等真实文件以及 backend 源码,详细讲解sync-configCLI 与 Kubernetes Operator 两种部署模型的配置方法、敏感字段引用机制、Replace 同步语义与底层实现原理。
为什么需要实例配置即代码
Windmill 实例的配置项(License Key、SMTP、OAuth、保留策略、Worker 标签等)默认是通过管理界面或数据库直接维护的,这带来两个问题:配置变更不可追溯、无法在多个环境(开发/预发/生产)间复用。实例配置即代码把整份配置收敛为一个版本可控的 YAML 文件,随部署流程自动应用,实现:
- 配置版本化:所有变更通过 git 留痕、可 review、可回滚;
- 环境可复现:同一份 YAML 在不同环境使用不同 secret 即可拉起等效实例;
- 声明式收敛:数据库状态与文件声明的状态保持完全一致(Replace 语义)。
Windmill 为此提供了两种部署模型,它们共用同一份 YAML 模式(InstanceConfig)与同一套 secret 引用机制:
| 方案 | 适用场景 | 前置要求 |
|---|---|---|
sync-configCLI | Docker Compose、虚拟机、CI/CD 流水线 | 数据库访问权限 |
| Kubernetes Operator | Kubernetes 集群 | operatorfeature flag、RBAC 权限 |
配置文件结构:InstanceConfig模式
一份 Windmill 实例配置文件包含两个顶层键,对应windmill_common::instance_config::InstanceConfig结构体的两个字段(见 backend/windmill-common/src/instance_config.rs):
global_settings: # 实例级全局设置,存储在 global_settings 数据表中 base_url: "https://windmill.example.com" retention_period_secs: 2592000 # ... worker_configs: # Worker group 配置,存储在 config 表中、键名为 worker__<name> default: worker_tags: ["deno", "python3", "bun", "go", "bash"] gpu: dedicated_worker: "ws:f/gpu_inference" # ...所有字段均为可选,只同步你在文件中显式声明的字段。global_settings会被扁平化为name → value键值对写入global_settings表;而worker_configs中每个组名对应 config 表中一条worker__<name>记录(例如worker__default、worker__native),Worker 启动时按WORKER_GROUP环境变量查找对应配置。
从源码结构看,InstanceConfig还实现了to_settings_map()/from_db()等序列化与数据库读取方法(instance_config.rs),支持将数据库当前状态反向导出为 YAML,便于审计或迁移。
敏感字段引用:三种格式
凡是包含敏感信息的字段(License Key、OAuth secret、SMTP 密码等)都支持三种写法:
# 1. 明文直写(生产环境不推荐) license_key: "my-license-key" # 2. 环境变量引用(所有环境通用) license_key: envRef: "WM_LICENSE_KEY" # 3. Kubernetes Secret 引用(仅 K8s 场景) license_key: secretKeyRef: name: windmill-secrets # Secret 资源名 key: license-key # Secret 内的键名在源码中,这两种引用分别由EnvRef与SecretRef两个结构体承载,并通过serde的untagged反序列化自动识别:字符串值按明文处理,{ envRef: "VAR" }反序列化为EnvRef,{ secretKeyRef: { name, key } }反序列化为SecretRef(instance_config.rs)。
支持envRef与secretKeyRef的字段清单:
license_keyhub_api_secretscim_tokensmtp_settings.smtp_passwordoauths.<provider>.secret(每个 OAuth 客户端各自的 secret)custom_instance_pg_databases.user_pwd
方案一:Docker Compose 中使用sync-config
sync-config是 Windmill 二进制提供的一个子命令:读取 YAML 配置文件,从进程环境解析envRef引用,然后将结果同步到数据库。
底层工作原理
sync-config的完整执行链位于 backend/src/main.rs,大致分五步:
- 读取并解析 YAML 文件(
serde_yml反序列化为InstanceConfig); - 调用
resolve_env_refs将envRef字段从容器环境变量解析为实际值,环境变量缺失会直接报错environment variable '<VAR>' not found; - 建立数据库连接(
initial_connection); - 读取数据库当前状态,计算 diff(使用
Replace模式:文件中未声明的设置会被删除,ducklake_settings、custom_instance_pg_databases等受保护设置除外); - 将变更写回数据库,输出
Synced instance config from <path>。
ApplyMode::Replace与受保护设置的过滤逻辑在diff_global_settings/diff_worker_configs中实现(instance_config.rs),受保护键列表位于 instance_config.rs,包括ducklake_user_pg_pwd、ducklake_settings、custom_instance_pg_databases——这些是运行时/凭据相关设置,配置同步永远不会将其清空。worker_configs也作为保留键被过滤,避免历史遗留的单 blob 记录干扰新式分组存储。
搭建步骤
仓库的 docker-compose.yml 提供了完整可运行的 Compose 示例,关键部分如下。
第一步:编写配置文件windmill-config.yaml(可直接参考仓库中的 windmill-config.yaml):
global_settings: base_url: "https://windmill.example.com" license_key: envRef: "WM_LICENSE_KEY" retention_period_secs: 2592000 # 30 天 job_default_timeout: 900 # 15 分钟 expose_metrics: false smtp_settings: smtp_host: "smtp.example.com" smtp_port: 587 smtp_from: "windmill@example.com" smtp_tls_implicit: false smtp_password: envRef: "SMTP_PASSWORD" oauths: google: id: "google-client-id" secret: envRef: "GOOGLE_OAUTH_SECRET" login_config: auth_url: "https://accounts.google.com/o/oauth2/v2/auth" token_url: "https://oauth2.googleapis.com/token" userinfo_url: "https://openidconnect.googleapis.com/v1/userinfo" scopes: ["openid", "profile", "email"] custom_tags: - gpu - high-mem worker_configs: default: worker_tags: ["deno", "python3", "bun", "go", "bash", "powershell"] init_bash: "echo 'Worker starting'" native: worker_tags: ["nativets"]注意worker_tags中的值(deno、python3、bun、go、bash、powershell、nativets等)是 Windmill 内置的脚本运行语言标签,Worker 会按标签认领对应语言的任务;native组对应 nativets 运行时,可与docker-compose.yml中WORKER_GROUP=native的 worker 服务一一对应。
第二步:在docker-compose.yml中添加一次性 init 容器:
services: windmill_config_sync: image: ${WM_IMAGE} # 启动时运行一次后退出 restart: "no" command: ["windmill", "sync-config", "/config/windmill-config.yaml"] environment: - DATABASE_URL=${DATABASE_URL} - WM_LICENSE_KEY=${WM_LICENSE_KEY} - SMTP_PASSWORD=${SMTP_PASSWORD} - GOOGLE_OAUTH_SECRET=${GOOGLE_OAUTH_SECRET} volumes: - ./windmill-config.yaml:/config/windmill-config.yaml:ro depends_on: db: condition: service_healthy仓库示例中,windmill_server通过depends_on: windmill_config_sync: condition: service_completed_successfully保证「先同步配置、后启动服务」的先后顺序;数据库服务带健康检查(pg_isready -U postgres)。生产环境可将该服务替换为 Kubernetes InitContainer 或 CI 流水线中的一步。
第三步:在.env中设置密钥(不要提交到版本控制):
DATABASE_URL=postgres://postgres:changeme@db/windmill WM_IMAGE=ghcr.io/windmill-labs/windmill-ee:main WM_LICENSE_KEY=your-license-key-here SMTP_PASSWORD=your-smtp-password GOOGLE_OAUTH_SECRET=your-google-oauth-secret启动:docker compose up -d。
配置变更后重新同步
sync-config容器运行一次即退出。编辑 YAML 后重新应用:
docker compose run --rm windmill_config_sync在 CI/CD 流水线中,可以直接以二进制方式运行:
windmill sync-config ./windmill-config.yamlReplace 语义:数据库状态与文件严格一致
sync-config采用Replace(替换)模式:数据库中存在但 YAML 中未声明的任何全局设置都会被删除(ducklake_settings、custom_instance_pg_databases等受保护设置除外)。这保证数据库状态与文件声明完全一致,但也意味着:如果你只想管理一部分设置,就必须把希望保留的所有设置都写进 YAML 文件,否则未声明的设置会被清理掉。对应测试用例见 backend/tests/instance_config.rs,其中覆盖了受保护设置不被误删、worker_configs禁止以 global setting 形式写入等回归场景。
方案二:Kubernetes Operator
Windmill Kubernetes operator 持续 watch 一个 ConfigMap,并不断调和(reconcile)数据库以匹配声明状态;同时原生支持通过secretKeyRef从 Kubernetes Secret 拉取值。
前置条件
- 以
operatorfeature flag 构建的 Windmill(OSS 构建中该能力不可用,windmill_operator::run在非private特性下会直接报错 "K8s operator is not available in this build",见 backend/windmill-operator/src/operator_oss.rs); - operator Pod 具备读取 ConfigMap / Secret 与创建 Event 的 RBAC 权限(见下文);
- 一个名为
windmill-instance的 ConfigMap(可通过OPERATOR_CONFIGMAP环境变量自定义名称)。
搭建步骤
第一步:为敏感值创建 Kubernetes Secret(仓库中的 k8s-secrets.yaml):
apiVersion: v1 kind: Secret metadata: name: windmill-secrets namespace: windmill type: Opaque stringData: license-key: "your-license-key-here" smtp-password: "your-smtp-password-here" google-oauth-secret: "your-google-oauth-secret-here"生产环境建议用 sealed-secrets、external-secrets 或你偏好的密钥管理方案管理该 Secret。
第二步:创建 ConfigMap(仓库中的 k8s-windmill-instance.yaml):
apiVersion: v1 kind: ConfigMap metadata: name: windmill-instance namespace: windmill data: spec: | global_settings: base_url: "https://windmill.example.com" license_key: secretKeyRef: name: windmill-secrets key: license-key retention_period_secs: 2592000 job_default_timeout: 900 expose_metrics: true smtp_settings: smtp_host: "smtp.example.com" smtp_port: 587 smtp_from: "windmill@example.com" smtp_tls_implicit: false smtp_password: secretKeyRef: name: windmill-secrets key: smtp-password oauths: google: id: "your-google-client-id" secret: secretKeyRef: name: windmill-secrets key: google-oauth-secret login_config: auth_url: "https://accounts.google.com/o/oauth2/v2/auth" token_url: "https://oauth2.googleapis.com/token" userinfo_url: "https://openidconnect.googleapis.com/v1/userinfo" scopes: ["openid", "profile", "email"] custom_tags: - gpu - high-mem worker_configs: default: worker_tags: ["deno", "python3", "bun", "go", "bash", "powershell"] init_bash: "echo 'Worker starting'" native: worker_tags: ["nativets"]配置放在data.spec下、作为一段 YAML 字符串,与sync-config使用完全相同的模式。
第三步:应用:
kubectl apply -f windmill-instance.yaml第四步:通过 Event 查看同步状态:
kubectl get events --field-selector involvedObject.name=windmill-instanceLicense Key 的幂等处理
如果 ConfigMap 中license_key缺失或为空、但数据库中已存在,operator 会保留数据库中的现有值。这意味着你可以把 License Key 交给 UI 单独管理,而不用担心 operator 的周期性调和把它覆盖掉——调和逻辑天然对 license_key 做了保护。
Operator 环境变量
| 变量 | 默认值 | 说明 |
|---|---|---|
OPERATOR_NAMESPACE | Pod 自身所在 namespace | ConfigMap 所在的 namespace |
OPERATOR_CONFIGMAP | windmill-instance | 要 watch 的 ConfigMap 名称 |
在 Kubernetes 中使用envRef
envRef在 operator 场景下同样可用,值从operator Pod 的环境变量解析。当 secret 通过 Pod 环境变量注入(例如来自 vault sidecar 或 external-secrets 的 env 注入)时非常有用:
data: spec: | global_settings: license_key: envRef: "WM_LICENSE_KEY" # 从 operator Pod 环境读取operator Pod 的 Deployment 中对应:
env: - name: WM_LICENSE_KEY valueFrom: secretKeyRef: name: windmill-secrets key: license-key这与在 ConfigMap 中直接使用secretKeyRef功能等价,区别在于它不要求 operator 具备 Secret 读取权限,且可以接入集群支持的任意 secret 注入机制(external-secrets、vault-agent 等)。
RBAC 最小权限
operator Pod 需要读取 ConfigMap、Secret 并创建 Event。最小 Role 如下(注意:因为没有 CRD 需要管理,所以是namespace 级 Role而非 ClusterRole):
apiVersion: rbac.authorization.k8s.io/v1 kind: Role metadata: name: windmill-operator namespace: windmill rules: - apiGroups: [""] resources: ["configmaps"] verbs: ["get", "list", "watch"] - apiGroups: [""] resources: ["secrets"] verbs: ["get", "list", "watch"] - apiGroups: [""] resources: ["events"] verbs: ["create", "patch"]运行 operator
# 作为独立进程运行(适合开发调试) DATABASE_URL=postgres://... windmill operator # 生产环境:以 Kubernetes Deployment 形式部署从源码看,windmill operator子命令由operatorfeature 编译(backend/src/main.rs),建立独立的 operator 数据库连接后调用windmill_operator::run(db)启动 ConfigMap watcher;在 enterprise + private 构建下还会注册 Ctrl-C 信号以优雅退出。
如何选择:envRef还是secretKeyRef
| 特性 | envRef | secretKeyRef |
|---|---|---|
| Docker Compose 可用 | 是 | 否 |
| Kubernetes 可用 | 是 | 是 |
| vault sidecar 注入 | 是 | 否(改用envRef) |
| 数据来源 | 进程环境变量 | K8s Secrets API |
| 是否需要 Secret 读取 RBAC | 否 | 是 |
建议:追求跨部署目标的可移植性时优先使用envRef;希望不经过中间环境变量、直接绑定 Kubernetes 原生 Secret 时使用secretKeyRef。
总结与最佳实践
- 两种方案共用
InstanceConfigYAML 模式与 secret 引用机制:sync-config适合 Docker Compose / VM / CI 的一次性同步,operator 适合 K8s 的持续调和; - 敏感字段一律使用
envRef或secretKeyRef,明文仅用于本地调试; - 理解 Replace 语义:文件即真相(source of truth),未声明的设置会被删除,受保护设置(
ducklake_settings、custom_instance_pg_databases等)除外; - License Key 可脱离配置文件单独管理,operator 不会覆盖数据库已有值;
- 所有配置变更走版本控制与代码 review,配合 CI 中的
windmill sync-config即可实现从「手工改库」到「声明式交付」的完整升级。
【免费下载链接】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),仅供参考