はじめに
業務で 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_id
と secret_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