Google Apps ScriptでGeminiをAPIキー無しでリクエスト【GAS】

これまで、当サイトでもGoogle Apps ScriptにてGemini APIを叩く場合にはAPIキーを取得して、それをつけたURLにてアクセスさせて、プログラムとして利用してきました。

しかし、よくよく考えたらGASはGoogle Cloudの内部の存在なので、Google Cloud側とプロジェクト連結すればそもそもAPIキー要らなくなり、よりセキュアに使えるのでは?と考えたので試してみました。

今回利用するスプレッドシート等

これまでのAPIキーを利用した方法ではなく、Google Apps ScriptとGoogle Cloudのプロジェクトを連結して、直接ScriptApp.getOAuthToken()でアクセストークンを取得してリクエストする方式です。Google内部の認証なのでよくREST APIで使ってる「OAuth2 for Apps Script is a library」は利用しません。

よって、Client IDやSecretも事前に用意は必要ありません

GeminiのAPIキーの取得と学習の可否

APIキーの問題点

APIキーを使った手法についてはあちこちで記載されていますし、自分自身も使ってきました。しかし、この手法の場合は現場の人間によるAPIキーの管理といった問題点が発生し、流出時に莫大な損害が発生したり情報漏洩の可能性も高まります。

また、スクリプト内に直接記述したファイルを外部に公開してしまうという可能性も十分に考えられます。よって、このようなことを未然に防ぐようにAPIキー方式は採用せず今回のようなプロジェクト連結を伴う手法にすることで、勝手に課金されたり使われることを防止することが可能で、管理手間も減らすことが可能です。

DX推進が進んだ結果、現場で簡単にプログラムも作れる時代ですので、このあたりのガイドラインも情シス部署で用意し布教活動する必要があるでしょう。

事前準備

今回の方法は、Google Cloud側でのプロジェクトと連結して利用する為、あらかじめGoogle Cloud側で課金アカウントとの紐付けや、「OAuth同意画面の設定」については事前に設定済みである必要があります。

プロジェクトの連結

Google Apps ScriptのプロジェクトをGCP側と連結する手順は以下の通り

  1. Google Cloud Consoleを開く
  2. 左上にある▼をクリックする
  3. ダイアログが出てくるので、新規プロジェクトを作るか?既存のプロジェクトを選択する。この時、Google Workspaceであれば選択元は「自分のドメイン」を選択する必要があります。
  4. プロジェクト情報パネルから「プロジェクト番号」をコピーする
  5. 対象のGoogle Apps Scriptのスクリプトエディタを開く
  6. サイドバーからプロジェクト設定を開く
  7. プロジェクトを変更ボタンをクリック
  8. GCPのプロジェクト番号に、4.の番号を入れてプロジェクトを設定をクリック

図:プロジェクト番号をコピーしておきます

図:プロジェクト変更画面

Google Cloud Consoleを弄ってみる

appsscript.jsonに記述を追加する

スクリプトエディタの左サイドバーから「プロジェクト設定」を開き、「appsscript.json」マニフェスト ファイルをエディタで表示するにチェックを入れて、appsscript.jsonを表示する。その後そのファイルを開き、以下のように記述を行います。必須の作業です。これをしてしまうと、今後メソッドを追加したときに追加の認証は手動で、Scopeを入れてあげないと認証されないので要注意。

{
  "timeZone": "Asia/Tokyo",
  "dependencies": {
  },
  "exceptionLogging": "STACKDRIVER",
  "runtimeVersion": "V8",
  "oauthScopes": [
    "https://www.googleapis.com/auth/script.external_request",
    "https://www.googleapis.com/auth/generative-language.tuning"
  ]
}

これを入れてあげないと403などのエラーになってしまうので要注意。GCPのAPIを叩く場合には必須の作業です。

この時肝になるのは、「https://www.googleapis.com/auth/generative-language.tuning」のスコープ。「https://www.googleapis.com/auth/cloud-platform」のスコープでも良いのですが、これはGCP全体のAPIすべてを使えてしまうので広すぎる。

AIチューニング不要ということであれば、「https://www.googleapis.com/auth/generative-language」のスコープでもOK。

