HCP Terraformの利用していないWorkspaceを一括削除するスクリプトを作成してみた

HCP Terraformの利用していないWorkspaceを一括削除するスクリプトを作成してみた

2025.08.25

私は一時的な検証環境をHCP Terraformで作成することが多いです。

HCP Terraformにはephemeral workspacesという機能があり、一定期間利用していないリソースを削除してくれます。

https://dev.classmethod.jp/articles/tfc-ephemeral-workspaces/

リソースは削除してくれるのですが、Workspaceは残ります。

HCP Terraformの場合は、Workspaceの数自体は課金に関わらないため問題ないのです。

しかし、使わなくなったWorkspaceが沢山あると気になります。

今回はProject内の使わなくなったWorkspace(リソース数0)を一括削除するスクリプトを作成してみました。

作成したスクリプト

作成したスクリプトは以下です。

HCP Terraform APIを利用しています。

指定したProject内のWorkspaceのうちリソース数0のWorkspaceを取得し表示します。

--forceオプションを付けて実行すると、上記に加えてリソース数0のWorkspaceの削除まで行います。

delete_empty_workspaces.sh
#!/bin/bash

# HCP Terraformの特定プロジェクト内でリソースが0のワークスペースを一括削除するスクリプト

set -euo pipefail

# 設定(環境変数または デフォルト値)
ORGANIZATION="${HCPTF_ORGANIZATION:-default-org}"
PROJECT="${HCPTF_PROJECT:-default-project}"
API_BASE_URL="https://app.terraform.io/api/v2"

# 色設定
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
NC='\033[0m' # No Color

# APIトークンチェック
if [[ -z "${TFE_TOKEN:-}" ]]; then
  echo -e "${RED}エラー: TFE_TOKEN環境変数が設定されていません${NC}"
  echo "HCP TerraformのAPIトークンを設定してください:"
  echo "export TFE_TOKEN=your-api-token"
  exit 1
fi

# 引数チェック
DRY_RUN=true
if [[ "${1:-}" == "--force" ]]; then
  DRY_RUN=false
  echo -e "${YELLOW}警告: --forceオプションが指定されました。実際に削除を実行します。${NC}"
elif [[ "${1:-}" == "--help" ]] || [[ "${1:-}" == "-h" ]]; then
  echo "使用方法: $0 [--force]"
  echo ""
  echo "オプション:"
  echo "  --force    実際に削除を実行 (デフォルトはdry-run)"
  echo "  --help     このヘルプを表示"
  echo ""
  echo "環境変数:"
  echo "  TFE_TOKEN            HCP TerraformのAPIトークン (必須)"
  echo "  HCPTF_ORGANIZATION   Organization名 (デフォルト: default-org)"
  echo "  HCPTF_PROJECT        Project名 (デフォルト: default-project)"
  exit 0
fi

if [[ "$DRY_RUN" == true ]]; then
  echo -e "${BLUE}=== DRY RUNモード ===${NC}"
  echo "削除対象のワークスペースを確認します(実際には削除されません)"
  echo "実際に削除するには --force オプションを使用してください"
  echo ""
fi

# API呼び出し関数
api_call() {
  local endpoint="$1"
  local method="${2:-GET}"
  local data="${3:-}"

  local curl_args=(
    -s
    -H "Authorization: Bearer ${TFE_TOKEN}"
    -H "Content-Type: application/vnd.api+json"
    -X "$method"
  )

  if [[ -n "$data" ]]; then
    curl_args+=(-d "$data")
  fi

  curl "${curl_args[@]}" "${API_BASE_URL}${endpoint}"
}

# プロジェクトID取得(qパラメータで検索)
echo -e "${BLUE}プロジェクト情報を取得中...${NC}"
projects_response=$(api_call "/organizations/${ORGANIZATION}/projects?q=${PROJECT}")
project_id=$(echo "$projects_response" | jq -r --arg name "$PROJECT" '.data[] | select(.attributes.name == $name) | .id')

if [[ -z "$project_id" ]] || [[ "$project_id" == "null" ]]; then
  echo -e "${RED}エラー: プロジェクト '${PROJECT}' が見つかりません${NC}"
  echo "検索結果:"
  echo "$projects_response" | jq -r '.data[] | "  - \(.attributes.name) (ID: \(.id))"'
  echo ""
  echo "デバッグ情報:"
  echo "検索で取得したプロジェクト数: $(echo "$projects_response" | jq '.data | length')"
  echo "検索対象プロジェクト名: '${PROJECT}'"
  exit 1
fi

echo -e "${GREEN}プロジェクトID: ${project_id}${NC}"

