🤖

【Google Cloud】アカウント棚卸を劇的に効率化!IAM権限の一括削除スクリプト紹介

に公開

クラウドエース DevSecOps 事業部の羽田です。
Google Cloud の IAM 権限削除を効率化するスクリプトを紹介します。

はじめに

本記事は、Google Cloud の複数リソース(組織/フォルダ/プロジェクト)にまたがる特定アカウント(ユーザー/グループ/サービスアカウント)の全ての IAM ロールを一括で削除するためのスクリプトを紹介します。

Google Cloud の環境において、IAM 権限の定期的な棚卸は非常に重要です。
退職者アカウントや使われなくなったサービスアカウントの権限を放置してしまうと、内部統制や外部監査といったコンプライアンス要件に違反してしまう可能性があります。

しかし、アカウントの削除を手作業で行うと工数もかかりますし、ヒューマンエラーの可能性も出てしまいます。
今回紹介するスクリプトによって、不要アカウントを削除するというアカウントの棚卸の一部作業を自動化することで、ヒューマンエラーを防ぐ効果が期待できます。

IAM 権限一括削除スクリプト概要

このスクリプトは、退職者のアカウントや、プロジェクトの終了などに伴い 完全に不要となったサービスアカウント から権限を削除する用途に適しています。
対象となるアカウントに付与されているすべての IAM 権限を一括で剥奪する設計となっており、個別の権限を指定して削除することはできません。

アカウントの棚卸やセキュリティ強化の一環として、こうした不要なアカウントの権限を迅速かつ確実に削除する手段としてご活用いただければと思います。


主な活用シーンとしては、以下のような場合が想定されます:

  • 組織から退職したメンバーのアカウント整理
  • メンバーの異動・交代に伴う不要アカウントの整理
  • 利用終了となったリソースやシステムに紐づくサービスアカウントの整理

このスクリプトでできること、できないこと

できること

  • 指定したアカウント(ユーザー/グループ/サービスアカウント)のIAMロールを一括削除
  • 組織、フォルダ、プロジェクトの階層を横断して権限を削除

できないこと

  • 特定のアカウントに付与された「特定のロールのみ」を選択して削除すること

前提条件

このスクリプトを実行するには、以下のツールがインストールされ、設定されている必要があります。

  • gcloud コマンドラインツール:
    Google Cloud リソースを操作するために必要です。対象リソースへのアクセス権限を持っている必要があります。
  • jq
    JSON データを処理するためのコマンドラインツールです。

スクリプト処理解説

このスクリプトは、以下のステップで動作します。

  1. IAM ポリシーの取得
    gcloud コマンドを使用し、指定された各リソース(組織、フォルダ、プロジェクト)の現在の IAM ポリシー(誰がどのロールを持っているかの情報)を取得します。
  2. 対象メンバーの検索
    取得した IAM ポリシーの中から、削除対象として指定されたメンバー(ユーザー、グループ、サービスアカウント)を検索します。
  3. ロールの特定
    対象メンバーが見つかった場合、そのメンバーにどの IAM ロールが割り当てられているかを特定します。
  4. ロールの削除
    特定したメンバーとロールの組み合わせに基づき、gcloud ... remove-iam-policy-binding コマンドを実行して、IAM ポリシーから該当のバインディング(紐付け)を削除します。
  5. 繰り返し実行
    Google Cloud の IAM ポリシーは階層構造(組織 > フォルダ > プロジェクト)で継承されるため、上位の権限を削除しても下位に直接付与された権限が残る場合があります。このスクリプトでは、確実に権限を削除するために、組織、フォルダ、プロジェクトに対する一連の削除処理を3回繰り返します。


本スクリプトの処理の流れ

IAM 権限の一括削除スクリプトの実装

Shell スクリプトによる実装

remove_iam_roles.sh
#!/bin/bash

# 対象の組織ID
ORG_ID="YOUR_ORG_ID"

# 対象のフォルダID
FOLDER_IDS=(
  "YOUR_FOLDER_ID_1"
)

# 対象のプロジェクトID
PROJECTS=(
  "your-project-id-1"
)

# 対象のメンバー(ユーザー、グループ、サービスアカウント)
MEMBERS=(
  "user:your-user@example.com"
  "group:your-group@example.com"
  "serviceAccount:your-sa@your-project-id.iam.gserviceaccount.com"
)

# --- ここから下は通常変更不要 ---

DRY_RUN=false
if [[ "$1" == "--dry-run" ]]; then
  DRY_RUN=true
  echo "[情報] Dry Run モードが有効です。実際のロール削除は行われません。"
  shift # オプションを引数リストから削除
