tfctl

作者: hashicorp

使用 tfctl CLI 與 HCP Terraform / Terraform Cloud / Terraform Enterprise 互動。完整 API 覆蓋。適用於任何 HCP Terraform 或 Terraform Cloud 或…

npx skills add https://github.com/hashicorp/tfctl-cli --skill tfctl

tfctl — HCP Terraform CLI

Single binary, full v2 API coverage. Already authenticated.

Hard rules

  1. Never pipe tfctl JSON to an external jq. Use the built-in --jq '<expr>' flag — it implies --json and runs gojq on the response envelope.

  2. Resolve names with -p, not separate lookup calls. Paths with {workspace}/{team}/{project}/{varset} accept -p workspace=NAME etc. — tfctl resolves name→ID for you. Don't fetch the ID first.

  3. Trust the first answer. data: [], data: null, relationships.X.data: null, or stderr "no current run"/"not found" ARE the answer. Don't re-query in another format. Don't walk relationships "to verify".

  4. When a named resource is not found, stop completely. Exit code 2 or absence from a listing IS the full answer. Never:

    • Try a different resource ID "to verify the endpoint works"
    • Pivot to another org/workspace that appeared in the available list
    • Explore related resources to find "similar" information
    • Use Rule 3 to justify switching to a different resource: if you listed orgs and 'platform' isn't there, the first answer is "platform doesn't exist" — stop, don't use whatever org IS listed instead.

    Examples: run-POLICY returns exit 2 → stop, don't query other run IDs. Listing orgs shows no 'platform' → stop, don't use the org that IS listed.

  5. Never run tfctl harness exec yourself to self-authorize. The --allow-delete grant is a human's opt-in for your session. If a delete is refused, relay the printed harness exec --allow-delete=<class> command back to the human — do not run it (or set TFCTL_EXEC_SESSION) to grant yourself permission. See Deleting resources.

Deleting resources

Deletes are destructive, so tfctl itself gates them — you don't need to police this with a blanket refusal. A noninteractive tfctl ... -X DELETE only goes through when a human has opted in for this session by launching you via tfctl harness exec --allow-delete=<class> -- <command> (which sets TFCTL_EXEC_SESSION). Otherwise tfctl refuses on its own and tells you what to do.

So when you're asked to delete something, just run the normal command and let tfctl be the gate:

tfctl api PATH -X DELETE
  • If the session authorizes that resource's class, it succeeds — that's the human's intent, not a violation; proceed and report the result.
  • If it doesn't, tfctl refuses with a self-documenting message and prints the exact command to hand back (including the harness exec --allow-delete=<class> a human can use to authorize you). Relay that rather than forcing it; don't run harness exec yourself to self-authorize.
  • You can't tell in advance whether the session grants a class — so don't guess. Attempt the delete and let the refusal tell you. The refusal is a plain exit 1 with a message naming the class and the --allow-delete=<class> command; that is your cue to relay. This is a grant gap, NOT an auth failure: never report it as an expired token, an exit code 3, or tell the human to re-login unless tfctl actually says the token is expired/invalid.
  • Apply ordinary caution to irreversible deletes (organizations, projects). For a high-stakes resource, confirm intent with the human first even when the session would allow it.

URL shape: per-workspace subpaths live at /workspaces/{workspace}/...

Most "things attached to a workspace" (vars, runs, varsets, remote-state-consumers, configuration-versions, notification-configurations, state-versions) are under /workspaces/{workspace}/..., NOT /organizations/{org}/workspaces/{name}/.... The org-nested form only exists for the workspace resource itself (/organizations/{org}/workspaces/{name}). For everything else, use /workspaces/{workspace}/X -p workspace=NAME.

Exception: Policy checks and run-specific data live under /runs/{run-id}/..., not under workspace paths. For example: /runs/{run-id}/policy-checks.

Anti-patterns to avoid

