はじめに
こんにちは、マイクロアドでアプリケーションエンジニアをしている渡部です。
12月初旬に、会社で契約しているGoogle Workspaceのユーザーのみが使用できるGoogleカレンダーアドオンを公開しました。
アドオンの目的と機能
開発の背景
マイクロアドでは、営業活動における顧客とのアポイントメントや商談履歴を管理するアポ管理アプリケーション(以降、アポ管理アプリ)を社内で運用しています。
従来、営業担当者はGoogleカレンダーでアポの予定を作成した後、別途このアポ管理アプリにも同じ内容を登録する必要があり、二重入力による工数が発生していました。
この課題を解決するため、Googleカレンダー上で予定を作成した時点でアポ管理アプリにデータを自動登録できるアドオンを開発しました。
アドオンの機能
このアドオンを使用することで、以下の機能を利用できます。
- 会社名・担当者の検索: 既存の顧客情報から会社名や担当者を検索・新規作成できる。
- 顧客データの作成: Googleカレンダー上の予定作成と同時に、アポ管理アプリのデータベースへ情報を登録する。
- 議事録の自動連携: Googleカレンダーの予定と紐づくZoomミーティングの議事録やGoogle Meetの議事録を、別途稼働しているバッチ処理で走査してデータベースへ登録し、社内アプリから閲覧できるようにしている。
この仕組みにより、営業担当者の作業負担を軽減できました。

アドオンの全体像
システムアーキテクチャ
本アドオンは、以下の3層構成で実装しています。

