SaaS

(サンプル付き)GASでGoogleグループ関連申請を自動化する

どーもばるすです。

自動化ネタばっかり書いてるので自動化おじさんになりつつあります。
今日はGoogleWorkspaceでGoogle Apps Script(GAS)を使ってグループ管理を自動化するお話です。

本記事でわかること

  1. Google フォームからの回答をトリガーとして利用する仕組み
  2. グループの作成やメンバー追加・削除を行うコードの解説
  3. 各種エラー処理とメール通知の方法

グループ管理自動化で何が嬉しいの?

GoogleWorkspace環境を運用しているとGoogleグループ関連の作業依頼がよく来ますよね。
GoogleグループでDriveやらの権限付与するのがスタンダードなのもあり。

組織内で新しいプロジェクトチームが立ち上がると、
「担当者に Google グループ作成依頼
→ 担当者が手作業で作成
→ 再度担当者にメンバー追加依頼
→ また作業…」
と、やり取りが頻発するわけですね。

こんなグループ作ってください、あのグループにメンバー追加よろしくーみたいな依頼がぽんぽん来るので地味に手がかかります。
こうした手動管理は手戻りや人為的な登録ミスなどのリスクも高いため、業務効率と情報管理の両面で問題が生じやすいです。

そこで自動化だ!

期待効果

1. 依頼対応時間の削減

今回のように、GASとフォームを組み合わせてグループ管理をワンストップで自動化すれば、

  • 管理者 : わざわざ G Admin コンソールを開いて作成・編集する手間が減る
  • 申請者 : 「どうやって申請すればいいか分からない」などの手間や問い合わせが減る

という双方にメリットがあります。

2. ミスや漏れの低減

手動管理ではどうしても起こりがちな、誤字・入力ミス・作成忘れ・削除忘れなどの人的エラーを大幅に低減できます。

今回はスクリプト内で「入力されたメールアドレスが妥当かどうか」「すでにグループが存在するか」などをチェックし、結果をフォーム回答者に通知する仕組みも作ったので透明性と精度が高いものになっています。

3. 申請〜完了の可視化

  • フォーム回答時: 「〇〇のグループを作りたい」
  • スクリプト実行時: 自動的にグループ作成・メンバー追加
  • 完了後: エラーの有無や追加されたメンバー一覧が、回答者にメール通知 される

という流れができるため、ステータスが「不透明」でイライラするといった問題が発生しにくくなります。
ユーザー側も「いつの間にか作業が終わってる!」という形で、スムーズに作業を進められます。

4. 運用ルールに基づいた承認ステップの拡張

今回のサンプルスクリプトは簡易なものですが、たとえば

  • 承認者がフォーム回答内容を確認し、承認ボタンを押す
  • スプレッドシートに「承認ステータス」カラムを追加して、承認済み だけをトリガーにグループ作成する
  • フォーム回答に組織部門・部長連絡先などを追加し、二重チェックをする

など、拡張性の高い運用設計が可能になります。企業規模やセキュリティポリシーに応じてガバナンスを効かせやすい点も、フォーム+GAS 連携の利点です。

具体的な活用シーン

  • 新入社員のオンボーディング
    • 新しく入ってきた社員やバイトスタッフの「共通メーリングリスト」への追加が必要なとき、フォームに氏名・メールアドレスを入力して送信するだけで、自動的に複数のグループ(部署、プロジェクト、全社など)に一括追加できます。
  • イベントの一括メール配信リストを作成
    • 社内勉強会やカンファレンス参加者を Google フォームで募集し、申し込みがあったユーザーを自動的に「イベント用グループ」に追加。メールリストへの連絡や開催後のフォローアップもスムーズに行えます。
  • 外部委託・プロジェクトチームの管理
    • 社外のパートナー企業やフリーランスをチームに加える必要がある場合にも、同じ仕組みで管理可能です。グループへの外部メンバー追加設定や権限設定をアプリケーションレベルで強制しておけば、アクセス制御 もしやすくなります。

実装の流れ

