Install on Kubernetes
Run OpenETL's API servers, orchestrators, runners and pod launcher on a Kubernetes cluster, and scale them with KEDA and Karpenter.
The manifests in deploy/kubernetes install OpenETL on Kubernetes through kubectl apply -k. You use them from the release's tag, as the base of a kustomization of your own. The manifests provide these components:
| File | What it runs |
|---|---|
config.yaml | The settings every component shares (ConfigMap openetl) |
server.yaml | The API server: two replicas, Service openetl-server on port 8080 |
orchestrator.yaml | Two orchestrators, which run workflows and place jobs on runners |
runner-default.yaml | The default pool's shared runners |
launcher-default.yaml | The default pool's pod launcher, which runs large jobs in pods of their own, and the PodTemplate they're made from |
keda/ | A component that scales the runners with KEDA |
karpenter/ | Karpenter node pools, and a component that sends runners and job pods to them |
The manifests do not include Postgres or object storage. Configure those services before installation.
Before you start
- Postgres for metadata. The default DBOS backend stores workflows in a second database, named after the metadata database with
_dbosappended. Orchestrators create it if their database user has permission. Otherwise, create it yourself or specify another database withOPENETL_DBOS_DATABASE_URL. Do not connect through a transaction pooler. - A bucket in S3, GCS or Azure Blob Storage for checkpoints and staging. Runners, job pods and the pod launcher need read and write access to it, through a workload identity or keys in the Secret
openetl-runner. - Network access to
ghcr.iofrom the cluster's nodes and runners. Nodes pull OpenETL's image,ghcr.io/openetl-io/openetl:0.1.0-alpha, forlinux/amd64orlinux/arm64. Runners download the connector packages that you install. The image holds no external connectors. - kubectl 1.27 or later, and git, which kubectl uses to read the manifests from GitHub.
- The
openetlCLI on your computer, as Install the CLI shows.
Install
-
Make a directory for your installation's kustomization. In it, write
kustomization.yaml. ReplaceBUCKETwith your bucket's URL, such ass3://my-bucket:apiVersion: kustomize.config.k8s.io/v1beta1 kind: Kustomization namespace: openetl resources: - https://github.com/openetl-io/openetl//deploy/kubernetes?ref=v0.1.0-alpha images: - name: openetl newName: ghcr.io/openetl-io/openetl newTag: 0.1.0-alpha patches: - patch: | apiVersion: v1 kind: ConfigMap metadata: name: openetl data: OPENETL_CHECKPOINTS: BUCKETThe resource is the release's manifests, at the tag
v0.1.0-alpha. The image is the same release's. The patch changes the ConfigMapopenetl, which every component reads. Add your other settings to itsdata, such asOPENETL_PUBLIC_URL. Buckets gives the order in which runners look for credentials.To check what you are about to apply, run
kubectl kustomize .in the directory. It prints the manifests and changes nothing. -
Give runners access to the bucket. On EKS, runners, job pods and the launcher can use the bucket through an IAM role for a service account or through Pod Identity, without keys. Give the role to two service accounts in the
openetlnamespace:default, which runners, job pods, the server and the orchestrators use.openetl-launcher, which the launcher uses. The launcher checks the bucket when it starts, and does not start without access.
Run on AWS shows the commands for both. The service account
openetl-launcherexists after step 4. If you annotate service accounts for IRSA, do it after that step. By default, connectors do not get this role. Credentials for connectors explains what to do. -
Create the namespace, and the Secret the server and orchestrators read:
kubectl create namespace openetl kubectl -n openetl create secret generic openetl \ --from-literal=OPENETL_DATABASE_URL='postgres://openetl:PASSWORD@HOST:5432/openetl' \ --from-literal=OPENETL_SECRETS_KEY="base64key://$(openssl rand -base64 32 | tr '+/' '-_')"OPENETL_SECRETS_KEYencrypts pipeline secrets. Keep a backup of this key; stored secrets cannot be decrypted without it. A KMS key is also supported (awskms://,gcpkms://,azurekeyvault://). -
In your kustomization's directory, apply the manifests and wait for the server:
kubectl apply -k . kubectl -n openetl rollout status deploy/openetl-serverRunners and the launcher wait for their Secret, made in step 6.
-
Create an owner key. This command writes and prints a new key in the server pod selected by
kubectl:kubectl -n openetl exec deploy/openetl-server -- sh -c 'openetld bootstrap-key 2>/dev/null && cat .openetl/admin.key' -
Replace
OWNER_KEYwith the key from step 5. Create a runner token and theopenetl-runnerSecret. If the pods have no workload identity, also add bucket credentials to this Secret:kubectl -n openetl port-forward svc/openetl-server 8080 & export OPENETL_API_URL=http://localhost:8080 OPENETL_API_KEY="OWNER_KEY" openetl dataplane token create --out runner.token default kubectl -n openetl create secret generic openetl-runner \ --from-literal=OPENETL_RUNNER_TOKEN="$(cat runner.token)" -
List the official connectors. On a new installation, the catalog can be empty for up to 16 minutes. This command lists the official registry now:
openetl connector catalog --refreshThe output lists each official connector and the command that installs it.
To expose the API outside the cluster, configure an Ingress or load balancer for the openetl-server Service. Set OPENETL_PUBLIC_URL in your ConfigMap patch to the public console URL, then apply again.
Install the connectors that your pipelines need with openetl connector apply. openetl connector catalog lists the official ones, with the command that installs each. Choose and manage connectors covers versions and private registries.
Every component serves Prometheus metrics on port 9464, and its pods carry the prometheus.io/scrape and prometheus.io/port annotations.
Credentials for connectors
By default, a connector does not get the role that IRSA or Pod Identity gives the pod. Give a source or destination its own role with aws.role, which the runner assumes for its connector. Set the roles that runners may assume in OPENETL_AWS_ASSUME_ROLES, in your ConfigMap patch. Run on AWS explains the setup, the other ways, and what OpenETL can and cannot block.
Connectors are trusted code lists what a connector gets from the runner.
How jobs are placed
Each job has a size, from xs (0.5 vCPU, 1 GiB) to xl (8 vCPUs, 16 GiB), which the pipeline's spec.resources sets or openetl picks. The orchestrators place it in its pool (spec.resources.pool, default if not set):
- On a shared runner with room. A runner's room is its limits less
OPENETL_RESERVE(0.5 vCPU and 512 MiB), so inrunner-default.yamlit's 3.5 vCPUs and 7.5 GiB. Keep its requests equal to its limits. - In a separate pod created by the pool's launcher for jobs at or above
OPENETL_POD_MIN_SIZE(mhere). This also applies when no runner in the pool can fit the job. Pod requests and limits include the job's size plus the reserve.OPENETL_MAX_PODS(20) andOPENETL_MAX_POD_CPU(64) limit total pod capacity. Jobs wait when these limits are reached.
Jobs wait until capacity is available. Before stopping, a runner finishes jobs at a checkpoint. It allows up to 2 minutes for a sync and 10 minutes for a stream. Its termination grace period covers that time. Jobs are then placed again.
A pod's spec comes from the PodTemplate openetl-attempt-default, to which the launcher adds the job, labels, resources and node constraints. To change it, add a patch for the PodTemplate to your kustomization. The launcher reads the template when it starts, so restart it after you apply the change (kubectl -n openetl rollout restart deploy/openetl-launcher-default).
Spot nodes
Sync pods tolerate GKE and AKS spot taints and can run on spot nodes. On an interruption notice, they stop at a checkpoint and are placed again.
Stream pods also tolerate spot taints but prefer non-spot nodes. They carry karpenter.sh/do-not-disrupt to prevent consolidation. An interrupted stream checkpoints within the notice period when possible. Otherwise, it resumes from its last checkpoint. Recovery preserves the stream's delivery guarantee.
To prohibit stream pods on spot nodes, set OPENETL_POD_STREAM_SPOT=avoid on the launcher. These pods cannot start if only spot nodes are available.
Karpenter requests a non-spot node before relaxing the default preference. The Cluster Autoscaler does not consider that preference when selecting a node group. With the Cluster Autoscaler, use avoid when streams must never run on spot.
To add a pool, download runner-default.yaml and launcher-default.yaml from the release's tag into your kustomization's directory. Change their names, the pool's label and OPENETL_POOL, and add both files to resources. Point pipelines at the pool with spec.resources.pool. A pool with only a launcher runs every job in pods.
Scaling
Runners, with KEDA
The keda component scales shared runners in the default pool using orchestrator placement metrics. It compares running and pending job demand with each runner's available CPU and memory. Jobs assigned to separate pods are excluded. This requires KEDA 2.x and Prometheus scraping the annotated pods.
-
Add the component, and a patch that sets
serverAddressto your Prometheus, to yourkustomization.yaml:components: - https://github.com/openetl-io/openetl//deploy/kubernetes/keda?ref=v0.1.0-alpha patches: - target: kind: ScaledObject name: openetl-runner-default patch: | - op: replace path: /spec/triggers/0/metadata/serverAddress value: http://prometheus.monitoring.svc:9090 - op: replace path: /spec/triggers/1/metadata/serverAddress value: http://prometheus.monitoring.svc:9090Put this patch in the same
patcheslist as the ConfigMap's. If you change the runners' size, also replace both triggers'threshold(/spec/triggers/0/metadata/thresholdand/spec/triggers/1/metadata/threshold) to match their room. -
Apply with
kubectl apply -k ..
The queries, for scaling with something else:
(sum(max by (size) (openetl_placement_cpu_reserved{data_plane="default", pool="default", runs_on="runner"})) or vector(0))
+ (sum(max by (size) (openetl_placement_cpu_pending{data_plane="default", pool="default", runs_on="runner"})) or vector(0))and the same over openetl_placement_memory_reserved_bytes and openetl_placement_memory_pending_bytes. Every orchestrator exports the same values, so max by (size) counts them once. runs_on="pod" is what the launcher's pods have or wait for, and openetl_placement_pending_age_seconds how long the oldest job has waited.
KEDA keeps at least one shared runner. Without one, a pool with a launcher sends every job to a separate pod. Scale-down starts after ten minutes of lower demand and removes one runner per minute. A removed runner can have active jobs; they move after a checkpoint.
Nodes, with Karpenter
karpenter/nodepools.yaml has two Karpenter node pools for AWS, which expect an EC2NodeClass named default:
openetl-attempts, for job pods, on spot or on-demand instances. Consolidation leaves stream pods alone, and sync pods on spot finish at a checkpoint on an interruption notice and are placed again.openetl-runners, for shared runners, on-demand only, removed only when empty.
Apply the node pools:
kubectl apply -f https://raw.githubusercontent.com/openetl-io/openetl/v0.1.0-alpha/deploy/kubernetes/karpenter/nodepools.yamlThen add https://github.com/openetl-io/openetl//deploy/kubernetes/karpenter?ref=v0.1.0-alpha to components in your kustomization.yaml, and apply. This gives runners and job pods the node selector and toleration for their pool.
A runner pool used only for scheduled syncs can also use spot instances. If the cluster can add only spot instances, allow spot in openetl-runners. Streams on those runners have the same interruption behavior as stream pods on spot.
With the Cluster Autoscaler instead, make node groups with the same openetl.io/nodes labels and taints.
The control plane
API servers and orchestrators hold no state but the database, so a plain HorizontalPodAutoscaler on CPU scales them. On DBOS, an orchestrator that stops has its workflows taken over by another within a minute.
Temporal
To run workflows on Temporal instead of DBOS, set OPENETL_ORCHESTRATOR: temporal, OPENETL_TEMPORAL_ADDRESS and OPENETL_TEMPORAL_NAMESPACE in your ConfigMap patch before the first install. OpenETL does not support a change of an installation's orchestrator later. Changing it later gives the steps for when there is no other way.
Upgrading
Each release has a version, such as 0.1.0-alpha. Its notes on the releases page list what changed. In the alpha, the pipeline format and the API can change between releases.
- Read the notes of each release between your version and the new one.
- Back up the metadata database and keep a copy of
OPENETL_SECRETS_KEY. Back up and restore lists what an installation's state is. - In your
kustomization.yaml, changeref=v0.1.0-alphain every URL andnewTagto the new version. - Check the change with
kubectl kustomize ., then apply it withkubectl apply -k .. - Upgrade the CLI to the same version, as Install the CLI shows.
New orchestrators resume existing workflows, and syncs resume from their checkpoints. Old runners checkpoint their jobs before stopping, then those jobs are placed again. Existing job pods finish with the image they started with.
A newer version can write a checkpoint format that older runners cannot read. An older runner refuses that pipeline with CONFIG_INVALID. If this occurs after a downgrade, run the affected pipelines with the newer version.
Uninstall
Warning
Deleting the namespace deletes the Secret openetl, with OPENETL_SECRETS_KEY. Without a copy of that key, nobody can read the secrets that the metadata database holds.
In your kustomization's directory, delete the manifests, then the namespace with its Secrets and any job pods that remain:
kubectl delete -k .
kubectl delete namespace openetlIf you applied the Karpenter node pools, delete them with kubectl delete -f and the same URL. The metadata database, the DBOS database and the bucket are outside the cluster. Delete them separately when you no longer need their data.