Google アカウント (ID) / グループの棚卸しを Cloud Functions で実装してみた

記事タイトルとURLをコピーする

当記事は みずほリサーチ&テクノロジーズ × G-gen エンジニアコラボレーション企画 で執筆されたものです。


G-gen の又吉です。当記事では、Cloud Functions を用いて Google Workspace もしくは Cloud Identity (以下、Google Workspace 等) で管理している Google アカウント / グループの棚卸し を日次で行う方法を紹介します。

なお、Google Cloud 側の設定は Terraform を使用します。

概要

背景

Google Cloud を利用する際、企業の監査要件等で Google アカウント (ID) / グループの管理・棚卸しを定期的に行わければいけない時はないでしょうか?

同じパブリッククラウドの AWS では、 ID は IAM User 、グループは IAM Group と呼ばれ AWS アカウント (テナント) の中で管理されているため、AWS のインベントリサービスである AWS Config を用いることで、ID / グループ のスナップショットも AWS Config ひとつで取得できます。

しかし、Google Cloud の場合、Google アカウント / グループは Google Cloud からは分離 されており、Google Workspace 等で管理されております。

したがって、Google Cloud のインベントリサービスである Cloud Asset Inventory を用いても、Google Cloud の組織配下のリソースやポリシーのメタデータは取得できるものの、組織の Google アカウント / グループは取得できません

Google Cloud の ID や IAM についての概念を、AWS との違いと合わせて詳しく知りたい方は以下の記事をご参照下さい。

blog.g-gen.co.jp

今回やること

今回は、Cloud Functions を用いて Directory API を実行し、Google Workspace 等で管理されている Google アカウント / グループの棚卸しを日次で行う方法をご紹介します。

構成図

準備

フォルダ階層

フォルダ階層は以下の通りです。

cloud_identity_inventory
├── terraform
│    └── main.tf
└── python_source_code
     ├── main.py
     └── requirements.txt

cloud_identity_inventory というフォルダ配下に、 Terraform を実行する terraform フォルダと、Cloud Functions で使うソースファイルを格納している python_source_code フォルダを作ります。

Cloud Functions を Terraform からデプロイする際、関数のソースコードの場所を Cloud Storage もしくは Cloud Source Repository から指定でき、今回は Cloud Storage から取得するように構成しています。

Terraform を実行すると、以下の流れで処理が進みます。

  1. python_source_code 配下のファイルを圧縮した ZIP ファイルを main.tf と同階層に作成
  2. ソースコード格納用 Cloud Storage バケットを作成し 1. で作成した ZIP ファイルを格納
  3. 2 でバケットに格納されたソースコードを指定して Cloud Functions をデプロイ

Python ソースコード

Python ソースコードは以下の通りです。

main.py

import os
from datetime import datetime, timedelta
  
import pandas as pd
import functions_framework
from googleapiclient.discovery import build
from google.cloud import storage
  
  
CUSTOMER_ID = os.environ["CUSTOMER_ID"]
UPLOAD_BUKET_NAME = os.environ["UPLOAD_BUKET_NAME"]
  
# storage クライアントのインスタンス化
storage_client = storage.Client()
  
  
def gcs_upload(kind, file_name, df):
    # 本日の日付を取得し、["YYYY", "MM", "DD"] のリストで格納
    now = (datetime.utcnow() + timedelta(hours=9)).strftime("%Y-%m-%d") 
    now_li = now.split("-")

    # file_name を作成
    kind = kind
    file_name = f"{kind}/{now_li[0]}/{now_li[1]}/{now_li[2]}/{file_name}.csv"
  
    # csv に変換し GCS にアップロード
    content_type = "text/csv"
    bucket = storage_client.get_bucket(UPLOAD_BUKET_NAME)
    blob = bucket.blob(file_name)
    blob.upload_from_string(
      df.to_csv(index=False, header=True, sep=","), 
      content_type=content_type
    )
  
  
@functions_framework.cloud_event
def main(cloud_event):
    # service オブジェクトの作成
    service = build("admin", "directory_v1")
  
    # ユーザー一覧を取得
    users_list_result = service.users().list(customer=CUSTOMER_ID).execute()
    users = users_list_result.get("users", [])
    # ユーザー数が 0 のときは処理を終了
    if len(users) == 0 :
        return "User does not exist."
    users_df = pd.DataFrame.from_records(users)
    gcs_upload(kind="users_list", file_name="users_list", df=users_df)
  
    # グループ一覧を取得
    groups_list_result = service.groups().list(customer=CUSTOMER_ID).execute()
    groups = groups_list_result.get("groups", [])
    # グループ数が 0 のときは処理を終了
    if len(groups) == 0 :
        return "Group does not exist."
    groups_df = pd.DataFrame.from_records(groups)
    gcs_upload(kind="groups_list", file_name="groups_list", df=groups_df)
  
    # グループ配下のメンバーを取得
    for i in range(len(groups_df)):
        groupKey = groups_df.at[i, "id"]
        groupName = groups_df.at[i, "name"]
        members_list_result = service.members().list(groupKey=groupKey).execute()
        members = members_list_result.get("members", [])
        members_df = pd.DataFrame.from_records(members)
        gcs_upload(kind="members_list", file_name=groupName, df=members_df)
  
    return "ok"

