Coming from Spacelift or env zero
Optional: hand this page to your coding agentThe steps work by hand too.Show the whole prompt
Read https://intentius.io/terragucci/guides/coming-from-spacelift-or-env-zero/.
Run `npx terragucci import spacelift --dry-run` (or `import env0 --dry-run` for env zero) and report what it would write, each setting it lists as not mapped, and each stack or environment it names as sharing a directory. Propose a directory per stack or environment where one directory serves several.
Run the import without `--dry-run`, then add `policy` for plan policies with `input.terraform` changed to `input`. Run `npx terragucci config check --json` and `npx terragucci init`, and open a pull request with terragucci.yml, the policies and the generated pipeline.
List the secrets I must recreate and add their names under `pass`; list each policy with no counterpart, and each stack or environment whose state the platform manages.
Do not change a `cloud` or `backend` block, do not run `init -migrate-state`, `state pull` or `state push`, and do not create secrets: a person moves the state.
Never apply, approve (a pull request review or `terragucci approve`), override a policy denial (`terragucci override`), use `--mode apply`, or merge; never touch `.chant/allowed_signers` or `chant/lifecycle`.Result
Section titled “Result”Each stack or environment becomes a root with its own state in your bucket. The jobs init writes run in your forge’s CI; there is no account and no server, and every record lands in your git and bucket.
Import
Section titled “Import”npx terragucci import spacelift # reads .spacelift/config.yml and the spacelift_* resources
npx terragucci import env0 # reads env0-discovery.yml, each env0.yml and the env0_* resourcesEach command writes terragucci.yml from what the repo holds: the runtime config or discovery file, and the admin code that declares the stacks or environments, read from every .tf file in the repo. It reads no API, so a stack, context or variable that exists only on the platform is not in it. Each setting is printed under one of the headings of terragucci import, quoting the terragucci cell of its row in the table below. --dry-run prints what would be written.
| Read from | Spacelift | env zero |
|---|---|---|
| A root per | spacelift_stack resource or stacks entry, at its project_root |
environment, at its env0_template’s path, else the root its first variable file is in |
binary and version |
terraform_workflow_tool, terraform_version, opentofu_version; a version per root glob when the stacks differ |
the template’s type, terraform_version, opentofu_version |
steps |
each hook list, for the roots of the stacks that run it, with attached contexts’ hooks around the stack’s own | env0.yml’s deploy steps, for the root the file is in |
waves.after |
spacelift_stack_dependency, unless the roots’ terraform_remote_state reads already give the order |
|
env |
stack_defaults.environment, and a variable that is not write_only on a context every stack gets |
a variable that is not sensitive on no environment or template; an echo NAME=value >> $ENV0_ENV line |
pass.secrets |
each write_only variable’s name, the provider’s default |
each sensitive variable’s name, TF_VAR_ first for a Terraform variable |
gate: always |
every stack’s autodeploy off, the provider’s default |
every environment’s requiresApproval on, or approve_plan_automatically off |
drift |
spacelift_drift_detection’s schedule |
driftDetectionCron, drift_detection_cron and env0_environment_drift_detection |
ephemeral |
the roots of environments with a ttl, or in a project whose policy’s default_ttl gives one, which becomes ephemeral.ttl |
|
| Named, nothing written | managed state, policies, mounted files, workspaces, runner images, worker pools, cloud integrations | the remote backend, workspaces, variable files outside the root, variable sets, credentials, agents |
Neither import moves state; a state move does, one root at a time.
Concepts
Section titled “Concepts”| Concept | Spacelift | env zero | terragucci |
|---|---|---|---|
| Root | stack | environment | a root: one directory, one state; an import writes roots from the stacks’ project roots or the environments’ template paths |
| Shared code | several stacks on one project root | several environments from one template | a directory per stack or environment, each with its backend key and terraform.tfvars, calling a shared module |
| Workspace | terraform_workspace |
workspaceName, workspace |
none: each root is a directory with one state |
| Managed state | managed state, manage_state |
env zero’s remote backend, isRemoteBackend |
a key in your S3 bucket, moved by a backend move; an import names each stack or environment whose state moves |
| Own backend | your own backend | your own S3 backend | the same backend; nothing moves |
| Variables | context and stack environment variables | variables | env for values every root gets; a secret’s name under pass, whose value you create as a CI secret; a root’s own values in its terraform.tfvars |
| Files | context and stack mounted files | files committed in the root, such as terraform.tfvars |
|
| Cloud credentials | cloud integration | credentials | oidc: a plan role and an apply role per cloud |
| Version | terraform_version, opentofu_version, terraform_workflow_tool |
a template’s type, terraform_version, opentofu_version |
binary, and version for every root or per root glob, or the root’s .terraform-version |
| Hooks | hooks: before_init, after_plan, before_apply and the rest |
custom flows (env0.yml) |
steps before or after init, plan, apply and drift, for the roots the hook ran in; nothing runs at a destroy or a task |
| Runner image | runner_image |
image: one image for every job, built FROM the terragucci image for the binary |
|
| Plan policy | plan policy | approval policy (OPA) | policy over each plan, with cost in input.cost |
| Approval | approval policy, autodeploy off |
approval policy, requiresApproval |
gate and approval modes; cost.approve_above; an import writes gate: always when every stack or environment waits for an approval |
| Triggers | push and trigger policies, project globs | triggers, continuousDeployment, pullRequestPlanDeployments |
the generated pipeline: a pull request plans the roots its files reach; merge applies them |
| Order | stack dependencies, output references | workflows | waves: a root that reads another through terraform_remote_state applies in a later wave; an import writes waves.after for a stack dependency the reads do not give |
| Access | login policy, spaces | RBAC | your forge’s permissions and cloud IAM (access and identity) |
| Notifications | notification policy | notifications | notify: Slack, Teams or a signed webhook |
| Drift | drift detection | drift detection, driftDetectionCron |
drift, one cron schedule over every root, and a pull request for drift on a literal; it never reconciles |
| TTL | environment TTL | ephemeral environments per pull request, with a TTL; an import writes ephemeral for the environments that expire |
|
| Cost | cost monitoring | cost: an estimate per plan, not billed spend |
|
| Workers | private workers, worker_pool_id |
self-hosted agents | your CI’s runners, your own ones by label with runner |
| Module registry | module registry | module registry | a module registry in your bucket |
| Run history | run history | deployment log | the report per run and chant/lifecycle |
-
Give each stack or environment a directory. Where several share one, put the code in a module that each root calls:
modules/network/ the shared codeenvs/dev/network/ main.tf calls the module; backend.tf; terraform.tfvarsenvs/prod/network/ the same, with prod's key and values -
Replace each output reference between stacks with a
terraform_remote_stateread of the upstream root’s key, so the waves follow it. -
Move each managed state into the bucket (state move), one root at a time, before its backend change merges.
-
Run the import for the settings it maps, then copy what it names as not mapped: the variables (variables) and the policies (policies).
-
Write the pipeline and open a pull request:
Terminal window npx terragucci config checknpx terragucci initThe plan note on that pull request should show no change for each moved root.
-
Disable each moved stack or environment on the platform so nothing runs against the old state.
State move
Section titled “State move”A backend move writes managed state to your bucket, approved by digest like any migration. For env zero it reads the environment’s state over the remote backend’s API: backend: remote, hostname: backend.api.env0.com, the organization ID and the workspace. For Spacelift it reads a state file you pulled, with backend: file. Migration files lists the keys. A stack or environment already on your own backend skips this.
By hand, with Terraform or OpenTofu (tofu in place of terraform):
-
Turn on external state access for the stack, and stop its runs.
-
In the root, put a
remotebackend that reads the stack’s state, with the account name as the organization and the stack ID as the workspace, and pull the state:terraform {backend "remote" {hostname = "spacelift.io"organization = "acme"workspaces {name = "prod-network"}}}Terminal window terraform init -reconfigureterraform state pull > prod-network.tfstateAuthenticate as Spacelift’s external state access page describes.
-
Replace the
remotebackend with the bucket, and push the file:terraform {backend "s3" {bucket = "acme-state"key = "envs/prod/network.tfstate"region = "us-east-1"use_lockfile = true}}Terminal window terraform init -reconfigureterraform state push prod-network.tfstate -
Run
terraform plan; it should report no changes. Delete the pulled file, commit thes3block.
-
Stop the environment’s deployments.
-
env zero’s remote backend speaks the same protocol as HCP Terraform’s. Replace the
cloudblock with aremotebackend naming the same organization ID and workspace, and initialise it. Neither binary migrates straight out of acloudblock.terraform {backend "remote" {hostname = "backend.api.env0.com"organization = "<org-id>"workspaces {name = "prod-network"}}}Terminal window terraform login backend.api.env0.comterraform init -reconfigure -
Swap in the
s3block from the Spacelift tab and runterraform init -migrate-state, answeringyes. -
Check that
terraform planfinds nothing to change, then commit thes3block.
Variables
Section titled “Variables”| Variable on the platform | Where it goes |
|---|---|
| Terraform variable, not sensitive | terraform.tfvars in the root |
| Environment variable, not sensitive | env in terragucci.yml, which every job gets |
| Cloud credentials | oidc roles; no static keys |
| Sensitive value | see the tabs |
Secret values cannot be read back from either platform; copy them from where they came from.
A masked CI/CD variable named TF_VAR_<name> reaches every job, and TF_ variables pass to the binary as set.
Create a repository secret named as the variable, such as TF_VAR_db_password, and list it under pass; the jobs that plan, apply and check drift get it under that name. A repository variable goes under vars.
pass:
secrets: [TF_VAR_db_password]Policies
Section titled “Policies”| Policy | terragucci |
|---|---|
| Spacelift plan policy | policy with input: plan; change input.terraform to input. input.spacelift (the run, stack and commit) has no counterpart, and deny and warn rules count as they do there |
| env zero approval policy | policy over the plan JSON at input, with cost at input.cost; a rule that asked for approval becomes cost.approve_above or gate: always |
| Spacelift approval policy | gate decides which waves wait, and an approval binds the wave’s plans; how many reviewers a change needs is the forge’s branch protection |
| Push, trigger policy | none: the pipeline plans what a pull request reaches, applies on merge, and orders waves from terraform_remote_state |
| Login policy | none: the forge’s sign-in |
| Task policy | none: a state change comes from an import, removed or moved block, or a migration, in a reviewed commit |
| Notification policy | notify |
Limits
Section titled “Limits”| On the platform | In terragucci today |
|---|---|
| State move | a backend move or by hand, as above |
| Stack and environment settings | terragucci import reads the repo’s files and admin code, not the platforms’ APIs; what lives only on the platform is copied by hand |
| Tasks (ad hoc commands) | none; state changes come through pull requests |
| Drift reconciliation | the drift job reports and opens a pull request for drift on a literal; it never applies to put the code back |
| Push, trigger, login and task policies | no counterpart; see policies |
| Blueprints, templates in the UI | none; a new root is a directory in a pull request |
| Sensitive variables | not exported by the platform; recreated from their source |
| Run history | stays on the platform |
- Add terragucci to a repo for the forge settings.
- Run steps around a stage for hooks.
- Coming from HCP Terraform, Scalr or OTF for the workspace platforms.
These docs count page views and clicks with PostHog. They set no cookies, store nothing in your browser, and send nothing when your browser asks not to be tracked.