7
8

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

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

Last updated at Posted at 2019-02-15

なにこれ

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

構成図

image.png
こんな感じ。
今回は機微な話なのでストレージ保存とかは割愛して取得するところまでの手順を書きます。

Serverless Framework

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

「Serverless Frameworkは一旦いらない」「boxの方説明して」という人はこの節はかっ飛ばしてください。

環境設定

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

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

$ npm install -g serverless

インストールが終わったらslsと叩いてみましょう(デカイので折りたたんでます)。

展開して中身を確認 :eyes:
$ 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

プロジェクトの初期化

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

$ 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"

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

$ 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

中身はこんな感じ(デフォルトがこれまたデカイので折りたたんでます)

展開して中身を確認 :eyes:
$ 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"

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

service: box-list

provider:
  name: aws
  runtime: ruby2.5

functions:
  hello:
    handler: handler.hello

読み解いていくと

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

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

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

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のプロファイルが利用されます)

$ sls deploy --aws-profile myenv

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

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

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...

マネジメントコンソール上ではこんな感じ。
スクリーンショット 2019-02-14 17.38.34.png
詳細。
スクリーンショット 2019-02-14 17.40.00.png
ばっちり。

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

Box API

本題。
Boxにあるファイルを取得していいようにいじくりまわします。
今回はrubyを使うのでBoxrという公式のgemを利用します。

Boxerのインストール

プロジェクトファイル内で以下を実行。

$ 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アプリを作成します。

開発者コンソールにアクセスします
スクリーンショット 2019-02-14 18.10.33.png

カスタムアプリを選択。
スクリーンショット 2019-02-14 18.13.48.png

認証設定を選びます。
ここについてですが、ユーザー認証はバッチ処理には向かないし、値を環境変数か何かに持たせるだけで認証できた方が便利やろっていうところからJWTを利用します。
スクリーンショット 2019-02-14 18.14.58.png

アプリ名はユニークであればなんでもOKです。
スクリーンショット 2019-02-14 18.26.52.png
ここまでで「アプリの作成」を実行するとアプリができます(できるまでに結構時間がかかります)。
完了すると以下のような画面が出るので、書かれているコマンドを実行してみてください。

スクリーンショット 2019-02-14 18.30.33.png
curl https://api.box.com/2.0/folders/0 -H "Authorization: Bearer ***" -vvv | jq .

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

アプリの構成設定

デフォルトだと構成はこんな感じになっています(長い)

Developerトークンを利用することで一時的なアクセスとかもできるのですが、今回は認証キーペアを作成してしまいます。
公開キーの追加と管理公開/秘密キーペアを生成を押してください。

注意:2段階認証が必要です
2段階認証してないとこんな感じ怒られます スクリーンショット 2019-02-14 18.55.37.png

ちなみに、2段階認証で日本探すのは結構大変です
スクリーンショット 2019-02-14 18.59.55.png

キーペアの生成ボタンを押すと秘密鍵がダウンロードされます(今回は資料としてダウンロード画面を出したかったためfirefox使っていますが、ブラウザはなんでもいいです)。
スクリーンショット 2019-02-14 19.02.57.png

中身はこんな感じです。

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

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

Boxの認証設定

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

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

全てのユーザーから認証させる場合

こちらの方が設定自体は簡単です。

アプリの構成で[アプリケーションアクセス]Enterpriseに指定します。
スクリーンショット 2019-02-15 17.57.55.png
ここのチェックが外れるので入れなおしてください。

[管理コンソール][Enterprise設定][アプリ][カスタムアプリケーション]
スクリーンショット 2019-02-14 20.03.37.png
新しいアプリケーションを承認を押します。
ポップアップするのでクライアントIDを入れます
スクリーンショット 2019-02-14 20.03.47.png
アクセス対象ユーザーすべてのユーザーになっていればOKです。
スクリーンショット 2019-02-15 17.43.09.png
こちらの場合は設定は以上です。

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

boxにはアプリユーザーという概念があります。
詳細の説明は公式に任せますが、端的にはより堅牢なパターンです。
ただし手順が複雑になります。

アプリの構成で[アプリケーションアクセス]アプリケーションに指定します
スクリーンショット 2019-02-15 17.57.47.png

[管理コンソール][Enterprise設定][アプリ][カスタムアプリケーション]
新しいアプリケーションを承認を押します(この処理自体はすべてのユーザーの場合と同じ)。
スクリーンショット 2019-02-14 20.03.54.png
ポップアップされるものが[このアプリのApp Usersのみ]になっていることに注意してください。
こちらのケースではAdminユーザーからアクセスした場合Cannot obtain user token based on the enterprise configuration for your appみたいなエラーが出るので、以下で作成するアプリユーザーを利用しなければなりません。

アプリユーザーの作成

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

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を取っておいてください。

{
  "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が追加されているはずです。

ユーザー設定で[フォルダの追加または作成]から管理したいフォルダを選んでください。
スクリーンショット 2019-02-15 18.20.40.png
以上で設定は終わりです。

boxのファイル一覧を取得するプログラムの作成

プライベートキーの切り出し

プログラム作成の前に、秘密鍵からprivateKey情報を別のファイルとして切り出します。

$ cat key.json | jq .boxAppSettings.appAuth.privateKey

適当にファイル名をつけて(box_private_keyとか)プロジェクトフォルダ内(input/配下とか)に保存しておきます。

Serverless Frameworkで作成したhandler.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を指定してください。

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の場合は内容を表示する のような処理をする場合は以下のようにすればよいかと思います。

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がイカしている部分としてローカルで簡単に試せるというところがあります。

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:
ドキュメントが地味にわかりにくいので色々試し試しやった感じです。
でも、一回動いてくれるようになると大変便利&色々応用できそうです:thumbsup::thumbsup::thumbsup:

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

7
8
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
7
8

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?