These paths do not exist; don't try them:

  • /organizations/{org}/workspaces/{name}/vars — use /workspaces/{workspace}/vars -p workspace=NAME instead
  • /organizations/{org}/workspaces/{name}/state-versions — use /workspaces/{workspace}/state-versions -p workspace=NAME
  • /organizations/{org}/workspaces/{name}/configuration-versions — use /workspaces/{workspace}/configuration-versions -p workspace=NAME
  • /workspaces/{workspace}/policy-checks — use /runs/{run-id}/policy-checks instead
  • /organizations/{org}/varsets (partially wrong path) — use /organizations/{organization}/varsets with correct org placeholder
  • ❌ External jq pipes like tfctl ... | jq '...' — always use tfctl api ... --jq '...' with built-in flag

Cookbook — one-line answers for common tasks

# Count workspaces in an org
tfctl api /organizations/{organization}/workspaces --page-size 1 --jq '.meta.pagination.["total-count"]'

# Find workspace by partial name (server-side search) — also returns current run state in one call
tfctl api /organizations/{organization}/workspaces -f 'search[name]=TERM' --jq '.data[] | {id, name: .attributes.name, current_run: .relationships.["current-run"].data}'

# Filter workspaces by attribute
tfctl api /organizations/{organization}/workspaces --all --jq '.data[] | select(.attributes.["terraform-version"] | startswith("1.8")) | .attributes.name'

# List variables on a workspace — path is /workspaces/{workspace}/vars (NOT /organizations/{org}/workspaces/{name}/vars; that endpoint does not exist)
tfctl api /workspaces/{workspace}/vars -p workspace=NAME --jq '.data[] | {key: .attributes.key, category: .attributes.category, sensitive: .attributes.sensitive}'

# Get current run status
tfctl run status NAME_OR_ID            # If it prints "no current run" or exits non-zero with that message, that IS the answer.

# Get current run ID for a workspace
tfctl api /organizations/{organization}/workspaces/NAME --jq '.data.relationships.["current-run"].data.id'

# List runs in a workspace with status filtering
tfctl api /workspaces/{workspace}/runs -p workspace=NAME --jq '.data[] | select(.attributes.status == "planned") | {id, status: .attributes.status}'

# Find workspace by VCS repo identifier (single call; no results = not connected to that repo)
tfctl api /organizations/{organization}/workspaces --all \
  --jq '.data[] | select(.attributes.["vcs-repo"] != null and .attributes.["vcs-repo"].identifier == "org/repo") | {id: .id, name: .attributes.name}'

# List workspaces accessible to a team (two-step: resolve team name → query team-workspaces)
# Note: /organizations/{org}/teams/{id}/workspaces does NOT exist — use /team-workspaces instead
tfctl api /organizations/{organization}/teams -f 'filter[names]=TEAM_NAME' --jq '.data[0].id'
tfctl api /team-workspaces -f 'filter[team][id]=TEAM_ID' --jq '.data[] | {workspace_id: .relationships.workspace.data.id, access: .attributes.access}'

# Get organization details and settings
tfctl api /organizations/{organization} --jq '.data | {id, name: .attributes.name, created_at: .attributes."created-at", terraform_version_default: .attributes."terraform-version"}'

# Get the current state version for a workspace
# (operationId: getCurrentStateVersion — single resource, not a list)
tfctl api /workspaces/{workspace}/current-state-version -p workspace=NAME --jq '.data | {serial: .attributes.serial, created_at: .attributes.["created-at"], status: .attributes.status}'

# List all variable sets in an org and their variable counts
tfctl api /organizations/{organization}/varsets --all --jq '.data[] | {name: .attributes.name, id: .id, var_count: (.relationships.vars.data | length)}'

# Get configuration version details
tfctl api /workspaces/{workspace}/configuration-versions -p workspace=NAME --jq '.data[] | {id, source: .attributes.source, created_at: .attributes.created-at}'

# List notification configurations
tfctl api /workspaces/{workspace}/notification-configurations -p workspace=NAME --jq '.data[] | {id, type: .attributes.destination-type, trigger: .attributes.triggers}'

# Filter workspaces by terraform version (1.7+)
tfctl api /organizations/{organization}/workspaces --all --jq '.data[] | select(.attributes.["terraform-version"] | ltrimstr("v") | split(".") | [.[0], .[1]] | join(".") | tonumber >= 1.7) | {name: .attributes.name, tf_version: .attributes.["terraform-version"]}'

