なにこれ
連携会社や他のチームと実績データや素材データをやりとりすることがままあります。
今回はその一例として、「閲覧権限のあるフォルダに定期的にファイルが追加されるので、それを利用して色々処理する定期バッチをLambdaで構築したい」というお話です。
あと、折角なのでServerless Frameworkを使ってみました。
その手順をまとめます。
構成図
こんな感じ。
今回は機微な話なのでストレージ保存とかは割愛して取得するところまでの手順を書きます。
Serverless Framework
デプロイはServerless Frameworkで行います。
Serverless Framework自体の詳細な説明は、公式でも手厚くしてくれているし記事も沢山あるのでここでは省きます。
ザックリ雑には**「CloudFormationとかTerraformで頑張らなくてもコマンド一つでポンとデプロイできるイカしたフレームワーク」**という認識で問題ないかと思います。
「Serverless Frameworkは一旦いらない」「boxの方説明して」という人はこの節はかっ飛ばしてください。
環境設定
事前準備として以下が設定済みであることと仮定します。
- npm
- awscli(AWSアカウントは持っている前提)
-
~/.aws/credentials
を設定 - bundlerが利用可能
-
gem install
でもできるけどね
-
まずはインストール。
npmコマンド一つで簡単に使えるようになります
$ npm install -g serverless
インストールが終わったらsls
と叩いてみましょう(デカイので折りたたんでます)。
展開して中身を確認
$ 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
中身はこんな感じ(デフォルトがこれまたデカイので折りたたんでます)
展開して中身を確認
$ 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関数作っちゃって、あとで*「ねぇ、知ってたらいいんだけどこれ何かわかる?・・・あ、君の?いや、勉強に使うのはとてもいいことなんだけど、試し打ち用のアカウントあるんだからさ。まぁ確かに最近そっちの利用料がちょっと高いかなとは思っていたんだけど、社内の方にあげるのは流石にやめてね。」*ってチームメンバーに諭される心配もなくなりますね!
で、デプロイを実行すると以下のようにモジャモジャターミナルが動きあっという間に完了してしまいます
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...
マネジメントコンソール上ではこんな感じ。
詳細。
ばっちり。
なお、デプロイは「プロジェクトフォルダにあるもの全てが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アプリを作成します。
開発者コンソールにアクセスします
認証設定を選びます。
ここについてですが、ユーザー認証はバッチ処理には向かないし、値を環境変数か何かに持たせるだけで認証できた方が便利やろっていうところからJWTを利用します。
アプリ名はユニークであればなんでもOKです。
ここまでで「アプリの作成」を実行するとアプリができます(できるまでに結構時間がかかります)。
完了すると以下のような画面が出るので、書かれているコマンドを実行してみてください。
curl https://api.box.com/2.0/folders/0 -H "Authorization: Bearer ***" -vvv | jq .
200OK
でファイル情報が返ってくればとりあえず準備OKです。
アプリの構成設定
Developerトークンを利用することで一時的なアクセスとかもできるのですが、今回は認証キーペアを作成してしまいます。
公開キーの追加と管理
で公開/秘密キーペアを生成
を押してください。
キーペアの生成ボタンを押すと秘密鍵がダウンロードされます(今回は資料としてダウンロード画面を出したかったためfirefox使っていますが、ブラウザはなんでもいいです)。
中身はこんな感じです。
{
"boxAppSettings": {
"clientID": "***",
"clientSecret": "***",
"appAuth": {
"publicKeyID": "***",
"privateKey": "-----BEGIN ENCRYPTED PRIVATE KEY-----\n******-----END ENCRYPTED PRIVATE KEY-----\n",
"passphrase": "***"
}
},
"enterpriseID": "***"
}
構成の設定自体はこれでOKで、boxerではこのjsonにある情報を利用します。
なのでキーをなくさないように注意。
Boxの認証設定
ユーザー側からアプリを認証します。
この認証は2種類あるので要件に合わせて対応を変えてください。
- 全てのユーザーから認証させる場合
- アプリユーザーからのみ認証させる場合
全てのユーザーから認証させる場合
こちらの方が設定自体は簡単です。
アプリの構成で[アプリケーションアクセス]
をEnterprise
に指定します。
ここのチェックが外れるので入れなおしてください。
[管理コンソール]
→ [Enterprise設定]
→ [アプリ]
→ [カスタムアプリケーション]
新しいアプリケーションを承認
を押します。
ポップアップするのでクライアントIDを入れます
アクセス対象ユーザー
がすべてのユーザー
になっていればOKです。
こちらの場合は設定は以上です。
アプリユーザーからのみ認証させる場合
boxにはアプリユーザー
という概念があります。
詳細の説明は公式に任せますが、端的にはより堅牢なパターンです。
ただし手順が複雑になります。
アプリの構成で[アプリケーションアクセス]
をアプリケーション
に指定します
[管理コンソール]
→ [Enterprise設定]
→ [アプリ]
→ [カスタムアプリケーション]
で新しいアプリケーションを承認
を押します(この処理自体はすべてのユーザーの場合と同じ)。
ポップアップされるものが[このアプリの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が追加されているはずです。
ユーザー設定で[フォルダの追加または作成]
から管理したいフォルダを選んでください。
以上で設定は終わりです。
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とかで処理をしているのであれば、
- 取引のある相手とディレクトリを共有
- 相手側から定期的にファイルが更新される
- 定期実行で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
まとめ
マジレスするとものすごく大変でした
ドキュメントが地味にわかりにくいので色々試し試しやった感じです。
でも、一回動いてくれるようになると大変便利&色々応用できそうです
[2019/07/09追記]
一応簡単にテンプレートを作成しました
https://github.com/mochisuna/box-list