Previewing and applying estate changes

Updated Jul 27, 2026

Use the plan and apply commands to run OpenTofu against the active estate without leaving the CLI. Both commands require GITHUB_TOKEN and the estate's github_owner to be recorded.

  • Preview changes with concordat plan. Additional OpenTofu arguments can be appended directly to the command (for example, -detailed-exitcode).
  uv run concordat plan -- -detailed-exitcode

The CLI refreshes the cached estate under $XDG_CACHE_HOME/concordat/estates/<alias>, clones it into a temporary directory, writes terraform.tfvars with the recorded owner, runs tofu init -input=false, and then tofu plan. Paths are echoed, so the workspace can be inspected; pass --keep-workdir to skip the cleanup step. Concordat preserves OpenTofu's standard CLI plan output (including the per-resource diff), so operators do not need to re-run tofu plan manually just to see what would change.

When backend/persistence.yaml exists with enabled: true, the CLI adds -backend-config=<path> to tofu init, maps SCW_ACCESS_KEY/ SCW_SECRET_KEY onto AWS_ACCESS_KEY_ID/AWS_SECRET_ACCESS_KEY when needed, and fails fast if neither pair is present. Standard error (stderr) logs echo the backend bucket, key, region, and config path—never credentials—for traceability. If the manifest is absent or enabled: false, plan and apply keep using the local state layout.

  • Reconcile the estate with concordat apply. The command requires an explicit --auto-approve to match OpenTofu's automation guard.
  uv run concordat apply --auto-approve

concordat apply uses the same workspace preparation as plan, adds -auto-approve for OpenTofu, and returns the exit code from the underlying tofu invocation, so pipelines can gate on it. Pass --keep-workdir to retain the apply workspace for inspection.

Importing pre-existing GitHub repositories into state

The GitHub provider rejects attempts to create a repository that already exists, even if the rest of the plan is valid. This typically shows up as a 422 response with a message such as "name already exists on this account".

When Concordat detects this specific failure during concordat apply, it offers to import the affected repositories into state and retry the apply. The prompt defaults to "no" so automated runs cannot accidentally mutate state:

One or more GitHub repositories already exist but are missing from state.
Import into state and retry apply? (example/repo-one, example/repo-two) [y/N]:

If the operator answers y, Concordat runs tofu import for each missing repository and then re-runs tofu apply. Import IDs are attempted in a resilient order:

  1. Repository name only (for example, repo-one).
  2. Full owner/name slug (for example, example/repo-one).

When running without a TTY (teletype; an interactive terminal session), for example in CI, Concordat does not prompt and does not attempt imports. Re-run with --keep-workdir, then import manually in the printed workspace directory.

Persisting estate state in object storage

Use concordat estate persist to move OpenTofu state into a shared, version-controlled backend for the active estate. The command:

  • prompts for bucket, region, endpoint, key prefix, and key suffix, seeding defaults from any existing backend/persistence.yaml
  • verifies the Scaleway bucket has versioning enabled and performs a zero-byte put/delete to confirm the supplied credentials can write to the prefix
  • writes backend/<alias>.tfbackend (no credentials) plus backend/persistence.yaml (schema_version: 1) describing the backend
  • pushes a branch named estate/persist-<timestamp> and opens a pull request when GITHUB_TOKEN resolves the estate remote to a GitHub repository

Re-running the command refuses to replace existing backend files unless --force is supplied; use --force when rotating buckets or prefixes. Secrets such as AWS_SECRET_ACCESS_KEY are validated in memory only and are never written to disk.

Non-interactive use for automation:

  • Provide backend values via flags (--bucket, --region, --endpoint, --key-prefix, --key-suffix) or the environment variables CONCORDAT_PERSIST_BUCKET, CONCORDAT_PERSIST_REGION, CONCORDAT_PERSIST_ENDPOINT, CONCORDAT_PERSIST_KEY_PREFIX, and CONCORDAT_PERSIST_KEY_SUFFIX.
  • When --endpoint (or CONCORDAT_PERSIST_ENDPOINT) omits a scheme, Concordat assumes https:// (for example, s3.fr-par.scw.cloud becomes https://s3.fr-par.scw.cloud).
  • Pass --no-input to fail fast instead of prompting when any required value is missing. Defaults from an existing backend/persistence.yaml are still honoured in non-interactive mode.