ということで、自社の環境に実装する方法を解説していきます。

  1. サンプルのGoogleフォームを開いてコピー
    • Googleフォームはこちら
      • 共有の都合上、私の個人環境で公開しています
      • Web公開している&コピー許可するには編集権限を渡す他ない ので、フォーム送信はしないでくださいね
  2. フォームを変更
  3. スクリプトの定数部分を変更
  4. GASのトリガーを設定
  5. 実際にフォーム送信して、自動化がうまく動くかチェック

具体的な実装手順

1.サンプルのGoogle フォームと紐づいたスプレッドシート を開いてコピー

スクリプトファイルも一緒にコピーしましょう

2.フォームを変更

入力値をチェックするためのバリデーションを入れたりしているので、そこを自社ドメインに変えましょう。
対象箇所については以下にそれぞれ画像を貼っていきます。

グループ作成 セクションの「作成するグループのアドレス」の説明欄
グループ作成 セクションの「作成するグループのアドレス」の入力規則のドメインを置き換える

次に、フォームの回答用シートを作っておきましょう。
後述のGAS書き換え部分で利用します。

スプレッドシートにリンク をクリック
作成 を選択

スプレッドシートを作成したら、URLのID部分を控えておきます。

3.スクリプトの定数部分を修正

コピーしたフォームの「Apps Script」をクリックしてGASを開く

定数をそれぞれ変更

FORM_RESPONSE_SHEET_ID : 作成したスプレッドシートのIDを貼り付け
CC_EMAIL_ADDRESS:完了メールの送信先を変更する

4.GASのトリガーを設定

画面左の「トリガー」をクリックし、画面右下の「トリガーを追加」をクリック

onFormSubmit を選択し、フォーム送信時に起動するように設定して保存をクリック

アカウントを選択

承認して完了

以上で準備完了です。


(詳細)スクリプトの解説

1. 変数と定数

// 設定
const FORM_RESPONSE_SHEET_ID = '1fv6ch3jjiIE4JhQDyi95cKSh1DzREjtKrF1JXXeCPoQ'; // 回答を取得するフォーム紐付けスプレッドシートのID
const CC_EMAIL_ADDRESS = 'admin@pleiades-lab.work'; // 必要に応じて複数のアドレスをカンマ区切りで指定できます

/**
 * フォーム送信時に実行されるメイン関数
 */
function onFormSubmit(e) {
  // ...(中略)...
}
  • FORM_RESPONSE_SHEET_ID: フォームの回答が保存されるスプレッドシートの ID を指定します。
  • CC_EMAIL_ADDRESS: 通知メール送信時に CC(複数設定可) として追加するメールアドレス。管理者やシステム担当者などを指定すると便利です。

2. メイン関数 onFormSubmit(e)

Google フォーム送信時に自動的に呼び出される関数です。

該当箇所のコード
/**
 * フォーム送信時に実行されるメイン関数
 */
