Experimental managed dbt¶
Run dbt Core commands against temporary Spark compute on an existing Kubernetes cluster. The wrapper creates one server when dbt first opens a connection, shares it across dbt worker threads and across the commands of one invocation, and deletes its driver, executors and private Services when the invocation ends. Choose Thrift or Spark Connect in the profile; you do not create the server yourself.
Experimental, separately installed integration
This feature lives in integrations/dbt and is not part of a normal
spark-on-k8s installation. Its profile and Python interfaces may change.
The initial compatibility target is dbt Core 1.12.6, dbt-spark 1.11.0,
Spark/client 4.3.0, PyHive 0.7.0, and Thrift 0.25.0. It is a Python
dbt Core adapter, not a dbt Fusion, dbt Cloud or dbt v2 integration. SQL models, seeds and data tests are supported;
Python models are explicitly rejected. dbt clone is explicitly rejected in this experiment.
Prepare the platform¶
The platform owner supplies:
- An existing namespace and driver service account with the permissions described in server setup. This integration does not create or delete the Kubernetes cluster, namespace or RBAC.
- A compatible Spark image with the bundled Connect or HiveThriftServer2 JAR.
- A shared warehouse reachable by the driver and executors and an external
Hive metastore reachable by each new driver. The profile requires both. An
embedded driver-local metastore would disappear when the driver is deleted.
Driver-local warehouse paths, including
file://, are rejected because this experiment does not mount persistent driver volumes. Supply a remote filesystem URI and its Spark connector; the integration fixture tests HDFS. - A trusted network for the private, plaintext server and catalog endpoints.
The experiment uses Thrift
auth: NONE; it is not a multi-tenant gateway.
For a local client, install kubectl and provide an explicit kubeconfig path and
context. The wrapper opens a loopback-only tunnel to its driver, using an
OS-assigned local port. If kubectl exits mid-run, statements on that tunnel fail
and are not replayed; the next dbt connection gets a new tunnel after the driver
UID and application identity are verified again. For a client already inside
Kubernetes, set in_cluster: true and omit both kubeconfig fields; it connects to the private
Service directly. The client's identity needs permission to create/read/delete
its driver and Services, read the namespace/service account and observe executor
pods, Services and ConfigMaps (including list permission for garbage-collection
checks); local access also needs pod port-forward permission.
spark-on-k8s-dbt check verifies these
permissions without creating anything.
Install from the repository¶
Until the experimental distribution is separately published, build both wheels from the same checkout. These commands use an isolated Python 3.12 environment:
# From the spark-on-k8s repository root:
uv build --wheel --out-dir dist/dbt-experiment
uv build --project integrations/dbt --wheel --out-dir dist/dbt-experiment
uv venv --python 3.12 .venv-dbt
uv pip install --python .venv-dbt/bin/python dist/dbt-experiment/*.whl
uv pip check --python .venv-dbt/bin/python
For reproducible development/testing, use the separate lockfile instead:
UV_PROJECT_ENVIRONMENT="$PWD/.venv-dbt" uv sync \
--project integrations/dbt --locked --python 3.12 --group test
The integration declares PyHive and patched Thrift directly instead of selecting
dbt-spark[pyhive], whose current extra caps Thrift below the patched releases.
Do not add that extra to this environment: let the normal dependency resolver
keep the transport compatible. Thrift's protocol size/recursion protections remain
enabled; unusually large individual result fields can be rejected by those limits.
The package accepts Python 3.11–3.13. Kubernetes execution is tested with Python 3.12. The independent lockfile prevents dbt's dependencies from changing the normal SDK, API or Airflow environments.
Configure a managed profile¶
Use this profile in your dbt project's profiles.yml, replacing the namespace,
service account, kubeconfig, metastore and warehouse with your platform values:
analytics:
target: connect
outputs:
connect:
type: spark_on_k8s
method: connect
schema: analytics
threads: 2
compute:
kube_config_file: /absolute/path/to/kubeconfig
kube_context: my-context
namespace: spark
service_account: spark
image: apache/spark:4.3.0-scala2.13-java21-python3-ubuntu
server_jar: local:///opt/spark/jars/spark-connect_2.13-4.3.0.jar
startup_timeout: 180
cleanup_timeout: 120
driver_resources: {cpu: 1, memory: 1024, memory_overhead: 512}
executor_resources: {cpu: 1, memory: 1024, memory_overhead: 512}
executor_instances: {initial: 2}
spark_conf:
spark.sql.catalogImplementation: hive
spark.sql.warehouse.dir: hdfs://namenode.data.svc:8020/warehouse
spark.hadoop.hive.metastore.uris: thrift://metastore.data.svc:9083
spark.sql.shuffle.partitions: '4'
Your dbt_project.yml must select profile: analytics. To use Thrift, change
target and the output name to thrift, set method: thrift, and replace
server_jar with:
server_jar: local:///opt/spark/jars/spark-hive-thriftserver_2.13-4.3.0.jar
The adapter selects the host and port. Do not supply host, port, SPARK_REMOTE
or the upstream method: session workaround. Connect uses a separate explicit
remote session for each dbt connection. Thrift uses PyHive's HiveServer2 protocol,
not a JDBC client.
compute accepts app_name (default dbt-connect or dbt-thrift; set one per project or
pipeline so its servers are recognizable), the explicit image, namespace, service account, resource,
executor-allocation, package, node-selector and max_runtime fields of
ConnectServerConfig, plus the cluster-selection
fields above, startup_timeout, cleanup_timeout and poll_interval (positive seconds, at
most 3600). max_runtime is unset by default; see
receipts, failure and recovery.
With method: connect, it also accepts the
image pull Secret, Secret-backed environment and toleration fields
and executor_node_selector, for example image_pull_secrets: [private-registry]
or executor_env_from_secrets: {AWS_ACCESS_KEY_ID: "s3-credentials:access-key"}.
method: thrift supports only driver_node_selector of these and rejects the
others before contacting the cluster.
Thrift fixes its managed port to 10000. Managed identity, ports and routing cannot
be overridden through spark_conf. Put static Hive/catalog configuration in
compute.spark_conf; server_side_parameters accepts runtime spark.sql.* settings; catalog, warehouse,
Hive metastore and extension settings are excluded.
Check a profile before running¶
spark-on-k8s-dbt check validates the selected output and its cluster access
without creating a Spark server or any other Kubernetes resource, so configuration
errors appear in seconds instead of after compute has been requested. It accepts
dbt's profile-selection options with dbt's defaults and environment variables:
--project-dir (DBT_PROJECT_DIR), --profiles-dir (DBT_PROFILES_DIR),
--profile (DBT_PROFILE), --target/-t (DBT_TARGET) and --vars. With
--profile, a dbt project is not required.
/path/to/spark-on-k8s/.venv-dbt/bin/spark-on-k8s-dbt check \
--project-dir /path/to/analytics --profiles-dir /path/to/analytics --target connect
Each check reports pass, fail or skip; a failure includes a next step:
| Check | Verifies |
|---|---|
profile |
dbt renders and selects the output (including env_var() and --vars) and the adapter accepts its credentials, through the same code dbt uses. |
compute |
The adapter's compute validation, including the kubeconfig path and the Thrift server's managed settings. |
client_libraries |
The transport client imports: the Spark Connect client, or PyHive with its Thrift SASL and HiveServer2 modules. |
kubectl |
kubectl is on PATH for a local client. Skipped with in_cluster: true. |
kubernetes_config |
The selected kubeconfig and context, or the in-cluster configuration, loads. |
namespace |
The namespace exists, is not terminating, and the invoking identity can read it. |
service_account |
The driver service account exists. Its own permissions are not verified. |
permission.<verb>.<resource> |
A SelfSubjectAccessReview for each request the wrapper makes as the invoking identity. |
The permission checks cover get on the namespace and driver service account;
create, get, list and delete on Pods and Services; list on ConfigMaps for
cleanup observation; and, for a local client, create on pods/portforward. The
wrapper itself creates no Secrets or ConfigMaps; executor resources are created by
the driver's service account. The API server evaluates access reviews without
storing them. A review that cannot be performed fails rather than being assumed
allowed. A missing namespace, rejected credentials or an unreachable API server
skip the dependent checks. Kubernetes requests share a total --timeout (default
30 seconds).
--output json prints one versioned document on stdout; dbt's own warnings and
other messages go to stderr. Every check ID appears once, in a fixed order:
{
"schema_version": 1,
"operation": "dbt_check",
"status": "fail",
"profile": "analytics",
"target": "connect",
"method": "connect",
"namespace": "spark",
"checks": [
{"id": "profile", "status": "pass", "message": "...", "remediation": null},
{"id": "permission.delete.pods", "status": "fail", "message": "Denied: delete pods in namespace spark.",
"remediation": "Grant the invoking identity delete on pods in namespace spark."}
],
"error": null
}
status is fail when any check fails. error is null unless the check was
interrupted (interrupted) or stopped unexpectedly (operation_error). The command
exits 0 when no check failed, 1 when any check failed, 2 for invalid options and
130 when interrupted.
A passing check is not a successful run
The check does not pull the image, schedule a driver, start Spark or contact
the warehouse and metastore. Image pull failures, quotas, admission policies,
node capacity, the driver service account's permissions, network policy and
Spark configuration errors still surface only when spark-on-k8s-dbt run
starts compute.
Run dbt with automatic compute cleanup¶
Use the installed executable's absolute path if your current directory is a separate dbt project:
/path/to/spark-on-k8s/.venv-dbt/bin/spark-on-k8s-dbt run -- \
build --project-dir /path/to/analytics --profiles-dir /path/to/analytics
Each wrapper invocation creates its own server. For example, seed, build,
run-operation, test and docs generate can each use the same wrapper; to pay
the server startup once for several of them, see
Run several commands on one server:
/path/to/spark-on-k8s/.venv-dbt/bin/spark-on-k8s-dbt run -- \
docs generate --project-dir /path/to/analytics --profiles-dir /path/to/analytics
The server survives dbt's intermediate connection cleanup, including the
compilation phase of docs generate. It is removed only when the entire command
finishes. Tables remain in the external warehouse/catalog for the next invocation.
Commands that never open a database connection, such as parse and ls, do not
provision compute. compile opens one to fill its relation cache unless you pass
--no-populate-cache.
Using plain dbt with this managed adapter reports that the wrapper is required;
there is no unbounded background server hidden behind a connection-close hook.
While the server starts, and while it is deleted at the end, the wrapper writes short progress lines with the elapsed time to stderr:
Managed Spark +0s: requesting a Spark Connect server in namespace spark (startup timeout 180s)
Managed Spark +1s: driver pod dbt-connect-0f3c...-driver created
Managed Spark +1s: waiting for the driver pod to be scheduled (Unschedulable: 0/3 nodes are available: 3 Insufficient cpu.)
Managed Spark +12s: driver pod scheduled on worker-2; container driver is ContainerCreating
Managed Spark +40s: driver container running
Managed Spark +40s: opening a kubectl port-forward tunnel to the driver
Managed Spark +41s: waiting for the Spark Connect endpoint to answer a query
Managed Spark +55s: Spark Connect server ready (application dbt-connect-0f3c...)
A new phase prints immediately; an unchanged phase repeats at most every 30
seconds. Image pull errors point at compute.image, so you can interrupt instead of
waiting for startup_timeout. The lines never go to stdout, so dbt's stdout,
including --log-format json events, is unchanged; they are also not written to
dbt's log file. They follow dbt's resolved console settings: --quiet
(DBT_QUIET) or --log-level warn, error or none (DBT_LOG_LEVEL, or the
project's flags) suppresses them. The receipt path is always printed.
Statements are not automatically replayed (query_retries must be 0). Spark SQL
is not transactional: failure and cancellation can leave successfully written
tables or rows. Review dbt's normal incremental/idempotency behavior before retrying.
Connect supports cancellation but does not accept query_timeout in this initial
version; Thrift retains dbt-spark's query timeout setting. Thrift serializes
requests on each connection and limits acquisition of the request lock and
individual blocking socket operations to 15 seconds. These transport limits do not cap the full SQL
execution time: queries run asynchronously and the adapter polls for completion.
A failed transport is closed rather than reused, and statements are not replayed.
Connect buffers SQL results for dbt metadata/seed/materialization operations; it
is not a streaming query-results interface.
Run several commands on one server¶
Every invocation pays one server startup: scheduling the driver, pulling its image
and starting Spark often take minutes. A job that runs seed, run and test, or
build and then docs generate, pays it once when the commands share one
invocation. Separate them with further --:
export DBT_PROJECT_DIR=/path/to/analytics DBT_PROFILES_DIR=/path/to/analytics
/path/to/spark-on-k8s/.venv-dbt/bin/spark-on-k8s-dbt run -- \
deps -- seed --full-refresh -- build -- docs generate
Each group after a -- is passed to dbt exactly as written, as dbt <group>
would receive it; the wrapper does not re-split or re-quote it. dbt reads
DBT_PROJECT_DIR, DBT_PROFILES_DIR and DBT_TARGET, so shared options need not
be repeated per command. Put wrapper options (--receipt-dir, --keep-going,
--deadline) before the first dbt argument: everything after it belongs to dbt,
and a standalone -- always starts the next command rather than being passed to
dbt.
The commands run one after another in the wrapper's process:
| Topic | Behavior |
|---|---|
| Server | Created by the first command that opens a connection (deps does not), reused by every later command and deleted once, when the sequence ends, with the cleanup guarantees of a single command. |
| Receipt | One receipt covers the server for the whole sequence. Its dbt_exit_code is the sequence's exit code. |
| Failure | The sequence stops after the first command that fails. With --keep-going, the remaining commands still start. |
| Exit code | 130/143 when interrupted, even after an earlier failure. Otherwise the first failed command's code (1 or 2), also with --keep-going; then 5 when --deadline left a command unstarted; 0 when every command succeeded. A cleanup failure turns 0 into 2. |
| Between commands | The wrapper confirms that the same driver (by UID) is still running, and reopens an exited kubectl tunnel to it, before the next command starts. A failed startup, or a driver that stopped, is being deleted or was replaced, ends the sequence with exit 2 unless a command already failed. A sequence never creates a second server. |
| Interruption | Ctrl-C or SIGTERM during a command stops it as for a single command, and no later command starts. Between commands, nothing further starts. Compute is deleted in both cases. |
--deadline SECONDS |
No command starts once this many seconds have passed since the wrapper started. A running command is not interrupted or shortened: the deadline is not a cancellation. Owned compute is still deleted when the sequence ends. |
| dbt semantics | Each command is a separate dbt invocation: dbt parses the project again and applies its own --fail-fast, hooks, artifacts and result. Each command opens its own SQL sessions, so session settings and temporary views do not carry over. |
Every command must select the same managed output. A command whose rendered
profile selects a different method or compute mapping fails its first
connection rather than creating another server; schema, threads and --vars
that do not change compute may differ.
With several commands, the wrapper also reports each command's result and a final summary on stderr:
Managed dbt 1/4 (dbt deps) succeeded in 4s
Managed Spark +0s: requesting a Spark Connect server in namespace spark (startup timeout 180s)
...
Managed dbt 2/4 (dbt seed) succeeded in 71s
Managed dbt 3/4 (dbt build) failed with exit 1 after 240s
Managed Spark +0s: deleting managed Spark compute and observing its removal
Managed Spark +6s: managed Spark compute deleted
Managed dbt run finished with exit 1: 2 succeeded, 1 failed, 1 not run (stopped after a failed command; --keep-going runs the rest)
Commands are named by their leading words only; option values such as --vars
are never echoed. Success lines follow dbt's console settings, as startup progress
does. A failed or interrupted command, and the summary of a sequence that failed
or left a command unstarted, are always printed. A single command prints none of
these lines.
The server belongs to this one process for the duration of the sequence. It is not a shared or warm pool: other processes, including concurrent CI jobs, cannot attach to it.
Receipts, failure and recovery¶
The wrapper prints the recovery receipt path to stderr. By default each invocation
gets a unique directory under .spark-on-k8s/dbt/ in the working directory. Set
--receipt-dir /new/private/directory before -- to choose it explicitly; the
directory must not already exist. Add .spark-on-k8s/ to the project's
.gitignore; every invocation, including one that starts no server, creates a
directory there. Receipts have private file permissions and retain
resource UIDs and cluster selection, not the original Spark configuration or its
catalog credentials. Protect the dbt profile and normal dbt logs separately.
With --fail-fast, dbt cancels running statements and then still runs on-run-end
hooks on the same compute. After Ctrl-C/SIGTERM the adapter opens no new connection.
Normal success, dbt failures and a graceful Ctrl-C/SIGTERM all attempt cleanup.
Exit codes are 0 for success, 1 for a dbt execution failure, 2 for invocation or
cleanup errors, 5 when a sequence's --deadline left a command unstarted, and
130/143 for interruption. A cleanup failure preserves an
already failing dbt exit code and records the separate cleanup failure. A successful
command becomes nonzero if cleanup fails. Cleanup verifies the recorded namespace,
driver and Service identities and observes disappearance before reporting success.
It never deletes the warehouse or metastore.
A hard kill, machine loss, unavailable Kubernetes API, or ambiguous creation can
leave compute running. If creation fails after the driver's UID is confirmed, the
wrapper requests deletion of that driver by UID (its Services are owned by it) and
records the outcome as creation_cleanup. The receipt stays incomplete because
disappearance was not observed; confirm the driver is gone. After inspecting a
complete receipt, retry safe cleanup:
/path/to/spark-on-k8s/.venv-dbt/bin/spark-on-k8s-dbt cleanup \
--receipt /path/to/invocation/receipt.json
Deletion refuses changed identities and incomplete receipts. In those cases, inspect the recorded application and Kubernetes UIDs manually; do not delete by name alone or discard the receipt. No external orphan-reaper controller is installed. Avoid concurrent invocations targeting the same dbt output tables; separate process ownership protects compute, not conflicting SQL writes.
To bound what a hard kill can leave running, set compute.max_runtime (whole seconds)
above the longest invocation you expect, including startup and on-run-end hooks. All
commands of a sequence share one server, so the
limit must cover the whole sequence. Kubernetes stops the server's driver that long
after its pod starts, whether or not the wrapper survives; a command still running at
that point fails like any lost compute. The stopped driver pod and its Services remain
until cleanup or apps prune
deletes them. See driver lifetime
limits.
Run in CI¶
This recipe uses plain shell steps, so it fits GitHub Actions, GitLab CI or any
runner that can reach the Kubernetes API. Install the wheels as in
Install from the repository and put the
environment's bin directory on PATH.
Keep credentials out of the repository¶
Commit a CI output that reads everything secret or job-specific from the environment, and let the CI system inject the values from its secret store:
analytics:
target: ci
outputs:
ci:
type: spark_on_k8s
method: connect
schema: "{{ env_var('DBT_CI_SCHEMA') }}"
threads: 4
compute:
kube_config_file: "{{ env_var('SPARK_KUBECONFIG') }}"
kube_context: "{{ env_var('SPARK_KUBE_CONTEXT') }}"
namespace: spark-ci
service_account: spark
image: registry.example.com/analytics/spark-dbt:4.3.0
server_jar: local:///opt/spark/jars/spark-connect_2.13-4.3.0.jar
startup_timeout: 600
executor_instances: {initial: 2}
spark_conf:
spark.sql.catalogImplementation: hive
spark.sql.warehouse.dir: hdfs://namenode.data.svc:8020/warehouse
spark.hadoop.hive.metastore.uris: thrift://metastore.data.svc:9083
Write the kubeconfig secret to a private temporary file, or use your CI's
file-type secret, and pass its path in SPARK_KUBECONFIG; never commit a
kubeconfig, token or literal credential. Give CI an identity limited to the
permissions that spark-on-k8s-dbt check
verifies. For catalog
or storage credentials in compute.spark_conf, read variables whose names start
with DBT_ENV_SECRET, which dbt scrubs from its logs. Receipts never contain the
compute configuration.
Give each pull request its own schema¶
Derive the schema from the pull request number so concurrent pull requests never write the same tables:
export DBT_CI_SCHEMA="analytics_pr_${PR_NUMBER:?}" # from your CI's pull request variable
dbt's default generate_schema_name prefixes custom schemas with the target
schema (analytics_pr_123_marts), so models with +schema stay separate too. If
your project overrides generate_schema_name, or a snapshot sets a fixed
target_schema, check that the CI target still produces pull-request names. The
wrapper never deletes warehouse or catalog data: drop a pull request's schemas when
it closes, for example with a run-operation macro that refuses any schema
without the analytics_pr_ prefix.
Different pull requests run separate invocations with separate servers and
receipts. Each running job adds a driver and its executors to the namespace, so
cap CI concurrency or the namespace quota: a driver that cannot be scheduled waits
until startup_timeout. Two jobs for the same pull request write the same schema;
let your CI cancel or serialize the older one (for example a GitHub Actions
concurrency group with cancel-in-progress, or GitLab interruptible jobs).
Steps¶
# Step 1: fail in seconds on configuration or permission errors; creates nothing.
spark-on-k8s-dbt check
# Step 2: one server for the whole job. exec makes the wrapper the step's main
# process, so a cancellation signal sent only to that process reaches it.
exec spark-on-k8s-dbt run --receipt-dir "$JOB_TMP/dbt-receipt" --deadline 2400 -- \
deps -- seed -- build -- docs generate
JOB_TMP is a private per-job directory, such as $RUNNER_TEMP on GitHub
Actions; the receipt directory must not exist yet. Choose --deadline below the
job timeout so that the last command and cleanup can finish: with a 60-minute
limit, --deadline 2400 starts no command after 40 minutes.
CI systems usually signal a cancelled or timed-out job and kill it after a grace
period that can be shorter than cleanup_timeout. Add a step that your CI runs
even after failure or cancellation (for example if: always() on GitHub Actions)
and keep the receipt as a job artifact. It contains resource identities and
cluster selection, not credentials:
# Step 3: always run. Retry cleanup unless the receipt shows nothing to delete.
receipt="$JOB_TMP/dbt-receipt/receipt.json"
if [ -f "$receipt" ]; then
stage=$(python -c 'import json, sys; print(json.load(open(sys.argv[1]))["stage"])' "$receipt")
case "$stage" in
cleaned | not_started | reserved) echo "No managed compute to delete ($stage)" ;;
*) spark-on-k8s-dbt cleanup --receipt "$receipt" ;;
esac
fi
reserved means the wrapper stopped before it requested any compute.
cleanup refuses a receipt without a complete server identity: that step then
fails, and the receipt tells you what to inspect manually. Nothing runs if the
runner itself disappears before this step; compute then keeps running until you
run cleanup with the preserved receipt, or until compute.max_runtime, if set,
stops the driver. Set it above the job timeout.
Size the managed server¶
compute sizes one Spark application: a driver that runs the Thrift or Connect
server and plans every query, and executors that run the tasks. dbt's threads
sets how many models run at once; each is a concurrent Spark query on the same
executors. Resources and executor allocation come only from these fields;
spark_conf cannot override them.
compute field |
Spark setting | Kubernetes Pod |
|---|---|---|
driver_resources.cpu |
none | Driver CPU request and limit |
driver_resources.memory (MiB) |
spark.driver.memory (JVM heap) |
Driver memory request and limit: memory + memory_overhead |
driver_resources.memory_overhead (MiB) |
none: headroom outside the heap | Included in the driver memory above |
executor_resources.cpu |
spark.executor.cores: concurrent tasks per executor |
Executor CPU request, unless spark_conf sets a smaller spark.kubernetes.executor.request.cores |
executor_resources.memory (MiB) |
spark.executor.memory |
Executor memory request and limit: memory + memory_overhead |
executor_resources.memory_overhead (MiB) |
spark.executor.memoryOverhead |
Included in the executor memory above |
executor_instances.initial alone |
spark.executor.instances (2 when nothing is set) |
A fixed number of executor Pods |
executor_instances.min or max |
Dynamic allocation with shuffle tracking, starting from initial (at least min) |
Executors added while tasks queue and removed after 60 idle seconds by default, later while they hold shuffle data still in use |
Each cpu and memory value applies to every executor; executors × cpu is the
number of tasks that run at once across all models. More threads than that
rarely helps, and Spark's default FIFO scheduling can let one large model's tasks
delay smaller ones. Set spark.sql.shuffle.partitions in compute.spark_conf to
a small multiple of the total executor cores for small data; Spark's default of
200 creates many tiny tasks.
Memory overhead. A container that exceeds its Pod memory limit is killed by
Kubernetes. For an executor, Spark retries the lost tasks elsewhere; for the
driver, the server and the running dbt command fail. The heap is memory;
everything else (JVM metadata, threads, network and shuffle buffers, native
libraries) must fit in memory_overhead. Spark's own default rule is 10% of
memory with a 384 MiB minimum, but spark-on-k8s always sets the overhead (512 MiB
when omitted), so raise it yourself above about 5 GiB of memory. 0 is accepted
and leaves no headroom. The driver also holds query plans and broadcast tables, and
every result returned to dbt passes through it: large seeds and docs generate on
big catalogs need driver memory, not more executors.
Startup time. These mostly decide how long startup_timeout must be:
- Image size and pull policy. Spark images are large. The default
image_pull_policy: IfNotPresentwith an immutable tag or digest lets nodes reuse a cached image;Alwayscontacts the registry on every start. Pre-pulling the image onto nodes removes the pull from startup. Executors pull it too, possibly on other nodes. - Node capacity and autoscaling. A driver that fits on no node stays
Unschedulable(shown in the startup progress) until a node autoscaler adds a node, which can take minutes. A request larger than any node never schedules. packagesdownloads Maven dependencies on every start; build them into the image instead.- Executors. Readiness does not wait for them, so the first queries may wait for executor Pods to be scheduled and their image pulled.
A sequence pays this once instead of once per command.
Starting points. The values below are illustrative starting points, not
measurements or recommendations for your data. Adjust them from the Spark UI
(task time, spill, garbage collection, skew) and from Pod memory usage. While a
server runs, kubectl --namespace <namespace> port-forward pod/<driver pod> 4040
opens its UI; the driver Pod name is in the startup progress and the receipt.
| Workload | threads |
driver_resources |
executor_resources |
executor_instances |
spark.sql.shuffle.partitions |
|---|---|---|---|---|---|
| Small: CI checks, sample data, models up to a few GB | 2–4 | {cpu: 1, memory: 2048, memory_overhead: 512} |
{cpu: 2, memory: 4096, memory_overhead: 512} |
{initial: 2} |
'8' |
| Medium: models of tens of GB | 4–8 | {cpu: 2, memory: 8192, memory_overhead: 1024} |
{cpu: 4, memory: 16384, memory_overhead: 2048} |
{min: 2, initial: 2, max: 10} |
'64' |
A fixed executor count starts every executor at once and suits short CI jobs. Dynamic allocation releases idle executors between models and commands, at the cost of waiting for new executors when work arrives. The integration tests run their small fixture with a 768 MiB driver and one 512 MiB executor on a single kind node; that shows the lifecycle works, not that those sizes perform.
What the tests exercise¶
The integration harness runs
installed wheels on disposable Kubernetes with a shared HDFS warehouse and an
external metastore fixture. It first runs spark-on-k8s-dbt check against each
generated profile and requires every check to pass without creating compute. It
checks both transports, parallel dbt workers, seeds/models/data tests, fresh
compute across invocations, one driver shared by the commands of a sequence
(including a command after a failure with --keep-going, and no further command
after an interrupt), startup and cleanup progress, catalog persistence,
documentation generation, failed SQL and graceful cancellation. Fixture storage
is temporary and is not a production storage deployment recipe. The CI recipe and
sizing values above are not exercised by these tests.