# ワークスペース一覧取得(プロジェクトでフィルタ、ページネーション対応)
echo -e "${BLUE}ワークスペース一覧を取得中...${NC}"
get_all_workspaces() {
  local page=1
  local all_data="[]"

  while true; do
    # URLエンコーディング: [=%5B, ]=%5D
    # filter[project][id] → filter%5Bproject%5D%5Bid%5D
    # page[number] → page%5Bnumber%5D, page[size] → page%5Bsize%5D
    local response=$(api_call "/organizations/${ORGANIZATION}/workspaces?filter%5Bproject%5D%5Bid%5D=${project_id}&page%5Bnumber%5D=${page}&page%5Bsize%5D=100")

    # APIエラーチェック
    if ! echo "$response" | jq . > /dev/null 2>&1; then
      echo -e "${RED}API呼び出しエラー (page $page): $response${NC}" >&2
      break
    fi

    local current_data=$(echo "$response" | jq '.data')
    local data_count=$(echo "$current_data" | jq 'length')

    if [[ "$data_count" -eq 0 ]]; then
      break
    fi

    # データをマージ
    all_data=$(echo "$all_data" | jq ". + $current_data")

    # 次のページがあるかチェック
    local has_next=$(echo "$response" | jq -r '.meta.pagination."next-page" // empty')
    if [[ -z "$has_next" ]] || [[ "$has_next" == "null" ]]; then
      break
    fi

    ((page++))
  done

  # 最終的なレスポンス形式を作成
  echo "{\"data\": $all_data}"
}

workspaces_response=$(get_all_workspaces)

