OpenTaco (Layer-0) is a CLI + lightweight service for state control—create/read/update/delete and lock Terraform/OpenTofu state files, and act as an HTTP state backend proxy, paving the way for dependency awareness and RBAC. Today, the service runs stateless with an S3 “bucket-only” adapter for state storage (with an in-memory fallback for local demos).
- Live docs: https://opentaco.mintlify.app/
- Source:
opentaco/docs/
(Mintlify). When changing APIs, CLI behavior, storage semantics, or examples, update the relevant docs pages (overview, getting-started, cli, backend-service, provider, storage, demo, troubleshooting, reference) in the same PR. - See
docs/backend-service.md
for HTTP backend and the S3‑compatible shim. - Roadmap →
docs/roadmap.mdx
(milestones and future buckets).
OpenTaco is an open-source "Terraform Companion" that starts with state control: a CLI + lightweight service focused on managing state files and access to them, not CI jobs. The long game is a self-hostable alternative to Terraform Cloud / Enterprise: state + RBAC first, then remote execution, PR automation, drift, and policy as later layers. This repo already includes a working S3 adapter, a Terraform HTTP backend proxy (GET/POST/PUT/LOCK/UNLOCK), and usable CLI + provider.
State management today; Remote Runs → VCS Integration + UI → Drift → Policies coming next. See docs/scope-today.md
and docs/roadmap.md
.
- Layer-0 = State control (CRUD + lock, plus a backend proxy). No runs, no PR automation, no UI in this layer.
- CLI-first to settle semantics; UI comes later.
- Self-hosted, bucket-only later (S3 as the only stateful store when we add real storage; the service remains stateless).
- Backwards compatibility with existing S3 layouts (incl. Terragrunt) when we wire storage later; adoption should be drop-in.
- Import from TFC should be easy (later); keep shapes friendly to that path.
- Go 1.25 or later
- Make
make all
make svc
# Service starts on http://localhost:8080
# Health: curl http://localhost:8080/healthz
# Ready: curl http://localhost:8080/readyz
Auth is enabled by default. To temporarily bypass it (e.g., provider dev):
./opentacosvc -auth-disable -storage memory
# Build the CLI
make cli
# Create a state
./taco unit create myapp/prod
# List states
./taco unit ls
# Get state metadata (size, lock status, last updated)
./taco unit info myapp/prod
# Delete a state
./taco unit rm myapp/prod
# Download state data
./taco unit pull myapp/prod output.tfstate
# Upload state data
./taco unit push myapp/prod input.tfstate
# Lock a state manually
./taco unit lock myapp/prod
# Unlock a state
./taco unit unlock myapp/prod
# Acquire (lock + download in one operation)
./taco unit acquire myapp/prod output.tfstate
# Release (upload + unlock in one operation)
./taco unit release myapp/prod input.tfstate
# Auth commands
./taco login --issuer <OIDC_ISSUER> --client-id <CLIENT_ID> # Runs PKCE flow and saves tokens
# or simply:
# ./taco login --server http://localhost:8080 # CLI fetches issuer/client_id from /v1/auth/config
./taco whoami # Prints current identity (if logged in)
./taco creds --json # Prints AWS Process Credentials JSON via service
./taco logout # Removes saved tokens for --server
Configure OIDC so taco login
works and protected endpoints require login.
- WorkOS setup
- Create a User Management project and a Native (PKCE) OAuth application.
- Add redirect URI:
http://127.0.0.1:8585/callback
. - Note values:
- Client ID:
<WORKOS_CLIENT_ID>
- Issuer:
https://api.workos.com/user_management
- Authorization endpoint:
https://api.workos.com/user_management/authorize
- Token endpoint:
https://api.workos.com/user_management/token
- Client ID:
- Service config (verifies ID tokens and issues OpenTaco tokens)
export OPENTACO_AUTH_ISSUER="https://api.workos.com/user_management"
export OPENTACO_AUTH_CLIENT_ID="<WORKOS_CLIENT_ID>"
./opentacosvc -storage memory
- Login via CLI (PKCE)
./taco login \
--server http://localhost:8080 \
--issuer https://api.workos.com/user_management \
--client-id <WORKOS_CLIENT_ID> \
--auth-url https://api.workos.com/user_management/authorize \
--token-url https://api.workos.com/user_management/token
This opens a browser (also prints the URL). After you authenticate, the CLI exchanges the OIDC ID token with the service and saves OpenTaco tokens to ~/.config/opentaco/credentials.json
. To force the login box even if an SSO session exists, add --force-login
.
Auth0 variant:
export OPENTACO_AUTH_ISSUER="https://<TENANT>.auth0.com" # or <region>.auth0.com
export OPENTACO_AUTH_CLIENT_ID="<AUTH0_NATIVE_APP_CLIENT_ID>"
./opentacosvc -storage memory
# No flags needed; CLI uses discovery via /v1/auth/config
./taco login --server http://localhost:8080
- Verify auth
# Without login (or in a fresh shell without saved tokens): should return 401
curl -i http://localhost:8080/v1/units
# Using CLI (adds bearer automatically)
./taco unit ls
# Build the provider
make build-prov
# Example usage
cd providers/terraform/opentaco/examples/basic
# Configure the provider
cat > main.tf << 'EOF'
terraform {
required_providers {
opentaco = {
source = "digger/opentaco"
}
}
}
provider "opentaco" {
endpoint = "http://localhost:8080" # Or use OPENTACO_ENDPOINT env var
}
# Create a state registration
resource "opentaco_unit" "example" {
id = "myapp/prod"
labels = {
environment = "production"
team = "infrastructure"
}
}
# Read unit metadata
data "opentaco_unit" "example" {
id = opentaco_unit.example.id
}
output "unit_info" {
value = {
id = data.opentaco_unit.example.id
size = data.opentaco_unit.example.size
locked = data.opentaco_unit.example.locked
updated = data.opentaco_unit.example.updated
}
}
EOF
# Run Terraform
terraform init
terraform apply
#### Local Provider Install (for development)
When using the local, unpublished provider:
Option A — dev overrides in `~/.terraformrc`:
```hcl
provider_installation {
dev_overrides { "digger/opentaco" = "/absolute/path/to/opentaco/providers/terraform/opentaco" }
direct {}
}
Option B — install into the plugin directory:
~/.terraform.d/plugins/digger/opentaco/0.0.0/<os>_<arch>/terraform-provider-opentaco
Then run terraform init
again to pick up the local build.
### Dependencies & Unit Status
OpenTaco supports output-level dependencies between units without any external database. All metadata is stored as digests in a dedicated graph workspace unit.
- Graph workspace ID: `__opentaco_system` (a normal tfstate managed via the OpenTaco backend)
- Provider resource: `opentaco_dependency` (one resource per edge)
- Service updates: Any unit write updates relevant edges in the graph tfstate (source refresh and target acknowledge)
- Status API: `GET /v1/units/{id}/status`
- CLI:
- `taco unit status [<id> | --prefix <pfx>]` shows a table across units.
- Status is printed as friendly, color-coded labels:
- up to date (green) — no incoming pending
- needs re-apply (red) — at least one incoming pending
- might need re-apply (yellow) — clean incoming, but an upstream is red
See a full runnable example under `examples/dependencies/`.
### Provider Bootstrap (taco provider init)
Quickly scaffold a Terraform workspace which:
- Stores its own TF state in the OpenTaco HTTP backend at `__opentaco_system`.
- Configures the OpenTaco provider against your server.
- Includes a demo `opentaco_unit` resource (e.g., `myapp/prod`).
Steps:
```bash
# 1) Start the service on S3
OPENTACO_S3_BUCKET=<bucket> \
OPENTACO_S3_REGION=<region> \
OPENTACO_S3_PREFIX=<prefix> \
./opentacosvc
# 2) Scaffold the provider workspace
./taco provider init opentaco-config --server http://localhost:8080
# 3) Initialize and apply
cd opentaco-config
terraform init
terraform apply -auto-approve
# 4) Verify in S3
aws s3 ls s3://$OPENTACO_S3_BUCKET/$OPENTACO_S3_PREFIX/__opentaco_system/
aws s3 ls s3://$OPENTACO_S3_BUCKET/$OPENTACO_S3_PREFIX/myapp/prod/
Notes:
- System unit defaults to
__opentaco_system
. Override with--system-unit <id>
if desired. - The CLI creates the system unit by convention (skip with
--no-create
). - You can scaffold into the current directory with:
./taco provider init . --server http://localhost:8080
.
- Reserved names starting with
__opentaco_
are platform‑owned and should not be used for user stacks. - Default system unit ID is
__opentaco_system
, stored alongside user units under the same S3 prefix. - The backend treats this like any other unit; the CLI drives creation by convention.
terraform {
backend "http" {
address = "http://localhost:8080/v1/backend/myapp/prod"
lock_address = "http://localhost:8080/v1/backend/myapp/prod"
unlock_address = "http://localhost:8080/v1/backend/myapp/prod"
}
}
-
Service (
cmd/opentacosvc/
) - HTTP server with two surfaces:- Management API (
/v1
) for CRUD operations on units - Terraform HTTP backend proxy (
/v1/backend/{id}
) for Terraform/OpenTofu
- Management API (
-
CLI (
cmd/taco/
) - Command-line interface that calls the service for all operations -
SDK (
pkg/sdk/
) - Typed HTTP client used by both CLI and Terraform provider -
Terraform Provider (
providers/terraform/opentaco/
) - Manage units as Terraform resources
- S3 Store (default): Uses your AWS account “bucket-only” layout. Configure via flags or env (standard AWS SDK chain is used for auth).
- Memory Store (fallback): Automatically used if S3 configuration is missing or fails at startup; resets on restart.
S3 object layout per unit:
<prefix>/<unit-id>/terraform.tfstate
<prefix>/<unit-id>/terraform.tfstate.lock
(present only while locked)
Auth: All management endpoints require Authorization: Bearer <access>
, unless the service is started with -auth-disable
.
Note: Unit IDs containing slashes (e.g., myapp/prod
) are URL-encoded by replacing /
with __
in the path.
-
POST /v1/units
- Create a new unit- Body:
{"id": "myapp/prod"}
- Response:
{"id": "myapp/prod", "created": "2025-01-01T00:00:00Z"}
- Body:
-
GET /v1/units?prefix=
- List units with optional prefix filter- Response:
{"units": [...], "count": 10}
- Response:
-
GET /v1/units/{encoded_id}
- Get unit metadata- Example:
/v1/units/myapp__prod
- Response:
{"id": "myapp/prod", "size": 1024, "updated": "...", "locked": false}
- Example:
-
DELETE /v1/units/{encoded_id}
- Delete a unit -
GET /v1/units/{encoded_id}/download
- Download unit tfstate- Returns: Raw tfstate content
-
POST /v1/units/{encoded_id}/upload
- Upload unit tfstate- Body: Raw tfstate content
- Query param:
?if_locked_by={lock_id}
(optional)
-
POST /v1/units/{encoded_id}/lock
- Lock a unit- Body:
{"id": "lock-uuid", "who": "user@host", "version": "1.0.0"}
(optional) - Response: Lock info or 409 Conflict with current lock info
- Body:
-
DELETE /v1/units/{encoded_id}/unlock
- Unlock a unit- Body:
{"id": "lock-uuid"}
- Body:
GET /v1/backend/{id}
- Get state for TerraformPOST /v1/backend/{id}
- Update state from TerraformPUT /v1/backend/{id}
- Update state from Terraform (alias of POST)LOCK /v1/backend/{id}
- Acquire lock for TerraformUNLOCK /v1/backend/{id}
- Release lock from Terraform
Note: Terraform lock coordination uses the X-Terraform-Lock-ID
header; the service respects this header on update and unlock operations.
GET /v1/auth/config
– Server OIDC config (issuer, client_id, optional endpoints, redirect URIs)POST /v1/auth/exchange
– Exchange OIDC ID token for OpenTaco access/refreshPOST /v1/auth/token
– Refresh to new access (rotates refresh)POST /v1/auth/issue-s3-creds
– Issue stateless STS creds; requiresAuthorization: Bearer <access>
GET /v1/auth/me
– Echo subject/roles/groups from Bearer if present
taco unit create <id>
- Register a new unittaco unit ls [prefix]
- List units, optionally filtered by prefixtaco unit info <id>
- Show unit metadata (aliases:show
,describe
)taco unit rm <id>
- Delete a unit (aliases:delete
,remove
)
taco unit pull <id> [file]
- Download unit tfstate (stdout if no file specified)taco unit push <id> <file>
- Upload unit tfstate from file
taco unit lock <id>
- Manually lock a unittaco unit unlock <id> [lock-id]
- Unlock a unit (uses saved lock ID if not provided)
taco unit acquire <id> [file]
- Lock + download in one operationtaco unit release <id> <file>
- Upload + unlock in one operation
--server URL
- OpenTaco server URL (default:http://localhost:8080
, env:OPENTACO_SERVER
)-v, --verbose
- Enable verbose output
taco provider init [dir]
- Scaffold a Terraform workspace for the OpenTaco provider- Flags:
--dir <path>
: Output directory (defaultopentaco-config
; positional[dir]
takes precedence if given)--system-unit <id>
: System unit for the backend (default__opentaco_system
)--force
: Overwrite files if they exist--no-create
: Do not create the system unit (scaffold only)
- Flags:
- CLI:
OPENTACO_SERVER
sets the default server URL fortaco
. - Terraform provider:
OPENTACO_ENDPOINT
sets the default provider endpoint.
taco login [--force-login]
– PKCE login; saves tokens to~/.config/opentaco/credentials.json
taco whoami
– Prints current identitytaco creds --json
– Prints AWS Process Credentials JSON via/v1/auth/issue-s3-creds
taco logout
– Removes saved tokens for--server
opentaco/
├── cmd/
│ ├── opentacosvc/ # Service binary
│ └── taco/ # CLI binary
│ └── commands/ # Cobra commands package
├── internal/
│ ├── api/ # HTTP handlers
│ ├── backend/ # Terraform backend proxy
│ ├── domain/ # Business logic
│ ├── auth/ # JWT auth handlers
│ ├── oidc/ # OIDC verifier abstraction (stub)
│ ├── sts/ # STS issuer interface (stub)
│ ├── rbac/ # RBAC checker (permissive stub)
│ ├── middleware/ # AuthN/AuthZ middlewares, 501 helper
│ ├── storage/ # Storage interfaces
│ └── observability/ # Health/metrics
├── pkg/
│ └── sdk/ # Go client library
└── providers/
└── terraform/ # Terraform provider
└── opentaco/
# Initialize modules (first time only; skip if go.mod files exist)
make init
# Build everything
make all
# Build individual components
make build-svc # Service only
make build-cli # CLI only
make build-prov # Provider only
make test
make lint
make clean
- Example auth config shape is provided in
configs/auth.yaml
(not yet enforced). - Docs placeholders for upcoming auth/STS work:
docs/backend_profile_guide.md
docs/auth_config_examples.md
docs/final_spec_state_auth_sts.md
# Run with S3 storage (default)
# Uses standard AWS credential/config chain (env, shared config, IAM role)
OPENTACO_S3_BUCKET=my-bucket \
OPENTACO_S3_PREFIX=opentaco \
OPENTACO_S3_REGION=us-east-1 \
./opentacosvc
# Explicit flags (optional)
./opentacosvc -storage s3 \
-s3-bucket my-bucket \
-s3-prefix opentaco \
-s3-region us-east-1
# Force in-memory storage
./opentacosvc -storage memory
-
405 on LOCK/UNLOCK during
terraform init/apply
:- Cause: routes for custom HTTP verbs not wired.
- Fix (service): add
e.Add("LOCK", "/v1/backend/*", handler)
ande.Add("UNLOCK", "/v1/backend/*", handler)
, rebuild, restart.
-
409 on POST/PUT ("Failed to save state"):
- Cause: backend not reading lock ID from Terraform query
?ID=<uuid>
. - Fix (service): in update handler, read lock ID from header
X-Terraform-Lock-ID
OR queryID
/id
.
- Cause: backend not reading lock ID from Terraform query
-
409 on Create in provider ("Unit already exists"):
- Cause: remote state with same
id
already exists; renaming the Terraform resource block does not change the backend ID. - Fix options:
- Import:
terraform import opentaco_unit.NAME <id>
. - Change ID: update
id = "..."
to a new value. - Remove remote:
./taco --server <url> unit rm <id>
then apply.
- Import:
- Cause: remote state with same
If Terraform cannot find the local provider, add a workspace-local CLI config and re-init:
# From repo root (path to provider source dir)
ABS="$(pwd)/providers/terraform/opentaco"
# Write a local override inside your scaffolded dir
cat > opentaco-config/.terraformrc <<EOF
provider_installation {
dev_overrides { "digger/opentaco" = "${ABS}" }
direct {}
}
EOF
export TF_CLI_CONFIG_FILE="$PWD/opentaco-config/.terraformrc"
cd opentaco-config && terraform init -upgrade
- User-facing IDs use natural paths:
myapp/prod
- HTTP routes encode slashes as double underscores:
myapp__prod
- This is handled automatically by the CLI and SDK
- Locks are cooperative - clients must respect them
- Lock IDs are UUIDs generated by clients
- The CLI saves lock IDs locally in
.taco/
for convenience - Terraform backend operations handle locking automatically
- Default storage: S3 (bucket-only). Uses AWS default credential chain.
- Fallback: if S3 is not configured or init fails, the service warns and falls back to in-memory storage.
- S3 object layout per unit:
<prefix>/<unit-id>/terraform.tfstate
<prefix>/<unit-id>/terraform.tfstate.lock
- System unit convention:
- Reserved IDs start with
__opentaco_
. - Default system unit is
__opentaco_system
and is created by the CLI (not auto-created by the service).
- Reserved IDs start with
- S3 Adapter: Production storage backend maintaining compatibility with existing tfstate layouts
- Unit Versioning: Keep history of tfstate changes
- Metrics: Prometheus metrics for monitoring
- Dependency Graph: Track outputs and dependencies between units
- RBAC: Organizations, teams, users with SSO integration
- Audit Logging: Track all unit operations
- Remote Execution: Run Terraform in controlled environments
- PR Automation: GitOps workflows with unit/tfstate management
- Policy Engine: OPA-based policy enforcement on unit changes
- UI: Web interface for state management
[License information to be added]
You can point Terraform’s S3 backend at OpenTaco’s /s3
endpoint using process credentials minted by the CLI.
- AWS profile (~/.aws/config):
[profile opentaco-state-backend]
region = auto
credential_process = "/absolute/path/to/taco" creds --json --server http://localhost:8080
- Terraform backend block:
terraform {
backend "s3" {
bucket = "opentaco"
key = "myapp/prod/terraform.tfstate"
endpoints = { s3 = "http://localhost:8080/s3" }
use_path_style = true
skip_credentials_validation = true
skip_region_validation = true
skip_requesting_account_id = true
use_lockfile = true # Terraform 1.13+
profile = "opentaco-state-backend"
}
}
- Run:
./taco login --server http://localhost:8080
export AWS_SDK_LOAD_CONFIG=1
export AWS_PROFILE=opentaco-state-backend
terraform init -reconfigure && terraform apply -auto-approve
More details in docs/s3-compat.md
.