- Google Apps Script(GAS): ユーザーインターフェース層
- Google Cloud Functions: 中継層
- オンプレサーバー(API): データ管理層
開発環境と本番環境でそれぞれGoogle Cloud(旧GCP)プロジェクトを用意しており、基本的に同じアーキテクチャを採用しています。 GASとCloud FunctionsのコードはそれぞれGit管理しており、コマンドを使って各環境へ反映できるようにしています。
アーキテクチャの選定理由
3層構成を採用した理由は、セキュリティ要件の遵守とネットワーク制約の解消を両立させるためです。
- ネットワーク制約の解消: オンプレサーバーは特定のVPCからの通信のみを許可する設定でしたが、GASはVPCへの直接接続に対応していません。そこで、VPCアクセスコネクタを利用可能なCloud Functionsを「ブリッジ」として採用しました。
- セキュアな通信経路の構築: GASからの直接アクセスを避け、中継層でGoogleのIDトークンによるサービス間認証(IAM認証)を挟むことで、外部からの不正な呼び出しを遮断しました。
このように、Cloud Functionsを中継層に置くことで、社内のセキュリティポリシーを満たしつつ、安全なVPC経由の通信を実現しています。
実装詳細
Google Apps Script
appsscript.jsonの設定
アドオンとして動作させるためには、appsscript.jsonでいくつかの重要な設定が必要です。
OAuthスコープ
Googleカレンダーの予定情報にアクセスし、外部へのHTTPリクエストを送信するために、以下のスコープを設定しました。
{ "oauthScopes": [ "https://www.googleapis.com/auth/calendar.addons.current.event.read", "https://www.googleapis.com/auth/calendar.addons.current.event.write", "https://www.googleapis.com/auth/calendar.addons.execute", "https://www.googleapis.com/auth/calendar.events", "https://www.googleapis.com/auth/calendar.readonly", "https://www.googleapis.com/auth/script.external_request", "https://www.googleapis.com/auth/script.scriptapp", "https://www.googleapis.com/auth/userinfo.email" ] }
URLフェッチホワイトリスト
GASから外部へHTTPリクエストを送信する際、接続先のURLをホワイトリストに登録する必要があります。 今回はCloud FunctionsのURLを設定しました。
アドオンの設定
カレンダーアドオンとして動作させるため、以下のトリガーを設定しました。
{ "addOns": { "calendar": { "currentEventAccess": "READ_WRITE", "homepageTrigger": { "runFunction": "onHomepageOpen" }, "eventOpenTrigger": { "runFunction": "onEventOpen" } } } }
homepageTrigger: カレンダーアドオンを開いたときに実行される関数eventOpenTrigger: カレンダーの予定を開いたときに実行される関数
UI構築
GASでのカスタムUIは、Card-based Interfaceを全面的に採用しました。
Card-based Interfaceとは
Card-based Interfaceは、Google Workspace アドオンで利用できるUI構築の仕組みです。 従来のHTMLベースのUIとは異なり、JSONオブジェクトでUIコンポーネントを定義することで、統一感のあるインターフェースを簡単に構築できます。
主な特徴は以下の通りです。
- 宣言的なUI定義: HTMLやCSSを書く代わりに、JavaScriptのオブジェクトでUIを記述
- Google Workspaceとの統合: Googleカレンダーに馴染むネイティブなデザイン
- レスポンシブ対応: デスクトップ、モバイル両方で最適な表示
- セキュリティ: サンドボックス環境で動作するため、セキュアな実装が可能
採用の理由
Card-based Interfaceを採用した主な理由は以下の通りです。
- Google公式が推奨する標準的なUI構築方法であること
- 複雑なHTMLやCSSを書かなくても、統一感のあるUIを構築できること
- カードの積み重ねによる直感的なナビゲーションが実現できること
- メンテナンス性が高く、UIの変更が容易であること
実装のポイント
Card-based Interfaceでは、CardServiceというGASの組み込みクラスを使用してUIを構築します。
基本的な構造は以下のようになります。
function buildCard() { const card = CardService.newCardBuilder() .setHeader(CardService.newCardHeader().setTitle('タイトル')) .addSection( CardService.newCardSection() .addWidget(CardService.newTextInput() .setFieldName('companyName') .setTitle('会社名')) ) .build(); return card; }
カードは複数のセクションで構成され、各セクションには様々なウィジェット(テキスト入力、ボタン、選択リストなど)を配置できます。
動的なUI切り替え
ユーザーのアクションによって表示するUIが異なります。
- 新規の予定作成時のアドオンUI
- 既存予定の修正時のアドオンUI
そのため、各シナリオに合わせた専用のUI Builderクラスを用意し、ユーザーのアクションを判断して適切なBuilderに処理を振り分けるRouter的なクラスを実装しました。
この設計により、コードの可読性と保守性を高めることができました。
UIの詳細とユーザーフロー
アドオンのUIには、以下の主要なコンポーネントを配置しました。
- 会社名の検索欄: これからアポを行う会社を検索するためのテキストボックス
- 担当者の検索欄: 実際のアポに来る担当者を検索するためのテキストボックス
- アポ種別選択プルダウン: アポの種別(新規商談、既存顧客フォローなど)を選択
- 新規登録ボタン: 会社や担当者が新規の場合に登録するためのボタン
ユーザーがアドオンを使用する際の一連のフローは以下の通りです。
- Googleカレンダー上でアドオンを開く
- Googleカレンダーで新規予定作成モーダルを開く
- アドオンが入力フォームを表示
- 会社名を入力し、既存の会社から選択または新規登録
- 担当者名を入力し、既存の担当者から選択または新規登録
- アポ種別をプルダウンから選択
- アドオンがカレンダータイトルを生成するので、それをコピペして予定作成
このように、シンプルなフローで必要な情報を入力できるように設計しました。
カレンダータイトルを生成してコピペしているのは、カレンダータイトルに一定のルールを持たせて関係ない予定作成までアポ登録されないようにするためです。
また、Googleカレンダーでは予定が作成完了するまでその予定を識別するiCalUIDが取得できず、予定作成が完了するまではアドオン側でGoogleカレンダー上の予定と社内アプリのアポを紐づけることができないという背景もあります。
Cloud Functions
役割
Cloud Functionsは、GASからのリクエストを受け取り、オンプレサーバーにリクエストを中継する役割に限定しています。 複雑なビジネスロジックは持たせず、シンプルな中継層として実装することで、保守性を高めています。
VPC接続
オンプレサーバーへはVPC経由でしかアクセスできない構成になっていたため、Serverless VPCコネクタを使用してCloud FunctionsからVPC経由でオンプレサーバーに接続しました。
Serverless VPCコネクタを設定することで、Cloud Functionsから社内ネットワーク内のリソースに安全にアクセスできるようになります。
オンプレサーバー(API)
オンプレサーバーでは、アポ管理アプリのデータベースへの登録処理を行います。
ここで注意が必要なのは、レスポンスがVoidだとGAS側でエラーとしてログに残ってしまうという点です。
そのため、処理が成功した場合でも何かしらのレスポンス(例:{ "status": "success" })を返すようにしました。
デプロイと公開
GASとCloud Functionsのデプロイ
GASのデプロイ
GASのデプロイには、Googleが提供するclaspというコマンドラインツールを使用しました。(下記サンプル)
clasp(Command Line Apps Script Projects)とは、Googleが公式に提供しているGoogle Apps Script (GAS) をローカル環境(自分のパソコン)で開発・管理するためのコマンドラインツールです。
通常、GASはブラウザ上の専用エディタでコードを書きますが、claspを使うことで、普段使っている開発環境(VS Codeなど)でGASの開発ができるようになります。
Node.js上で動作するため、npmコマンドを使ってインストールします。
claspを使うことで、ローカルのVS CodeでGASの開発をしつつコードはGitで管理し、デプロイがコマンド1つで実行できる環境を構築しました。
Cloud Functionsのデプロイ
Cloud Functionsのデプロイには、gcloudコマンドを使用しました。(下記サンプル)
gcloud functions deploy FUNCTION_NAME \ --runtime nodejs18 \ --trigger-http \ --vpc-connector VPC_CONNECTOR_NAME \ --region REGION
VPCコネクタの設定もデプロイコマンドで指定できるため、環境ごとの切り替えも容易です。
社内限定公開の設定
アドオンを社内限定で公開するために、Google Cloudの「Google Workspace Marketplace SDK」を使用しました。
具体的な手順は以下の通りです。
- Google Cloudコンソールで対象のプロジェクトを開く
- 「Google Workspace Marketplace SDK」を有効化
- SDKの設定画面から「ドメイン内部からのアクセスのみ許可」を選択
- 限定公開としてデプロイ
この設定により、会社で契約しているGoogle Workspaceへ所属するアカウントからのみアドオンへアクセスできるようになりました。

