Google App ScriptからAWSへの接続方法の検討

## 概要

現場環境の変化に伴い、GWS を利用するようになりました。  
また、AWS 上のリソース利用の機会が多く、なにかと自動化を検討する上では GWS⇒AWS の接続が必要となるため、方法について検討した内容を記録しています。

## 今回の目的

Google App Script でスプレッドシートの内容を整形してから、AWS S3 へアップロードする。

## 結論

やり方は複数存在しますが、どれも制限がありそうでした。  
今回は Rest API 方式を選択した、というのが結論となります。  
この場合の制限は、IAM ユーザー(アクセスキー、シークレット)の利用になることで、この情報を Google 側で厳重に管理する必要がある、というものになります。  
以下へ上に至るまでに辿った情報を記録しています。今後発生する用途によって使い分け出来ればと思います。

## 確認内容

### Google App Script と AWS SDK

Google Apps Script から AWS SDK を直接利用することはできません。  
AWS SDK for JavaScript も実行環境が異なるため直接利用することはできません。  
AWS SDK が利用できれば Cognito 利用も選択肢にあげられます。  
無理矢理組み合わせて動かす方法も確認されますものの、選択には至らなかった経緯も後述しています。

### アクセスキー、シークレットを厳重に管理する前提で Rest API 方式を検討する

Rest API 方式では実行に漕ぎ着けるまでに、複雑な手順を追う必要があるのが難点と思います。

