Files
windmill/examples/deploy/instance-config-iac
Ruben Fiszel 82e5f6de48 feat: add Kubernetes operator and instance settings YAML editor (#7836)
* Add windmill-operator crate for Kubernetes CRD-based instance config

Introduces a new `windmill-operator` crate that enables declarative
management of Windmill instance configuration via a Kubernetes
`WindmillInstance` CRD. The operator watches CRD resources and performs
full declarative sync of global_settings and worker configs to the
database, supporting GitOps workflows for instance-level configuration.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Add tests for windmill-operator CRD and db_sync

- 9 unit tests for CRD serialization, deserialization, metadata, and
  status field behavior
- 15 integration tests for db_sync using #[sqlx::test] with full
  declarative sync coverage: upsert, delete, protected keys,
  idempotency, worker config prefix handling, and end-to-end sync

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Replace untyped BTreeMap CRD fields with typed structs for schema validation

GlobalSettings, SmtpSettings, IndexerSettings, and WorkerGroupConfig now
have explicit typed fields with serde(flatten) catch-all for forward
compatibility. The generated CRD YAML includes a full OpenAPI v3 schema
that Kubernetes validates on kubectl apply.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Type opaque serde_json::Value CRD fields with real structs

Replace most remaining serde_json::Value fields in WindmillInstance CRD
with properly typed structs derived from the codebase:
- oauths: BTreeMap<String, OAuthClient>
- otel: OtelSettings
- otel_tracing_proxy: OtelTracingProxySettings with ScriptLang enum
- critical_error_channels: Vec<CriticalErrorChannel> (untagged enum)
- critical_alerts_on_db_oversize: DbOversizeAlert
- ducklake_settings: DucklakeSettings with nested catalog/storage types
- custom_instance_pg_databases: CustomInstancePgDatabases
- autoscaling (worker config): AutoscalingConfig with integration struct
- custom_tags, default_tags_workspaces: Vec<String>
- default_tags_per_workspace: bool

Still opaque (serde_json::Value): object_store_cache_config (kube-core
can't generate schemas for internally-tagged enums), secret_backend
(EE-private), slack, teams (no clear struct definitions).

Regenerated CRD YAML with full OpenAPI schema (352→703 lines).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Unify instance config types and add bulk GET/PUT API

Move all typed settings (GlobalSettings, WorkerGroupConfig, etc.) from
windmill-operator/crd.rs into windmill-common/instance_config.rs so both
the API server and operator share a single source of truth. Add diff/apply
logic (Merge mode for UI, Replace mode for operator) and InstanceConfig::from_db().

Add GET/PUT /settings/instance_config endpoints so the frontend loads all
settings in 1 call instead of 42, and saves with a single bulk PUT. The
backend handles the diff internally, running pre-write hooks for changed keys.

Refactor windmill-operator/db_sync.rs to use the shared diff+apply functions
and slim crd.rs down to the CRD wrapper with re-exports.

Includes 32 unit tests and 30 integration tests covering serialization,
diff logic, DB roundtrips, protected settings, and edge cases.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* feat: add Form/YAML toggle to instance settings UI

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix: show Form/YAML toggle regardless of hideTabs prop

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* refactor: replace toggle button group with simple YAML toggle

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* feat: mask sensitive fields in YAML view with show/hide toggle

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* feat: hide internal settings and mask sensitive fields in YAML view

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* feat: hide jwt_secret and min_keep_alive_version from API and config exports

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* all

* feat: add secretKeyRef support for sensitive fields in operator CRD

Allow sensitive fields (license_key, hub_api_secret, scim_token,
smtp_password, OAuthClient.secret, custom PG user_pwd) to reference
Kubernetes Secrets via the standard secretKeyRef pattern instead of
inlining values as plaintext YAML. The reconciler resolves all refs
by reading K8s Secrets before syncing to the database.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* all

* all

* all

* fix: merge main and update dev environment docs

Resolve merge conflicts from origin/main, fix duplicate
UV_INDEX_STRATEGY_SETTING import, and add Playwright MCP
testing instructions to CLAUDE.md.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* all

* fix: init tracing for CLI subcommands and deduplicate setting side-effects

Initialize tracing subscriber before early-return CLI paths (sync-config,
operator) so tracing calls are not silently dropped. Refactor
set_global_setting_internal to call run_setting_pre_write_hook instead of
duplicating the side-effect logic.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* feat: add `wmill instance get-config` CLI command

Dumps the current instance config (global settings + worker configs) as
YAML. Supports --output-file to write to a file instead of stdout.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* all

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-14 22:42:01 +00:00
..

Windmill Instance Configuration as Code

Windmill supports managing instance configuration (global settings + worker group configs) declaratively through YAML files. This enables Infrastructure-as-Code (IaC) workflows where your Windmill instance settings are version-controlled and applied automatically.

Two deployment models are supported:

Approach Best for Requires
sync-config CLI Docker Compose, VMs, CI/CD pipelines Database access
Kubernetes Operator Kubernetes clusters operator feature flag, RBAC

Both use the same YAML schema (InstanceConfig) and the same secret reference mechanisms.


Config File Reference

A Windmill instance config file has two top-level keys:

global_settings:
  # Instance-wide settings (stored in the global_settings table)
  base_url: "https://windmill.example.com"
  retention_period_secs: 2592000
  # ...

worker_configs:
  # Worker group configurations (stored in the config table as worker__<name>)
  default:
    worker_tags: ["deno", "python3", "bun", "go", "bash"]
  gpu:
    dedicated_worker: "ws:f/gpu_inference"
    # ...

All fields are optional. Only the fields you specify are synced to the database.

Sensitive Field References

Fields that contain secrets (license keys, OAuth secrets, SMTP passwords, etc.) support three formats:

# 1. Plain literal (not recommended for production)
license_key: "my-license-key"

# 2. Environment variable reference (works everywhere)
license_key:
  envRef: "WM_LICENSE_KEY"

# 3. Kubernetes Secret reference (K8s only)
license_key:
  secretKeyRef:
    name: windmill-secrets    # Secret resource name
    key: license-key          # Key within the Secret

Fields that support envRef and secretKeyRef:

  • license_key
  • hub_api_secret
  • scim_token
  • smtp_settings.smtp_password
  • oauths.<provider>.secret (each OAuth client secret)
  • custom_instance_pg_databases.user_pwd

Docker Compose (sync-config)

The sync-config subcommand reads a YAML config file, resolves any envRef references from the process environment, and syncs the result to the database.

How it works

  1. Windmill reads and parses the YAML file
  2. Any envRef fields are resolved from the container's environment variables
  3. The current database state is read
  4. A diff is computed (using Replace mode: settings absent from the file are deleted, except protected ones like ducklake_settings)
  5. Changes are applied to the database

Setup

See the included docker-compose.yml for a complete working example. The key parts:

1. Create your config file (windmill-config.yaml):

global_settings:
  base_url: "https://windmill.example.com"
  license_key:
    envRef: "WM_LICENSE_KEY"
  retention_period_secs: 2592000
  expose_metrics: true
  smtp_settings:
    smtp_host: "smtp.example.com"
    smtp_port: 587
    smtp_from: "windmill@example.com"
    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"]

2. Add an init container to docker-compose.yml:

services:
  windmill_config_sync:
    image: ${WM_IMAGE}
    # Run once at startup then exit
    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

3. Set secrets in your .env file (not committed to version control):

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

Re-syncing after config changes

The sync-config container runs once and exits. To re-apply after editing the YAML:

docker compose run --rm windmill_config_sync

Or, for CI/CD pipelines, run the binary directly:

windmill sync-config ./windmill-config.yaml

Replace semantics

sync-config uses Replace mode: any global setting present in the database but absent from your YAML file will be deleted (except protected settings like ducklake_settings and custom_instance_pg_databases). This ensures the database state matches the file exactly.

If you only want to manage a subset of settings, include all settings you want to keep in the YAML file.


Kubernetes (Operator)

The Windmill Kubernetes operator watches WindmillInstance Custom Resources and continuously reconciles the database to match the declared state. It also supports secretKeyRef to pull values from Kubernetes Secrets natively.

Prerequisites

  • Windmill built with the operator feature flag
  • RBAC permissions for the operator pod (see below)
  • The CRD installed in the cluster

Setup

1. Install the CRD:

windmill operator crd | kubectl apply -f -

2. Create a Kubernetes Secret for sensitive values:

apiVersion: v1
kind: Secret
metadata:
  name: windmill-secrets
  namespace: windmill
type: Opaque
stringData:
  license-key: "your-license-key-here"
  smtp-password: "your-smtp-password"
  google-oauth-secret: "your-google-oauth-secret"

3. Create the WindmillInstance resource (windmill-instance.yaml):

apiVersion: windmill.dev/v1alpha1
kind: WindmillInstance
metadata:
  name: production
  namespace: windmill
spec:
  global_settings:
    base_url: "https://windmill.example.com"
    license_key:
      secretKeyRef:
        name: windmill-secrets
        key: license-key
    retention_period_secs: 2592000
    expose_metrics: true
    smtp_settings:
      smtp_host: "smtp.example.com"
      smtp_port: 587
      smtp_from: "windmill@example.com"
      smtp_password:
        secretKeyRef:
          name: windmill-secrets
          key: smtp-password
    oauths:
      google:
        id: "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"]

4. Apply:

kubectl apply -f windmill-instance.yaml

5. Check status:

kubectl get wmi
# NAME         SYNCED   LAST SYNCED              AGE
# production   true     2025-01-15T10:30:00Z     2d

Using envRef in Kubernetes

envRef also works in the operator context. Values are resolved from the operator pod's environment. This is useful when secrets are injected via pod env vars (e.g., from a vault sidecar):

spec:
  global_settings:
    license_key:
      envRef: "WM_LICENSE_KEY"   # Read from operator pod env

The operator pod's Deployment would include:

env:
  - name: WM_LICENSE_KEY
    valueFrom:
      secretKeyRef:
        name: windmill-secrets
        key: license-key

This is functionally equivalent to using secretKeyRef directly in the CRD, but lets you use any secret injection mechanism your cluster supports (external-secrets, vault-agent, etc.).

RBAC

The operator pod needs permissions to read Secrets and manage the CRD. Minimal ClusterRole:

apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
  name: windmill-operator
rules:
  - apiGroups: ["windmill.dev"]
    resources: ["windmillinstances", "windmillinstances/status"]
    verbs: ["get", "list", "watch", "patch", "update"]
  - apiGroups: [""]
    resources: ["secrets"]
    verbs: ["get", "list", "watch"]
  - apiGroups: [""]
    resources: ["events"]
    verbs: ["create", "patch"]

Running the operator

# As a standalone process (for development)
DATABASE_URL=postgres://... windmill operator

# In production, deploy as a Kubernetes Deployment

Choosing Between envRef and secretKeyRef

Feature envRef secretKeyRef
Works in Docker Compose Yes No
Works in Kubernetes Yes Yes
Works with vault sidecars Yes No (use envRef instead)
Reads from Process environment K8s Secrets API
Requires RBAC for Secrets No Yes

Recommendation: Use envRef for portability across deployment targets. Use secretKeyRef when you want direct Kubernetes-native secret binding without intermediate env vars.


Full Settings Reference

For a complete list of available settings fields, generate the CRD schema:

windmill operator crd

The CRD's OpenAPI schema documents every field, its type, and whether it's optional. The same schema applies to sync-config YAML files.