Skip to content

Set up and configureยป

Steps 1โ€“4 of the migration walkthrough: scaffold the workspace, pick a source, set its credentials, and validate the configuration before anything runs.

Before you start โ€” what you needยป

liftoff moves an estate onto Spacelift, so it needs credentials for both ends and an account ready to receive the work. Have these in place before step 1:

  • A Spacelift account, and an API key with root-space admin access. The account is read from the very first step, and a repository and admin stack are created later, so a narrowly scoped key fails partway through.
  • An account on the system you are migrating from, with credentials that can read everything in scope. Each source declares exactly which keys it needs; liftoff sources lists them and step 4 proves them.
  • A VCS integration in Spacelift that can reach the repositories your source uses. Generated stacks name the integration they bind to, so one has to exist for every provider in the estate.
  • At least one worker โ€” a private worker pool with a worker attached, or the public pool. Applying the generated code is itself a Spacelift run, so an account with no worker cannot finish a migration.
  • Network access to both APIs from wherever you run liftoff.
  • A decision about where the generated code will live: the Spacelift-managed repository, which needs no setup, or your own git provider.

liftoff checks the ones it can rather than leaving you to find out later. liftoff configure validate proves both key pairs, and liftoff audit reports a repository it cannot bind to an integration, one whose integration is not connected to the account it lives under, an integration Spacelift cannot reach at all, or an account with nothing to run on โ€” all before you generate anything.

Before you start โ€” install liftoffยป

First, know which version you're meant to run. Most operators want the current stable release, installed just below. But if you were pointed at a release candidate โ€” anything ending -rc, or a specific pre-release version โ€” stop here and jump to Release candidates. The stable brew install below and the RC cask both provide the same liftoff command, so running the stable one now would replace an RC you'd already installed, quietly and with no warning from Homebrew. Confirm the target first; install second.

If stable is what you want, install it:

1
brew install spacelift-solutions/tap/liftoff

Without Homebrew:

1
curl -fsSL https://raw.githubusercontent.com/spacelift-solutions/liftoff/main/install.sh | sh

On Windows, download the zip from the releases page and put liftoff.exe on your PATH.

Either way you get one binary and nothing else โ€” no runtime, no container, no services. Check it answers and prints the version you meant to install before going further โ€” if you were sent to a release candidate, this is where you catch having landed on stable instead:

1
liftoff --version

To upgrade later, brew upgrade liftoff, or re-run the install script.

Release candidatesยป

brew install spacelift-solutions/tap/liftoff is always the current stable release โ€” a candidate can never arrive there by surprise. Candidates are a separate cask you have to ask for by name:

1
brew install spacelift-solutions/tap/liftoff-rc

Both provide the same liftoff command, so Homebrew will not let you have both at once. Switch back with:

1
brew uninstall liftoff-rc && brew install spacelift-solutions/tap/liftoff

Installing a specific versionยป

Homebrew carries the newest build of each line and nothing older, so an exact version comes from the install script:

1
curl -fsSL https://raw.githubusercontent.com/spacelift-solutions/liftoff/main/install.sh | VERSION=v1.3.0 sh

The setting goes on sh, not in front of curl โ€” in front of curl it reaches the download and not the script that reads it.

Pinning is what you want in CI, where "whatever is newest" is not a build you can reproduce. The tags are on the releases page.

INSTALL_DIR puts the binary somewhere other than /usr/local/bin โ€” reach for it when that directory needs root:

1
curl -fsSL https://raw.githubusercontent.com/spacelift-solutions/liftoff/main/install.sh | INSTALL_DIR="$HOME/.local/bin" sh

Step 1 โ€” initialize the workspaceยป

1
liftoff init

Creates ./.liftoff/ in the current directory with an empty config.yaml and the SQLite store.

1
2
3
4
5
6
7
8
Config Created  yes
Config Dir      /tmp/migration-demo/.liftoff
Config Path     /tmp/migration-demo/.liftoff/config.yaml
Db Path         /tmp/migration-demo/.liftoff/liftoff.db

Next
  $ liftoff sources
  $ liftoff configure --source <id> --set source.<key>=value