requirements.txt

DateTime==5.0
pandas==1.5.2
functions-framework==3.3.0
google-api-python-client==2.72.0
google-auth==2.16.0
google-cloud-storage==2.7.0

説明

今回、Python クライアントライブラリを用いて以下の 3 つの Directory API を実行しています。

No Method 名 説明
1 users.list 削除されたユーザーまたはドメイン内のすべてのユーザーのリストを取得します。
2 groups.list ドメインまたはユーザーのすべてのグループのリストを取得します。
3 members.list グループ内のすべてのメンバーのリストを取得します。

groups.list のレスポンスからはグループの一覧は取得できますが、グループに所属しているユーザー情報が取得できないため、groups.list で取得したグループ数だけ members.list の処理を回すようにしています。 そうすることで、グループに所属しているメンバー一覧も取得できます。

データの形成については、後々分析しやすいようデータは csv 形式に変換して Cloud Storage へアップロードしています。

また、生データをそのままの形式で蓄積しつつ、分析しやすいようにデータ形成も行いたい場合は以下のアーキテクチャも検討できます。

今回はサンプルのため実装しておりませんが、 Directory API を実行する時の注意点として、1 回のリクエストで大量のレスポンスデータを取得することが保証されていない点です。 本番運用では、 nextPageToken/pageToken を考慮し、大量のレスポンスデータは複数回に分けて取得する必要があります。

Terraform コード

main.tf

# ローカル変数の定義
locals {
  project_id  = “<プロジェクト ID>”
  customer_id = “<顧客 ID>”
}
  
# terraform / providers の設定
terraform {
  required_providers {
    google = {
      source  = "hashicorp/google"
      version = ">= 4.0.0"
    }
  }
  required_version = ">= 1.3.0"
  backend "gcs" {
    bucket  = "<state 管理バケット名>"
    prefix  = "<任意の prefix>"
  }
}
  
  
## プロジェクトの作成
resource "google_project" "poc" {
  name            = local.project_id
  project_id      = local.project_id
  folder_id       = "<folder_id>"
  billing_account = "<billing_account>"
}
  
  
## API 有効化
module "audit_project_services" {
  source  = "terraform-google-modules/project-factory/google//modules/project_services"
  version = " 14.1.0 "
  project_id  = google_project.poc.project_id
  enable_apis = true
  activate_apis = [
    "run.googleapis.com",
    "cloudfunctions.googleapis.com",
    "eventarc.googleapis.com",
    "cloudscheduler.googleapis.com",
    "admin.googleapis.com",
    "artifactregistry.googleapis.com",
    "pubsub.googleapis.com",
    "cloudbuild.googleapis.com",
    "storage.googleapis.com",
  ]
  disable_services_on_destroy = false
}
  
  
## ソースコード格納用バケットの作成
# バケットの作成
resource "google_storage_bucket" "source" {
  project       = local.project_id
  location      = "ASIA-NORTHEAST1"
  name          = "ggen-matayuuu-admin-sdk-api-gcf-source-002"
  force_destroy = true
}
  
# Cloud Functions で使うソースコードを ZIP 化
data "archive_file" "source_code" {
  type        = "zip"
  source_dir  = "../python_source_code"
  output_path = "./source_code.zip"
}
  
# ZIP 化したソースコードをバケットに追加
resource "google_storage_bucket_object" "source_code" {
  name   = "code/function-run-1.${data.archive_file.source_code.output_md5}.zip"
  bucket = google_storage_bucket.source.name
  source = data.archive_file.source_code.output_path
}
  
  
## API で取得したデータ格納用バケットの作成
# バケットの作成
resource "google_storage_bucket" "cloud_identity_inventory" {
  project       = local.project_id
  location      = "ASIA-NORTHEAST1"
  name          = "ggen-cloud-identity-inventory"
  force_destroy = true
}
  
  
## Cloud Functions と Eventarc に付与するサービスアカウントを作成
# サービスアカウント作成
resource "google_service_account" "sa_gcf" {
  project      = local.project_id
  account_id   = "sa-gcf"
  display_name = "Cloud Functions & Eventarc 用サービスアカウント"
}
  
# cloud function 実行権限を付与
resource "google_project_iam_member" "invoke_gcf" {
  project = local.project_id
  role    = "roles/run.invoker"
  member  = "serviceAccount:${google_service_account.sa_gcf.email}"
}
  