function onFormSubmit(e) {
  const sheet = SpreadsheetApp.openById(FORM_RESPONSE_SHEET_ID).getActiveSheet();
  const headers = sheet.getRange(1, 1, 1, sheet.getLastColumn()).getValues()[0];
  const lastRow = sheet.getLastRow();
  const newRow = sheet.getRange(lastRow, 1, 1, sheet.getLastColumn()).getValues()[0];

  // フォーム回答をオブジェクト形式に変換
  const responses = {};
  headers.forEach((header, index) => {
    responses[header] = newRow[index];
  });

  Logger.log('受信したデータ: ' + JSON.stringify(responses));

  const applicantEmail = responses['メールアドレス'] || '';
  const requestType = responses['申請タイプ'];
  let result = '';
  let targetGroupAddress = '';

  Logger.log('申請タイプ: ' + requestType);
  Logger.log('申請者メールアドレス: ' + applicantEmail);

  try {
    if (!requestType) {
      throw new Error('申請タイプが指定されていません。');
    }

    switch(requestType) {
      case 'グループ作成':
        const groupName = responses['グループ名'];
        const groupDescription = responses['グループの説明'];
        targetGroupAddress = responses['作成するグループのアドレス'];
        
        if (!groupName || !targetGroupAddress) {
          throw new Error('グループ名とグループアドレスは必須です。');
        }
        
        const membersToAdd = (responses['追加したいメンバー'] || '')
          .split(',')
          .map(e => e.trim())
          .filter(Boolean);

        Logger.log('グループ作成 - アドレス: ' + targetGroupAddress + ', メンバー: ' + membersToAdd.join(', '));
        result = createGroupWithMembers(targetGroupAddress, groupName, groupDescription, membersToAdd);
        break;

      case 'メンバー追加':
        targetGroupAddress = responses['対象グループアドレス:メンバー追加'];
        if (!targetGroupAddress) {
          throw new Error('対象グループアドレスが指定されていません。');
        }

        const membersToAdd2 = (responses['追加するメンバーのアドレス'] || '')
          .split(',')
          .map(e => e.trim())
          .filter(Boolean);

        if (membersToAdd2.length === 0) {
          throw new Error('追加するメンバーが指定されていません。');
        }

        Logger.log('メンバー追加 - グループ: ' + targetGroupAddress + ', メンバー: ' + membersToAdd2.join(', '));
        result = addMembers(targetGroupAddress, membersToAdd2);
        break;

      case 'メンバー削除':
        targetGroupAddress = responses['対象グループアドレス:メンバー削除'];
        if (!targetGroupAddress) {
          throw new Error('対象グループアドレスが指定されていません。');
        }

        const membersToRemove = (responses['削除するメンバーのアドレス'] || '')
          .split(',')
          .map(e => e.trim())
          .filter(Boolean);

        if (membersToRemove.length === 0) {
          throw new Error('削除するメンバーが指定されていません。');
        }

        Logger.log('メンバー削除 - グループ: ' + targetGroupAddress + ', メンバー: ' + membersToRemove.join(', '));
        result = removeMembers(targetGroupAddress, membersToRemove);
        break;

      case 'グループ削除':
        targetGroupAddress = responses['削除対象グループアドレス'];
        if (!targetGroupAddress) {
          throw new Error('削除対象のグループアドレスが指定されていません。');
        }

        Logger.log('グループ削除 - グループ: ' + targetGroupAddress);
        result = deleteGroup(targetGroupAddress);
        break;

      default:
        throw new Error('不明な申請タイプです: ' + requestType);
    }

    // 正常完了時の通知
    sendNotificationEmail(applicantEmail, requestType, result, true);

  } catch (error) {
    Logger.log('エラーの詳細: ' + error.stack);
    logError(error.message, applicantEmail, requestType, targetGroupAddress, responses['グループ名'] || '');
  }
}

▼処理概要

  1. フォーム回答が保存されているスプレッドシートを開き、回答データを取得
  2. 回答を 申請タイプ で分岐(グループ作成、メンバー追加、メンバー削除、グループ削除)
  3. 各処理が成功したか失敗したかをログに残し、メールで通知

▼ポイント

  • e はフォーム送信時に自動的に渡されるイベント引数
  • responses というオブジェクトにカラム名(設問名)と回答内容を詰めています
  • switch(requestType) で選択された「申請タイプ」ごとに処理を振り分けています 

3. グループ作成 createGroupWithMembers(...)

該当箇所のコード
/**
 * グループを作成し、メンバーを追加する
 */