開発体制と期間
チーム構成
本プロジェクトは、社内のフロントエンド開発チームにて、3名で開発を進めました。
主な役割分担は以下の通りです。
- GAS担当: アドオンのUI構築とGoogleカレンダーとの連携を実装
- Cloud Functions担当: Google CloudのセットアップとCloud Functionsの中継処理を実装
- API担当: オンプレサーバー上のアポ管理アプリのAPI開発
ただし、実際の開発ではそれぞれの役割を超えて協力するタイミングも多く、チーム全体で問題解決に取り組みました。
開発期間
計画から公開まで約3ヶ月かかりました。
- 1ヶ月目: 要件定義と技術調査、アーキテクチャ設計
- 2ヶ月目: 各コンポーネントの実装と結合テスト
- 3ヶ月目: ユーザーテストとバグ修正、本番環境へのデプロイ
特に初期の技術調査では、GASでのアドオン開発の知見が社内にほとんどなかったため、公式ドキュメントや先行事例の調査に時間を費やしました。
開発での苦労と解決方法
知見が少ない中での開発
今回のプロジェクトでは、Google Cloud上にアプリケーションを構築すること自体が初の取り組みでした。
特にGASでのカスタムUI構築やappsscript.jsonの設定に関しては、社内に知見がほとんどなく、手探りでの開発となりました。
公式ドキュメントを熟読しながら、試行錯誤を繰り返すことで少しずつ理解を深めていきました。
デプロイ後のトラブルシューティング
GASとCloud Functionsのデプロイが完了した後も、実際にアドオンを動かすとオンプレサーバーまでリクエストが到達しない問題を経験しました。
原因を調査した結果、デプロイ時の公開設定が誤っていたことが判明しました。 特に、Cloud Functionsの認証設定や、GASからのリクエストを許可する設定など、細かい設定ミスが原因でした。
1つずつログを確認しながら、どの層で処理が止まっているかを特定し、問題を解決していきました。
テスト戦略
本プロジェクトでは、結合テストとユーザーテストを厚く行いました。
単体テスト
Cloud FunctionsとオンプレサーバーのAPIについては、単体テストを実装しました。 ただし、GASに関しては以下の理由から、単体テストのカバレッジを厚くできませんでした。
- GAS APIのモックコストが高いこと
- 実行環境がGoogleサーバーに依存しており、実際に開発用Google Cloudへデプロイしないと挙動を確認できない部分があったこと
- UI構築ロジックとビジネスロジックが密結合しているため、単体テストが書きづらかったこと
結合テスト
結合テストでは、GASからCloud Functions、そしてオンプレサーバーまでの一連のフローを確認しました。
テスト環境
本番環境と開発環境の2つのGoogle Cloud環境を構築していたため、基本的に開発環境でテストを実施しました。 また、開発環境で作成したアドオンをテスト用として社内に限定公開することで、実際の利用シーンに近い形でのテストも行いました。
テストケース
主に以下のようなテストケースを実施しました。
正常系の登録フロー
- 既存の会社と既存の担当者を選択して、アポを登録
- 新規の会社と新規の担当者を登録して、アポを作成
- 既存の会社に新規の担当者を追加して登録
- カレンダー予定の作成と同時に、アポ管理アプリへのデータ登録が正しく行われることを確認
異常系のエラーハンドリング
- 必須項目が未入力の場合のエラー表示
- ネットワークエラー時の適切なエラーメッセージ表示
- オンプレサーバーとの通信が失敗した場合のリトライ処理
- 無効なデータ入力時のバリデーションエラー
既存データの選択
- 会社名の検索機能が正しく動作するか
- 選択した会社に紐づく担当者の自動表示
新規データの登録
- 新規会社の登録フォームが正しく表示されるか
- 登録後、即座に選択肢として利用可能になるか
- データベースへの登録が正しく行われているか
テスト観点
結合テストでは、以下の観点を重点的に確認しました。
データの整合性
- GAS、Cloud Functions、オンプレサーバー間でデータが正しく受け渡されるか
- Googleカレンダーの予定情報とアポ管理アプリのデータが一致しているか
- 文字コードや特殊文字の扱いが適切か
各層間の通信
- GASからCloud Functionsへのリクエストが正しく送信されるか
- Cloud Functionsからオンプレサーバーへの中継が適切に機能するか
- VPC経由での接続が安定して確立されるか
- 各層でのエラーログが適切に記録されているか
これらのテストを通じて、システム全体が期待通りに動作することを確認し、本番環境へのデプロイに備えました。
ユーザーテスト
社内の営業担当者がエンドユーザーでしたので、比較的容易にユーザーテストを実施できました。 実際の業務フローに即したテストケースを用意し、UIの使いやすさや機能の不足点などのフィードバックを得ることができました。
導入状況と今後の展開
現在は一部の部署に利用してもらい、徐々に利用する人数を増やしている段階です。 この段階的なロールアウトにより、以下のメリットがあります。
- 本番環境での問題を早期に発見し、対処できる
- ユーザーからのフィードバックを収集し、機能改善に繋げられる
- システムの負荷を監視しながら、必要に応じてスケーリングできる
今後は、利用部署を全社に広げていく予定です。
課題と今後の改善
現在の課題
今回の実装方法で課題として挙げられるのは、以下の点です。
メトリクスの取得
GASでは詳細なメトリクスの取得をサポートしていないため、パフォーマンスの監視や問題の早期発見が困難です。
クォータ制限
ユーザー数が増えた場合、GASのクォータ(実行時間制限、API呼び出し回数制限など)がボトルネックとなる可能性があります。
レスポンス速度
現状、アドオンからAPIを叩いてレスポンスが返るまでの速度が十分に速いとは言えません。 ユーザー体験を向上させるために、レスポンス速度の改善が必要です。
今後の改善計画
以下の改善を検討しています。
- パフォーマンスの最適化: Cloud Functions側でのキャッシュ機構の導入や、データベースクエリの最適化によるレスポンス速度の向上
- 負荷分散の検討: 大勢のユーザーが利用した場合の負荷分散機構の導入
- モニタリングの強化: Cloud Functionsやオンプレサーバー側でのログ収集・監視機能の強化
まとめ
本記事では、社内限定で公開したGoogleカレンダーアドオンの開発から公開までの過程を紹介しました。
今回の開発を通じて得られた知見は以下の通りです。
appsscript.jsonの設定方法とOAuthスコープの理解- GASでのCard-based Interfaceを用いたUI構築方法
- Cloud Functionsを中継してVPC経由でオンプレサーバーに接続する方法
claspやgcloudコマンドを用いたデプロイフロー- Google Workspace Marketplace SDKを用いた社内限定公開の設定方法
GASやGoogle Cloudでのアドオン開発は初めての取り組みでしたが、暗中模索しながらも公開まで持っていくことができました。 この経験が、同じようにGoogleカレンダーアドオンの開発に取り組む方の参考になれば幸いです。