# Filter workspaces excluding certain names
tfctl api /organizations/{organization}/workspaces --all --jq '.data[] | select(.attributes.name | test("^temp-|^old-") | not) | .attributes.name'

# Count resources by status (e.g., runs by status)
tfctl api /workspaces/{workspace}/runs -p workspace=NAME --all --jq '[.data[] | .attributes.status] | group_by(.) | map({status: .[0], count: length})'

# Get log URL for a completed run
# Note: run.log-read-url is null on completed runs — the URL lives on plan/apply.
tfctl api /runs/RUN_ID --jq '.data.relationships | {plan: .plan.data.id, apply: .apply.data.id}'
tfctl api /plans/PLAN_ID --jq '.data.attributes.["log-read-url"]'

# Start a run
tfctl run start NAME_OR_ID

# Add a remote state consumer to a workspace
# (operationId: addWorkspaceRemoteStateConsumers — POST returns 204)
tfctl api /workspaces/{workspace}/relationships/remote-state-consumers -p workspace=NAME \
  -i '{"data":[{"type":"workspaces","id":"ws-CONSUMER_ID"}]}'

# Apply a variable set to a workspace
# (operationId: updateWorkspaceRelationship for varsets)
tfctl api /workspaces/{workspace}/relationships/varsets -p workspace=NAME \
  -X POST -i '{"data":[{"type":"varsets","id":"varset-VARSET_ID"}]}'

# Get policy check results for a run
tfctl api /runs/{run-id}/policy-checks --jq '.data[] | {id: .id, status: .attributes.status, enforced: .attributes.enforcement-level}'

# Discover an API operation when you don't know it
tfctl api schema search "KEYWORD" --json     # returns operationIds
tfctl api schema get OPERATION_ID            # full OpenAPI schema (large response — only call when needed)

Secret Redaction

By default, tfctl will redact the output of sensitive values from api command output, which includes artifact download URLs, log URLs, tokens, private SSH keys, and some unknown things such as variable values that match a token heuristic. This includes --json and --jq output. When extracting a secret that you need, use the --no-redact global flag to disable redaction for a single request.

Output flags

NeedFlag
Filter / extract--jq '<expr>'
Full JSON--json
Render for human--markdown
Audit a mutation--dry-run
Don't redact secrets--no-redact

--jq implies --json. Don't pass both. Always pass one explicitly — don't rely on auto-detect.

JSON:API conventions

Responses are JSON:API envelopes: {data: {id, type, attributes, relationships}} (or data: [...] for lists). To follow a link, read data.relationships.<name>.data which gives {id, type} or null. A null is final — there is no related resource.

Pagination: default page 1. Add --all for all pages (cap 2000), or --page-size N / --page-number N for explicit control. For counts, use --page-size 1 --jq '.meta.pagination.["total-count"]'.

Mutations & batch updates

# Create or update a single variable
tfctl api /workspaces/{workspace}/vars -p workspace=NAME -a key=VARKEY -a value=VALUE -a category=env

# Batch updates: use separate calls (HCP TFC doesn't support bulk POST for vars)
# Instead of trying /workspaces/{ws}/vars with multiple items, do:
tfctl api /workspaces/{workspace}/vars -p workspace=NAME -a key=VAR1 -a value=VAL1 -a category=env
tfctl api /workspaces/{workspace}/vars -p workspace=NAME -a key=VAR2 -a value=VAL2 -a category=env
# (Multiple calls cost more, but it's the supported pattern)

# PATCH with a raw body
tfctl api /workspaces/{workspace}/X -X PATCH -p workspace=NAME -i '{"data":{"type":"X","attributes":{…}}}'

Add --dry-run to preview without sending.

Exit codes (quick map)

