tfctl

โดย hashicorp

โต้ตอบกับ HCP Terraform / Terraform Cloud / Terraform Enterprise โดยใช้ tfctl CLI ครอบคลุม 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.

Skills เพิ่มเติมจาก hashicorp

provider-actions
hashicorp
Implement การทำงานของ Terraform Provider โดยใช้ Plugin Framework ใช้เมื่อพัฒนาการดำเนินการแบบ imperative ที่ทำงานในเหตุการณ์ lifecycle (ก่อน/หลัง…
official
new-terraform-provider
hashicorp
ใช้สิ่งนี้เมื่อสร้าง Terraform provider ใหม่ด้วย Plugin Framework: โครงสร้าง workspace, การตั้งค่า go module, provider server main.go และ provider.go…
official
terraform-test
hashicorp
คู่มือที่ครอบคลุมสำหรับการเขียนและรันการทดสอบ Terraform ใช้เมื่อสร้างไฟล์ทดสอบ (.tftest.hcl) เขียนสถานการณ์ทดสอบด้วย run blocks ตรวจสอบความถูกต้อง…
official
terraform-test
hashicorp
คู่มือที่ครอบคลุมสำหรับการเขียนและรันการทดสอบ Terraform พร้อมการยืนยันผล การจำลอง และการตรวจสอบโมดูล เขียนไฟล์ทดสอบโดยใช้ไวยากรณ์ .tftest.hcl พร้อมบล็อก run ที่ทำงานในโหมด plan หรือ apply รองรับการทำงานแบบลำดับและแบบขนานพร้อมการแยกสถานะที่เป็นทางเลือก ยืนยันเงื่อนไขบนแอตทริบิวต์ของทรัพยากร เอาต์พุต และแหล่งข้อมูล ใช้ expect_failures เพื่อตรวจสอบว่าอินพุตที่ไม่ถูกต้องถูกปฏิเสธอย่างเหมาะสม จำลองผู้ให้บริการ (Terraform 1.7.0+) เพื่อจำลองพฤติกรรมของโครงสร้างพื้นฐานโดยไม่ต้อง...
official
provider-actions
hashicorp
ใช้การดำเนินการของ Terraform Provider แบบ Imperative ในเหตุการณ์วงจรชีวิตของทรัพยากรโดยใช้ Plugin Framework รองรับทริกเกอร์วงจรชีวิตก่อน/หลังการสร้างและก่อน/หลังการอัปเดต (เหตุการณ์การทำลายไม่พร้อมใช้งานใน Terraform 1.14.0) ต้องมีการกำหนดสคีมาที่ถูกต้องพร้อมประเภทเฟรมเวิร์กที่เหมาะสม ElementType สำหรับคอลเล็กชัน และตัวตรวจสอบสำหรับการตรวจสอบอินพุต รวมถึงการรายงานความคืบหน้า การจัดการหมดเวลา และการจัดการข้อผิดพลาดที่ครอบคลุมสำหรับการดำเนินการที่ใช้เวลานาน รองรับการโพลและ...
official
aws-ami-builder
hashicorp
สร้าง Amazon Machine Images แบบกำหนดเองด้วย builder amazon-ebs ของ Packer อัตโนมัติการสร้าง AMI จาก AMI ต้นทางโดยใช้เทมเพลต HCL พร้อม provisioners สำหรับการปรับแต่ง (สคริปต์เชลล์, อัปโหลดไฟล์, การจัดการการกำหนดค่า) รองรับการกระจาย AMI หลายภูมิภาคผ่าน ami_regions และการกรอง AMI ต้นทางแบบยืดหยุ่นตามชื่อ, เจ้าของ, และประเภทการจำลองเสมือน ยืนยันตัวตนผ่านตัวแปรสภาพแวดล้อม, ไฟล์ข้อมูลรับรอง AWS, หรือโปรไฟล์อินสแตนซ์ IAM; รวมคำสั่งตรวจสอบและสร้างสำหรับเทมเพลต...
official
new-terraform-provider
hashicorp
สร้างโครงร่างผู้ให้บริการ Terraform ใหม่โดยใช้ Plugin Framework สร้างพื้นที่ทำงานโมดูล Go ใหม่ตามรูปแบบการตั้งชื่อมาตรฐาน "terraform-provider-" และเริ่มต้นการพึ่งพาที่จำเป็น ให้ไฟล์ main.go ต้นแบบตามรูปแบบ Plugin Framework ของ HashiCorp พร้อมเครื่องหมาย TODO สำหรับการปรับแต่ง ตรวจสอบการตั้งค่าโดยรันคำสั่ง build และ test เพื่อให้แน่ใจว่าผู้ให้บริการคอมไพล์และผ่านการตรวจสอบเบื้องต้น จัดการพื้นที่ทำงานโดยยืนยันความตั้งใจก่อนสร้างใหม่...
official
azure-verified-modules
hashicorp
ข้อกำหนดการรับรองและแนวปฏิบัติที่ดีที่สุดสำหรับโมดูล Azure Terraform ที่ต้องการความสอดคล้องกับ AVM บังคับใช้ข้อจำกัดเวอร์ชันของ provider (azurerm >= 4.0, < 5.0; azapi >= 2.0, < 3.0) และห้ามการอ้างอิงโมดูลแบบ git โดยกำหนดให้ใช้แหล่งที่มาจาก Terraform registry ที่ถูกตรึงไว้ กำหนดให้ใช้ snake_casing ตัวพิมพ์เล็กสำหรับ identifiers ทั้งหมด ชนิดตัวแปรที่แม่นยำ แอตทริบิวต์เอาต์พุตแบบแยกส่วนผ่านรูปแบบ anti-corruption layer และ locals ที่เรียงตามลำดับตัวอักษร กำหนดให้มีตัวแปรสลับฟีเจอร์สำหรับทรัพยากรใหม่ที่ถูกเพิ่ม...
official