Kumoss

Repository-declared backend

Blank the state bucket and let each repository's own Terraform or OpenTofu backend block own state.

Use this model when your repositories already declare a backend that must keep working: the same state then serves Kumoss, your CI, and your engineers' laptops. Kumoss writes no backend configuration and reads the repository’s block as committed.

While Kumoss-managed state is on, which is the shipped default, the repository’s backend block is ignored: the core’s backend_override.tf replaces it, and every plan runs against a different, usually empty, state. Step 1 below is not optional.

Prerequisites

  • Every repository Kumoss operates on declares a remote backend (azurerm, s3, gcs, or any other the engine supports). Without one, the engine falls back to local state, which is lost with the workspace.

  • An identity for the IaC sidecar that can read and write that state store.

  • IAC_BACKEND_CONFIG unset in services/iac/.env. If it is set, you are in the Sidecar-supplied backend model instead.

Steps

  1. Turn Kumoss-managed state off in config.yaml with an explicit empty string. Deleting the key does not turn it off (see Turn managed state off):

    storage:
      terraform_state_bucket: ""

    Rebuild the core so the baked-in configuration changes:

    docker compose build core && docker compose up -d core

    No state bucket is created at boot, and no backend_override.tf is written from now on.

  2. Make sure the repository declares its backend, and give the IaC sidecar the credentials to reach it in services/iac/.env:

    terraform {
      backend "azurerm" {
        subscription_id      = "_STATE_SUBSCRIPTION_ID_"
        resource_group_name  = "tfstate-rg"
        storage_account_name = "acmetfstate"
        container_name       = "tfstates"
        key                  = "platform.tfstate"
        use_azuread_auth     = true
      }
    }
    # services/iac/.env
    ARM_TENANT_ID=_TENANT_ID_
    ARM_CLIENT_ID=_CLIENT_ID_
    ARM_CLIENT_SECRET=_CLIENT_SECRET_
    ARM_USE_AZUREAD=true

    Grant the service principal Storage Blob Data Contributor on the storage account or on the tfstates container.

    • Keep subscription_id in the block. init runs with ARM_SUBSCRIPTION_ID set from the request’s cloud scope, so without it the backend looks for the storage account in the workload’s subscription and fails when the two differ.

    • Without use_azuread_auth, the backend fetches the account key through the management plane, which needs listKeys (Contributor, or Storage Account Key Operator) rather than a blob data role.

    • To use the account key instead of RBAC, set ARM_ACCESS_KEY to it and drop use_azuread_auth.

    terraform {
      backend "s3" {
        bucket       = "acme-team-tfstate"
        key          = "envs/prod/terraform.tfstate"
        region       = "eu-west-1"
        use_lockfile = true
      }
    }
    # services/iac/.env
    AWS_ACCESS_KEY_ID=_ACCESS_KEY_ID_
    AWS_SECRET_ACCESS_KEY=_SECRET_ACCESS_KEY_

    The identity needs s3:ListBucket on the bucket and s3:GetObject, s3:PutObject, and s3:DeleteObject on <bucket>/*. When the state bucket lives in another account, set AWS_PROFILE or AWS_ROLE_ARN to an identity in that account.

    terraform {
      backend "gcs" {
        bucket = "acme-team-tfstate"
        prefix = "envs/prod"
      }
    }
    # services/iac/.env
    GOOGLE_APPLICATION_CREDENTIALS=/etc/kumoss/gcp-sa.json
    # Optional, when state uses a different identity than the providers:
    # GOOGLE_BACKEND_CREDENTIALS=/etc/kumoss/gcp-state-sa.json

    The identity needs Storage Object Admin on the bucket.

    Per-authentication-method variable sets (OIDC, managed identity, profiles, and local CLI logins) are in Backend minimums.

  3. Restart the sidecar so it reads the new variables:

    docker compose up -d iac

Verify

To check all three layers in a deployed environment, see Find the active state.

  1. Run a session against the repository, then confirm that the core wrote no override. This command must print nothing:

    docker compose logs core | grep "Terraform state:"
  2. In the session’s init output, check that the engine bound the repository’s backend, for example Successfully configured the backend "azurerm"!, and that the first plan shows your existing resources as unchanged rather than to be created.

Two sets of credentials on the sidecar

services/iac/.env must satisfy two different authentications. A setup that only covers the first fails at init, before a single resource is planned:

What Used by Grants access to

Provider credentials

plan, apply

The cloud resources your modules create

State-backend credentials

init

The state store the repository’s backend block names

Often the same variables serve both (an s3 backend and the aws provider both read AWS_ACCESS_KEY_ID), but the identity behind them must still be granted access to the state store, which commonly lives in a different account, subscription, or project. Some backends read variables the providers never look at, so the two identities can differ:

Backend Backend-only variables

azurerm

ARM_ACCESS_KEY (storage account key), ARM_SAS_TOKEN, ARM_USE_AZUREAD

gcs

GOOGLE_BACKEND_CREDENTIALS, GOOGLE_IMPERSONATE_SERVICE_ACCOUNT

s3

AWS_PROFILE / AWS_ROLE_ARN pointing at the state account

Kumoss-managed state can spare you the second set: where the core embeds static keys in the override, the sidecar needs nothing for state. See Credentials required.

Next steps