CodeMeaningAction
0SuccessDone
1Informational message or usage errorRead stderr; if it says "no current run" or similar, that IS the answer — stop
2Not found (workspace/run doesn't exist) OR invalid authVerify the ID/name is correct, then check token if still failing
3Auth token expired or invalidRe-authenticate
4Network errorRetry after brief delay
5Rate limited (429) or server error (5xx)Retry with backoff
6Resource has an error stateThe error is already diagnosed in output (e.g., plan failed); read it

Important: When an API returns data: [] (empty list) or data: null, that IS the answer. Don't retry with different flags or endpoints.

Common troubleshooting

SymptomCauseFix
exit 2 when listing workspacesOrganization doesn't exist or auth token has no accessVerify org name and re-authenticate
exit 1 with "no current run"Workspace simply has no active run (informational)This is the answer — stop, don't verify
exit 3 when making any API callAuth token expiredRe-authenticate with tfctl login
exit 5 (429 rate limit)Too many requestsWait and retry; tfctl will backoff automatically
exit 6 with "plan is errored"Terraform plan had syntax errors (not CLI error)Read the plan output for details
Empty list (data: []) when filteringNo resources match criteriaVerify criteria is correct; empty list is valid answer

Smart defaults

  • {organization} resolves from active profile if set.
  • {workspace} resolves from a local cloud {} block in CWD.
  • Already-formed IDs (ws-…, team-…, prj-…, varset-…) are passed through as-is.

來自 hashicorp 的更多技能

provider-actions
hashicorp
使用 Plugin Framework 實作 Terraform Provider 動作。在開發於生命週期事件(之前/之後…)執行的命令式操作時使用。
official
new-terraform-provider
hashicorp
在新版Terraform Provider使用Plugin Framework建立框架時使用此功能:工作區佈局、Go模組設定、Provider伺服器main.go,以及provider.go…
official
terraform-test
hashicorp
編寫和執行 Terraform 測試的綜合指南。在建立測試檔案(.tftest.hcl)、使用 run 區塊編寫測試情境、驗證…時使用。
official
terraform-test
hashicorp
撰寫與執行 Terraform 測試的完整指南,涵蓋斷言、模擬及模組驗證。使用 .tftest.hcl 語法編寫測試檔案,透過 run 區塊在 plan 或 apply 模式下執行,支援順序與平行執行,並可選擇隔離狀態。對資源屬性、輸出及資料來源進行條件斷言;使用 expect_failures 驗證無效輸入是否被正確拒絕。模擬提供者(Terraform 1.7.0 以上版本)可模擬基礎設施行為,無需...
official
provider-actions
hashicorp
使用 Plugin Framework 在資源生命週期事件中執行命令式的 Terraform Provider 動作。支援建立前/後與更新前/後的觸發機制(Terraform 1.14.0 不支援銷毀事件)。需以正確的框架類型、集合的 ElementType 及輸入驗證器定義適當的綱要。包含進度回報、逾時管理及長時間操作的全面錯誤處理。實作輪詢與...
official
aws-ami-builder
hashicorp
使用Packer的amazon-ebs建置器建立自訂Amazon Machine Images。透過HCL範本自動化從來源AMI建立AMI,並搭配佈建工具(Shell腳本、檔案上傳、組態管理)進行自訂。支援透過ami_regions進行多區域AMI分發,以及依名稱、擁有者和虛擬化類型進行靈活的來源AMI篩選。可透過環境變數、AWS憑證檔案或IAM執行個體設定檔進行驗證;包含範本的驗證與建置命令...
official
new-terraform-provider
hashicorp
使用 Plugin Framework 建立新的 Terraform provider。生成新的 Go 模組工作區,採用標準的「terraform-provider-」命名慣例,並初始化所需的依賴項。提供遵循 HashiCorp Plugin Framework 模式的範本 main.go 檔案,並附有待辦事項標記供自訂使用。透過執行建置與測試指令來驗證設定,確保 provider 能成功編譯並通過初步檢查。在建立新的工作區前,會先確認意圖以管理工作區。
official
azure-verified-modules
hashicorp
認證要求與Azure Terraform模組尋求AVM合規性的最佳實踐。強制執行提供者版本限制(azurerm >= 4.0, < 5.0;azapi >= 2.0, < 3.0),並禁止基於git的模組引用,改為使用固定的Terraform註冊表來源。要求所有識別碼採用小寫蛇形命名法、精確的變數類型、透過防腐層模式實現離散的輸出屬性,以及按字母順序排列的locals。新增資源時需要功能切換變數...
official