Running init again on an existing workspace reports Config Created no and changes nothing, so it is safe to repeat. On a workspace created by an older binary it migrates the store schema forward.

The only decision here is where to stand. init creates ./.liftoff under the current directory, one directory per migration. Later commands walk up from $PWD to find that folder, so you can run them from a subdirectory without re-passing the path. --config-dir or LIFTOFF_CONFIG_DIR still override the walk when you need a workspace somewhere else.

One warning, and it grows over the migration: ./.liftoff/ becomes the most sensitive thing on this machine. config.yaml may hold source credentials after step 3 (unless you keep them in env references), and the store โ€” liftoff.db, a plain, unencrypted SQLite file โ€” holds everything discover pulls, variable values included, and later any sensitive values and stack state that capabilities capture. Treat the directory accordingly. If you are running inside a git repository, ignore the workspace now:

1
echo '.liftoff/' >> .gitignore

That .gitignore line is necessary but not the whole story โ€” it keeps the workspace out of git, not off backups, syncs, or a shared machine, and it does nothing once the migration is done and the directory should simply be gone. What the directory contains and how to dispose of it safely is covered at the end of the walkthrough, in finalize.

Next up, liftoff sources to see what you can migrate from.

Step 2 โ€” pick a sourceยป

1
liftoff sources

Lists every source this binary can migrate from, with the settings each one takes. Read-only; run it as often as you like.

Terraform Cloud / Enterprise

The capture below shows the Terraform source included in this release.

Use the source id terraform for both Terraform Cloud and self-hosted Terraform Enterprise. A Terraform Enterprise migration also needs source.api_endpoint set to that installation's API endpoint.