- [API リクエストに対する AWS Signature Version 4](https://docs.aws.amazon.com/ja_jp/IAM/latest/UserGuide/reference_sigv.html)

ただ、こうした課題を解消してくださっている方々がおられるので、頂いた情報を参考にさせて頂きます。

#### S3 に特化した処理の場合

こちらがとても参考になりました。  
https://github.com/eschultink/S3-for-Google-Apps-Script

よく見るのが Google-Apps-Script のライブラリとして利用する方法なのですが、AWS リージョンが「us-east-1」でハードコーディングされているので要注意と思います。

https://github.com/eschultink/S3-for-Google-Apps-Script/blob/7e4b2f963d0fb3533eda2f55b8013acad8055d76/S3Request.gs#L24

- 該当箇所

  ```java script
  function S3Request(service) {
    this.service = service;

    this.httpMethod = "GET";
    this.contentType = "";
    this.content = "";
    this.bucket = "";
    this.objectName = "";
    this.headers = {};

    this.date = new Date();
    this.serviceName = 's3';
    this.region = 'us-east-1'; // 'ap-northeast-1' にするか、regionセットできるようにすればOK
    this.expiresHeader = 'presigned-expires';
    this.extQueryString = '';
  }
  ```

  ```java script
  // こちらをS3Request.gsに追加する案もあり
  S3Request.prototype.setRegion = function(region) {
    if (typeof method != 'string') throw "region must be string";
    this.region = region;
    return this;
  };
  // ↓
  // 追加後にはS3.gsでregionセット
  var request = new S3Request(this);
  request.setRegion('ap-northeast-1');
  ```

#### AWS リソースへの汎用的な目的の場合

こちらがとても参考になりました。  
いろいろできて便利な分、リスクも天秤にかけて参考とする形でしょうか。

https://github.com/smithy545/aws-apps-scripts/blob/master/aws.js

- S3 での利用例

  ```java script
  function myFunction() {
    AWS.init("accessKeyId", "secretAccessKey");
    S3PutObject();
  }

  function S3PutObject() {
    const bucket = 'backet-test-20250125'
    const key = 'my-content.txt'
    const content = 'My Content'
    const res = AWS.request(
      's3',
      'ap-northeast-1',
      'PutObject',
      {},
      'PUT',
      content,
      { 'Content-Type': MimeType.PLAIN_TEXT },
      `/${key}`,
      { Bucket: bucket },
    )
    const code = res.getResponseCode()
    const text = res.getContentText()
    if (code < 200 || code >= 300) throw Error(`AWS.request failed: ${code} - ${text}`)
    Logger.log(`OK: ${bucket}/${key}`)
  }
  ```

  ※引用：[AWS API を GAS (Google Apps Script) から直接呼び出す](https://akkinoc.dev/posts/2022/05/15/aws-api-from-google-apps-script/)

### AWS アクセスキー、シークレット利用が出来ない場合に備えて

AWS SDK javascript を無理矢理動かす上で、スプレッドシートから呼び出す HTML を介す方法を参考にさせてもらいました。

- AWS SDK javascript v2 利用
  もうじきサポート終了するので、記録する意味合いもあまりないのですが、v3 使えるようになった時のために記録します。

  - Google App Script の .gs に下記を記載。スプレッドシートが開かれたときにメニューが追加されるので「AWS 操作」を選択する。

    ```java script
    var app = SpreadsheetApp;

    // スプレッドシートが開かれたときにメニューを追加
    function onOpen() {
      var ui = app.getUi();
      ui.createMenu('AWS操作') // カスタムメニューを作成
        .addItem('S3にアップロード', 'showContentApp') // メニュー項目と対応する関数を指定
        .addToUi(); // メニューをUIに追加
    }

    // HTML を表示する関数
    function showContentApp() {
      var html = HtmlService.createHtmlOutputFromFile('sample2') // HTMLファイルを指定
        .setWidth(400) // モーダルダイアログの幅
        .setHeight(300); // モーダルダイアログの高さ
      app.getUi().showModalDialog(html, 'S3にアップロード'); // モーダルダイアログを表示
    }

    // スプレッドシートのデータを取得する関数
    function getSpreadsheetData() {
      var sheet = app.getActiveSpreadsheet().getActiveSheet();
      var data = sheet.getRange(1, 1, sheet.getLastRow(), sheet.getLastColumn()).getValues();
      return data;
    }
    ```

  - 出力された HTML では「AWS SDK for JavaScript(v3 はこの形式で公開されず)」を読み込んでいる。ボタンを押すと、Cognito を利用した認証で S3 に putObject を実行する。

    ```
    <!DOCTYPE html>
    <html>
    <head>
      <title>Upload to S3</title>
      <script src="https://sdk.amazonaws.com/js/aws-sdk-2.1242.0.min.js"></script>
      <script>
        document.addEventListener('DOMContentLoaded', function() {
          document.getElementById('btn').addEventListener('click', uploadToS3);
        });

        function uploadToS3() {
          // AWS Cognito を利用した認証

          var albumBucketName = "backet-test-20250125";
          var bucketRegion = "ap-northeast-1";
          var IdentityPool_Id = <IdentityPool_Id>;

          AWS.config.region = bucketRegion;
          AWS.config.credentials = new AWS.CognitoIdentityCredentials({
            IdentityPoolId: IdentityPool_Id
          });

          AWS.config.credentials.get(function(err) {
            if (err) {
              console.error('Cognito認証エラー:', err);
              document.getElementById('status').innerText = 'Cognito認証エラー';
              return;
            }
            console.log('認証成功: ', AWS.config.credentials);

            // S3オブジェクトを作成
            const s3 = new AWS.S3();

            // GASでスプレッドシートデータを取得
            google.script.run.withSuccessHandler(function(data) {
              const csv = data.map(row => row.join(',')).join('\n');
              const params = {
                Bucket: albumBucketName,
                Key: 'spreadsheet_data.csv',
                Body: csv,
                ContentType: 'text/csv'
              };

              // S3にアップロード
              s3.putObject(params, function(err, data) {
                if (err) {
                  console.error('アップロードエラー:', err);
                  document.getElementById('status').innerText = 'アップロード失敗';
                  return;
                }
                console.log('アップロード成功:', data);
                document.getElementById('status').innerText = 'アップロード成功';
              });
            }).getSpreadsheetData();
          });
        }
      </script>
    </head>
    <body>
      <h1>Upload to S3</h1>
      <button id="btn">Upload Spreadsheet</button>
      <p id="status"></p>
    </body>
    </html>
    ```

- AWS SDK javascript v3 利用

  - AWS SDK javascript v3 バンドル方法
    [AWS SDK v3 をシンプルに使う開発環境](https://qiita.com/nobrin/items/f298a983b31a9949c195)を参考にさせて頂きました。  
    ローカルブラウザの場合、IIFE 形式、ESM 形式ともに良好に稼働。  
    GAS 上では稼働しませんでした。なんでか、、については深追いしませんでした。

## メモ

記事には直接関係しません。  
稼働確認をする際に利用した個人メモです。

### S3 への配置素材

- HTML

  ```html<!DOCTYPE html>
  <!DOCTYPE html>
  <html lang="ja">
  <head>
      <meta charset="UTF-8">
      <title>テーブル列の表示・非表示</title>
      <style>
          .hidden {
              display: none;
          }
          .header {
              background-color: #0078d4;
              color: white;
              padding: 1rem;
              text-align: center;
          }
          .message {
              color: black;
              padding: 1rem;
              text-align: center;
          }
          .container {
              max-width: 900px;
              margin: 2rem auto;
              background-color: white;
              border-radius: 8px;
              box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
              padding: 1rem;
          }
      </style>
      <script>
          function toggleColumn() {
              var cols = document.querySelectorAll('.toggle-col');
              cols.forEach(function(col) {
                  col.classList.toggle('hidden');
              });
          }
      </script>
  </head>
  <body>
      <div class="header">
          <h1>Service Status</h1>
      </div>
      <div class="message">
          <h1>現在の Service Status は正常です。</h1>
      </div>
      <div class="container">
          <button onclick="toggleColumn()">列の表示/非表示</button>
          <table border="1">
              <tr>
                  <th>項目1</th>
                  <th>項目2</th>
                  <th class="toggle-col">項目3</th>
              </tr>
              <tr>
                  <td colspan=3>機能A</td>
              </tr>
              <tr>
                  <td>データ1</td>
                  <td>データ2</td>
                  <td class="toggle-col">データ3</td>
              </tr>
              <tr>
                  <td>データ4</td>
                  <td>データ5</td>
                  <td class="toggle-col">データ6</td>
              </tr>
              <tr>
                  <td colspan=3>機能B</td>
              </tr>
              <tr>
                  <td>データ7</td>
                  <td>データ8</td>
                  <td class="toggle-col">データ9</td>
              </tr>
              <tr>
                  <td>データ10</td>
                  <td>データ11</td>
                  <td class="toggle-col">データ12</td>
              </tr>
          </table>
          <!-- 他のステータス項目を追加 -->
      </div>
  </body>
  </html>
  ```

- svg ファイルの書き方

  - Healthy.svg

    ```
    <?xml version="1.0" encoding="UTF-8"?>
    <svg aria-labelledby="__bolt-status-58-desc" height="16" role="img" viewBox="0 0 16 16" width="16" xmlns="http://www.w3.org/2000/svg">
      <style>
        circle { fill: #008000; }
        path { fill: #fff; }
      </style>
      <desc id="__bolt-status-58-desc">Healthy: Green circle with a white check mark inside</desc>
      <circle cx="8" cy="8" r="8"></circle>
      <path d="M6.062 11.144l-.003-.002-1.784-1.785A.937.937 0 1 1 5.6 8.031l1.125 1.124 3.88-3.88A.937.937 0 1 1 11.931 6.6l-4.54 4.54-.004.004a.938.938 0 0 1-1.325 0z"></path>
    </svg>
    ```

  - Degraded.svg

    ```
    <?xml version="1.0" encoding="UTF-8"?>
    <svg aria-labelledby="__bolt-status-59-desc" height="16" role="img" viewBox="0 0 16 16" width="16" xmlns="http://www.w3.org/2000/svg">
      <style>
        circle { fill: #FFA500; } /* オレンジ */
        path { fill: #fff; } /* 白 */
      </style>
      <desc id="__bolt-status-59-desc">Degraded: Yellow circle with a white exclamation mark inside</desc>
      <circle cx="8" cy="8" r="8"></circle>
      <path fill-rule="evenodd" clip-rule="evenodd" d="M8.91 3.9a.9.9 0 0 0-1.8 0v4.7a.9.9 0 1 0 1.8 0V3.9zm-.95 8.65a.9.9 0 1 0 0-1.8.9.9 0 0 0 0 1.8z"></path>
    </svg>
    ```

  - Unhealthy.svg

    ```
    <?xml version="1.0" encoding="UTF-8"?>
    <svg aria-labelledby="__bolt-status-60-desc" height="16" role="img" viewBox="0 0 16 16" width="16" xmlns="http://www.w3.org/2000/svg">
      <style>
        circle { fill: #FF0000; } /* 赤色 */
        path { fill: #fff; } /* 白 */
      </style>
      <desc id="__bolt-status-60-desc">Unhealthy: Red circle with a white letter x inside</desc>
      <circle cx="8" cy="8" r="8"></circle>
      <path fill-rule="evenodd" clip-rule="evenodd" d="M10.984 5.004a.9.9 0 0 1 0 1.272L9.27 7.99l1.74 1.741a.9.9 0 1 1-1.272 1.273l-1.74-1.741-1.742 1.74a.9.9 0 1 1-1.272-1.272l1.74-1.74-1.713-1.714a.9.9 0 0 1 1.273-1.273l1.713 1.713 1.714-1.713a.9.9 0 0 1 1.273 0z"></path>
    </svg>
    ```

  - Advisory.svg

    ```
    <?xml version="1.0" encoding="UTF-8"?>
    <svg aria-labelledby="__bolt-status-61-desc" height="16" role="img" viewBox="0 0 16 16" width="16" xmlns="http://www.w3.org/2000/svg">
      <style>
        circle { fill: #0000FF; } /* 青色 */
        path { fill: #fff; } /* 白 */
      </style>
      <desc id="__bolt-status-61-desc">Advisory: Blue circle with a white letter i inside</desc>
      <circle cx="8" cy="8" r="8"></circle>
      <text x="7" y="12" font-family="Arial" font-size="12" fill="#fff">i</text>
    </svg>
    ```

### 検討用メモ

S3 にもっていく HTML の吐き出し方法にも選択肢はいくつかあがり、運用上の負荷軽減具合や、自動化可能な範囲が分かれる。  
最終的に HTML のもととなるマスタファイルを方々から同時にアクセスする必要性があり、そのまま実行可能な「Google Apps Script」の選択となった。  
が、環境が整わず以外に使う場面がありそうなので、他の方法の記載も一応残しておくことに。  
生成 AI 使うとほとんど時間がかからず、感謝。

[インプット情報（Google スプレッドシートか CSV）を HTML テーブルに変換する方法](https://qiita.com/wf-kenji-ikeda/items/53a8100b62491840ecd8)