StackShip keeps raw state accessible to authorized customers so an existing workspace can move in or out without a private migration API. Migration uses ordinary OpenTofu cloud commands and the same authenticated, locked, lineage-aware state manager used by normal operations.
Migrate one workspace at a time. Freeze writes in the source before changing custody.
Preconditions
- Create the destination organization, project, and workspace.
- Use the same or a compatible exact OpenTofu version.
- Recreate non-sensitive variables and retrieve sensitive values from their original secret manager; write-only values cannot be exported from most control planes.
- Configure and verify destination credential profiles.
- Install the GitHub App if the destination will use GitHub source runs.
- Record the source lineage, serial, outputs, and resource addresses.
StackShip runs OpenTofu only. If the source applied with Terraform, validate the state and provider-lock compatibility with the exact destination OpenTofu version before transferring custody.
Native cloud configuration
Authenticate interactively:
tofu login stackship.runAutomation may instead use an explicitly scoped service-account token in:
TF_TOKEN_stackship_runFrom the destination workspace dashboard, open CLI access in the
workspace section navigation. Choose State migration, select a bounded expiry, create
the token, and put its one-time plaintext in an approved secret manager. That
preset grants only workspace:read, state:outputs:read, state:read,
state:write, state:lock, and state:recover, bounded to the selected
project and workspace. OpenTofu marks its native state handoff as a forced
write, so state:recover is required for this preset. StackShip still enforces
the workspace boundary, lock ownership, upload checksum, and state validation.
Expose it to the OpenTofu process without writing it into configuration:
export TF_TOKEN_stackship_run='<service-account-token>'Configure the destination:
terraform {
cloud {
hostname = "stackship.run"
organization = "example"
workspaces {
name = "network-foundation"
}
}
}Use the destination workspace’s cloud_name shown by CLI access.
This is separate from its human display name and is unique within the
organization.
stackship.run serves OpenTofu discovery and advertises the API compatibility
endpoint at https://api.stackship.run/api/v2/.
Do not configure the generic HTTP backend. StackShip implements the native cloud/TFE-compatible contract.
During a closed launch, native cloud access is available only to an approved
destination organization and workspace pair. The unauthenticated
/api/v2/ping compatibility probe remains available, but organization-wide
requests and ownership-unresolved requests return a generic retryable
503 service_unavailable. A valid configuration-upload, state-upload, or log
capability does not bypass this boundary: StackShip resolves the capability’s
owning workspace and applies the same exact-pair check before consuming it.
Invalid cloud credentials still return 401 invalid_token.
Confirm the exact destination workspace is available before a migration window. A generic 503 intentionally does not say whether the workspace exists or whether it is available; wait 60 seconds and retry only after confirming availability out of band.
Move local state
Start in the directory that owns the existing local terraform.tfstate.
Create a protected backup and record its checksum before editing the cloud
block.
cp terraform.tfstate terraform.tfstate.pre-stackship
sha256sum terraform.tfstate.pre-stackship
tofu state pull > source-state.jsonAdd the cloud block, then run:
tofu initOpenTofu prompts to migrate the existing state. Read the source and destination names, then confirm.
OpenTofu 1.12.5 rebases metadata when this native-cloud flow targets an empty
workspace. It clears the local lineage and serial while refreshing the empty
destination, then sends StackShip a new lineage at serial 1. StackShip stores
the lineage and serial that the client sends; it cannot recover the source
metadata after OpenTofu has replaced it.
The release browser covers both boundaries explicitly:
- the publication/read case creates fresh cloud state and reads it through the native API; it is not migration proof; and
- the local-to-cloud case starts with two local applies, confirms the native migration prompt, proves outputs and resources are preserved, and proves the rebased request, pulled state, and control-plane metadata agree.
The current release gate therefore does not prove preservation of the source lineage or serial. Keep the protected source snapshot and record the mapping from source metadata to the new destination metadata.
After migration:
tofu state pull > stackship-state.json
sha256sum stackship-state.json
tofu plan -refresh=falseCompare lineage, serial, outputs, and resource addresses before allowing a new
write. For an empty-workspace migration with OpenTofu 1.12.5, expect the
destination lineage to differ and its first serial to be 1; outputs and
resource addresses must still match exactly.
Move from Terraform Cloud or another cloud-compatible service
While the source configuration and credential are still active:
umask 077
tofu state pull > source-state.jsonKeep the source workspace locked or otherwise frozen. In a separate clean directory, configure the StackShip cloud hostname, organization, workspace, and credential without copying the source backend’s local initialization data. Initialize that destination without applying, then push the protected snapshot:
tofu init -input=false
tofu state push source-state.json
tofu state pull > stackship-state.jsonThe upload must pass tenant authorization, the workspace writer reservation, 100 MB plaintext limit, checksum verification, lineage rules, and serial ordering before it becomes current.
The release browser proves this sequence with a serial-greater-than-one source
containing a real built-in resource, a separate clean destination directory,
plain cloud initialization, and tofu state push without -force. OpenTofu
1.12.5 still replaces the source lineage and serial before the first upload to
an empty destination. The pulled state and StackShip control-plane record share
the new lineage at serial 1; the non-empty resource and output remain
structurally identical at the state model boundary. The release case uses a
streaming loopback probe to observe data.attributes.force as false on the
state-create request. Its token also deliberately lacks state:recover, so a
regression to force: true would fail closed before StackShip reserved an
upload.
This is evidence that the documented export/import path preserves state content, not source metadata. Record the source-to-destination lineage mapping, rehearse against a non-production workspace, and retain the protected source export until the destination has completed a clean plan and apply.
Recreate the surrounding control plane
State migration does not copy:
- GitHub App installations or source settings;
- variable values;
- cloud trust relationships;
- policy source or attachments;
- organization members; or
- prior run and audit history.
Recreate those deliberately. StackShip’s policy feature implements its documented Rego contract, not Sentinel compatibility. Rewrite Sentinel policy as Rego before attaching it. There is no bulk importer.
Cut over
Before the first destination apply:
- prove only StackShip can write the state;
- run a non-applying plan and investigate every unexpected difference;
- review the attached policy result;
- confirm the expected predecessor lineage and serial;
- approve the exact current evidence; and
- keep the protected source snapshot until the destination has completed a clean apply.
Never let two control planes apply against the same infrastructure.
Move out again
An authorized principal can download a state version from the dashboard or
native API, or use tofu state pull through the cloud protocol. Raw state
downloads are audited and streamed; StackShip does not make the underlying R2
object public.