fi

show_current_roles() {
  local resource_type=$1
  local resource_id=$2
  echo "  リソース ($resource_type): $resource_id の権限を確認中..."

  local iam_policy
  iam_policy=$(gcloud $resource_type get-iam-policy "$resource_id" --format=json 2>/dev/null)
  if [ $? -ne 0 ]; then
    echo "    [警告] IAMポリシーの取得に失敗しました。スキップします。"
    return 1
  fi
  if [ -z "$iam_policy" ] || [ "$iam_policy" == "{}" ]; then
     echo "    [情報] IAMポリシーが見つからないか空です。"
     return 0
  fi

  local member_found_roles=false

  for member in "${MEMBERS[@]}"; do
    local roles
    roles=$(echo "$iam_policy" | jq -r --arg MEMBER "$member" '
      .bindings[]? | select(.members[]? == $MEMBER) | .role
    ')

    if [ -n "$roles" ]; then
      member_found_roles=true
      echo "    メンバー: $member"
      echo "$roles" | while IFS= read -r role; do
        if [ -n "$role" ]; then
          echo "      - $role"
        fi
      done
    fi
  done

  if [ "$member_found_roles" = false ]; then
    echo "    [情報] 指定されたメンバーには、このリソース上のロールが見つかりませんでした。"
  fi
  return 0
}


remove_roles() {
  local resource_type=$1
  local resource_id=$2
  echo "$resource_type を処理中: $resource_id"

  local iam_policy
  iam_policy=$(gcloud $resource_type get-iam-policy "$resource_id" --format=json 2>/dev/null)
  if [ $? -ne 0 ]; then
    echo "  [警告] IAMポリシーの取得に失敗しました ($resource_type $resource_id)。スキップします。"
    return 1 # エラーを示すために 0 以外を返す
  fi

  if [ -z "$iam_policy" ] || [ "$iam_policy" == "{}" ]; then
     echo "  [情報] IAMポリシーが見つからないか空です ($resource_type $resource_id)。スキップします。"
     return 0 # 正常終了として 0 を返す
  fi

  for member in "${MEMBERS[@]}"; do
    local roles
    roles=$(echo "$iam_policy" | jq -r --arg MEMBER "$member" '
      .bindings[]? | select(.members[]? == $MEMBER) | .role
    ')

    if [ -z "$roles" ]; then
      # echo "  No roles found for $member on this resource." 
      continue # 次のメンバーへ
    fi

    echo "$roles" | while IFS= read -r role; do
       if [ -n "$role" ]; then
          if [ "$DRY_RUN" = true ]; then
            echo "  [Dry Run] 削除対象: $member - $role"
          else
            gcloud $resource_type remove-iam-policy-binding "$resource_id" \
              --member="$member" \
              --role="$role" \
              --condition=None \
              --quiet > /dev/null # 標準出力を /dev/null にリダイレクトして抑制
            if [ $? -eq 0 ]; then
              echo "  削除しました: $member - $role"
            else
              echo "  [警告] 削除に失敗しました: $member - $role (リソース: $resource_id)"
            fi
          fi
       fi
    done
  done
  return 0 # 正常終了
}

echo "=================================================="
echo "スクリプト実行前の設定内容と権限確認"
echo "=================================================="
echo ""
echo "--- 設定内容 ---"
if [ -n "$ORG_ID" ]; then
  echo "対象組織ID: $ORG_ID"
else
  echo "対象組織ID: (指定なし)"
fi
if [ ${#FOLDER_IDS[@]} -gt 0 ]; then
  echo "対象フォルダID:"
  printf "  - %s\n" "${FOLDER_IDS[@]}"
else
  echo "対象フォルダID: (指定なし)"
fi
if [ ${#PROJECTS[@]} -gt 0 ]; then
  echo "対象プロジェクトID:"
  printf "  - %s\n" "${PROJECTS[@]}"
else
  echo "対象プロジェクトID: (指定なし)"
fi
if [ ${#MEMBERS[@]} -gt 0 ]; then
  echo "対象メンバー:"
  printf "  - %s\n" "${MEMBERS[@]}"
  echo "対象メンバーの権限を削除します。" 
else
  echo "[警告] 対象メンバーが指定されていません!"
fi
echo ""
echo "--- 現在の権限状況(実行前) ---"

if [ ${#PROJECTS[@]} -gt 0 ]; then
  echo ">>> プロジェクトの権限確認中..."
  for project in "${PROJECTS[@]}"; do
    if [ -n "$project" ]; then
        show_current_roles "projects" "$project"
    fi
  done
else
  echo "  [情報] プロジェクトが指定されていないため、権限確認をスキップします。"
fi
# フォルダの権限確認
if [ ${#FOLDER_IDS[@]} -gt 0 ]; then
  echo ""
  echo ">>> フォルダの権限確認中..."
  for folder_id in "${FOLDER_IDS[@]}"; do
    if [ -n "$folder_id" ]; then
        show_current_roles "resource-manager folders" "$folder_id"
    fi
  done
else
  echo "  [情報] フォルダが指定されていないため、権限確認をスキップします。"
fi

if [ -n "$ORG_ID" ]; then
  echo ""
  echo ">>> 組織の権限確認中..."
  show_current_roles "organizations" "$ORG_ID"
else
  echo "  [情報] 組織が指定されていないため、権限確認をスキップします。"
fi
echo ""
echo "=================================================="
echo "事前確認完了。メイン処理を開始します。"
echo "=================================================="
sleep 3 # 確認のための待機時間

for i in {1..3}; do
  echo ""
  echo "=================================================="
  if [ "$DRY_RUN" = true ]; then
    echo "Dry Run を開始します (#$i/3)"
  else
    echo "削除実行を開始します (#$i/3)"
  fi
  echo "=================================================="

  if [ ${#PROJECTS[@]} -gt 0 ]; then
      echo ""
      echo ">>> プロジェクトを処理中 (実行 $i)..."
      for project in "${PROJECTS[@]}"; do
        # プロジェクトIDが空でないことを確認
        if [ -n "$project" ]; then
            remove_roles "projects" "$project"
        else
            echo "  [警告] 空のプロジェクトIDをスキップします。"
        fi
      done
  else
      echo "  [情報] プロジェクトが指定されていません。プロジェクトの処理をスキップします。"
  fi

  if [ ${#FOLDER_IDS[@]} -gt 0 ]; then
      echo ""
      echo ">>> フォルダを処理中 (実行 $i)..."
      for folder_id in "${FOLDER_IDS[@]}"; do
        # フォルダIDが空でないことを確認
        if [ -n "$folder_id" ]; then
            remove_roles "resource-manager folders" "$folder_id"
        else
            echo "  [警告] 空のフォルダIDをスキップします。"
        fi
      done
  else
      echo "  [情報] フォルダIDが指定されていません。フォルダの処理をスキップします。"
  fi

  if [ -n "$ORG_ID" ]; then
      echo ""
      echo ">>> 組織を処理中 (実行 $i)..."
      remove_roles "organizations" "$ORG_ID"
  else
      echo "  [情報] 組織IDが指定されていません。組織の処理をスキップします。"
  fi

  echo ""
  echo "=================================================="
  if [ "$DRY_RUN" = true ]; then
    echo "Dry Run が完了しました (#$i/3)"
  else
    echo "削除実行が完了しました (#$i/3)"
  fi
  echo "=================================================="
  # Dry Run モードでなければ、次の実行前に待機
  if [ $i -lt 3 ] && [ "$DRY_RUN" = false ]; then
      echo "次の実行まで5秒待機します..."
      sleep 5
  fi
done

echo ""
echo "=================================================="
echo "スクリプト実行後の権限確認"
echo "=================================================="
echo ""
echo "--- 現在の権限状況(実行後) ---"
# プロジェクトの権限確認 (実行後)
if [ ${#PROJECTS[@]} -gt 0 ]; then
  echo ">>> プロジェクトの権限再確認中..."
  for project in "${PROJECTS[@]}"; do
    if [ -n "$project" ]; then
        show_current_roles "projects" "$project"
    fi
  done
else
  echo "  [情報] プロジェクトが指定されていないため、権限確認をスキップします。"
fi

if [ ${#FOLDER_IDS[@]} -gt 0 ]; then
  echo ""
  echo ">>> フォルダの権限再確認中..."
  for folder_id in "${FOLDER_IDS[@]}"; do
    if [ -n "$folder_id" ]; then
        show_current_roles "resource-manager folders" "$folder_id"
    fi
  done
else
  echo "  [情報] フォルダが指定されていないため、権限確認をスキップします。"
fi

if [ -n "$ORG_ID" ]; then
  echo ""
  echo ">>> 組織の権限再確認中..."
  show_current_roles "organizations" "$ORG_ID"
else
  echo "  [情報] 組織が指定されていないため、権限確認をスキップします。"
fi
echo ""
echo "=================================================="
if [ "$DRY_RUN" = true ]; then
  echo "スクリプトの実行が Dry Run モードで完了しました。"
else
  echo "スクリプトの実行が3回の削除実行後に完了しました。"
fi
echo "=================================================="

作成した Shell スクリプトの実行方法

  1. スクリプトファイルを保存します。 (例: remove_iam_roles.sh)
  2. ターミナルで実行権限を付与します。 (例: chmod +x remove_iam_roles.sh)
  3. スクリプトを実行します。 (例: ./remove_iam_roles.sh)
  4. 実行中は、どのリソースのどのメンバーのどのロールを削除しようとしているかが出力されます。

実行結果

前節で作成したスクリプトを、指定した環境下で、作業者が所属する組織の特定プロジェクトに対して実行してみます。

  • プロジェクト ID:ca-haneda-package
  • 対象のメンバー:iam-test@ca-haneda-package.iam.gserviceaccount.com

組織 ID は、リソースの管理の Google Cloud Console のページから、所属している組織の ID の数値を入力してください。プロジェクト ID は同じページのプロジェクトの ID を入力してください。

なお、今回はセキュリティ上の理由により、組織 ID をこの場で共有することはできません。そのため、対象となるサービスアカウントに紐づく権限のみを削除する対応とします。
対象のサービスアカウントには、以下の権限が付与されています。

上記の環境にスクリプトを実行します。
スクリプトが完了すると自動で権限が削除されているかを確認します。

実際の出力内容も下記に記載していますので気になる方は確認をしてみてください。

実際の出力(セキュリティ上変更している場所があります。)
$ ./remove_iam_roles.sh                 
==================================================
スクリプト実行前の設定内容と権限確認
==================================================

--- 設定内容 ---
対象組織ID: (指定なし)
対象フォルダID:
 - 
対象プロジェクトID:
 - ca-haneda-package
対象メンバー:
 - serviceAccount:iam-test@ca-haneda-package.iam.gserviceaccount.com
対象メンバーの権限を削除します。

--- 現在の権限状況(実行前) ---
>>> プロジェクトの権限確認中...
 リソース (projects): ca-haneda-package の権限を確認中...
   メンバー: serviceAccount:iam-test@ca-haneda-package.iam.gserviceaccount.com
     - roles/viewer

>>> フォルダの権限確認中...
 [情報] 組織が指定されていないため、権限確認をスキップします。

==================================================
事前確認完了。メイン処理を開始します。
==================================================

==================================================
削除実行を開始します (#1/3)
==================================================

>>> プロジェクトを処理中 (実行 1)...
projects を処理中: ca-haneda-package
Updated IAM policy for project [ca-haneda-package].
 削除しました: serviceAccount:iam-test@ca-haneda-package.iam.gserviceaccount.com - roles/viewer

>>> フォルダを処理中 (実行 1)...
 [警告] 空のフォルダIDをスキップします。
 [情報] 組織IDが指定されていません。組織の処理をスキップします。

==================================================
削除実行が完了しました (#1/3)
==================================================
次の実行まで5秒待機します...

==================================================
削除実行を開始します (#2/3)
==================================================

>>> プロジェクトを処理中 (実行 2)...
projects を処理中: ca-haneda-package

>>> フォルダを処理中 (実行 2)...
 [警告] 空のフォルダIDをスキップします。
 [情報] 組織IDが指定されていません。組織の処理をスキップします。

==================================================
削除実行が完了しました (#2/3)
==================================================
次の実行まで5秒待機します...

==================================================
削除実行を開始します (#3/3)
==================================================

>>> プロジェクトを処理中 (実行 3)...
projects を処理中: ca-haneda-package

>>> フォルダを処理中 (実行 3)...
 [警告] 空のフォルダIDをスキップします。
 [情報] 組織IDが指定されていません。組織の処理をスキップします。

==================================================
削除実行が完了しました (#3/3)
==================================================

==================================================
スクリプト実行後の権限確認
==================================================

--- 現在の権限状況(実行後) ---
>>> プロジェクトの権限再確認中...
 リソース (projects): ca-haneda-package の権限を確認中...
   [情報] 指定されたメンバーには、このリソース上のロールが見つかりませんでした。

>>> フォルダの権限再確認中...
 [情報] 組織が指定されていないため、権限確認をスキップします。

==================================================
スクリプトの実行が3回の削除実行後に完了しました。
==================================================

おわりに

手作業によるアカウントの棚卸には、ヒューマンエラーや作業負荷の増大といった課題がつきものです。しかし、今回紹介したスクリプトを活用することで、作業の自動化やミスの防止に繋げることが可能になります。

ただし、一番重要なのは、定期的な権限の見直しと最小権限の原則に基づいた運用です。

本スクリプトが、皆さんの Google Cloud 環境における安全かつ効率的な IAM 管理の一助となれば幸いです。

Discussion