
「Gemini でアプリは作れるようになった。でも、社内で配る方法がない」
そんな課題を解消するために、Gemini Canvas で生成したコードをコピペするだけで、Apps Script プロジェクトを動的に生成・デプロイし、社内限定 URL を即時発行するツールを開発しました。
AI によるコード自動修正とAPIキーを隠蔽する仕組みにより、リリース1カ月で 200 以上の社内アプリを生み出した「サーバーレス・デプロイ基盤」を公開します。
はじめに
AI 活用チームの高玉です。Google Apps Script で自作したツールが好評だったのでご紹介します。
Gemini Canvasの「配れない」ジレンマ
「自然言語で指示するだけで、動くアプリケーションが目の前で組み上がっていく」
Gemini Canvas は、ソフトウェアエンジニアでなくても自身のアイデアを即座に形にできる強力なツールです。特に素晴らしいのは、コード内に const apiKey = ""; と空文字を定義しておくだけで、実行環境が自動的にキーを補完し、Gemini API を呼び出せる仕組みです。
しかし、企業内で Google Workspace 環境を利用している場合、ある一つの壁に直面します。
「これ、どうやって社内共有する?」
Canvas上では動作していたアプリも、HTMLとして書き出すとAPIキーの自動補完機能は失われます。かといって、生のAPIキーをコードにハードコードして共有するのはセキュリティ上ご法度です。 つまり、「作る」ハードルは劇的に下がったのに、「安全に配る(公開・デプロイする)」ハードルは高いままなのです。
1カ月で200アプリを生み出した解決策
この課題を解決するために、「Gemini Canvas で生成した HTML を貼り付けるだけで、裏側で Google Apps Scriptプロジェクトを動的に生成・デプロイするツール」を開発しました。
社内ツールとして公開したところ、反響は凄まじく、リリースからわずか1カ月で200以上ものウェブアプリが社内で作成されました。
本記事では、このツールを支える技術的な裏側、特に Apps Script API を活用した動的なプロジェクト生成アーキテクチャと、AI 生成コード特有の課題を解決するための工夫について解説します。
システムの概要
このシステムは、ユーザーインターフェースとなる「管理用ウェブアプリ」と、その裏側で Apps Script API を操作する「バックエンドロジック」で構成されています。 操作はとてもシンプルです。
1. コピー&ペーストでデプロイ
Gemini Canvas で生成された HTML コードをコピーし、ツールの入力欄に貼り付けて「デプロイ」ボタンを押すだけです。

2. AIによるコード自動修正の提案
貼り付けたコードに Apps Script と互換性のない記述(テンプレートリテラル等)が含まれている場合、ツールが自動的に検知し、ワンクリックで AI に修正を依頼できます。

3. 公開設定とURLの発行
デプロイが完了すると、社内限定公開の URL が即座に発行されます。システム全体が Apps Script 上で完結していて、組織の Google Workspace 内で安全に共有できます。

