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-approveto 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:
- Repository name only (for example,
repo-one). - Full
owner/nameslug (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) plusbackend/persistence.yaml(schema_version: 1) describing the backend - pushes a branch named
estate/persist-<timestamp>and opens a pull request whenGITHUB_TOKENresolves 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 variablesCONCORDAT_PERSIST_BUCKET,CONCORDAT_PERSIST_REGION,CONCORDAT_PERSIST_ENDPOINT,CONCORDAT_PERSIST_KEY_PREFIX, andCONCORDAT_PERSIST_KEY_SUFFIX. - When
--endpoint(orCONCORDAT_PERSIST_ENDPOINT) omits a scheme, Concordat assumeshttps://(for example,s3.fr-par.scw.cloudbecomeshttps://s3.fr-par.scw.cloud). - Pass
--no-inputto fail fast instead of prompting when any required value is missing. Defaults from an existingbackend/persistence.yamlare 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
--bucketand--region(or theCONCORDAT_PERSIST_*env vars) point to the intended bucket; the endpoint must be in the same region. - Need to retry without prompts: Pass
--no-inputplus the required flags to fail fast in automation; defaults frombackend/persistence.yamlare 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 applyblocks and echoes the.tflockobject key and metadata (holder ID, timestamp) in its output. If the lock persists beyond 60 seconds and the persistence manifest defines anotification_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
.tflockobject 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
.tflocksupport, coordinate applies externally (for example, via CI job serialization or manual team communication). Concurrent applies may corrupt state. - Planned
--check-lockmode: A futureconcordat apply --check-lockflag 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.tflockmetadata 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):
- 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
- Identify the desired
VersionIdfrom 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
- Run
concordat planto 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 = truein 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 plancalls 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_estateis optional; the firstestate initcall populates it automatically.github_owneridentifies the GitHub organization or user managed by the estate.concordat enrolandconcordat lsrely on the stored owner to guard against cross-organization drift.branchandinventory_pathdefault tomainandtofu/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 invokingconcordat enrolwhen switching estates (for example, when working on a fork). - Ensure repositories expose an
originremote 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 showto confirm the inventory contents before debugging the enrolment.