This guide covers the migration procedure and several behaviors worth knowing that this migration surfaced in practice.
The deployment backend for DABs is moving from the Terraform engine to the new direct deployment engine. The direct engine talks to the Databricks REST APIs directly. It no longer uses the Terraform provider or a terraform.tfstate file, which removes a common source of state-related deployment failures and makes deployments simpler and faster.
The transition is already in progress:
| Date | Change |
|---|---|
| Jun 10, 2026 (CLI 1.3.0) | Direct engine becomes the default for new bundles. Existing Terraform bundles keep their engine until migrated. |
| Jul 24, 2026 - Aug 11, 2026 | Direct engine becomes the default for all bundles deployed in DABs-in-the-Workspace. Bundles still on Terraform are auto-migrated. |
| Aug 26, 2026 | Terraform deployments will be auto-migrated for CLI v 1.14.0 or later. |
| Sep 2026 | Terraform engine is disabled in new CLI releases. |
| Later | The Terraform engine is removed entirely. |
Sources: What's coming and the Bundles release notes. The official guidance states that "the Terraform deployment engine will soon be deprecated."
Why migrate proactively: the migration will happen either way. Doing it on your own schedule lets you test each step and fix any issues in advance, rather than being migrated automatically on the cutover date.
For a bundle currently on the Terraform engine, migrate one target at a time (dev → staging → prod), starting with the lowest-risk environment.
# 1. Deploy on Terraform first so the state is current (recommended). databricks bundle deploy --target <env> # 2. Add `engine: direct` to databricks.yml, then convert the state. # This reads terraform.tfstate and writes resources.json (the direct-engine state file). databricks bundle deployment migrate --target <env> # 3. Verify with a read-only plan. It must show 0 add / 0 change / 0 delete. databricks bundle plan --target <env> # 4. Finalize. This step is what persists the switch to the workspace. databricks bundle deploy --target <env> |
Enable the engine with a top-level key in databricks.yml:
bundle: name: my_bundle engine: direct |
You can also set it per target with targets.<env>.engine: direct, or via the DATABRICKS_BUNDLE_ENGINE=direct environment variable.
Points confirmed by the official documentation:
Rolling back before step 4: delete resources.json and restore terraform.tfstate from the .backup file that migrate created (migrate prints the exact paths). The workspace is unaffected until you deploy.
For a straightforward migration, bind is not required. migrate converts the state in place, and every resource already managed by the bundle is carried over automatically. If you are only switching an existing bundle from Terraform to direct and changing nothing else, you can skip bind and proceed to the cutover procedure.
bind is a separate, optional step that adopts a resource not yet present in the bundle's state, so that DABs manages it without recreating it. The common case during a migration is adopting a resource that was previously managed outside the bundle, for example, a Genie space maintained by a custom API script, or a dashboard or job created manually in the UI. You bind the resource defined in your bundle to the live resource's ID so it is adopted rather than recreated. This is the worked example below.
When you do this, run bind as a deliberate cutover step, never as a quick test (see Item 1).
Each item notes briefly how it was established during this migration.
Seen in a customer environment.
This item applies only when you are adopting a resource with bind (see "When is bind required?" above). A plain migration does not use bind.
bundle deployment bind KEY RESOURCE_ID links an existing workspace resource to one defined in your bundle, so that DABs manages it without recreating it. The command is documented on the Migrate existing resources to a bundle page. A detail worth calling out is its timing: bind writes to the workspace's deployment state immediately, before you run deploy. Because of this, running migrate and then bind against a shared target is not a safe way to "test" a migration; it changes real state.
What happened in practice: running migrate → bind → plan against production as a test left the state ahead of the configuration, and a later routine deployment was blocked by the safety gate (Item 2). The Case study below describes this incident in full.
Guidance:
Under the direct engine, if a bundle deploy would delete or recreate any resource, the CLI aborts the entire deployment (not only the affected resource) unless an explicit bypass flag is passed. In non-interactive CI, it fails with: "the deployment requires destructive actions, but the current console does not support prompting."
When adopting a direct-engine-only resource (such as a Genie space) during migration, run migrate first. Running bundle deployment bind while the bundle is still on the Terraform engine fails with no converter for resource type genie_spaces. This is not specific to Genie spaces; it happens for any direct-engine-only resource type added to a Terraform-engine bundle, because these types have no converter under the Terraform provider. Run migrate (which converts the state to direct) and then bind.
There is no IS_OWNER permission level for genie_spaces and no ownership-transfer path for them (unlike dashboards, queries, and alerts). If your bundle deploys as a service principal, the Genie space must retain a human owner. Grant the deploy service principal CAN_MANAGE so that DABs can update the space's content. Note that run_as on a Genie space is an execution setting, not an ownership assignment.
A bundle with run_as: <service-principal> and any dashboards using embed_credentials: true will fail local validation for a human user (… do not support a run_as user different from the owner), because your local identity differs from the run_as service principal. CI succeeds because it runs as the service principal (the running identity matches run_as). So the only reliable way to preview the migration for such a bundle is a read-only bundle plan run in CI as the service principal, for example, a plan-only workflow on a temporary branch. Do not run migrate, bind, or deploy in that preview workflow.
Native Genie support is one of the primary benefits of the direct engine, so it is worth a complete walkthrough. genie_spaces is a direct-engine-only resource type; it does not exist under the Terraform engine. Before the direct engine, a Genie space could only be managed outside the bundle.
Teams typically maintained the space with a custom deployment script that called the Genie REST API on each change, together with a JSON representation of the space definition checked into the repository:
scripts/deploy_genie_space.py # custom code that PATCHes the space via the /genie/spaces API src/genie_space_definition.json # the space content, maintained by hand |
This approach has several drawbacks: the space is managed outside the bundle (a separate CI step with its own authentication and its own way of failing), the JSON format is custom, there is no plan step to preview changes, and the space is not deployed together with the dashboards and jobs it relates to.
Once the bundle is on the direct engine, generate the resource from the existing live space so that nothing is recreated:
# Generates resources/<key>.genie_space.yml and a serialized <key>.geniespace.json content file. databricks bundle generate genie-space \ --existing-id <existing_genie_space_id> \ --key my_genie_space |
This produces a resource definition similar to the following:
# resources/my_genie_space.genie_space.yml
resources:
genie_spaces:
my_genie_space:
title: "My Genie Space"
warehouse_id: ${var.warehouse_id}
parent_path: /Workspace/Shared/my_project
# file_path points at the generated serialized content.
file_path: ../src/my_genie_space.geniespace.json
permissions:
# A service principal cannot own a Genie space (Item 4). Keep a human owner
# and grant the deploy service principal CAN_MANAGE so DABs can update content.
- level: CAN_MANAGE
service_principal_name: <deploy-service-principal>
- level: CAN_MANAGE
user_name: <human-owner>
|
The objective is to take over the live space, including its conversation history, rather than create a new one. Once the resource exists in configuration:
# 1. Migrate the bundle to direct first. bind fails under Terraform with # "no converter for resource type genie_spaces" (Item 3). databricks bundle deployment migrate --target <env> # 2. Adopt the live space into the bundle state by its ID. databricks bundle deployment bind my_genie_space <existing_genie_space_id> --target <env> # 3. Run a read-only plan. The space should appear as an in-place update, not a create + delete. databricks bundle plan --target <env> # 4. Deploy to finalize (in CI as the service principal for run_as bundles). databricks bundle deploy --target <env> |
A clean adoption reports the space as an in-place update with its ID preserved, for example, 1 to add (the permissions block), 2 to change, 0 to delete, with everything else unchanged. The space and its conversation history are retained.
Note: as in Item 1, this bind persists to remote state, so preview with a plan first and run migrate → bind → deploy only as a deliberate cutover, never as a test against production.
Once the DAB-managed space deploys cleanly and you have verified the title, ID, and queries, remove the previous deploy_genie_space.py script and the legacy JSON file in a follow-up change. Retain them until the DAB-managed path is proven, then remove them so that there is a single source of truth.
Result: the Genie space now resides in the same bundle as the dashboards and jobs that feed it, receives the same plan, review, and CI treatment as every other resource, and no longer requires a separate API script.
The production cutover follows the standard procedure above, with two production-specific points. Deploy in CI as the service principal for run_as bundles, and run bind only for a resource you are genuinely adopting.
Gate before deploying: the plan must show 0 delete / 0 recreate. Metadata-only differences (see the standard procedure) are acceptable. Any create + delete or recreate means something is wrong; stop, do not deploy, and fix the bind step first.
After the deploy, verify the result: the Genie space ID is unchanged, the apps are still running, and no service principal was changed.
Do not run migrate, bind, or deploy against production as a test. Preview with bundle plan only.
I was migrating a single large bundle (jobs, dashboards, a Genie space, and two Apps, deploying as a service principal). To make sure the production cutover would be safe, I ran migrate → bind → plan against production first, as a test.
The plan looked clean. But the bind step had already changed production's state: it switched production to the direct engine and added the Genie space to the state, even though the configuration that defined the Genie space was still in an unmerged pull request. A few days later, my colleague's normal deployment ran. It compared the configuration (no Genie space) against the state (Genie space present), decided to delete it, hit the safety gate, and blocked all production deployments; not just Genie changes. Nothing was actually deleted (the gate stopped it, and the space and its history were safe), but production stayed blocked until the pull request was merged.
Root cause: treating migrate and bind as a test. Lesson: the only safe way to preview a migration is bundle plan. bind changes state on its own, without a deploy. A clean plan only tells you the plan step succeeded; the bind before it had already changed the state.
You must be a registered user to add a comment. If you've already registered, sign in. Otherwise, register and sign in.