tfctl について
tfctlは、API経由でHCP Terraformを管理するためのコマンドラインスキルです。組み込みのJSONフィルタリングや名前解決に加え、機密出力のマスキングとセッション単位の削除認可に対応します。
主な機能
- 01HCP Terraform v2 API全般に対応
- 02gojqによるJSONフィルタリング
- 03名前からリソースを解決
- 04機密情報を標準でマスキング
- 05削除にセッション認可が必要
- 06変更内容の事前確認
活用シーン
- 01組織やワークスペースのリソース確認
- 02APIレスポンスの抽出と絞り込み
- 03変数などの変更を事前確認
- 04認可済みセッションでリソースを削除
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ hashicorp/ tfctl-cli --skill tfctlインストール後、Claude Code で/tfctlを入力して読み込まれたか確認します。
同梱ファイルknown_release_hashes
詳細
tfctl — HCP Terraform CLI
単一バイナリで、v2 API を完全にカバーします。認証済みです。
厳守事項
-
tfctlの JSON を外部のjqにパイプしないでください。 組み込みの--jq '<expr>'フラグを使用してください。このフラグは--jsonを暗黙的に有効にし、レスポンスエンベロープに対して gojq を実行します。 -
名前の解決には、個別の検索呼び出しではなく
-pを使用してください。{workspace}/{team}/{project}/{varset}を含むパスでは、-p workspace=NAMEなどを指定します。tfctl が名前から ID への解決を行います。先に ID を取得しないでください。 -
最初の回答を信頼してください。
data: []、data: null、relationships.X.data: null、または stderr の「no current run」/「not found」は、そのまま回答です。別の形式で再検索しないでください。関係をたどって「確認」しないでください。 -
名前付きリソースが見つからない場合は、そこで完全に停止してください。 終了コード 2、または一覧に存在しないことが、そのまま回答のすべてです。決して次のことをしないでください。
- エンドポイントが機能するか「確認」するために、別のリソース ID を試す
- 利用可能な一覧に表示された別の組織/ワークスペースに切り替える
- 関連リソースを調べて「似た」情報を探す
- 別のリソースに切り替える根拠としてルール 3 を使う。一覧に組織を表示した際に「platform」がなければ、最初の回答は「platform は存在しない」です。停止し、一覧に表示された組織を代わりに使わないでください。
例:
run-POLICYが終了コード 2 を返したら停止し、他の run ID を照会しないでください。組織一覧に「platform」がなければ停止し、一覧に表示された組織を使わないでください。 -
自分自身に権限を付与するために
tfctl harness execを実行しないでください。--allow-deleteの許可は、このセッションで人間が選択するものです。削除が拒否された場合は、表示されたharness exec --allow-delete=<class>コマンドを人間に伝えてください。自分で実行したり、TFCTL_EXEC_SESSIONを設定したりして、権限を付与してはいけません。リソースの削除を参照してください。
リソースの削除
削除は破壊的な操作ですが、tfctl 自体が制御するため、一律に拒否する必要はありません。非対話形式の tfctl ... -X DELETE が実行されるのは、人間が tfctl harness exec --allow-delete=<class> -- <command> を介して起動し、このセッションで許可している場合のみです(このコマンドは TFCTL_EXEC_SESSION を設定します)。それ以外の場合は tfctl が自動的に拒否し、対応方法を知らせます。
したがって、削除を依頼されたら、通常のコマンドを実行し、tfctl に制御を任せてください。
tfctl api PATH -X DELETE
- セッションがそのリソースのクラスを許可している場合は、削除が成功します。 これは人間の意図であり、違反ではありません。そのまま処理を続け、結果を報告してください。
- 許可されていない場合、
tfctlは説明付きのメッセージで拒否し、人間が許可に使えるharness exec --allow-delete=<class>を含む、伝えるべき正確なコマンドを表示します。 無理に実行せず、その内容を伝えてください。自分自身に権限を付与するためにharness execを実行しないでください。 - セッションがクラスを許可しているか、事前にはわかりません。推測せずに削除を試み、拒否された場合はその指示に従ってください。 拒否は、クラス名と
--allow-delete=<class>コマンドを示すメッセージを伴う、通常の終了コード 1 です。これが人間に伝える合図です。この拒否は許可の不足であり、認証の失敗ではありません。tfctlがトークンの期限切れまたは無効を実際に示していない限り、期限切れやexit code 3と報告したり、人間に再ログインを求めたりしないでください。 - 取り消し不能な削除(
organizations、projects)には、通常の注意を払ってください。重大なリソースの場合は、セッションで許可されていても、事前に人間に意図を確認してください。
URL の形式:ワークスペースごとのサブパスは /workspaces/{workspace}/... にあります
ワークスペースに紐づく多くのリソース(vars、runs、varsets、remote-state-consumers、configuration-versions、notification-configurations、state-versions)は、/organizations/{org}/workspaces/{name}/... ではなく、/workspaces/{workspace}/... の下にあります。組織配下の形式が存在するのは、ワークスペース自体のリソース(/organizations/{org}/workspaces/{name})のみです。それ以外では、/workspaces/{workspace}/X -p workspace=NAME を使用してください。
例外:ポリシーチェックと run 固有のデータは、ワークスペースのパスではなく、/runs/{run-id}/... の下にあります。例:/runs/{run-id}/policy-checks。
避けるべきアンチパターン
以下のパスは存在しません。使用しないでください:
- ❌
/organizations/{org}/workspaces/{name}/vars— 代わりに/workspaces/{workspace}/vars -p workspace=NAMEを使用してください - ❌
/organizations/{org}/workspaces/{name}/state-versions— 代わりに/workspaces/{workspace}/state-versions -p workspace=NAMEを使用してください - ❌
/organizations/{org}/workspaces/{name}/configuration-versions— 代わりに/workspaces/{workspace}/configuration-versions -p workspace=NAMEを使用してください - ❌
/workspaces/{workspace}/policy-checks— 代わりに/runs/{run-id}/policy-checksを使用してください - ❌
/organizations/{org}/varsets(パスが一部誤っています) — 正しい組織のプレースホルダーを使い、/organizations/{organization}/varsetsを使用してください - ❌
tfctl ... | jq '...'のように外部のjqにパイプすること — 常に組み込みフラグを使ってtfctl api ... --jq '...'を使用してください
クックブック — よくあるタスクへの一行回答
# 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)
シークレットのマスキング
デフォルトでは、tfctl は api コマンドの出力に含まれる機密値をマスキングします。対象には、アーティファクトのダウンロード URL、ログ URL、トークン、SSH 秘密鍵、およびトークンらしさのヒューリスティックに一致する変数値など、一部の未知の値が含まれます。これは --json と --jq の出力にも適用されます。必要なシークレットを抽出するときは、--no-redact グローバルフラグを使って、そのリクエストに限りマスキングを無効にしてください。
出力フラグ
| 用途 | フラグ |
|---|---|
| フィルタリング/抽出 | --jq '<expr>' |
| 完全な JSON | --json |
| 人が読みやすい形式で表示 | --markdown |
| 変更を監査する | --dry-run |
| シークレットをマスキングしない | --no-redact |
--jq は --json を暗黙に指定します。両方を渡さないでください。自動判定に頼らず、必ずどちらか一方を明示的に指定してください。
JSON:API の規約
レスポンスは JSON:API エンベロープです:{data: {id, type, attributes, relationships}}(リストの場合は data: [...])。リンクをたどるには、data.relationships.<name>.data を読み取ります。これは {id, type} または null を返します。null の場合はそこで終了です。関連リソースはありません。
ページネーション:デフォルトは 1 ページ目です。すべてのページを取得するには --all(上限 2000)を追加するか、明示的に制御するには --page-size N/--page-number N を指定します。件数を取得するには、--page-size 1 --jq '.meta.pagination.["total-count"]' を使用してください。
変更と一括更新
# 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":{…}}}'
送信せずにプレビューするには、--dry-run を追加してください。
終了コード(クイックガイド)
| コード | 意味 | 対応 |
|---|---|---|
| 0 | 成功 | 完了 |
| 1 | 情報メッセージまたは使用方法のエラー | stderr を確認してください。「no current run」などと表示されている場合は、それが回答です。そこで終了してください |
| 2 | 見つからない(ワークスペース/実行が存在しない)、または認証が無効 | ID/名前が正しいことを確認し、それでも失敗する場合はトークンを確認してください |
| 3 | 認証トークンの期限切れまたは無効 | 再認証してください |
| 4 | ネットワークエラー | 少し待ってから再試行してください |
| 5 | レート制限(429)またはサーバーエラー(5xx) | バックオフしながら再試行してください |
| 6 | リソースがエラー状態 | エラーの診断結果は出力にすでに示されています(例:プランの失敗)。内容を確認してください |
重要:API が data: [](空のリスト)または data: null を返した場合、それが回答です。異なるフラグやエンドポイントで再試行しないでください。
よくあるトラブルシューティング
| 症状 | 原因 | 解決方法 |
|---|---|---|
ワークスペースの一覧表示で exit 2 | 組織が存在しない、または認証トークンにアクセス権がない | 組織名を確認して再認証してください |
「no current run」とともに exit 1 | ワークスペースにアクティブな実行がない(情報メッセージ) | これが回答です — 確認せずに終了してください |
API 呼び出し時に exit 3 | 認証トークンの期限切れ | tfctl login で再認証してください |
exit 5(429 レート制限) | リクエストが多すぎます | 待ってから再試行してください。tfctl は自動的にバックオフします |
「plan is errored」とともに exit 6 | Terraform プランに構文エラーがある(CLI エラーではありません) | 詳細はプランの出力を確認してください |
フィルタリング時に空のリスト(data: []) | 条件に一致するリソースがありません | 条件が正しいことを確認してください。空のリストも有効な回答です |
スマートデフォルト
{organization}は、設定されている場合、アクティブなプロファイルから解決されます。{workspace}は、CWD にあるローカルのcloud {}ブロックから解決されます。- すでに形成済みの ID(
ws-…、team-…、prj-…、varset-…)はそのまま渡されます。