The source's secrets capability covers workspace variables. context-secrets covers variable-set variables and creates a temporary workspace to read them. Both restore every source-side change before finishing.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
Sources (1)
  Terraform Cloud / Enterprise (id: terraform)
    Config Keys (used in `liftoff discover`) (4)
      โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
      โ”‚ Label              โ”‚ Key                   โ”‚ Required โ”‚ Default             โ”‚ Help                โ”‚ When Unset โ”‚
      โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค
      โ”‚ API endpoint       โ”‚ api_endpoint          โ”‚ โ€“        โ”‚ https://app.terrafo โ”‚ Base URL of the     โ”‚            โ”‚
      โ”‚                    โ”‚                       โ”‚          โ”‚ rm.io               โ”‚ Terraform API; set  โ”‚            โ”‚
      โ”‚                    โ”‚                       โ”‚          โ”‚                     โ”‚ it to reach a self- โ”‚            โ”‚
      โ”‚                    โ”‚                       โ”‚          โ”‚                     โ”‚ hosted Terraform    โ”‚            โ”‚
      โ”‚                    โ”‚                       โ”‚          โ”‚                     โ”‚ Enterprise          โ”‚            โ”‚
      โ”‚ API token          โ”‚ api_token             โ”‚ โœ“        โ”‚                     โ”‚ Token used to       โ”‚            โ”‚
      โ”‚                    โ”‚                       โ”‚          โ”‚                     โ”‚ authenticate with   โ”‚            โ”‚
      โ”‚                    โ”‚                       โ”‚          โ”‚                     โ”‚ the Terraform API โ€” โ”‚            โ”‚
      โ”‚                    โ”‚                       โ”‚          โ”‚                     โ”‚ must be a user      โ”‚            โ”‚
      โ”‚                    โ”‚                       โ”‚          โ”‚                     โ”‚ token from an admin โ”‚            โ”‚
      โ”‚                    โ”‚                       โ”‚          โ”‚                     โ”‚ user (team and      โ”‚            โ”‚
      โ”‚                    โ”‚                       โ”‚          โ”‚                     โ”‚ organization tokens โ”‚            โ”‚
      โ”‚                    โ”‚                       โ”‚          โ”‚                     โ”‚ do not work)        โ”‚            โ”‚
      โ”‚ Requests per       โ”‚ requests_per_second   โ”‚ โ€“        โ”‚ 30                  โ”‚ Per-source API rate โ”‚            โ”‚
      โ”‚ second             โ”‚                       โ”‚          โ”‚                     โ”‚ limit               โ”‚            โ”‚
      โ”‚ Workspace          โ”‚ workspace_concurrency โ”‚ โ€“        โ”‚ 8                   โ”‚ How many workspaces โ”‚            โ”‚
      โ”‚ concurrency        โ”‚                       โ”‚          โ”‚                     โ”‚ are enriched at     โ”‚            โ”‚
      โ”‚                    โ”‚                       โ”‚          โ”‚                     โ”‚ once                โ”‚            โ”‚
      โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

    Repair Keys (used in `liftoff audit --repair`) (4)
      โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
      โ”‚ Label                โ”‚ Key                  โ”‚ Required โ”‚ Default โ”‚ Help                 โ”‚ When Unset           โ”‚
      โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค
      โ”‚ Module workflow tool โ”‚ module_workflow_tool โ”‚ โ€“        โ”‚         โ”‚ What to write for    โ”‚ empty module         โ”‚
      โ”‚                      โ”‚                      โ”‚          โ”‚         โ”‚ modules exported     โ”‚ workflow tools stay  โ”‚
      โ”‚                      โ”‚                      โ”‚          โ”‚         โ”‚ with an empty        โ”‚ unrepaired until     โ”‚
      โ”‚                      โ”‚                      โ”‚          โ”‚         โ”‚ workflow tool:       โ”‚ this is set          โ”‚
      โ”‚                      โ”‚                      โ”‚          โ”‚         โ”‚ `TERRAFORM_FOSS`,    โ”‚                      โ”‚
      โ”‚                      โ”‚                      โ”‚          โ”‚         โ”‚ `OPEN_TOFU`, or      โ”‚                      โ”‚
      โ”‚                      โ”‚                      โ”‚          โ”‚         โ”‚ `CUSTOM`             โ”‚                      โ”‚
      โ”‚ Default branch       โ”‚ default_branch       โ”‚ โ€“        โ”‚ main    โ”‚ What to write for    โ”‚                      โ”‚
      โ”‚                      โ”‚                      โ”‚          โ”‚         โ”‚ stacks and modules   โ”‚                      โ”‚
      โ”‚                      โ”‚                      โ”‚          โ”‚         โ”‚ exported with no     โ”‚                      โ”‚
      โ”‚                      โ”‚                      โ”‚          โ”‚         โ”‚ branch (they track   โ”‚                      โ”‚
      โ”‚                      โ”‚                      โ”‚          โ”‚         โ”‚ their repo's         โ”‚                      โ”‚
      โ”‚                      โ”‚                      โ”‚          โ”‚         โ”‚ default)             โ”‚                      โ”‚
      โ”‚ Custom runner image  โ”‚ custom_runner_image  โ”‚ โ€“        โ”‚         โ”‚ Untagged Docker      โ”‚ CUSTOM-workflow      โ”‚
      โ”‚                      โ”‚                      โ”‚          โ”‚         โ”‚ image carrying the   โ”‚ stacks stay          โ”‚
      โ”‚                      โ”‚                      โ”‚          โ”‚         โ”‚ Terraform binaries   โ”‚ unrunnable and       โ”‚
      โ”‚                      โ”‚                      โ”‚          โ”‚         โ”‚ for CUSTOM-workflow  โ”‚ `liftoff audit`      โ”‚
      โ”‚                      โ”‚                      โ”‚          โ”‚         โ”‚ stacks; the repair   โ”‚ flags each until     โ”‚
      โ”‚                      โ”‚                      โ”‚          โ”‚         โ”‚ tags it with each    โ”‚ this is set          โ”‚
      โ”‚                      โ”‚                      โ”‚          โ”‚         โ”‚ stack's version      โ”‚                      โ”‚
      โ”‚ Worker pool          โ”‚ worker_pool_id       โ”‚ โ€“        โ”‚         โ”‚ Spacelift private    โ”‚ generated stacks run โ”‚
      โ”‚                      โ”‚                      โ”‚          โ”‚         โ”‚ worker pool id to    โ”‚ on the public pool   โ”‚
      โ”‚                      โ”‚                      โ”‚          โ”‚         โ”‚ assign to generated  โ”‚ when the account has โ”‚
      โ”‚                      โ”‚                      โ”‚          โ”‚         โ”‚ stacks; must match a โ”‚ one; `liftoff audit` โ”‚
      โ”‚                      โ”‚                      โ”‚          โ”‚         โ”‚ pool discover        โ”‚ flags each while     โ”‚
      โ”‚                      โ”‚                      โ”‚          โ”‚         โ”‚ recorded on the      โ”‚ private pools exist  โ”‚
      โ”‚                      โ”‚                      โ”‚          โ”‚         โ”‚ account              โ”‚                      โ”‚
      โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

    Mutations (4)
      โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
      โ”‚ Name                โ”‚ Description                                โ”‚ When Unset                                  โ”‚
      โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค
      โ”‚ secrets             โ”‚ capture sensitive variable values via a    โ”‚ sensitive variable values come over empty;  โ”‚
      โ”‚                     โ”‚ temporary agent โ€” mutates the source,      โ”‚ stage the workspaces and run `liftoff       โ”‚
      โ”‚                     โ”‚ always reverted                            โ”‚ mutate --allow-mutation secrets` to capture โ”‚
      โ”‚                     โ”‚                                            โ”‚ them, or set them in Spacelift after the    โ”‚
      โ”‚                     โ”‚                                            โ”‚ migration                                   โ”‚
      โ”‚ context-secrets     โ”‚ capture sensitive variable-set values via  โ”‚ sensitive variable-set values come over     โ”‚
      โ”‚                     โ”‚ a temporary agent โ€” creates and deletes    โ”‚ empty; run `liftoff mutate --allow-mutation โ”‚
      โ”‚                     โ”‚ one throwaway workspace per organization,  โ”‚ context-secrets` to capture them, or set    โ”‚
      โ”‚                     โ”‚ briefly attaches each variable set to it,  โ”‚ them on the migrated contexts in Spacelift  โ”‚
      โ”‚                     โ”‚ always reverted                            โ”‚ afterwards                                  โ”‚
      โ”‚ state               โ”‚ capture each staged workspace's Terraform  โ”‚ no Terraform state is captured, so `liftoff โ”‚
      โ”‚                     โ”‚ state โ€” reads the source, changes nothing  โ”‚ finalize state` has nothing to push and the โ”‚
      โ”‚                     โ”‚                                            โ”‚ migrated stacks start empty                 โ”‚
      โ”‚ module-git-versions โ”‚ resolve each published module version's    โ”‚ module versions keep no commit SHA, so      โ”‚
      โ”‚                     โ”‚ commit SHA from its VCS โ€” reads the        โ”‚ `liftoff finalize modules` skips them and   โ”‚
      โ”‚                     โ”‚ repository, changes nothing                โ”‚ the private registry migrates without its   โ”‚
      โ”‚                     โ”‚                                            โ”‚ published versions                          โ”‚
      โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

Next
  $ liftoff configure --source <id> --set source.<key>=value

What to take from this screen:

  • The id in the title is what you pass to configure --source.
  • Config Keys is your settings checklist. Only the checked Required keys must be set. Everything else has a Default you can live with, or a When Unset telling you what not setting it means.
  • Repair Keys are not for discover. They feed liftoff audit --repair in step 7 and only come into play when an audit has findings to fix. You can set them now with everything else, and change them later by re-running configure.
  • Mutations preview a later, opt-in step. Discover never changes the source; the one step that does โ€” liftoff mutate โ€” is opt-in and per run, so you pass --allow-mutation <name> on every mutate that should use it, which is why mutations are not config keys. Nothing to do now โ€” the mutate step shows the flags.

The decision at this step is which source you are migrating from and which of its optional settings your environment needs. Note the id and move on.

Step 3 โ€” configure the sourceยป

1
liftoff configure --source <id> --set source.<required-key>='${SOURCE_VALUE}'

Records the source and its settings in config.yaml. Use the id and required keys reported by liftoff sources. Values can reference environment variables: single-quote the reference so your shell doesn't expand it, and config.yaml stores the literal ${SOURCE_VALUE} โ€” the value is resolved when a command runs, so the secret itself never has to be written to disk. A reference that doesn't resolve is an error, not an empty string. Pasting a raw secret works too; it just lives in config.yaml (and your shell history) instead.

Terraform Cloud / Enterprise

Configure the Terraform source with an admin user token:

1
liftoff configure --source terraform --set source.api_token='${TFC_TOKEN}'

Set source.api_endpoint as well for Terraform Enterprise. source.workspace_concurrency controls how many workspaces are processed at once, while source.requests_per_second caps API requests.

--set repeats, and configure is incremental โ€” later runs merge into what's already saved. A later --set of a key that is already in config.yaml replaces that value (it does not keep the first write):

1
liftoff configure --set source.<key>=value --set source.<another-key>=value

Settings for a deployment transform use one more level: the transform subcommand owns the keys below it. For example:

1
liftoff configure --set transform.workflow-tool.target=OPEN_TOFU --set transform.workflow-tool.version=1.8.7

These settings do nothing during discovery. They are consumed only when you run liftoff transform workflow-tool for the current staged batch.

When you need to write config.yaml by handยป

liftoff configure --set is the normal path: it preserves the rest of the file, checks section names, and keeps secret values out of the report. Writing .liftoff/config.yaml directly is the escape hatch for pre-seeding a workspace from automation, reviewing the whole configuration, or repairing malformed YAML.

The file has four core top-level entries:

  • exporter is the source id from liftoff sources.
  • source holds settings declared by that source.
  • spacelift holds the destination account and API key.
  • vcs holds repository credentials used by capabilities that read from git.

An optional transform entry holds settings for commands such as liftoff transform workflow-tool.

Terraform Cloud / Enterprise

Here is a complete Terraform source example:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
exporter: terraform
source:
  api_endpoint: https://app.terraform.io
  api_token: ${TFC_TOKEN}

spacelift:
  endpoint: https://example.app.spacelift.io
  api_key_id: ${SPACELIFT_KEY_ID}
  api_key_secret: ${SPACELIFT_KEY_SECRET}
  repo_name: liftoff
  admin_stack_name: liftoff-admin
  space: root

vcs:
  token: ${VCS_TOKEN}

transform:
  workflow-tool:
    target: OPEN_TOFU
    version: 1.8.7

Environment references stay exactly as written on disk. The command that uses a value resolves it from the environment, so export each referenced variable before validating or running the migration. After any hand edit, run liftoff configure validate. It reports missing and unknown keys, validates transform settings, and authenticates against both configured systems when the required credentials are available.

If a required setting is still missing, configure saves your progress and errors with exactly what's left โ€” the specimen in when something goes wrong is this very case.

Terraform Cloud / Enterprise

With the Terraform source's token set, the current capture reads:

1
2
3
4
5
6
7
Source  terraform

Set Keys (1)
  - api_token

Next
  $ liftoff configure validate

Values are never echoed back, only key names.

Step 4 โ€” validate before running anythingยป

1
liftoff configure validate

The verdict on your configuration: what every key resolves to right now and what the run will mean, and โ€” once the config is complete โ€” it authenticates both key pairs and names who each resolved to. Read-only (it never writes), so run it as often as you like. This is the moment to catch a Spacelift endpoint pointed at the wrong account.

Terraform Cloud / Enterprise

The capture below validates the Terraform source shown in step 2.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
Source  terraform

Auth
  User  apollorion
  Role  user

Spacelift Auth
  Endpoint  https://apps-tfc-migration-apollorion.app.spacelift.io
  Account   apps-tfc-migration-apollorion
  User      migration-key
  Role      admin

Config Keys (used in `liftoff discover`) (4)
  โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
  โ”‚ Key                   โ”‚ Set โ”‚ Required โ”‚ Secret โ”‚ Effect                                                  โ”‚
  โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค
  โ”‚ api_endpoint          โ”‚ โ€“   โ”‚ โ€“        โ”‚ โ€“      โ”‚ connects to https://app.terraform.io                    โ”‚
  โ”‚ api_token             โ”‚ โœ“   โ”‚ โœ“        โ”‚ โœ“      โ”‚ uses the configured value                               โ”‚
  โ”‚ requests_per_second   โ”‚ โ€“   โ”‚ โ€“        โ”‚ โ€“      โ”‚ the Terraform API is called at up to 30 requests/second โ”‚
  โ”‚ workspace_concurrency โ”‚ โ€“   โ”‚ โ€“        โ”‚ โ€“      โ”‚ up to 8 workspaces are enriched concurrently            โ”‚
  โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

Repair Keys (used in `liftoff audit --repair`) (4)
  โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
  โ”‚ Key                  โ”‚ Set โ”‚ Required โ”‚ Secret โ”‚ Effect                                                        โ”‚
  โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค
  โ”‚ module_workflow_tool โ”‚ โ€“   โ”‚ โ€“        โ”‚ โ€“      โ”‚ empty module workflow tools stay unrepaired until this is set โ”‚
  โ”‚ default_branch       โ”‚ โ€“   โ”‚ โ€“        โ”‚ โ€“      โ”‚ missing branches will default to: main                        โ”‚
  โ”‚ custom_runner_image  โ”‚ โ€“   โ”‚ โ€“        โ”‚ โ€“      โ”‚ CUSTOM-workflow stacks stay unrunnable and `liftoff audit`    โ”‚
  โ”‚                      โ”‚     โ”‚          โ”‚        โ”‚ flags each until this is set                                  โ”‚
  โ”‚ worker_pool_id       โ”‚ โ€“   โ”‚ โ€“        โ”‚ โ€“      โ”‚ generated stacks run on the public pool when the account has  โ”‚
  โ”‚                      โ”‚     โ”‚          โ”‚        โ”‚ one; `liftoff audit` flags each while private pools exist     โ”‚
  โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

Mutations (4)
  โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
  โ”‚ Name                โ”‚ Effect                                       โ”‚ Enable With                                   โ”‚
  โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค
  โ”‚ secrets             โ”‚ sensitive variable values come over empty;   โ”‚ liftoff mutate --allow-mutation secrets       โ”‚
  โ”‚                     โ”‚ stage the workspaces and run `liftoff mutate โ”‚                                               โ”‚
  โ”‚                     โ”‚ --allow-mutation secrets` to capture them,   โ”‚                                               โ”‚
  โ”‚                     โ”‚ or set them in Spacelift after the migration โ”‚                                               โ”‚
  โ”‚ context-secrets     โ”‚ sensitive variable-set values come over      โ”‚ liftoff mutate --allow-mutation context-      โ”‚
  โ”‚                     โ”‚ empty; run `liftoff mutate --allow-mutation  โ”‚ secrets                                       โ”‚
  โ”‚                     โ”‚ context-secrets` to capture them, or set     โ”‚                                               โ”‚
  โ”‚                     โ”‚ them on the migrated contexts in Spacelift   โ”‚                                               โ”‚
  โ”‚                     โ”‚ afterwards                                   โ”‚                                               โ”‚
  โ”‚ state               โ”‚ no Terraform state is captured, so `liftoff  โ”‚ liftoff mutate --allow-mutation state         โ”‚
  โ”‚                     โ”‚ finalize state` has nothing to push and the  โ”‚                                               โ”‚
  โ”‚                     โ”‚ migrated stacks start empty                  โ”‚                                               โ”‚
  โ”‚ module-git-versions โ”‚ module versions keep no commit SHA, so       โ”‚ liftoff mutate --allow-mutation module-git-   โ”‚
  โ”‚                     โ”‚ `liftoff finalize modules` skips them and    โ”‚ versions                                      โ”‚
  โ”‚                     โ”‚ the private registry migrates without its    โ”‚                                               โ”‚
  โ”‚                     โ”‚ published versions                           โ”‚                                               โ”‚
  โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

Next
  $ liftoff discover

Where liftoff sources showed templates, this shows results: every Effect is rendered with the value the run will actually use. Two more sections appear only when something needs attention: Missing Required (required keys with no value) and Unknown Keys (settings in config.yaml the source doesn't recognize โ€” usually a typo'd --set).

Read the Effect column top to bottom and check it against your intent. Worth deciding now:

  • Rate and concurrency โ€” use the source's reported keys to keep discovery within its API limits.
  • Repair keys โ€” they change nothing until liftoff audit --repair has a matching finding, and you can re-run configure to adjust them whenever.
  • The mutations โ€” opt-ins for the later mutate step. A mutation is never remembered: you pass --allow-mutation <name> on every mutate that should use it, which is why they aren't config keys. Decide which capabilities the configured source should run; the mutate step shows the flag.

Terraform Cloud / Enterprise

The default request rate and workspace concurrency are safe for Terraform Cloud; lower them when a Terraform Enterprise installation needs a gentler load.

The repair keys are module_workflow_tool, default_branch, custom_runner_image, worker_pool_id, and repository_map. They change nothing during discovery. liftoff audit --repair reads them only when the matching finding is present.

custom_runner_image applies to workspaces whose Terraform version requires the CUSTOM workflow tool. Give it an untagged image name; repair adds each stack's Terraform version as the tag. The image must contain that Terraform binary. A private image requires a private worker pool.

Terraform Cloud / Enterprise

Terraform Cloud / Enterprise: building the runner image for CUSTOM stacksยป

If a Terraform workspace runs a version newer than Spacelift bundles, its stack migrates onto the CUSTOM workflow tool and runs the binary your image carries. Spacelift downloads nothing for it โ€” you hold the licence for those versions, so you build the image.

You need one tag per version in the batch. liftoff audit names the version in each finding, so the audit output is the list:

1
2
3
custom-workflow-missing-runner-image (error) (2)
  stack ws-abc123 runs version 1.9.0 on the CUSTOM workflow tool and has no runner image
  stack ws-def456 runs version 1.10.3 on the CUSTOM workflow tool and has no runner image

A Dockerfile that adds one version on top of Spacelift's runner image. Build on that image rather than a bare Alpine or hashicorp/terraform: the worker calls ps to watch the container, and the image has to carry the spacelift user (UID 1983) that jobs run as. The fetch stage keeps the build from depending on which archive tools the base image ships:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
ARG TERRAFORM_VERSION

FROM alpine:3 AS fetch
ARG TERRAFORM_VERSION
RUN apk add --no-cache curl unzip \
 && curl -sSLo /tmp/tf.zip \
      "https://releases.hashicorp.com/terraform/${TERRAFORM_VERSION}/terraform_${TERRAFORM_VERSION}_linux_amd64.zip" \
 && unzip /tmp/tf.zip -d /out

FROM public.ecr.aws/spacelift/runner-terraform:latest
USER root
COPY --from=fetch /out/terraform /usr/local/bin/terraform
RUN chmod 0755 /usr/local/bin/terraform
USER spacelift

Build and push one tag per version, tagging each with the version itself โ€” that is what the repair writes:

1
2
3
4
for v in 1.9.0 1.10.3; do
  docker build --build-arg "TERRAFORM_VERSION=$v" -t "$REGISTRY/liftoff-runner:$v" .
  docker push "$REGISTRY/liftoff-runner:$v"
done

Where you push matters. On Spacelift's public worker pool the image must be public, and only these registries are accepted: public.ecr.aws, dkr.ecr.<region>.amazonaws.com, docker.io, registry.hub.docker.com, ghcr.io, gcr.io, docker.pkg.dev, azurecr.io, quay.io, registry.gitlab.com. A private image requires a private worker pool: the public pool caches images across accounts, so it only ever pulls public ones. liftoff audit checks this rather than leaving you to find out from a run that never starts: a stack heading for the public pool with an image from anywhere else is an error, runner-image-not-pullable (audit).

Then point the setting at the untagged name and repair:

1
2
liftoff configure --set source.custom_runner_image=$REGISTRY/liftoff-runner
liftoff audit --repair

Each CUSTOM stack gets $REGISTRY/liftoff-runner:<its own version>. Build for linux/amd64 โ€” that is what Spacelift workers run. A missing or non-executable binary shows up as sh: terraform: not found, with the run stopping in INITIALIZING.

When the effects read the way you intend and the source credentials authenticated, you are ready for discover.