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
Use this when scaffolding a new Terraform provider with the Plugin Framework: workspace layout, go module setup, provider server main.go, and a provider.go…
official
terraform-test
hashicorp
Comprehensive guide for writing and running Terraform tests. Use when creating test files (.tftest.hcl), writing test scenarios with run blocks, validating…
official
terraform-test
hashicorp
Terraform 테스트 작성 및 실행을 위한 종합 가이드로, 어설션, 모킹, 모듈 검증을 포함합니다. .tftest.hcl 구문을 사용하여 테스트 파일을 작성하며, plan 또는 apply 모드로 실행되는 run 블록을 지원하고, 선택적 상태 격리와 함께 순차 및 병렬 실행을 지원합니다. 리소스 속성, 출력, 데이터 소스에 대한 조건을 어설션하고, expect_failures를 사용하여 잘못된 입력이 적절히 거부되는지 검증합니다. Mock 제공자(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 머신 이미지를 구축합니다. HCL 템플릿과 프로비저너(셸 스크립트, 파일 업로드, 구성 관리)를 사용해 소스 AMI에서 AMI 생성을 자동화합니다. ami_regions를 통한 다중 리전 AMI 배포와 이름, 소유자, 가상화 유형별 유연한 소스 AMI 필터링을 지원합니다. 환경 변수, AWS 자격 증명 파일 또는 IAM 인스턴스 프로파일을 통해 인증하며 템플릿 검증 및 빌드 명령을 포함합니다...
official
new-terraform-provider
hashicorp
Plugin Framework를 사용하여 새로운 Terraform Provider를 스캐폴딩합니다. 표준 "terraform-provider-" 명명 규칙을 따르는 새로운 Go 모듈 워크스페이스를 생성하고 필요한 종속성을 초기화합니다. HashiCorp의 Plugin Framework 패턴을 따르는 템플릿 main.go 파일을 제공하며, 사용자 정의를 위한 TODO 마커가 포함되어 있습니다. 빌드 및 테스트 명령을 실행하여 Provider가 컴파일되고 초기 검사를 통과하는지 확인함으로써 설정을 검증합니다. 새 워크스페이스를 생성하기 전에 의도를 확인하여 워크스페이스 관리를 처리합니다.
official
azure-verified-modules
hashicorp
Azure Terraform 모듈이 AVM 규정을 준수하기 위한 인증 요구 사항 및 모범 사례입니다. 공급자 버전 제약 조건(azurerm >= 4.0, < 5.0; azapi >= 2.0, < 3.0)을 적용하고, git 기반 모듈 참조를 금지하며 고정된 Terraform 레지스트리 소스를 사용하도록 합니다. 모든 식별자에 소문자 스네이크 케이스, 정확한 변수 유형, 반부패 계층 패턴을 통한 개별 출력 속성, 알파벳 순서로 정렬된 로컬 변수를 요구합니다. 새 리소스가 추가될 때 기능 토글 변수를 요구합니다...
official