# データ格納用バケットのストレージ管理者権限を付与
resource "google_storage_bucket_iam_member" "member" {
  bucket = google_storage_bucket.cloud_identity_inventory.name
  role   = "roles/storage.admin"
  member = "serviceAccount:${google_service_account.sa_gcf.email}"
}
  
  
## Cloud Functions の作成
# pub/subトピックの作成
resource "google_pubsub_topic" "topic" {
  project = local.project_id
  name = "cloud-scheduler"
}
  
# cloud schedulerの作成
resource "google_cloud_scheduler_job" "job" {
  project = local.project_id
  region = "asia-northeast1"
  name     = "invoke-gcf"
  schedule = "0 10 * * *"
  pubsub_target {
    topic_name = google_pubsub_topic.topic.id
    data       = base64encode("run")
  }
}
  
  
# cloud functionsの作成
resource "google_cloudfunctions2_function" "execute_admin_sdk_api" {
  project = local.project_id
  location = "asia-northeast1"
  name     = "execute-admin-sdk-api"
  build_config {
    runtime     = "python310"
    entry_point = "main"
    source {
      storage_source {
        bucket = google_storage_bucket.source.name
        object = google_storage_bucket_object.source_code.name
      }
    }
  }
  service_config {
    max_instance_count             = 3
    min_instance_count             = 1
    available_memory               = "256M"
    timeout_seconds                = 60
    ingress_settings               = "ALLOW_ALL"
    all_traffic_on_latest_revision = true
    service_account_email          = google_service_account.sa_gcf.email
    environment_variables = {
      CUSTOMER_ID       = local.customer_id
      UPLOAD_BUKET_NAME = google_storage_bucket.cloud_identity_inventory.name
    }
  }
  event_trigger {
    trigger_region        = "asia-northeast1"
    event_type            = "google.cloud.pubsub.topic.v1.messagePublished"
    retry_policy          = "RETRY_POLICY_DO_NOT_RETRY"
    pubsub_topic          = google_pubsub_topic.topic.id
    service_account_email = google_service_account.sa_gcf.email
  }
}
  

当記事では Terraform の概要等について触れないため、コマンドや tfstate ファイル等については以下の記事をご参照ください。

blog.g-gen.co.jp blog.g-gen.co.jp

実行

Terraform 実行

main.tf ファイルのある階層に移動した後、terraform init で初期化し、terraform plan でドライランを行い問題がなければ、terraform apply で実環境に適用します。

Apply complete! Resources: 20 added, 0 changed, 0 destroyed.と表示されれば成功です。

matayuuu@penguin:~/cloud_identity_inventory/terraform$ terraform apply
data.archive_file.source_code: Reading...

〜省略〜


Plan: 20 to add, 0 to change, 0 to destroy.

Do you want to perform these actions?
  Terraform will perform the actions described above.
  Only 'yes' will be accepted to approve.

  Enter a value: yes

〜省略〜

Apply complete! Resources: 20 added, 0 changed, 0 destroyed.

Google Workspace 管理コンソールでの設定

今回使用する Directory API は、 Google Workspace 等のユーザー読み取り権限グループ読み取り権限が必要です。 したがって、必要最小限の権限を持ったカスタムロールを作成し、Cloud Functions のサービスアカウントに付与します。

カスタムロールの作成

  1. Google Workspace ( Cloud Identity ) 管理コンソール > [アカウント] > [管理者ロール] > [新しいロールを作成] をクリック
  2. 任意の[名前]と[説明]を入力し、[続行]をクリック
  3. [ユーザー / 読み取り]と[グループ / 読み取り]を選択し、[続行]をクリック
  4. [ロールを作成] をクリック

ロールの付与

  1. Google Workspace ( Cloud Identity ) 管理コンソール > [アカウント] > [管理者ロール] > [先程作成したカスタムロール] をクリック
  2. [ロールを割り当て] をクリック
  3. [サービスアカウントへの割り当て]をクリック
  4. [<Cloud Functions のサービスアカウント>]を入力し、[追加]を選択
  5. [ロールを割り当て]をクリック

ここまでで設定は完了です。

確認

日次で Cloud Scheduler が実行されるのですが、強制的に即時実行させてみます。

  1. Google Cloud コンソール > [Cloud Scheduler] > [操作] の「︙」をクリック
  2. [ジョブを強制実行する] をクリック

すると Cloud Scheduler から Pub / Sub を経由し Cloud Functions が実行されます。

Cloud Storage バケットを確認すると、3 つのフォルダができてました。

また、それぞれのフォルダ配下には、年 / 月 / 日 / csv ファイル が存在しました。

このように、組織で管理する Google アカウント / グループ の棚卸しを自動化することができます。

又吉 佑樹(記事一覧)

クラウドソリューション部

はいさい、沖縄出身のクラウドエンジニア!

セールスからエンジニアへ転身。Google Cloud 全 11 資格保有。Google Cloud Champion Innovator (AI/ML)。Google Cloud Partner Top Engineer 2024。Google Cloud 公式ユーザー会 Jagu'e'r でエバンジェリストとして活動中。好きな分野は AI/ML。