6
2

More than 1 year has passed since last update.

Google Apps Script(GAS)から S3 にあるファイルを取得する

Last updated at Posted at 2021-10-06

はじめに

業務で s3 からデータを取得してスプレッドシート に吐き出すというタスクを行なっていた際、Google Apps Script(gas) で aws sdk for js v3(aws-sdk) が使えず s3 からのデータ取得するコードをスクラッチで実装する必要があったのでやり方をまとめます。

コードだけ知りたい方はこちらをどうぞ。

試したこと

aws-sdk

下記の aws-sdk を試しました。

https://docs.aws.amazon.com/ja_jp/AWSJavaScriptSDK/v3/latest/index.html

具体的には、以下の手順を踏みました

  • node_modules として、aws-sdk をインストール
  • webpack を使ってビルド
  • ビルドしたコードを gas にアップロード

gas のエディターから関数を実行すると、以下のようなエラーがでました。

ReferenceError: URL is not defined

ビルドされたコードの中を見ると以下のような部分でエラーが発生していました。

var parseUrl = function (url) {
    var _a = new URL(url) // ReferenceError: URL is not defined
    // ...
};

つまり、gas では URL API は使えないということです。gas は js の文法で書けるものの、js ではないので今回のように js では使えて gas では使えない機能がわりとあります。

AWS Signature Version 4 を使って api 経由で取得

こちらの記事にあるよに、api 経由でデータを取得するために、host や authorization などをヘッダーに指定してリクエストを送ります。

リクエスト時に、どんな値をヘッダーに指定するかを取り決めたものが AWS Signature Version 4 です。主に authorization ヘッダーの作成の仕様を定義しています。こちらについては下記の記事でまとめています。

https://qiita.com/masachoco/items/3904053b7e0910e91218

よし、この方法でできる!と思いきや、うまくいきません。

理由と指定は、gas で http リクエストを使う際に使用する UrlFetchApp の fetch モジュールで、host ヘッダーを指定できないためです。以下のようなエラーが出ます。

// Exception: Attribute provided with invalid value: Header:Host
UrlFetchApp.fetch("http://www.google.com/", {headers: {host: "www.google.com"}})

これはおそらく gas 側の仕様で host ヘッダーを指定できないようにしていると思われます。他のヘッダーは問題なく指定できました。

AWS Signature Version 4 では、リクエストに host ヘッダーを必ず指定する必要があるためこの方法も gas では使えません。

https://docs.aws.amazon.com/ja_jp/AmazonS3/latest/API/sig-v4-header-based-auth.html

解決策

どうしたものかと途方に暮れている時下記のドキュメントを発見しました。

https://docs.aws.amazon.com/ja_jp/AmazonS3/latest/userguide/RESTAuthentication.html

こちらは、AWS Signature Version 2 になります。こちらの方法なら、host ヘッダーを指定する必要がなく、 authorization ヘッダーに署名を含めるだけで良いです。

この方法なら!

と試したところ、gas から s3 のデータを取得できました!

やり方を以降のセクションでまとめます。

gas から AWS Signature Version 2 の署名を使って s3 からデータを取得する

準備

iam ユーザの作成

AmazonS3ReadOnlyAccess ポリシーのアタッチされたユーザーを作成してください。この時に発行される、access_key_idsecret_access_key をメモしておいてください。後で使います。

バケットを作成

東京リージョン(ap-northeast-1)にバケットを作成してください。この時のバケット名をメモしておいて下さい。後で使います。

バケットにファイルを保存

先ほど作成したバケットにファイルを保存します。ファイル名やディレクトリ名は何でも構いません。本記事では、以下の内容のテキストファイルをバケット直下に test.txt という名前で保存します。

s3 fetchObject test

環境変数の設定

スクリプトプロパティーに以下の環境変数を設定します。先ほどメモしておいた値を使います。

AWS_ACCESS_KEY=<access_key_id>
AWS_SECRET_KEY=<secret_access_key>
BUCKET=<bucket名>
REGION=ap-northeast-1

スクリプトプロパティーの設定は、旧エディターもしくは下記のスクリプトを用いて設定できます。

PropertiesService
    .getScriptProperties()
    .setProperty(<Key>, <Value>)

コード

const AWS_ACCESS_KEY = getScriptProperty("AWS_ACCESS_KEY")
const AWS_SECRET_KEY = getScriptProperty("AWS_SECRET_KEY")
const BUCKET = getScriptProperty("BUCKET")
const REGION = getScriptProperty("REGION")
const ENDPOINT = `https://s3.${REGION}.amazonaws.com/${BUCKET}/`

class S3Service {
  constructor() {
    this.requestDate = new Date()
  }

  fetchObject(filepath) {
    this.setRequestDate()
    const url = this.getUrl(filepath)
    const opt = this.getOption({
      httpMethod: "GET",
      canonicalizedResource: this.canonicalizedResource(filepath),
    })
    const res = UrlFetchApp.fetch(url, opt)
    return res
  }

  getUrl(filepath) {
    return ENDPOINT + filepath
  }

  getOption(params) {
    return {
      method: params.httpMethod.toLowerCase(),
      headers: {
        Authorization: this.authorization(params),
        Date: this.requestDate.toUTCString(),
      },
      muteHttpExceptions: true,
    }
  }

  setRequestDate() {
    this.requestDate = new Date()
  }

  authorization(params) {
    return `AWS ${AWS_ACCESS_KEY}:${this.signature(params)}`
  }

  signature(params) {
    const sign = this.stringToSign(params)
    const hash = Utilities.computeHmacSignature(
      Utilities.MacAlgorithm.HMAC_SHA_1,
      sign,
      AWS_SECRET_KEY
    )
    return Utilities.base64Encode(hash)
  }

  stringToSign(params) {
    const signs = []
    signs.push(params.httpMethod || "")
    signs.push(params.contentMd5 || "")
    signs.push(params.contentType || "")
    signs.push(this.requestDate.toUTCString() || "")
    signs.push(
      (params.canonicalizedAmzHeaders || "") + (params.canonicalizedResource || "")
    )
    return signs.join("\n")
  }

  canonicalizedResource(filepath) {
    return "/" + BUCKET + "/" + filepath
  }
}

function getScriptProperty(key) {
  const scriptProperties = PropertiesService.getScriptProperties()
  return scriptProperties.getProperty(key)
}

function fetchObject() {
  const s3 = new S3Service()
  const res = s3.fetchObject("test.txt")
  Logger.log(res)
}

エディターから fetchObject を実行して以下のように表示されれば成功です

s3 fetchObject test
6
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
6
2