Configuring remote-state credentials

Remote-state backends rely on environment variables; the CLI simply checks that they exist before shelling out to OpenTofu. Export the pair that matches the selected provider:

Provider Required variables Optional variables Notes
AWS S3 / Spaces AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY AWS_SESSION_TOKEN (when using temporary credentials such as STS) Values are passed straight to OpenTofu's S3 backend.
Scaleway Object Storage SCW_ACCESS_KEY, SCW_SECRET_KEY AWS_SESSION_TOKEN (only when Scaleway issues temporary AWS-style keys) Concordat maps these onto the AWS variable names before invoking OpenTofu.

When AWS_SESSION_TOKEN is present, Concordat forwards it alongside whichever credential pair is selected so temporary AWS STS, Scaleway, or Spaces sessions work without additional flags.

The stack declares an explicit s3 backend in platform-standards/tofu/backend.tf and ships a Scaleway starter config at platform-standards/tofu/backend/scaleway.tfbackend. Initialize estates with OpenTofu 1.12 or newer using:

GITHUB_TOKEN=placeholder \
  tofu -chdir=platform-standards/tofu \
  init -backend-config backend/scaleway.tfbackend

Example shell snippet:

export AWS_ACCESS_KEY_ID=AKIA... # or SCW_ACCESS_KEY for Scaleway
export AWS_SECRET_ACCESS_KEY=xxxx # or SCW_SECRET_KEY
# export AWS_SESSION_TOKEN=...    # optional for temporary sessions

When multiple estates exist, run concordat estate persist for each remote stack using the appropriate credentials. The roadmap and design doc (§2.8) describe lock troubleshooting steps and disaster-recovery procedures that build on this environment setup.