# リソース数0のワークスペースをフィルタ
empty_workspaces=$(echo "$workspaces_response" | jq -r '
  .data[]
  | select(.attributes["resource-count"] == 0)
  | "\(.id)|\(.attributes.name)|\(.attributes["resource-count"])"
')

if [[ -z "$empty_workspaces" ]]; then
  echo -e "${GREEN}リソース数0のワークスペースは見つかりませんでした${NC}"
  exit 0
fi

echo -e "${YELLOW}削除対象のワークスペース:${NC}"
echo "$empty_workspaces" | while IFS='|' read -r ws_id ws_name resource_count; do
  echo "  - ${ws_name} (ID: ${ws_id}, リソース数: ${resource_count})"
done

workspace_count=$(echo "$empty_workspaces" | wc -l)
echo -e "${YELLOW}合計 ${workspace_count} 個のワークスペースが削除対象です${NC}"

if [[ "$DRY_RUN" == true ]]; then
  echo ""
  echo -e "${BLUE}=== DRY RUN完了 ===${NC}"
  echo "実際に削除するには --force オプションを付けて実行してください:"
  echo "$0 --force"
  exit 0
fi

# 実際の削除確認
echo ""
echo -e "${RED}警告: 削除されたワークスペースは復元できません${NC}"
read -p "本当に削除しますか? [y/N]: " -n 1 -r
echo
if [[ ! $REPLY =~ ^[Yy]$ ]]; then
  echo "キャンセルされました"
  exit 0
fi

# ワークスペース削除実行
echo -e "${BLUE}ワークスペース削除を開始...${NC}"
deleted_count=0
failed_count=0

# プロセス置換を使用してサブシェル問題を回避
while IFS='|' read -r ws_id ws_name resource_count; do
  echo -n "削除中: ${ws_name} ... "

  # Safe deleteを試行
  delete_response=$(api_call "/workspaces/${ws_id}/actions/safe-delete" "POST" "" 2>&1)
  delete_status=$?

  if [[ $delete_status -eq 0 ]]; then
    echo -e "${GREEN}成功${NC}"
    ((deleted_count++))
  else
    echo -e "${RED}失敗${NC}"
    echo "  エラー: $delete_response"
    ((failed_count++))
  fi
done <<< "$empty_workspaces"

echo ""
echo -e "${BLUE}=== 削除完了 ===${NC}"
echo -e "${GREEN}成功: ${deleted_count}${NC}"
if [[ $failed_count -gt 0 ]]; then
  echo -e "${RED}失敗: ${failed_count}${NC}"
fi

使い方

前提条件

以下のツールのインストールが必要です。

  • curl
  • jq

準備

スクリプトに実行権限を付与します。

chmod +x delete_empty_workspaces.sh

それぞれ環境変数をセットします。

export HCPTF_ORGANIZATION="your-organization"
export HCPTF_PROJECT="your-project"
export TFE_TOKEN="your-api-token"

TFE_TOKENはWorkspaceが削除が可能なトークンをセットしてください。

Macだったら、デフォルトで~/.terraform.d/credentials.tfrc.json にトークンが格納されています。

cat ~/.terraform.d/credentials.tfrc.json

HCPTF_ORGANIZATIONHCPTF_PROJECTは環境変数で渡す以外にも、スクリプトでデフォルト値を設定できるようにしています。

デフォルト値を設定したい場合は、default-*の部分を変更してください。

# 設定(環境変数または デフォルト値)
ORGANIZATION="${HCPTF_ORGANIZATION:-default-org}"
PROJECT="${HCPTF_PROJECT:-default-project}"

ドライランと実行

オプションなしで実際の削除を行わないドライランになります。

./delete_empty_workspaces.sh

--forceオプションをつけると実際の削除が行われます。

./delete_empty_workspaces.sh --force

動作確認

実際に空のWorkspaceをスクリプトを使って削除してみます。

検証用Workspaceは以下で作成しました。

main.tf
terraform {
  required_providers {
    tfe = {
      source  = "hashicorp/tfe"
      version = "0.68.2"
    }
  }
}

provider "tfe" {}

locals {
  organization = "hoge" # 環境に合わせて修正
}

# Project for workspace deletion testing
resource "tfe_project" "delete_test" {
  organization = local.organization
  name         = "sato-masaki-ws-delete-test"
  description  = "Project for testing workspace bulk deletion"
}

# Workspaces for deletion testing
resource "tfe_workspace" "test_1" {
  name         = "delete-test-1"
  organization = local.organization
  project_id   = tfe_project.delete_test.id
  description  = "Test workspace 1 for bulk deletion"
}

resource "tfe_workspace" "test_2" {
  name         = "delete-test-2"
  organization = local.organization
  project_id   = tfe_project.delete_test.id
  description  = "Test workspace 2 for bulk deletion"
}

resource "tfe_workspace" "test_3" {
  name         = "delete-test-3"
  organization = local.organization
  project_id   = tfe_project.delete_test.id
  description  = "Test workspace 3 for bulk deletion"
}

テスト用のリソースを作成します。

terraform init
terraform apply

sato-masaki-ws-delete-test___classmethod-sandbox___HCP_Terraform.png

リソースを管理しているWorkspaceが削除対象にならないことを確認するために、delete-test-3でリソースを作成しました。

Cursor_と_Overview___classmethod-sandbox___HCP_Terraform.png

まずはドライランです。

./delete_empty_workspaces.sh
出力例
=== DRY RUNモード ===
削除対象のワークスペースを確認します(実際には削除されません)
実際に削除するには --force オプションを使用してください

プロジェクト情報を取得中...
プロジェクトID: prj-XXXXX
ワークスペース一覧を取得中...
削除対象のワークスペース:
  - delete-test-1 (ID: ws-XXXXX, リソース数: 0)
  - delete-test-2 (ID: ws-XXXXX, リソース数: 0)
合計        2 個のワークスペースが削除対象です

=== DRY RUN完了 ===
実際に削除するには --force オプションを付けて実行してください:
./delete_empty_workspaces.sh --force

リソース数0のWorkspaceだけが削除対象になっていることを確認できました。

削除を実行します。

./delete_empty_workspaces.sh --force
出力例
./delete_empty_workspaces.sh --force

警告: --forceオプションが指定されました。実際に削除を実行します。
プロジェクト情報を取得中...
プロジェクトID: prj-XXXXX
ワークスペース一覧を取得中...
削除対象のワークスペース:
  - delete-test-1 (ID: ws-XXXXXX, リソース数: 0)
  - delete-test-2 (ID: ws-XXXXXX, リソース数: 0)
合計        2 個のワークスペースが削除対象です

警告: 削除されたワークスペースは復元できません
本当に削除しますか? [y/N]: y
ワークスペース削除を開始...
削除中: delete-test-1 ...成功
削除中: delete-test-2 ... 成功

=== 削除完了 ===
成功: 2

削除が完了しました。

HCP Terraformのコンソール上からも、リソース数0のWorkspaceだけが削除されたことを確認できました。

Cursor_と_sato-masaki-ws-delete-test___classmethod-sandbox___HCP_Terraform.png

おわりに

使わなくなったWorkspaceの一括削除スクリプトについてでした。

検証用のWorkspaceをよく作るので、スクリプトで整理が楽になりました。

今回はとりあえずシェルスクリプトで実装してみました。

HashiCorpがメンテしているHCP TerraformのSDK go-tfeを使った実装も試してみたいと思います。

この記事をシェアする

facebookのロゴhatenaのロゴtwitterのロゴ

© Classmethod, Inc. All rights reserved.