技術的な詳細と実装のポイント
ここからは、Apps Script に精通したエンジニア向けに、実装のポイントを解説します。
1. Apps Script API による動的なプロジェクト操作
通常はスクリプトエディターで記述する Apps Script プロジェクトを、プログラムから動的に生成します。
特に重要なのがデプロイに利用する projects.updateContent メソッドです。
利用者の HTML を保存するだけでなく、ウェブアプリとして動作させるための doGet 関数やマニフェスト (appsscript.json) を JSON 構造として組み立て、API 経由で PUT します。
[Note]
Apps Script API には1日あたりの作成・更新回数の制限(Quotas)があります。通常の利用頻度では問題ありませんが、数百人が短時間に一斉にデプロイするような研修等で利用する場合はご注意ください。[Note]
本ツールで生成された Apps Script プロジェクトは、実行したユーザー自身の「マイドライブ」に保存されます。 チームで永続的にメンテナンスする場合は、作成後にGoogle Drive APIを使って「共有ドライブ」へファイルを移動させることも可能です。こうすることで、退職等によるアカウント削除でアプリが消滅する「野良アプリ化」のリスクを防げます。
class AppsScriptApi { /** * プロジェクトのコンテンツを更新する */ updateContent(appName, scriptId, htmlContent, isGeminiApiUsed) { // 1. サーバーサイドのエントリーポイント(doGet) let codeGsContent = ` function doGet(e) { return HtmlService.createHtmlOutputFromFile('index') .setTitle('${appName}') .addMetaTag('viewport', 'width=device-width, initial-scale=1.0'); } `; // 2. マニフェスト(appsscript.json)の基本定義 const manifestContent = { timeZone: Session.getScriptTimeZone(), dependencies: {}, webapp: { executeAs: "USER_ACCESSING", access: "DOMAIN" }, oauthScopes: [] }; // 3. Gemini API 利用時の認可コード注入処理(後述) // ここで動的にライブラリ依存関係やフロントエンド用スクリプトを注入します if (isGeminiApiUsed) { this.injectGeminiAuth_(codeGsContent, manifestContent, htmlContent); } const files = [ { name: 'appsscript', type: 'JSON', source: JSON.stringify(manifestContent, null, 2) }, { name: 'Code', type: 'SERVER_JS', source: codeGsContent }, { name: 'index', type: 'HTML', source: htmlContent } ]; // API経由でファイルを上書き return this.fetch_(`projects/${scriptId}/content`, { method: 'put', payload: { files } }); } }
2. AIによるテンプレートリテラルの自動修復
Gemini Canvas で生成される JavaScript コードは、ES6 のテンプレートリテラル (${...}) を多用します。しかし、Apps Script の HtmlService において、${...} はバックエンドの印刷スクリプトレット (<?= ... ?>) と構文解析上の競合を起こす場合があります。
そこで、利用者が貼り付けたコードにテンプレートリテラルを見つけると、Gemini API を呼び出して「Apps Script 互換の形式(文字列結合など)にリファクタリングさせる」機能を実装しました。
function fixHtmlWithGemini(htmlContent) { // Gemini APIへのプロンプト const prompt = ` ${htmlContent} --- あなたはGoogle Apps Scriptの専門家です。 上記のHTMLコード内のテンプレートリテラル(${...})を、 Apps Scriptと互換性のある文字列結合などの形式に全て書き換えてください。 `; // ... Gemini API呼び出し処理 }
「AI が生成したコードの非互換性を、デプロイパイプラインの中で AI が自動修正する」ことで、ユーザー体験を損なうことなく問題を解決しています。
3. フロントエンドへの認可機能の自動注入(API キーの隠蔽)
Gemini Canvas の魅力である apiKey="" のままで動く利便性を、公開アプリでも安全に再現する必要があります。
そこで「バックエンドで短命のアクセストークンを発行し、フロントエンドのリクエストに自動付与する仕組み」を構築しました。
この仕組みは、「認可トークン発行ライブラリ(GeminiAuth)」と、それを動的にアプリに組み込む 「デプロイ基盤(Publisher)」 の 2 つで構成されています。 利用者が Gemini API を利用するコードを貼り付けると、ツールはそれを自動的に検出し、以下のように「Google Cloud プロジェクトの設定」が必要であることを画面上で案内します。

さらに、利用者が迷わないよう、具体的な設定手順(Google Cloud プロジェクト番号の入力など)もモーダルで案内します。これにより、Apps Script に不慣れな人でもスムーズに設定を完了できます。

この裏側で動いている認可フローの全体像は以下の通りです。デプロイ時(静的解析と注入)と、実行時(動的なトークン取得)で役割を分担しています。

それぞれの実装詳細を以下に解説します。
【前提】実行環境の制約と Google Cloud プロジェクト紐付け
Apps Script から IAM Service Account Credentials API のような権限の強い Google API を利用する場合は、デフォルトの管理プロジェクトではなく、新しく作成した Google Cloud プロジェクトを準備してその Apps Script に紐づける必要があります。ツールがユーザーに「プロジェクト番号の入力」を求めているのはそのためです。
なお、次に紹介する2つのステップで、それぞれ Google Cloud プロジェクトを作ります。上記の画面で利用者にプロジェクト番号の入力を求めていますが、(Step 2で作成する)認可トークン発行ライブラリーのプロジェクトであることに注意してください。
[Important] 請求先アカウント(Billing)の設定について
Gemini APIを実行できるサービスアカウントを作るためには、対象の Google Cloud プロジェクトが請求先アカウント(クレジットカード等の支払い情報)にリンクされている必要があります。組織のポリシー等で課金設定ができない場合、この方法は利用できませんのでご注意ください。
Step 1: Gemini APIを利用するサービスアカウントの作成
まず、トークン発行の主体となるサービスアカウントを作成し、必要なAPIを有効化します。
- Google Cloud プロジェクトの作成: 課金アカウントにリンクされたプロジェクトを作成(または選択)し、以下の2つのAPIを有効にします。
- Vertex AI API (IAM権限管理のため)
- Gemini API (Generative Language API) (実際のAPIコールのため)
- サービスアカウントの作成: 「IAMと管理」>「サービスアカウント」から、新規サービスアカウントを作成します(例:
gemini-executor@...)。 - IAM権限の付与: 作成したサービスアカウントの「アクセス権を持つプリンシパル」に、このツールを利用する社員(または Google グループ)を追加します。 付与するロールは 「サービス アカウント トークン作成者 (
iam.serviceAccountTokenCreator)」 です。
[Important] セキュリティ上の注意
トークン発行用のサービスアカウントには、Vertex AI ユーザー 以外の権限(例: Storage管理者 や BigQuery管理者 等)を絶対に付与しないでください。roles/iam.serviceAccountTokenCreator権限を持つユーザーは、このサービスアカウントになりすますことができます。「最小権限の原則」を徹底し、意図しないリソースへのアクセスを防いでください。[Tips]
全社員を個別に登録するのは管理コストが高いため、Google グループ(例:gemini-users@example.com)を作成し、そのグループに対してこのロールを付与すると運用が楽になります。
Step 2: 認可トークン発行ライブラリの作成
利用者のアプリが共通して利用するトークン発行ロジックを、独立した Apps Script ライブラリとして作成します。
- Google Cloud プロジェクトの作成: GeminiAuth 用に任意のプロジェクトを作成し、IAM Service Account Credentials API を有効にします。
- 利用するサービスアカウントの指定: スクリプトプロパティ
SERVICE_ACCOUNT_EMAILに Step 1 で作成したサービスアカウントのメールアドレスを保存しておきます。
認可ライブラリのコード(GeminiAuth)は以下のようになります。IAM Service Account Credentials API を呼び出し、Gemini API スコープを持つ短命アクセストークン(有効期限 15 分など)を取得します。
/** * Gemini API 用の短命なアクセストークンを生成する * @params {Number} lifetimeSec トークンの有効期限 [秒]。デフォルト 900 秒(15 分)。 * @returns {Object} { accessToken, expireTime } */ function fetchAccessToken(lifetimeSec = 900) { // 事前にスクリプトプロパティに保存しておいたサービスアカウントのアドレス const serviceAccountEmail = PropertiesService.getScriptProperties().getProperty('SERVICE_ACCOUNT_EMAIL'); const iamApiUrl = `https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/${serviceAccountEmail}:generateAccessToken`; const payload = { scope: ["https://www.googleapis.com/auth/generative-language"], lifetime: `${lifetimeSec}s` }; const response = UrlFetchApp.fetch(iamApiUrl, { method: "post", headers: { "Authorization": "Bearer " + ScriptApp.getOAuthToken() }, contentType: "application/json", payload: JSON.stringify(payload), muteHttpExceptions: true }); if (response.getResponseCode() !== 200) { throw new Error(`Token generation failed: ${response.getContentText()}`); } return JSON.parse(response.getContentText()); }
このスクリプトをライブラリとしてデプロイし、スクリプト ID を控えておきます。
Step 3: デプロイ基盤による動的な依存関係の注入
次に、利用者がアプリをデプロイする際、Step2で作成したライブラリを自動的にプロジェクトに追加するロジックを「Publisher」側に実装します。
- Google Cloud プロジェクトの作成: Publisher 用に任意のプロジェクトを作成し、Apps Script API を有効にします。
Apps Script Api を使ってデプロイするアプリの appsscript.json (マニフェストファイル)を書き換える際、dependencies に先ほどの GeminiAuth のスクリプト ID を追加し、バックエンド (コード.gs) にはライブラリを呼び出すためのブリッジ関数を動的に追記します。最後に、フロントエンド側で window.fetch を上書きするスクリプトを HTML の <head> 内に強制注入します。これにより、フロントエンドの fetch リクエストが自動的に横取りされ、トークンが付与されます。
// Gemini API の利用が検出された場合の注入ロジック if (isGeminiApiUsed) { // A. マニフェストへのライブラリ依存関係の追加 manifestContent.dependencies.libraries = [{ "userSymbol": "GeminiAuth", "libraryId": "...", // GeminiAuth のスクリプト ID "version": "1" }]; // B. サーバーサイド(Code.gs)へのトークン取得関数の追記 codeGsContent += ` function getGeminiToken() { return GeminiAuth.fetchAccessToken(); } `; // C. フロントエンド(index.html)へのfetchフック処理の注入 const authHandlerScript = ` <script id="gemini-auth-handler"> (function() { // 後述 })(); </script> `; // HTMLの先頭にスクリプトを挿入 htmlContent = authHandlerScript + htmlContent; }
なお単純に毎回バックエンドにトークンを要求すると、google.script.run の呼び出しに時間がかかり使い勝手が悪くなります。 そこで、このスクリプトでは次の工夫をしています。
- キャッシュ管理: 取得したトークンをメモリ上に保持し、有効期限内であれば再利用します。
- プリフェッチ: ページが開かれた瞬間(
DOMContentLoaded)に裏側でトークン取得を開始しておき、利用者が操作を始める頃にはトークンの準備を整えておきます。 - Promise共有: トークン取得中に複数のAPIリクエストが発生しても、APIコールは1回だけで済むようにPromiseを共有します。
// C. フロントエンド(index.html)へのfetchフック処理の注入 // トークンのキャッシュ管理とプリフェッチを実現するスクリプトを注入します const authHandlerScript = ` <script id="gemini-auth-handler"> (function() { let geminiAccessToken = null; let tokenExpireTime = 0; let fetchingPromise = null; // トークンを新規取得する関数(多重リクエスト防止付き) const fetchNewToken = () => { // 既に取得処理中ならそのPromiseを返す if (fetchingPromise) return fetchingPromise; fetchingPromise = new Promise((resolve, reject) => { google.script.run .withSuccessHandler(tokenInfo => { geminiAccessToken = tokenInfo.accessToken; // 有効期限より少し早め(例:30秒前)に切れ扱いにする安全マージン tokenExpireTime = new Date(tokenInfo.expireTime).getTime() - 30000; console.log("New access token acquired via server."); resolve(tokenInfo.accessToken); }) .withFailureHandler(error => { console.error("Failed to get access token:", error); reject(error); }) .getGeminiToken(); }).finally(() => { fetchingPromise = null; // 処理完了後にクリア }); return fetchingPromise; }; // 有効なトークンを返す関数(キャッシュがあればそれを使う) const getValidToken = () => { const now = new Date().getTime(); if (geminiAccessToken && now < tokenExpireTime) { return Promise.resolve(geminiAccessToken); } console.log("Token expired or missing. Fetching new one..."); return fetchNewToken(); }; // ページ読み込み時にトークンを先行取得(プリフェッチ)しておく window.addEventListener('DOMContentLoaded', () => { fetchNewToken().catch(e => console.warn("Token pre-fetch failed, will retry on request.")); }); // window.fetch をオーバーライド const originalFetch = window.fetch; window.fetch = async function(resource, options) { const requestUrl = resource instanceof Request ? resource.url : resource; // Gemini APIへのリクエストのみフック if (typeof requestUrl === 'string' && requestUrl.includes('generativelanguage.googleapis.com')) { try { // 有効なトークンを取得(待機またはキャッシュ利用) const token = await getValidToken(); const newOptions = { ...options }; newOptions.headers = { ...(newOptions.headers || {}), 'Authorization': 'Bearer ' + token }; // APIキーパラメータの削除 const urlObject = new URL(requestUrl); urlObject.searchParams.delete('key'); return originalFetch(urlObject.toString(), newOptions); } catch (error) { console.error("Authorization failed:", error); throw error; } } return originalFetch(resource, options); }; })(); </script> `;
このアーキテクチャにより、API キーなしで Gemini API を呼び出せるという Gemini Canvas の恩恵を、そのまま享受できます。
まとめ
今回構築したシステムは、Apps Script を「API 経由で操作可能なサーバーレス・プラットフォーム」として捉え直すことで実現しました。
- Apps Script API による動的な環境構築
- AI によるコードの自動サニタイズ
- 動的なコード注入によるAPIキーの隠蔽
これらの技術を組み合わせることで、Gemini の優れた能力を最大限に引き出しつつ、社内ツール開発のハードルを劇的に下げることができました。
Apps Script は枯れた技術ですが、最新の AI 技術と組み合わせることで、まだまだ面白い使い方ができます。皆さんの組織でも、埋もれている「Canvas の力作」を救い出すツールを作ってみてはいかがでしょうか?