Remote state FAQ (Scaleway-focused)

  • Versioning disabled error: In the Scaleway console, open Object Storage → your bucket → Settings → Object versioning and set it to Enabled. Re-run concordat estate persist.
  • Access denied when checking versioning: Confirm the access key/secret match the correct project, the bucket name is spelled correctly, and the endpoint matches the region (for example, https://s3.fr-par.scw.cloud). Retry after updating credentials; network glitches also show the same message.
  • Wrong bucket or region: Ensure --bucket and --region (or the CONCORDAT_PERSIST_* env vars) point to the intended bucket; the endpoint must be in the same region.
  • Need to retry without prompts: Pass --no-input plus the required flags to fail fast in automation; defaults from backend/persistence.yaml are still honoured.

Lock troubleshooting

OpenTofu's S3 backend supports native .tflock files when use_lockfile = true (the default for AWS and DigitalOcean backends generated by Concordat). Scaleway Object Storage does not implement Terraform locking, so Concordat omits the lockfile option for Scaleway backends and relies on single-writer discipline.

  • Identifying lock contention: When another process holds the lock, tofu apply blocks and echoes the .tflock object key and metadata (holder ID, timestamp) in its output. If the lock persists beyond 60 seconds and the persistence manifest defines a notification_topic, Concordat emits a structured JSON log line for downstream alerting pipelines.
  • Stuck locks on AWS/DigitalOcean: If a previous apply crashed without releasing the lock, manually delete the .tflock object from the bucket using the provider's CLI or console. Confirm no other apply is running before removal.
  • Scaleway single-writer discipline: Because Scaleway lacks .tflock support, coordinate applies externally (for example, via CI job serialization or manual team communication). Concurrent applies may corrupt state.
  • Planned --check-lock mode: A future concordat apply --check-lock flag will attempt to acquire the backend lock and report the blocking key when contention persists. This flag is not yet implemented; operators currently depend on the .tflock metadata echoed in apply logs.

Disaster recovery

Concordat requires bucket versioning on all remote backends, so every state update preserves earlier versions. Use versioning to recover from corrupted or accidentally overwritten state files. If versioning is not enabled, concordat estate persist fails with a blocking error and does not write any backend configuration files.

Locating version IDs: Whenever concordat apply updates state, the CLI logs the bucket, key, and region to standard error (stderr). Combine this information with the provider's object-versioning API to list available versions.

Restoring a previous state version (Scaleway example):

  1. List versions of the state object:
   aws s3api list-object-versions \
     --bucket df12-tfstate \
     --prefix estates/example/main/terraform.tfstate \
     --endpoint-url https://s3.fr-par.scw.cloud
  1. Identify the desired VersionId from the output and copy it over the current object:
   aws s3api copy-object \
     --bucket df12-tfstate \
     --copy-source "df12-tfstate/estates/example/main/terraform.tfstate?versionId=VERSION_ID" \
     --key estates/example/main/terraform.tfstate \
     --endpoint-url https://s3.fr-par.scw.cloud
  1. Run concordat plan to verify the restored state matches the expected infrastructure. Investigate any unexpected drift before applying.

Object Lock compliance mode (Scaleway): Enable Object Lock with a retention window that matches regulatory requirements to prevent accidental or malicious deletion of state versions. Object Lock is orthogonal to .tflock mutexes; it protects historical data, not concurrent access.

Important: Concordat does not automate rollbacks. Operators are responsible for identifying the correct version and restoring it manually.

At-rest encryption

State files often contain credentials and other sensitive data. Protect them with the following measures:

  • Avoid storing secrets in state: Mark sensitive attributes with sensitive = true in OpenTofu configurations. Split secrets into external vaults (for example, HashiCorp Vault, AWS Secrets Manager) or replace cleartext values with references.
  • Enforce strict bucket policies: Limit access via IAM (AWS) or Scaleway access policies. Grant the minimum permissions required for Concordat operations (read/write/delete on the state prefix).
  • Server-side encryption (AWS): Enable SSE-S3 or SSE-KMS on the bucket. OpenTofu's S3 backend automatically uses SSE when the bucket enforces it.
  • Client-side encryption (Scaleway): Scaleway Object Storage supports SSE-C (customer-provided keys) but not SSE-S3. OpenTofu's S3 backend does not natively support SSE-C, so encryption must happen outside OpenTofu. Wrap tofu state/tofu plan calls with tooling that encrypts state files before upload, or use external envelope-encryption workflows.
  • Audit access logs: Periodically review bucket access logs to detect unauthorized reads or unexpected access patterns.

Estate configuration file

Concordat stores estate metadata in $XDG_CONFIG_HOME/concordat/config.yaml (~/.config/concordat/config.yaml when the environment variable is unset). The file is regular YAML 1.2 with an estate section:

estate:
  active_estate: core
  estates:
    core:
      github_owner: example
      repo_url: [email protected]:example/platform-standards.git
      branch: main
      inventory_path: tofu/inventory/repositories.yaml
    sandbox:
      github_owner: example
      repo_url: [email protected]:example/sandbox-standards.git
      branch: main
      inventory_path: tofu/inventory/repositories.yaml
  • active_estate is optional; the first estate init call populates it automatically.
  • github_owner identifies the GitHub organization or user managed by the estate. concordat enrol and concordat ls rely on the stored owner to guard against cross-organization drift.
  • branch and inventory_path default to main and tofu/inventory/repositories.yaml. Override them when the remote uses another branch name or inventory layout.
  • Manual edits are allowed, but prefer the CLI to ensure validation is applied.

Interaction with enrolment

The concordat enrol command automatically targets the active estate and refuses to run unless that estate records github_owner. The --platform-standards-url flag still overrides the repository where the OpenTofu pull request is opened, but the namespace guard always uses the active estate owner.

  • Run concordat estate use <alias> before invoking concordat enrol when switching estates (for example, when working on a fork).
  • Ensure repositories expose an origin remote pointing at GitHub (or pass the SSH URL directly) so the CLI can resolve the slug and enforce the owner guard.
  • If the estate inventory misses a repository, run concordat estate show to confirm the inventory contents before debugging the enrolment.