図:手動でスコープ追加が必要

APIの有効化

Google Cloud側で今回利用するAPIを有効化する必要性があります。

  1. Google Cloudを開き、サイドバーからAPIとサービスを開きます。
  2. 上部にある「APIとサービスの有効化」をクリック
  3. 「gemini」を検索し、Gemini APIクリックします。
  4. 有効化をクリックします。

有効化するのがGemini APIなのに、ダッシュボード上ではGenerative Language APIとなってる点に要注意です。

図:今回はこのAPIを有効化する

図:ただ表記はGenerative Language APIとなってる

初回認証時

ここまできたら、Google Apps Scriptで適当な関数を作って初回実行→認証を実行します。すると、Gemini APIへのアクセス権についての許可を求められるので、許可をしてあげれば以降はこの作業は不要です。

この時点で既にもうメソッド1つでAccess Tokenが取得出来るので、APIキーのようにスクリプトプロパティやらに格納しておくといったことも不要になります。

図:初回認証時の様子

ソースコード

今回は、Gemini 2.5 Pro APIを使って処理を実行させています。リクエストする本体の関数と実際にAPIの処理を行う2つの関数で構成されており、APIキーの時とは異なり、エンドポイントURLにAPIキーを繋げるといった必要はありません。

一方で、リクエストヘッダーでは「'Authorization': 'Bearer ' + accessToken,」といったアクセストークンでの実行をする処理を追加する必要があり、これをもってPOSTでリクエストを実行します。

実行すると、画面下部の実行ログに答えが返ってきて表示されるようになります。

図:直近1年の為替変動について問い合わせしてみた

// テスト実行
function testGeminiman() {
  //プロンプトを作って問い合わせ実行
  let prompt = "直近1年のドル/円の為替の変動について教えてください。";
  let responseText = geminiWithScriptAppToken(prompt);
  console.log(responseText);
}

//GeminiをTokenで利用する
function geminiWithScriptAppToken(prompt) {
  //アクセストークンを取得する
  let accessToken = ScriptApp.getOAuthToken();

  //トークン取得結果の検証
  if (!accessToken) {
    console.log('トークンが取得できませんでした。プロジェクトの権限を確認してください。');
    return 'エラー: トークンが取得できませんでした。';
  }

  //Gemini APIのエンドポイントURL
  const url = "https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-pro-preview-05-06:generateContent";

  //リクエストヘッダ
  let headers = {
    'Authorization': 'Bearer ' + accessToken,
    'Content-Type': 'application/json'
  };

  //payloadの構築
  let payload = JSON.stringify({
    contents: [
      {
        parts: [
          {
            text: prompt
          }
        ]
      }
    ]
  });

  //リクエストオプション
  let options = {
    'method': 'post',
    'headers': headers,
    'payload': payload,
    'muteHttpExceptions': true
  };

  try {
    //Gemini APIリクエスト実行
    let response = UrlFetchApp.fetch(url, options);
    let result = JSON.parse(response.getContentText());

    //コード200が返ってきたら中身を処理
    if (response.getResponseCode() === 200) {
      //返却された回答を取得する
      if (result && result.candidates && result.candidates.length > 0 &&
          result.candidates[0].content && result.candidates[0].content.parts &&
          result.candidates[0].content.parts.length > 0) {
        //回答をそのまま返す
        return result.candidates[0].content.parts[0].text;
      } else {
        //レスポンスがオカシイ場合の処理
        console.log('APIレスポンスの形式が予期しないものでした: ' + response.getContentText());
        return 'APIレスポンスの形式が予期しないものでした。';
      }
    } else {
      //リクエスト時にエラーが発生した場合
      console.log('APIエラー: ' + response.getContentText());
      return 'Generative Language API呼び出しでエラーが発生しました: ' + response.getContentText();
    }
  } catch (e) {
    //スクリプト実行時のエラーを捕捉
    console.log('スクリプト実行中にエラーが発生しました: ' + e.toString());
    return 'スクリプト実行中にエラーが発生しました: ' + e.toString();
  }
}

関連リンク

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です

日本語が含まれない投稿は無視されますのでご注意ください。(スパム対策)