Skip to content

Coming from Spacelift or env zero

llms.txtlists every page for an agent
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`.

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.

Terminal window
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_* resources

Each 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.

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
  1. 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 code
    envs/dev/network/ main.tf calls the module; backend.tf; terraform.tfvars
    envs/prod/network/ the same, with prod's key and values
  2. Replace each output reference between stacks with a terraform_remote_state read of the upstream root’s key, so the waves follow it.

  3. Move each managed state into the bucket (state move), one root at a time, before its backend change merges.

  4. Run the import for the settings it maps, then copy what it names as not mapped: the variables (variables) and the policies (policies).

  5. Write the pipeline and open a pull request:

    Terminal window
    npx terragucci config check
    npx terragucci init

    The plan note on that pull request should show no change for each moved root.

  6. Disable each moved stack or environment on the platform so nothing runs against the old state.

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):

  1. Turn on external state access for the stack, and stop its runs.

  2. In the root, put a remote backend 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 -reconfigure
    terraform state pull > prod-network.tfstate

    Authenticate as Spacelift’s external state access page describes.

  3. Replace the remote backend 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 -reconfigure
    terraform state push prod-network.tfstate
  4. Run terraform plan; it should report no changes. Delete the pulled file, commit the s3 block.

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.

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
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

terragucci

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.