FileMakerでS3にREST APIでアクセスする (AWS Signature Version 4)

![AWS4.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2908/5df6b0bf-95d1-c916-c58d-b1c0d84c6c85.png)


[FileMaker Advent Calendar 2020](https://qiita.com/advent-calendar/2020/filemaker) 8日目の記事です。


最近はAWSにFileMaker Serverをデプロイして利用することも増えてきており、他のAWSサービスと組み合わせてFileMakerを利用している現場も増えてきていると思います。

そのためAWSのREST APIをFileMakerから直接利用するシーンは少なくないはずですが、それに必要な"AWS Signature Version 4"のいい感じの計算式が見当たらなかったので、今回1から作ることにしました。

タイトルにS3とありますが、S3をサンプルの題材に使用しているだけで実際はだいたいのREST APIは同じ方法でアクセスできるはずです。

## AWS Signature Version 4とは
AWS Signature Version 4とはAWSのREST APIで必要なSignature（署名）のことです。

具体的にはHTTPヘッダーに下記のようなAuthorizationヘッダー情報を加えてアクセスする必要があります。

Authorizationヘッダー例:

```
Authorization: AWS4-HMAC-SHA256
 Credential=AKIDEXAMPLE/20150830/us-east-1/iam/aws4_request,
 SignedHeaders=content-type;host;x-amz-date,
 Signature=5d672d79c15b13162d9279b0855cfba6789a8edb4c82c400e06b5924a6f2b5d7
```

_※見やすくするために改行を挟んでいますが、実際には改行はされません。_

Authorizationヘッダーの`Signature=`以降の部分がSignature（署名）です。

Signatureの作成には色々ルールがあり、厳密にこれを守らないと正しいSignatureが作れないため、1から実装するのは結構面倒です。

なので、そのSignatureを含め、HTTPヘッダーをまとめて作る計算式を今回は作成してきました。


## S3ダウンロードを実行してみる

まず、サンプルファイルを使ってS3のダウンロードを実行してみましょう。

### Step 1. アクセスキーの発行

Signatureの作成にはまずAWS IAMユーザーのAccessKeyID, SecretAccessKeyが必要です。

IAMユーザーはFileMakerファイル毎に作成するのがオススメです。ファイルが多い場合は面倒ですが、ファイル毎にアクセス可能な範囲を細かくコントロールする方が後々便利だと思います。
ユーザー名はファイル名などにしておくとわかりやすくていいと思います（例: `FM_FileStore`）。

ルートユーザーのアクセスキーでも利用は可能ですが、セキュリティーの観点からルートユーザーのアクセスキーの発行は推奨されてません。

[IAM ユーザーのアクセスキーの管理 \- AWS Identity and Access Management](https://docs.aws.amazon.com/ja_jp/IAM/latest/UserGuide/id_credentials_access-keys.html#Using_CreateAccessKey)

### Step 2. サンプルファイルのダウンロード

GitHubにサンプルファイル計算式、サンプルファイルを置いておきました。
[hazi/FileMaker\-AwsSignature: AWS Signature Version 4 in FileMaker formula\.](https://github.com/hazi/FileMaker-AwsSignature)

こちらからサンプルファイルをダウンロードしてください。

### Step 3. アクセスキーの設定

ファイルを開くと書いてありますが、カスタム関数にAccessKeyID, SecretAccessKeyを保存する形式をとっています（定数としてのカスタム関数の利用）。

AWS.SecretAccessKeyの場合はこんな感じです。

```java:AWS.SecretAccessKey
Let([
  ~yourSecretAccessKey = "wJalrXUtnFEMI/K7MDENG+bPxRfiCYEXAMPLEKEY";
_=0];
  Case(
    not IsEmpty($AWS_SecretAccessKey); $AWS_SecretAccessKey;
    ~yourSecretAccessKey
  )
)
```

必要に応じて、スクリプト内でアクセスキーを書き換えられるようになっています。不要な場合は削除してもらっても構いません。

基本的には関数の
`~yourAccessKeyID = "AKIDEXAMPLE";`
や
`~yourSecretAccessKey = "wJalrXUtnFEMI/K7MDENG+bPxRfiCYEXAMPLEKEY";`
の行を書き換えて使用してください。

### Step 4. スクリプト修正

`S3 Download(path)` スクリプトにスクリプト引数を渡すことで、ファイルのダウンロードが可能です。

まず、`S3 Download(path)` の設定値を変えます。

```swift
# Setting
変数を設定 [ $host ; 値: "example.s3.amazonaws.com" ]
変数を設定 [ $region ; 値: "us-east-1" ]
```

`$host` は `バケット名.s3.amazonaws.com` の形式です。
`$region` は東京の場合は `ap-northeast-1` です。

すべて引数で実装してもいいのですが、ファイル内で複数のバケット、リージョンを利用することもあまりないと思うので、あえて固定値にしています。必要に応じて書き換えてください。

### Step 5. スクリプト実行

次に、新規にスクリプトを作成し、引数にダウンロードパス（バケットルートからのパス）を指定します。

```swift
スクリプト実行 [ 指定: 一覧から ; 「S3 Download(path)」 ; 引数: "/path/to/file.txt" ]
変数を設定 [ $result ; 値: Get(スクリプトの結果) ]
```

これで実行すればS3のファイルがダウンロードできるはずです。

#### XMLパース
ここで1つ注意点があります。

S3のAPIの戻り値は基本的XMLです。
XMLを手動でパースするのはなかなか面倒なので、今回はBaseElements Pluginを使用しています。

ファイルのダウンロードだけならそのまま動くかもしれませんが、エラー処理などはBaseElements Pluginがないとうまく動作しません。

BaseElements Pluginを長いこと使っていますが、特段クラッシュしたりということはないので使用をオススメしています。不安な場合やクライアントの関係で難しい方パース部分の計算式を修正してください。

[Downloads \- BaseElements Plugin Support](https://docs.baseelementsplugin.com/article/522-downloads)

### サンプル以外のアクセス

S3のDownload, Upload, List など以外にも細かくいろんなアクションがあります。
それらのアクセス方法は公式ドキュメントと、下記の解説を参考に独自に作成してみてください。

[Amazon S3 REST API Introduction \- Amazon Simple Storage Service](https://docs.aws.amazon.com/ja_jp/AmazonS3/latest/API/Welcome.html)
[Amazon Simple Storage Service \- Amazon Simple Storage Service](https://docs.aws.amazon.com/ja_jp/AmazonS3/latest/API/API_Operations_Amazon_Simple_Storage_Service.html)

## 実装・引数解説

サンプルファイルの `AWS REST API(JSON)` がコアなスクリプトとなっており、メインの計算式（`#Main Logic` コメントの後ろ）の全文はGitHubに別途テキストファイルで上げてありますのでそちらと合わせてご覧ください。

計算式: [AWS Signature Version 4.fmfn](https://github.com/hazi/FileMaker-AwsSignature/blob/master/AWS%20Signature%20Version%204.fmfn)


ポイントとしては、1つのJSON引数のみで動作するようになっています。
Signatureの作成にはヘッダーやURLの情報がキーになっているので、まずそれらを1つの引数にまとめました。
そうすることで、1ステップでSignatureを生成できるようになっています。

引数の中身はこんな感じです。
基本的にはAWSのAPI解説に出てくる名前そのままなので、APIドキュメントを見ながら使用して頂ければ問題ないと思います。

```yaml:$AWS4Options
{
  # Required Items
  "service":    "string", # "s3"
  "scheme":     "https",  # (default: "https")
  "host":       "string", # "s3.amazonmws.com"
  "path":       "string", # String starting with a "/" (default: "/")
  "query":      "string", # "?" Not including (default: "")
  "region":     "string", # "ap-northeast-1"
  "httpMethod": "GET",    # (default: "GET")

  # Options
  "httpHeaderJSON": {...} # to curl `--head` option: `{"Content-MD5":"1B2M2Y8AsgTpgAmY7PhCfg==","Content-Type":"text/plain","x-amz-meta-mode":"33188"}`
  "file": null, # Base64Encoded Object: `Base64EncodeRFC(4648; $file)` (default: null)
  "createContentSha256HexHeader": true, # When true, "X-Amz-Content-Sha256" will be automatically generated and added to the header. (default: true)
  "timestampFaceOverride": null, # Force the timestamp to be rewritten For debugging (default: null)
  "curl--FM-return-container-variable": false, # Script always returns the value as an object (default: false)
}
```

Optionsの内容だけ独自仕様なので少し解説します。

- **file**  
  ファイルをアップロードする場合などに使用します。JSONにはオブジェクト形式のデータを混ぜられないので、Base64Encodeした文字列を受け付けるようになっています。BodyにJSONなどのテキストを渡す場合はJSONを`TextEncode`などでエンコードしファイル化して渡してあげると動作すると思います。
- **curl--FM-return-container-variable**  
  [cURLオプション](https://help.claris.com/ja/pro-help/content/curl-options.html)の `FM-return-container-variable` を有効にします。
  テキストファイルをダウンロードする際などにオブジェクトとして受け取らないと誤った文字コードで解釈されて文字化けします。ダウンロード時は基本的には`true`を指定し、戻り値を適切に`TextEncode`を使ってエンコードした方が良いでしょう。デフォルトは`false`です。
- **‌createContentSha256HexHeader**  
  デフォルトでヘッダーに`X-Amz-Content-Sha256`を追加する仕様になっています。利用したくない場合に`false`を指定してください。
- **timestampFaceOverride**  
  基本的には使うことはないと思いますが、計算式内で使うタイムスタンプを手動で指定したい場合に使用します。デフォルトでは実行時のタイムスタンプを使用します。
- **httpHeaderJSON**  
  httpヘッダー情報もSignatureを作成する上で必要なため、httpヘッダーに情報を追加する必要がある場合はJSON形式で渡す必要があります。
  `Content-MD5`, `Content-Type`, `x-amz-meta-mode` をヘッダーに追加する場合は下記のようなJSONを `httpHeaderJSON` に追加してください。
  
  ```yaml:$AWS4Options
  {
  #...
    "httpHeaderJSON": {
      "Content-MD5":"1B2M2Y8AsgTpgAmY7PhCfg==",
      "Content-Type":"text/plain",
      "x-amz-meta-mode":"33188"
    }
  }
  ```

## 実装の振り返り

細かい実装の解説は膨大になるので今回は省きます。

基本的にはAWSの指定するロジックをFileMakerに書き換えただけです。
「抽象化のためにヘッダー情報などを全部JSON引数で受け取る」と決めたら意外とスッキリ実装できました。

細かいバグを潰すのに半日かかってしまいましたが、特別なことは何もないです。

### 改行コードの変更
改行区切りのデータは基本的には `¶` や `List()` を使っていますが、これはAWSの改行コードとは一致しません。

FileMakerの改行コードはCR(`Char(13)`)なのに対し、AWSはLF(`Char(10)`)です。

最初から`Char(10)`を使って改行コードを挟んでもいいのですが、見た目に何しているのかよくわからないのと改行コードが混在すると、バグになりやすいので必要時にだけ `TextEncode(string; "utf-8"; 3)` で改行コードを変更するようにしました。

### JSON引数
今更ですが、キーワード引数の代替えとしてJSONが使えるようになったのはとても大いいですね。

今回Base64Encodeを組み合わせることで、オブジェクトのやりとりも問題なく行えることが確認できました（デバッグ中データビュアーを表示して大きいファイルをやりとりすると死にますw）。

引数を作るのが面倒なのでできるだけ使いたくないとか、引数が無限に渡せるから何でもかんでも渡すようにした結果、誰もメンテできないブラックボックスになるなんてことも容易に想像できますが、上限個数をルール化するなどとすれば問題なさそうです。

## 終わりに

実はサンプルファイルはあまりテストしてません…ちょっと時間がありませんでした。
もしバグってたらコメントかissueください。

あ、あと英語がダメダメなので変だったら教えてください！w

## 参考
- [AWS API リクエストの署名 \- AWS 全般のリファレンス](https://docs.aws.amazon.com/ja_jp/general/latest/gr/signing_aws_api_requests.html)
- [【Ruby】AWS署名バージョン4のAuthorization作成手順 \- Qiita](https://qiita.com/yutoo/items/d4176530f578b47c285f)
- [Amazon Simple Storage Service \- Amazon Simple Storage Service](https://docs.aws.amazon.com/ja_jp/AmazonS3/latest/API/API_Operations_Amazon_Simple_Storage_Service.html)
- [Generate AWS Signature Version 4 in FileMaker \| Goya](https://goya.com.au/blog/generate-aws-signature-version-4-in-filemaker)