function createGroupWithMembers(groupAddress, groupName, groupDescription, members) {
  try {
    // グループを作成
    const group = AdminDirectory.Groups.insert({
      email: groupAddress,
      name: groupName,
      description: groupDescription
    });

    // 作成直後に少し待つ(2秒)
    Utilities.sleep(2000);

    // 固定設定を適用(必要に応じて修正)
    const groupSettings = {
      allowExternalMembers: 'false',
      whoCanPostMessage: 'ANYONE_CAN_POST',
      allowExternalPosts: 'true',
      whoCanViewGroup: 'ALL_MEMBERS_CAN_VIEW'
    };

      for (var i = 0; i < 3; i++) {
        try {
          AdminGroupsSettings.Groups.update(groupSettings, group.email);
          break;  // 成功したらループを抜ける
        } catch (err) {
          if (err.message.includes('GROUP_METADATA_DOES_NOT_EXIST')) {
            // 数秒待って再試行
            Utilities.sleep(2000);
          } else {
            // 別のエラーであればそのままスロー
            throw err;
          }
        }
      }
    // メンバーを追加
    const memberAdditionResult = addMembers(groupAddress, members);

    return 'グループ ' + groupName + ' (' + groupAddress + ') を作成しました。\n説明: ' 
           + groupDescription + '\n' + memberAdditionResult;

  } catch (e) {
    if (e.message.includes('Entity already exists')) {
      throw new Error('グループ ' + groupAddress + ' は既に存在します。');
    } else if (e.message.includes('Invalid Input')) {
      throw new Error('グループアドレス ' + groupAddress + ' が無効です。');
    } else {
      throw new Error('グループ作成中に予期せぬエラーが発生しました: ' + e.message);
    }
  }
}

▼処理概要

  1. グループを作成
    • AdminDirectory.Groups.insert() を使い、新規グループを作成
  2. グループが作成されてすぐには設定が反映されないことがあるので、Utilities.sleep(2000); で 2 秒待機
  3. グループ設定を更新
    • AdminGroupsSettings.Groups.update() を用いて外部投稿可否や閲覧権限などを設定
    • グループ作成の直後に行うと GROUP_METADATA_DOES_NOT_EXIST エラーが出る場合があるため、リトライを組み込んでいます
  4. 最後に、受け取ったメンバーを追加するため、addMembers 関数をコール

▼エラー処理

  • 同名グループ(アドレス)の重複 (Entity already exists)
  • 不正なアドレス (Invalid Input)

などを検知し、適切にメッセージを返しています。


4. メンバー追加 addMembers(...)

該当箇所のコード
/**
 * グループにメンバーを追加する
 */
function addMembers(groupAddress, members) {
  const addedMembers = [];
  const failedMembers = [];

  Logger.log('メンバー追加処理開始 - グループアドレス: ' + groupAddress);
  Logger.log('追加するメンバー: ' + members.join(', '));

  // グループの存在確認
  try {
    AdminDirectory.Groups.get(groupAddress);
  } catch (e) {
    throw new Error('指定されたグループ ' + groupAddress + ' が見つかりません。エラー: ' + e.message);
  }

  // メンバー追加
  members.forEach(function(email) {
    if (validateEmail(email)) {
      try {
        AdminDirectory.Members.insert({email: email, role: 'MEMBER'}, groupAddress);
        addedMembers.push(email);
        Logger.log('メンバー追加成功: ' + email);
      } catch (e) {
        failedMembers.push(email + ' (' + e.message + ')');
        Logger.log('メンバー追加エラー: ' + email + ' - ' + e.message);
      }
    } else {
      failedMembers.push(email + ' (無効なメールアドレス形式)');
      Logger.log('無効なメールアドレス: ' + email);
    }
  });

  let resultMessage = 'グループ ' + groupAddress + ' へのメンバー追加結果:\n';
  if (addedMembers.length > 0) {
    resultMessage += '追加されたメンバー: ' + addedMembers.join(', ') + '\n';
  }
  if (failedMembers.length > 0) {
    resultMessage += '追加に失敗したメンバー: ' + failedMembers.join(', ') + '\n';
  }
  return resultMessage;
}

▼処理概要

  1. AdminDirectory.Groups.get(...)グループが存在するか チェック
  2. メールアドレス形式を validateEmail(...) 関数で確認
  3. 問題なければ AdminDirectory.Members.insert(...) を呼び出し、一人ずつグループに追加
  4. 成功したメンバーと失敗したメンバーをわかりやすくログ出力

もし指定したアドレスが無効であったり、すでにメンバー追加されている場合などは failedMembers に詳細を格納し、最終的な結果メッセージにまとめて返しています。
エラー内容がわかりやすいのもポイントです。


5. メンバー削除 removeMembers(...)

該当箇所のコード
/**
 * グループからメンバーを削除する
 */
