boxからファイル一覧を取得するLambda関数をServerless Frameworkで構築する

# なにこれ
連携会社や他のチームと実績データや素材データをやりとりすることがままあります。
今回はその一例として、「閲覧権限のあるフォルダに定期的にファイルが追加されるので、それを利用して色々処理する定期バッチをLambdaで構築したい」というお話です。
あと、折角なのでServerless Frameworkを使ってみました。
その手順をまとめます。

# 構成図
![image.png](https://qiita-image-store.s3.amazonaws.com/0/287363/d99b7da0-e754-e25a-b51a-ab9d4b74c029.png)
こんな感じ。
今回は機微な話なのでストレージ保存とかは割愛して取得するところまでの手順を書きます。

# Serverless Framework
デプロイは[Serverless Framework](https://serverless.com/framework/)で行います。
Serverless Framework自体の詳細な説明は、公式でも手厚くしてくれているし記事も沢山あるのでここでは省きます。
ザックリ雑には**「CloudFormationとかTerraformで頑張らなくてもコマンド一つでポンとデプロイできるイカしたフレームワーク」**という認識で問題ないかと思います。

「Serverless Frameworkは一旦いらない」「boxの方説明して」という人は[この節はかっ飛ばして](https://qiita.com/mochisuna/items/e6cea16025504eddb35d#box-api)ください。

## 環境設定
事前準備として以下が設定済みであることと仮定します。

* npm
* awscli（AWSアカウントは持っている前提）
    * FYI: [
AWS CLI](https://aws.amazon.com/jp/cli/)
    * Python（というか[pip](https://www.python.jp/install/windows/pip.html)が必要）
* `~/.aws/credentials`を設定
    * FYI: [開発用の AWS 認証情報とリージョンのセットアップ ](https://docs.aws.amazon.com/ja_jp/sdk-for-java/v1/developer-guide/setup-credentials.html)
* bundlerが利用可能
    * `gem install` でもできるけどね

まずはインストール。
npmコマンド一つで簡単に使えるようになります

```bash
$ npm install -g serverless
```

インストールが終わったら`sls`と叩いてみましょう（デカイので折りたたんでます）。
<details>
  <summary>展開して中身を確認 :eyes:</summary><div>

```bash
$ sls

Commands
* You can run commands with "serverless" or the shortcut "sls"
* Pass "--verbose" to this command to get in-depth plugin info
* Pass "--no-color" to disable CLI colors
* Pass "--help" after any <command> for contextual help

Framework
* Documentation: https://serverless.com/framework/docs/

config ........................ Configure Serverless
config credentials ............ Configures a new provider profile for the Serverless Framework
create ........................ Create new Serverless service
install ....................... Install a Serverless service from GitHub or a plugin from the Serverless registry
package ....................... Packages a Serverless service
deploy ........................ Deploy a Serverless service
deploy function ............... Deploy a single function from the service
deploy list ................... List deployed version of your Serverless Service
deploy list functions ......... List all the deployed functions and their versions
invoke ........................ Invoke a deployed function
invoke local .................. Invoke function locally
info .......................... Display information about the service
logs .......................... Output the logs of a deployed function
metrics ....................... Show metrics for a specific function
print ......................... Print your compiled and resolved config file
remove ........................ Remove Serverless service and all resources
rollback ...................... Rollback the Serverless service to a specific deployment
rollback function ............. Rollback the function to the previous version
slstats ....................... Enable or disable stats
plugin ........................ Plugin management for Serverless
plugin install ................ Install and add a plugin to your service
plugin uninstall .............. Uninstall and remove a plugin from your service
plugin list ................... Lists all available plugins
plugin search ................. Search for plugins

Plugins
AwsConfigCredentials, Config, Create, Deploy, Info, Install, Invoke, Logs, Metrics, Package, Plugin, PluginInstall, PluginList, PluginSearch, PluginUninstall, Print, Remove, Rollback, SlStats
```
</div></details>

## プロジェクトの初期化
プロジェクトディレクトリを作成し初期化します。
初期化は、`sls create`にパラメータとしてランタイム、関数名を指定するだけでOKです。
今回はrubyで`box-list`という関数を作成します。

```bash
$ mkdir box-list
$ cd box-list/
$ sls create -t aws-ruby --name box-list
Serverless: Generating boilerplate...
 _______                             __
|   _   .-----.----.--.--.-----.----|  .-----.-----.-----.
|   |___|  -__|   _|  |  |  -__|   _|  |  -__|__ --|__ --|
|____   |_____|__|  \___/|_____|__| |__|_____|_____|_____|
|   |   |             The Serverless Application Framework
|       |                           serverless.com, v1.37.1
 -------'

Serverless: Successfully generated boilerplate for template: "aws-ruby"
```

実行すると何やら色々画面に出て、対象のディレクトリに設定ファイルが配置されます。

```bash
$ ll ~/box-list/
total 16
-rw-r--r--  1 maruta-hirokazu  staff   151B  2 14 16:34 handler.rb
-rw-r--r--  1 maruta-hirokazu  staff   2.8K  2 14 16:34 serverless.yml
```

中身はこんな感じ（デフォルトがこれまたデカイので折りたたんでます）
<details>
  <summary>展開して中身を確認 :eyes:</summary><div>


```bash
$ cat ./handler.rb
require 'json'

def hello(event:, context:)
  { statusCode: 200, body: JSON.generate('Go Serverless v1.0! Your function executed successfully!') }
end
$ cat  serverless.yml
# Welcome to Serverless!
#
# This file is the main config file for your service.
# It's very minimal at this point and uses default values.
# You can always add more config options for more control.
# We've included some commented out config examples here.
# Just uncomment any of them to get that config option.
#
# For full config options, check the docs:
#    docs.serverless.com
#
# Happy Coding!

service: box-list # NOTE: update this with your service name

# You can pin your service to only deploy with a specific Serverless version
# Check out our docs for more details
# frameworkVersion: "=X.X.X"

provider:
  name: aws
  runtime: ruby2.5

# you can overwrite defaults here
#  stage: dev
#  region: us-east-1

# you can add statements to the Lambda function's IAM Role here
#  iamRoleStatements:
#    - Effect: "Allow"
#      Action:
#        - "s3:ListBucket"
#      Resource: { "Fn::Join" : ["", ["arn:aws:s3:::", { "Ref" : "ServerlessDeploymentBucket" } ] ]  }
#    - Effect: "Allow"
#      Action:
#        - "s3:PutObject"
#      Resource:
#        Fn::Join:
#          - ""
#          - - "arn:aws:s3:::"
#            - "Ref" : "ServerlessDeploymentBucket"
#            - "/*"

# you can define service wide environment variables here
#  environment:
#    variable1: value1

# you can add packaging information here
#package:
#  include:
#    - include-me.py
#    - include-me-dir/**
#  exclude:
#    - exclude-me.py
#    - exclude-me-dir/**

functions:
  hello:
    handler: handler.hello

#    The following are a few example events you can configure
#    NOTE: Please make sure to change your handler code to work with those events
#    Check the event documentation for details
#    events:
#      - http:
#          path: users/create
#          method: get
#      - s3: ${env:BUCKET}
#      - schedule: rate(10 minutes)
#      - sns: greeter-topic
#      - stream: arn:aws:dynamodb:region:XXXXXX:table/foo/stream/1970-01-01T00:00:00.000
#      - alexaSkill: amzn1.ask.skill.xx-xx-xx-xx
#      - alexaSmartHome: amzn1.ask.skill.xx-xx-xx-xx
#      - iot:
#          sql: "SELECT * FROM 'some_topic'"
#      - cloudwatchEvent:
#          event:
#            source:
#              - "aws.ec2"
#            detail-type:
#              - "EC2 Instance State-change Notification"
#            detail:
#              state:
#                - pending
#      - cloudwatchLog: '/aws/lambda/hello'
#      - cognitoUserPool:
#          pool: MyUserPool
#          trigger: PreSignUp

#    Define function environment variables here
#    environment:
#      variable2: value2

# you can add CloudFormation resource templates here
#resources:
#  Resources:
#    NewResource:
#      Type: AWS::S3::Bucket
#      Properties:
#        BucketName: my-new-bucket
#  Outputs:
#     NewOutput:
#       Description: "Description for the output"
#       Value: "Some output value"
```
</div></details>

とりあえずワチャワチャ書いてあるのですが、コメントを全部消してみるとこれしか記述がありません。

```yaml
service: box-list

provider:
  name: aws
  runtime: ruby2.5

functions:
  hello:
    handler: handler.hello
```

読み解いていくと

* サービス名: `box-list`
* ランタイム: `ruby 2.5`
* 実行時の関数: `handler.hello`

という設定が書かれています。

これだけでデプロイの準備がOKになってしまいます。
すごいよね。
で、対象のhandlerファイルは以下。

```rb
require 'json'

def hello(event:, context:)
  { statusCode: 200, body: JSON.generate('Go Serverless v1.0! Your function executed successfully!') }
end
```

定義ファイル側で指定した`ファイル名.ハンドラ名`にし、`(event:, context:)`を引数とするだけで動いてくれます。

## プロジェクトのデプロイ
先ほど初期化した状態で`serverless deploy`をしてあげるだけでデプロイ完了です。
また、以下のようにprofileを指定することで環境の切り替えもできます。
（何も指定しない場合は`default`のプロファイルが利用されます）

```bash
$ sls deploy --aws-profile myenv
```

これによって間違って会社のアカウントにLambda関数作っちゃって、あとで*「ねぇ、知ってたらいいんだけどこれ何かわかる？・・・あ、君の？いや、勉強に使うのはとてもいいことなんだけど、試し打ち用のアカウントあるんだからさ。まぁ確かに最近そっちの利用料がちょっと高いかなとは思っていたんだけど、社内の方にあげるのは流石にやめてね。」*ってチームメンバーに諭される心配もなくなりますね！

で、デプロイを実行すると以下のようにモジャモジャターミナルが動きあっという間に完了してしまいます:tada:

```bash
Serverless: Packaging service...
Serverless: Excluding development dependencies...
Serverless: Uploading CloudFormation file to S3...
Serverless: Uploading artifacts...
Serverless: Uploading service box-list.zip file to S3 (266 B)...
Serverless: Validating template...
Serverless: Updating Stack...
Serverless: Checking Stack update progress...
.......................
Serverless: Stack update finished...
Service Information
service: box-list
stage: dev
region: us-east-1
stack: box-list-dev
resources: 5
api keys:
  None
endpoints:
  None
functions:
  hello: box-list-dev-hello
layers:
  None
Serverless: Removing old service artifacts from S3...
```
マネジメントコンソール上ではこんな感じ。
<img width="1747" alt="スクリーンショット 2019-02-14 17.38.34.png" src="https://qiita-image-store.s3.amazonaws.com/0/287363/76d1d41f-d1ab-b3fc-27b4-3ead2ea70bed.png">
詳細。
<img width="1224" alt="スクリーンショット 2019-02-14 17.40.00.png" src="https://qiita-image-store.s3.amazonaws.com/0/287363/bc731941-18c9-ad5b-3aed-dfad4506f730.png">
ばっちり。

なお、デプロイは「プロジェクトフォルダにあるもの全てがzipに圧縮されてアップロードされる」という方式なので、必要なパッケージ（rubyだとgem周りとか）はプロジェクトフォルダ内に全部入れてあげる必要があります。

# Box API
本題。
[Box](https://www.box.com/ja-jp/home)にあるファイルを取得していいようにいじくりまわします。
今回はrubyを使うので[Boxr](https://github.com/cburnette/boxr)という公式のgemを利用します。

## Boxerのインストール
プロジェクトファイル内で以下を実行。

```bash
$ bundle init
Writing new Gemfile to /Users/maruta-hirokazu/box-list/Gemfile
$ echo "gem 'boxr'" >> Gemfile 
$ cat Gemfile
# frozen_string_literal: true

source "https://rubygems.org"

git_source(:github) {|repo_name| "https://github.com/#{repo_name}" }

# gem "rails"
gem 'boxr'
$ bundle install --path vendor/bundle
```
以上。

## Box側でアプリの作成
アプリを作成しないことにはAPIは利用できないので、まずはBoxアプリを作成します。

[開発者コンソール](https://app.box.com/developers/console)にアクセスします
<img width="400" alt="スクリーンショット 2019-02-14 18.10.33.png" src="https://qiita-image-store.s3.amazonaws.com/0/287363/4ce76a90-0582-df97-f993-01cef84ed29c.png">

カスタムアプリを選択。
<img width="400" alt="スクリーンショット 2019-02-14 18.13.48.png" src="https://qiita-image-store.s3.amazonaws.com/0/287363/595503f3-8f7a-31a9-68a3-fcbe82dc075c.png">

認証設定を選びます。
ここについてですが、ユーザー認証はバッチ処理には向かないし、値を環境変数か何かに持たせるだけで認証できた方が便利やろっていうところからJWTを利用します。
<img width="400" alt="スクリーンショット 2019-02-14 18.14.58.png" src="https://qiita-image-store.s3.amazonaws.com/0/287363/de90db35-2ebc-87ae-e054-0f2aa5571cb2.png">

アプリ名はユニークであればなんでもOKです。
<img width="400" alt="スクリーンショット 2019-02-14 18.26.52.png" src="https://qiita-image-store.s3.amazonaws.com/0/287363/903b1328-a69e-d1e1-683f-af6d26a346e2.png">
ここまでで「アプリの作成」を実行するとアプリができます（できるまでに結構時間がかかります）。
完了すると以下のような画面が出るので、書かれているコマンドを実行してみてください。

<img width="690" alt="スクリーンショット 2019-02-14 18.30.33.png" src="https://qiita-image-store.s3.amazonaws.com/0/287363/978f01ad-2aea-3483-9f42-6a8a19a154a4.png">

```bash
curl https://api.box.com/2.0/folders/0 -H "Authorization: Bearer ***" -vvv | jq .
```

`200OK` でファイル情報が返ってくればとりあえず準備OKです。

## アプリの構成設定
デフォルトだと構成はこんな感じになっています（長い）
<img size=400 src="https://qiita-image-store.s3.amazonaws.com/0/287363/2522a4dd-8f00-f68c-96e4-f0dc278d5ad3.png">

Developerトークンを利用することで一時的なアクセスとかもできるのですが、今回は認証キーペアを作成してしまいます。
`公開キーの追加と管理`で`公開/秘密キーペアを生成`を押してください。
<details>
  <summary>注意：２段階認証が必要です</summary><div>
2段階認証してないとこんな感じ怒られます
<img width="400" alt="スクリーンショット 2019-02-14 18.55.37.png" src="https://qiita-image-store.s3.amazonaws.com/0/287363/18c25bf0-f018-4d3f-c849-9bb95ba7f182.png">

ちなみに、２段階認証で日本探すのは結構大変です
<img width="400" alt="スクリーンショット 2019-02-14 18.59.55.png" src="https://qiita-image-store.s3.amazonaws.com/0/287363/7b570317-d5ec-0c82-ad8b-d6534d254d14.png">
</div></details>


キーペアの生成ボタンを押すと秘密鍵がダウンロードされます（今回は資料としてダウンロード画面を出したかったためfirefox使っていますが、ブラウザはなんでもいいです）。
<img width="400" alt="スクリーンショット 2019-02-14 19.02.57.png" src="https://qiita-image-store.s3.amazonaws.com/0/287363/06a563c1-c78f-a6eb-cbd9-2245e11b9008.png">

中身はこんな感じです。

```json
{
  "boxAppSettings": {
    "clientID": "***",
    "clientSecret": "***",
    "appAuth": {
      "publicKeyID": "***",
      "privateKey": "-----BEGIN ENCRYPTED PRIVATE KEY-----\n******-----END ENCRYPTED PRIVATE KEY-----\n",
      "passphrase": "***"
    }
  },
  "enterpriseID": "***"
}
```

構成の設定自体はこれでOKで、boxerではこのjsonにある情報を利用します。
なのでキーをなくさないように注意。

## Boxの認証設定
ユーザー側からアプリを認証します。
この認証は２種類あるので要件に合わせて対応を変えてください。

- 全てのユーザーから認証させる場合
- アプリユーザーからのみ認証させる場合

### 全てのユーザーから認証させる場合
こちらの方が設定自体は簡単です。

アプリの構成で`[アプリケーションアクセス]`を`Enterprise`に指定します。
<img width="400" alt="スクリーンショット 2019-02-15 17.57.55.png" src="https://qiita-image-store.s3.amazonaws.com/0/287363/7820523a-aa4f-84e9-8940-02bd232cce45.png">
ここのチェックが外れるので入れなおしてください。
<img width="400" src="https://qiita-image-store.s3.amazonaws.com/0/287363/aa9d9467-1eb8-5dd2-0fb7-567f364cd07a.png">

`[管理コンソール]` → `[Enterprise設定]` → `[アプリ]` → `[カスタムアプリケーション]` 
<img width="400" alt="スクリーンショット 2019-02-14 20.03.37.png" src="https://qiita-image-store.s3.amazonaws.com/0/287363/8a879f3e-7fb2-6910-cb0e-575f982f0da4.png">
`新しいアプリケーションを承認`を押します。
ポップアップするのでクライアントIDを入れます
<img width="400" alt="スクリーンショット 2019-02-14 20.03.47.png" src="https://qiita-image-store.s3.amazonaws.com/0/287363/b7e30ce5-832a-e148-204e-55e5f0407f44.png">
`アクセス対象ユーザー`が`すべてのユーザー`になっていればOKです。
<img width="400" alt="スクリーンショット 2019-02-15 17.43.09.png" src="https://qiita-image-store.s3.amazonaws.com/0/287363/2eb0771f-f869-f728-f42c-8715bcd69341.png">
こちらの場合は設定は以上です。

### アプリユーザーからのみ認証させる場合
boxには`アプリユーザー`という概念があります。
[詳細の説明は公式に任せます](https://ja.developer.box.com/docs/user-types#section-app-user)が、端的にはより堅牢なパターンです。
ただし手順が複雑になります。

アプリの構成で`[アプリケーションアクセス]`を`アプリケーション`に指定します
<img width="400" alt="スクリーンショット 2019-02-15 17.57.47.png" src="https://qiita-image-store.s3.amazonaws.com/0/287363/1c133540-8c7a-f466-da51-a94a9a213027.png">

`[管理コンソール]` → `[Enterprise設定]` → `[アプリ]` → `[カスタムアプリケーション]` 
で`新しいアプリケーションを承認`を押します（この処理自体はすべてのユーザーの場合と同じ）。
<img width="400" alt="スクリーンショット 2019-02-14 20.03.54.png" src="https://qiita-image-store.s3.amazonaws.com/0/287363/27677eb5-9dcd-36d0-5b13-e40cacc39d4e.png">
ポップアップされるものが`[このアプリのApp Usersのみ]`になっていることに注意してください。
こちらのケースではAdminユーザーからアクセスした場合`Cannot obtain user token based on the enterprise configuration for your app`みたいなエラーが出るので、以下で作成するアプリユーザーを利用しなければなりません。

#### アプリユーザーの作成
コマンドで作成します。
`Bearer`の部分はアプリ構成の`Developerトークン`で生成してください（60分で利用できなくなります）。

```bash
curl https://api.box.com/2.0/users \
     -H "Authorization: Bearer ***" \
     -d '{"name": "box-list-user", "is_platform_access_only": true}' \
     -X POST -vvv
```

コマンドを実行すると、以下のようなレスポンスが来るのでUserIDを取っておいてください。

```json
{
  "type": "user",
  "id": "これを保存してください",
  "name": "box-list-user",
  "login": "AppUser_***@boxdevedition.com",
  "created_at": "2019-02-15T01:15:53-08:00",
  "modified_at": "2019-02-15T01:15:53-08:00",
  "language": "en",
  "timezone": "America/Los_Angeles",
  "space_amount": ***,
  "space_used": 0,
  "max_upload_size": ***,
  "status": "active",
  "job_title": "",
  "phone": "",
  "address": "",
  "avatar_url": "***"
}
```
また、`[管理コンソール]`→`[ユーザーとグループ]`にAppUserが追加されているはずです。

ユーザー設定で`[フォルダの追加または作成]`から管理したいフォルダを選んでください。
<img width="400" alt="スクリーンショット 2019-02-15 18.20.40.png" src="https://qiita-image-store.s3.amazonaws.com/0/287363/a3f05547-662b-e3f0-0366-b28b2e49251c.png">
以上で設定は終わりです。

## boxのファイル一覧を取得するプログラムの作成
### プライベートキーの切り出し
プログラム作成の前に、秘密鍵からprivateKey情報を別のファイルとして切り出します。

```bash
$ cat key.json | jq .boxAppSettings.appAuth.privateKey
```
適当にファイル名をつけて（`box_private_key`とか）プロジェクトフォルダ内（`input/`配下とか）に保存しておきます。

Serverless Frameworkで作成した`handler.rb`を以下のように編集します。
（単純にファイルの一覧を表示し、ファイルの数を調べるプログラムです）

```rb
require "boxr"

def run(event:, context:)
  token = Boxr::get_user_token(
  ENV.fetch('BOX_USER_ID'),
  private_key: File.open('input/box_private_key').read.gsub("\\n", "\n"),
  private_key_password: ENV.fetch('BOX_JWT_PRIVATE_KEY_PASSWORD'),
  public_key_id: ENV.fetch('BOX_JWT_PUBLIC_KEY_ID'),
  client_id: ENV.fetch('BOX_CLIENT_ID'),
  client_secret: ENV.fetch('BOX_CLIENT_SECRET'))
  client = Boxr::Client.new(token.access_token)
  cnt = show(client, '0', '')

  { statusCode: 200, num_of_files: JSON.generate(cnt) }
end

# ファイルの一覧を全て捜査する
def show(client, folder_id, parent_path)
  cnt = 0
  items = client.folder_from_id(folder_id).item_collection.entries
  items.each do |item|
    if item.type == 'folder'
      cnt += show(client, item.id, "#{parent_path}/#{item.name}")
    elsif item.type == 'file'
      cnt += 1
      p "#{parent_path}/#{item.name}"
    end
  end
  cnt
end
```
補足すると

* `Boxr::get_user_token`でクライアントに接続
* ` client.folder_from_id(folder_id)`でフォルダ内の内容の取得
    * `0`を指定するとルートフォルダからになります
    * フォルダIDはブラウザから確認できるので、指定のフォルダに対し検索もかけられます
* `item.type`でフォルダなのかコンテンツなのかを判断しています

handlerができたら、`serverless.yml`を以下のようにします。
ただし、`BOX_USER_ID`の部分は`すべてのユーザーの場合`は自身のアカウントIDを、`AppUserのみの場合`はAppUser生成時に得られたIDを指定してください。

```yaml
service: box-list

provider:
  name: aws
  runtime: ruby2.5

  stage: dev
  region: us-east-1

  environment:
    BOX_USER_ID: <user_id / app_user_id>
    BOX_JWT_PRIVATE_KEY_PASSWORD: ***
    BOX_JWT_PUBLIC_KEY_ID: ***
    BOX_CLIENT_ID: ***
    BOX_CLIENT_SECRET: ***


functions:
  box:
    handler: handler.run
    events:
      - schedule: rate(100 minutes)
    timeout: 300
```
* 今回は本当に雑に100分ごとに実行していますが、好きなようにスケジュール式を変えてください。
* environment配下に、それぞれの値を入れてください。
    * セキュリティ的にはシステムの環境変数から取得する方がいいかも。


[2019/10/13追記]
`ファイルがcsvの場合は内容を表示する` のような処理をする場合は以下のようにすればよいかと思います。

```ruby
items = client.folder_from_id(folder_id).item_collection.entries
item.name.include?(".csv")
  CSV.parse(client.download_file(item.id).encode('UTF-8', 'Shift_JIS'), headers: true) do |row|
    p "#{row}"
  end
end
```
lambdaとかで処理をしているのであれば、
1. 取引のある相手とディレクトリを共有
2. 相手側から定期的にファイルが更新される
3. 定期実行でcsvファイルの中を確認し、更新がある場合はDBに書き込み
というようなこともできます（まぁ、あまり一般的ではないかもだけど・・・）

# 動作確認
ここまで行ったら一旦動作確認をします。
Serverless Frameworkがイカしている部分としてローカルで簡単に試せるというところがあります。

```bash
sls invoke local -f box
```

これで動いているみたいならデプロイするだけ！

## Tips
CSVファイルをパースする。

```
CSV.parse(client.download_file(item.id).encode('UTF-8', 'Shift_JIS'), headers: true)
```
他のファイルも`client.download_file`で制御できそうです。
詳しくは公式のAPI仕様を確認してください。
FYI: https://box-content.readme.io/reference

# まとめ
マジレスするとものすごく大変でした :innocent: :innocent: :innocent: 
ドキュメントが地味にわかりにくいので色々試し試しやった感じです。
でも、一回動いてくれるようになると大変便利＆色々応用できそうです:+1::+1::+1:

[2019/07/09追記]
一応簡単にテンプレートを作成しました
https://github.com/mochisuna/box-list