function removeMembers(groupAddress, members) {
  const removedMembers = [];
  const failedMembers = [];

  Logger.log('メンバー削除処理開始 - グループアドレス: ' + groupAddress);
  Logger.log('削除するメンバー: ' + members.join(', '));

  // グループの存在確認
  try {
    AdminDirectory.Groups.get(groupAddress);
  } catch (e) {
    throw new Error('指定されたグループ ' + groupAddress + ' が見つかりません。エラー: ' + e.message);
  }

  // メンバー削除
  members.forEach(function(email) {
    if (validateEmail(email)) {
      try {
        AdminDirectory.Members.remove(groupAddress, email);
        removedMembers.push(email);
        Logger.log('メンバー削除成功: ' + email);
      } catch (e) {
        failedMembers.push(email + ' (' + e.message + ')');
        Logger.log('メンバー削除エラー: ' + email + ' - ' + e.message);
      }
    } else {
      failedMembers.push(email + ' (無効なメールアドレス形式)');
      Logger.log('無効なメールアドレス: ' + email);
    }
  });

  let resultMessage = 'グループ ' + groupAddress + ' からのメンバー削除結果:\n';
  if (removedMembers.length > 0) {
    resultMessage += '削除されたメンバー: ' + removedMembers.join(', ') + '\n';
  }
  if (failedMembers.length > 0) {
    resultMessage += '削除に失敗したメンバー: ' + failedMembers.join(', ') + '\n';
  }
  return resultMessage;
}

メンバー追加と同様の手順で削除を行います。

  1. AdminDirectory.Groups.get(...) でグループの有無をチェック
  2. 一人ずつ AdminDirectory.Members.remove(...) を呼んで削除
  3. 成功・失敗したメンバーをそれぞれ結果メッセージにまとめる

6. グループ削除 deleteGroup(...)

該当箇所のコード
/**
 * グループを削除する
 */
function deleteGroup(groupAddress) {
  try {
    AdminDirectory.Groups.remove(groupAddress);
    return 'グループ ' + groupAddress + ' が削除されました。';
  } catch (e) {
    if (e.message.includes('Resource Not Found')) {
      throw new Error('グループ ' + groupAddress + ' が見つかりません。');
    } else if (e.message.includes('Invalid Input')) {
      throw new Error('グループアドレス ' + groupAddress + ' が無効です。');
    } else {
      throw new Error('グループ削除中に予期せぬエラーが発生しました: ' + e.message);
    }
  }
}

AdminDirectory.Groups.remove(...) を呼び出すだけでOKなんですが、以下の例外処理があります。

  • 存在しないグループの場合は Resource Not Found
  • 不正なアドレス形式の場合は Invalid Input

いずれの場合も throw new Error(...) で処理を中断し、エラーメッセージを logError(...) に引き継ぎます。


7. エラー処理 logError(...)/sendNotificationEmail(...)

本スクリプトでは、何らかのエラーが発生した際に以下を行うようにしています。

  1. Logger.log() で Stackdriver Logging にログを残す
  2. sendNotificationEmail(...) を呼び出し、申請者 (フォーム回答者) と CC (管理者) に対してメール通知

sendNotificationEmail では、処理結果に応じて:

  • 成功時: 「完了のお知らせ」
  • エラー時: 「エラーのお知らせ」

という件名で本文を生成して送信します。


まとめ

今回紹介した仕組みは、フォームの入力内容 → スクリプトによる自動処理 というシンプルな構造でありながら、運用範囲を拡張しやすいところが特徴です。

  • 運用メリット
    • 担当者・申請者ともに手間を削減
    • 誤入力や二重手続きなどのトラブルを削減
    • フォーム回答と連動した可視化・通知で安心運用
  • 拡張性
    • 承認フローを追加
    • 作成するグループ設定のカスタマイズ
    • メンバー一括削除や別システム連携などの追加機能

以上です。
まあとりあえず使ってみてください。

ではでは。

ばるす

パチンコ屋→焼き肉屋→情シスを経てクラウドネイティブへ入社。
趣味はギター,キーボード,アウトプット,散歩,読書など。
苦手なものは朝と事務作業。得意分野は眠ること。