Function Compute + API GatewayでPOSTのエンドポイントを作る

アリババクラウド のFunction Compute + API GatewayでPOSTのエンドポイントを作ってリクエストボディを拾うのが大変でした。

今回のソースコード

// index.js
const atob = require('atob');

const createMessage = (parameters) => {
  // データを保存するなりなんなりと
}

module.exports.handler = (event, context, callback) => {
  const query = JSON.parse(event.toString('utf8'))
  createMessage(JSON.parse(atob(query["body"])));
  callback(null, { statusCode: 201, body: 'success!' });
};

ここでポイントが2つあります。

  • fc-helperhookは使うことができない
  • リクエストボディを拾うのに手間がかかる

それぞれに関して補足します。

fc-helperhookが使えない

hookを使うときはhook(async (ctx) => {})のようなインターフェイスで使うのですが、API GatewayからFunction Computeにリクエストボディが渡らないためにctx.reqが空になってしまうので使い物になりません。

ですので(event, context, callback) => {})のスタイルを使いましょう。

fc-helper使えないならテストがめんどくさいですね・・・

改善を期待します。

リクエストボディを拾うのに手間がかかる

やたらと遠くにいて拾いにくいです。

> const request = JSON.parse(event.toString('utf8'))
{"body":"CxxxxxxxxxxxxogInxxxxxxxxxxlIGJvZHkgfSB9In0K","headers":{"X-Ca-Api-Gateway":"101CFC98-4F9C-46EF-BBDF-36D280F45D33","X-Real-IP”:”xxx.xxx.xxx.xxx”,”X-Forwarded-Proto":"http","X-Forwarded-For":"xxx.xxx.xxx.xxx","User-Agent":"curl/7.54.0","Content-Type":"application/json","Accept":"*/*","CA-Host":"10ba8f4b3ea74xxxxxx0e5c4-cn-shanghai.alicloudapi.com"},"httpMethod":"POST","isBase64Encoded":true,"path":"/","pathParameters":{},"queryParameters":{}}
> atob(request["body"])
CxxxxxxxxxxxxogInxxxxxxxxxxlIGJvZHkgfSB9In0K

さらに注意

本題とはあまり関係ありませんが、callbackの引数に気をつけましょう。第二引数のオブジェクトのstatusCodebodyは必須です。

API GatewayのmethodをPOSTにするのを忘れないようにしましょう。template.ymlを使いまわしてGETのままで少し詰まりました。

Function Computeの自動テスト

Function Computeに対するテストはfc-helpertestを使って行います。

ここではアクセスがあったらhello world!\nというbodyと200のステータスを返すエンドポイントを想定します。

// index.js
const { hook } = require('fc-helper');

exports.handler = hook(async (ctx) => {
  ctx.body = 'hello world!\n';
});

このコードに対するテストコードは以下のようになります。

// test/index.test.js
const assert = require('assert');
const { test } = require('fc-helper');
const index = require('../index.js')

describe('hello world', () => {
  it('should return correct response', async () => {
    const res = await test(index.handler).run('{}', '{}');
    assert.equal(res.statusCode, 200, 'status code')
    assert.equal(res.body, 'hello world!\n', 'http body')
  });
});

めちゃくちゃ便利ですね。

serverless frameworkを使ってFunction ComputeとAPI Gatewayにデプロイする

serverlessを使う

github.com

serverlessはlambdaなどの各サービスの上へ簡単にサーバレスなアプリケーションを作成/デプロイできるCLIツールです。ロゴがカッコいいですね。

serverlessのプラグインとしてアリババクラウドが出しているserverless-aliyun-function-computeがあるのでこれを使います。

まずyarn global add serverlessでserverlessをインストールしておきましょう。

aliyun-nodejsのインストール

$ serverless install --url https://github.com/aliyun/serverless-function-compute-examples/tree/master/aliyun-nodejs
Serverless: Downloading and installing "aliyun-nodejs"...
Serverless: Successfully installed "aliyun-nodejs"
$ ls -lah aliyun-nodejs
total 32
drwxr-xr-x  6 ryouta  staff   192B Aug  7  2018 .
drwxr-xr-x  3 ryouta  staff    96B Aug  7  2018 ..
-rw-r--r--  1 ryouta  staff    86B Aug  7  2018 .gitignore
-rw-r--r--  1 ryouta  staff   191B Aug  7  2018 index.js
-rw-r--r--  1 ryouta  staff   322B Aug  7  2018 package.json
-rw-r--r--  1 ryouta  staff   712B Aug  7  2018 serverless.yml
$ serverless plugin install --name serverless-aliyun-function-compute
Serverless: Installing plugin "serverless-aliyun-function-compute@latest" (this might take a few seconds...)
Serverless: Successfully installed "serverless-aliyun-function-compute@latest"

credentialの設定をする

アリババクラウド、各サービスがそれぞれで秘密情報の場所とフォーマットを設定しているから同じ内容のファイルがバラけて気持ち悪い。

デフォルトのファイルの場所は~/.aliyuncli/credentialsです。変更も可能です。

$ mkdir ~/.aliyuncli
$ vim ~/.aliyuncli/credentials
$ cat ~/.aliyuncli/credentials
[default]
aliyun_access_key_id = xxxxxxxxxxxxxxx
aliyun_access_key_secret = xxxxxxxxxxxxxxxxxxx
aliyun_account_id = xxxxxxxxxxxxxxx

deployを実行する。

$ serverless deploy
Serverless: Packaging service...
Serverless: Excluding development dependencies...
Serverless: Compiling function "hello"...
Serverless: Finished Packaging.
Serverless: Log project sls-xxxxxxxx-logs already exists.
Serverless: Log store sls-xxxxxxxxxx-logs/aliyun-nodejs-dev already exists.
Serverless: Log store sls-xxxxxxxxxx-logs/aliyun-nodejs-dev already has an index.
Serverless: RAM role sls-aliyun-nodejs-dev-exec-role exists.
Serverless: RAM policy fc-aliyun-nodejs-dev-access exists.
Serverless: RAM policy fc-aliyun-nodejs-dev-access has been attached to sls-aliyun-nodejs-dev-exec-role.
Serverless: Service aliyun-nodejs-dev already exists.
Serverless: Bucket sls-xxxxxxxxxxx already exists.
Serverless: Uploading serverless/aliyun-nodejs/dev/xxxxxxxxxxx-2018-08-07T04:01:01.158Z/aliyun-nodejs.zip to OSS bucket sls-xxxxxxxxxxxxx...
Serverless: Uploaded serverless/aliyun-nodejs/dev/xxxxxxxxxxxx-2018-08-07T04:01:01.158Z/aliyun-nodejs.zip to OSS bucket sls-xxxxxxxxxxxxx
Serverless: Updating function aliyun-nodejs-dev-hello...
Serverless: Updated function aliyun-nodejs-dev-hello
Serverless: RAM role sls-aliyun-nodejs-dev-invoke-role exists.
Serverless: Attaching RAM policy AliyunFCInvocationAccess to sls-aliyun-nodejs-dev-invoke-role...
Serverless: Attached RAM policy AliyunFCInvocationAccess to sls-aliyun-nodejs-dev-invoke-role
Serverless: Creating API group aliyun_nodejs_dev_api...
Serverless: Created API group aliyun_nodejs_dev_api
Serverless: Creating API sls_http_aliyun_nodejs_dev_hello...
Serverless: Created API sls_http_aliyun_nodejs_dev_hello
Serverless: Deploying API sls_http_aliyun_nodejs_dev_hello...
Serverless: Deployed API sls_http_aliyun_nodejs_dev_hello
Serverless: GET http://xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx-cn-shanghai.alicloudapi.com/foo -> aliyun-nodejs-dev.aliyun-nodejs-dev-hello
$ 
$ serverless invoke -f hello
Serverless: Invoking aliyun-nodejs-dev-hello of aliyun-nodejs-dev
Serverless: {"statusCode":200,"body":"{\"message\":\"Hello!\"}"}

serverless invokeに渡す必要がある-fオプションは、jsファイル中のhandler関数の名前です。(ここではhello)

出たエラー

serverless deployを実行した時に以下の2つのエラーが発生しましたが、ログを見ると特にリソース自体は作成されていたので何もせずdeployを再実行しました。私は3回目で正常に成功しましたw

きっとプラグイン側のバグでしょう。

POST /services failed with 400. requestid: 5a98e7ee-8159-cc22-0d5a-18bec02a9cc8, message: project 'sls-5326127221743842-logs' does not exist.

The role not exists:sls-aliyun-nodejs-dev-invoke-role

確認

コンソールで確認して見ると、Function ComputeとAPI Gatewayにリソースが作られていました。

f:id:asmsuechan:20180807130720p:plain

f:id:asmsuechan:20180807130742p:plain

funserverlessどちらを使うべきか

上記のように謎に失敗する時があることや鍵の指定方法を考えると、funがいいと思います。公式なので変更にも強そうです。

funだとタイムトリガーなど他のトリガーにも対応できますし。

asmsuechan.hatenablog.com

タイムトリガーでFunction ComputeからSlackに投稿する

やること

この記事ではFunction ComputeからSlackに投稿する最小のコードを紹介します。

関数、トリガーの作成

まずFunction Computeでタイムトリガーの関数を作成します。

以下のコードがFunction ComputeからSlackに現在時刻を投稿する最小の(多分)コードになります。webhook urlが分からない方ははググってください。私はいつもググってます。

const { IncomingWebhook } = require('@slack/client');

module.exports.handler = function(request, context, callback) {
  const url = 'https://hooks.slack.com/services/**********/*******************'; // YOUR_WEBHOOK_URL
  const webhook = new IncomingWebhook(url);
  const date = new Date();

  webhook.send(date.toString(), (err, res)  => {
    const message = err || res;
    callback(null, message);
  });
};

handlerの関数の引数がHTTPトリガーのものと違っていることに注意してください。私はここで結構ハマりました。

なお、ローカルでnpm installしてnode_modulesごとアップロードしなければ@slack/clientは動きません。

タイムトリガーを1分ごとに設定して動くことが確認できました。

f:id:asmsuechan:20180806143642p:plain

まとめ

タイムトリガーの「動く最小のコード」が見当たらなかったので書きました。

Function ComputeとAPI GatewayにTravis CIからデプロイする

GitHubにpushしたFunction Compute用のコードをTravis CIからfunを使ってデプロイします。

やること

Function Compute用のjsファイルをTravis CIからFunction ComputeとAPI Gatewayに自動でデプロイします。

f:id:asmsuechan:20180731173754p:plain

準備

以下を参考にしてまずローカルからfunを使ってFunction Computeにデプロイできるようになっていてください。

Function ComputeとAPI Gatewayのデプロイツールであるfunを使う - asmsuechan’s blog

.travis.ymlの追加

fun deployコマンドによって、node_modules/を含めてzipに固めてFunction Computeにデプロイしてくれます。

アクセスキー等は環境変数に入れておけばコマンド実行時に拾ってくれます。travis encryptで.travis.ymlに追加しておきましょう。

$ travis encrypt -r asmsuechan/fc_deploy_travis ACCESS_KEY_ID=xxxxxxxxxxxxxx --add
$ travis encrypt -r asmsuechan/fc_deploy_travis ACCESS_KEY_SECRET=xxxxxxxxxxxxxxxxxxxxx --add
$ travis encrypt -r asmsuechan/fc_deploy_travis ACCOUNT_ID=xxxxxxxxxxxx --add # エンドポイントの数字部分

funはnode.jsのバージョン8以上じゃないと動きません。

language: node_js
node_js:
- 8
install:
- yarn
script:
- yarn run lint
before_deploy:
- yarn add global @alicloud/fun
deploy:
  provider: script
  skip_cleanup: true
  script:
  - fun deploy
  on:
    repo: asmsuechan/fc_deploy_travis
env:
  global:
  - secure: xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
  - secure: xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
  - secure: xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
  - DEFAULT_REGION=cn-shanghai

REGIONじゃなくてDEFAULT_REGIONにしないとエラーが出ます。

Waiting for service fc to be deployed...
TypeError: "config.region" must be passed in
    at new Client (/home/travis/build/asmsuechan/fc_deploy_travis/node_modules/@alicloud/fc2/lib/client.js:54:13)
    at getFcClient (/home/travis/build/asmsuechan/fc_deploy_travis/node_modules/@alicloud/fun/lib/deploy/deploy-support.js:39:10)
    at <anonymous>
Script failed with status 255

OSSにTravisCIから自動アップロードする

Alibaba CloudのOSSにTravisCIから自動でアップロードします。

やること

vue-cliで作成されたVue.jsのプロジェクトを、GitHubにプッシュされたらtravisでビルド後ossutil cp dist/ oss://test-cli --recursive -i xxxxxxxxxxx -k xxxxxxxxxxxxxxx -e oss-cn-shanghai.aliyuncs.comを実行してOSSにアップロードする。

f:id:asmsuechan:20180730093141p:plain

準備

デプロイ用のRAMユーザー、OSSバケットtravisのアカウント、travisコマンド(gem install travis)、GitHubリポジトリが必要です。適当に準備しておきましょう。

参考

一度ossutilをローカルで試してみたほうが良さそうです。

asmsuechan.hatenablog.com

asmsuechan.hatenablog.com

.travis.ymlの追加

S3みたいにtravisコマンドが自動でいい感じにしてくれるわけでもtravisがproviderとして準備してくれているわけでもないのでprovider: scriptとしてゴリゴリ書く必要があります。

追加した.travis.ymlは以下のようになりました。

language: node_js
node_js:
- 7
go:
- 1.9.x
install:
- yarn
script:
- yarn run build
before_deploy:
- go get github.com/aliyun/ossutil
deploy:
  provider: script
  skip_cleanup: true
  script:
  - ossutil cp dist/ oss://test-cli --recursive -f -i $ACCESS_KEY_ID -k $ACCESS_KEY_SECRET -e oss-cn-shanghai.aliyuncs.com
env:
  global:
  - secure: qEjBMfJ5NLNN8dNDGetvi1C8/QCTOfmukY9RGCsOpMlaHUZVdFCAMBtmbuHMcRqJSyu/fBEOL2Nj1dy66EYOFWNdy6Nit7urvjn6ch0RLbdsVC/suuIZv4ROtnYRMIORvvyvtLw7C3pt19bYeKY/iGuP9UizCuu+Q2nz0yoYDWg7HT+PPuYtCwZBFUe8OhQppQNdJw4/WdLr8nAPAP0slR0gIt6Vr54KjRjLPldIuKgsQVahNNaZdgGJ6LBBJuk1C7xfywOF04VfpKzE+3DlFt8Z7e8eP69+fJ5KUl+8RRM5OOeqDdElNeUm0yfnyvUfeMlRj8U9jVja5bdIXqtIIXpngxAM9JrwPhrnh4m1+DYtsPAyk7SiOGeBkBxLJdxvcHyMepxWOgXkNl1kqY6COGWLrtrTN7ljPWxDcZcgIRPumFxddnI0qgYCB6kG4bJHpOsO0CbaqlGggFmt0S03HW2GBX+ccZK/4bCZ83teoeANuvkONVwqRfiUgdmxJtSnr1lSsklpcu90fs2lDmXmbYfgyfbtPME02/pTRSVYE1LrWSzZKDBlAmoDUfsv9Xc6J0/Q5rzSMFV73835tbAR95zvUwiCYcGWvx/c+yvzcRO7eAVQoJr4Itpm06uQGXi96NeNR9ROChM3/GNs71llV8qtuO5VW83iRfLn2JrZZbU=
  - secure: rWQ+Jen7PQBT69x0Vqu4sZqZY/PbxXFe7+gyQcSDpEFCzZPdPzPDra599G8ft4cxs6FnqXINH88gF++cW1K51o4CU6zRPEML556rPBjATtn7Dy2utoWurWU4JKZZTwTgKkCOgSG/geKRZnq7qUw5dGQIn8ULIrWfqRG5CGljzLk1WqdYgc6jI1vuZLaXcsnyIdLqv5IVKpNM6KDlLmYhZaX8U9A/YFwtCTbEHo7fBlhV2S0bAi6qnMhsWozxY6ATX5pkZtX8RQaZATku25dyNV4cvX6Y9+GLj/9zB6BWtbcbfWU/6sWVbzwpXpo/kMtWpqYsMiT9mIEzAyNPyIhdi1mGo28w/jKh7rkNGqHjdYfgouINK7VCmTbV+hJVVlT7RmWf1YwXIofl3X89QE3/H83LxrToL/ze6qEBBZ+G4rTTcEzxJqqC4rggLOOmB4OYjhaMe1i0RgLtD3jlIMAsvxLbzV9kBdeMLHPpeq8hK2ClVomt8uXT7qadR+WwqPxexl0g/+mEpdCyXPv1nvA9iHh1l5IPDQvpnE7lTlEiplMuy9E3p/Ywq3Di3Xr/yhgUJZgGalwMD/0750XQapAldg0bV1UVsZLJsl3snVZhTedYOKI4+9w/irTtk+ET04VhuCVASWuAVBztfaWF0olh0YxCLwDZsxJhbDtB7Ni/8Q0=

ちなみにossutilコマンドを実行するとき-fオプションがないとcp: overwrite "oss://test-cli/static/css/"(y or N)?で動きを止めてしまいます。

鍵情報の暗号化

travisciにはそのままGitHubで公開したくない鍵情報などを暗号化してくれる機能があります。

travis encryptコマンドが環境変数を暗号化して.travis.ymlに追加してくれます。

$ travis encrypt ACCESS_KEY_ID=xxxxxxxxxxxxxxxx --add
$ travis encrypt ACCESS_KEY_SECRET=xxxxxxxxxxxxxxxxxxxxxxx --add

まとめ

今回作成したリポジトリはこちらになります。

github.com

S3にTravisCIから自動でアップロードする

travis ciでVue.jsのプロジェクトをビルドしてdist/をS3にアップロードするようにします。

.travis.ymlの設定

.travis.ymlの設定は、travis setup s3コマンドが色々と便利にやってくれます。入れていない方はインストールしましょう。

$ gem install travis
$ travis version
1.8.8
$ travis setup s3                                                                                                        [master]
Detected repository as asmsuechan/asmsuechan.com, is this correct? |yes| yes
Access key ID: xxxxxxxxxxxxxxxxxxxx
Secret access key: ****************************************
Bucket: s3://asmsuechan.com
Local project directory to upload (Optional):
S3 upload directory (Optional): dist
S3 ACL Settings (private, public_read, public_read_write, authenticated_read, bucket_owner_read, bucket_owner_full_control): public_read
Encrypt secret access key? |yes| yes
Push only from asmsuechan/asmsuechan.com? |yes| yes

実際はこのままじゃ動かないので色々追記して以下のようになりました。

# .travis.yml
language: node_js
node_js:
  - 7
install:
- yarn
script:
- yarn run lint
- yarn run build
deploy:
  provider: s3
  skip_cleanup: true
  access_key_id: xxxxxxxxxxxxxxxxxxxx
  secret_access_key:
    secure: xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
  bucket: asmsuechan
  region: ap-northeast-1
  local_dir: dist
  acl: public_read
  on:
    repo: asmsuechan/asmsuechan.com

bucketの値にはs3://を含めていはいけないようです。

やった事

以下の3つのことをしました。

  • nodejsのバージョンを7に指定。
  • regionをap-northeast-1に指定。
  • local_dir: distを追加
  • skip_cleanup: trueを追加

それぞれちょっと詳しく書いていきます。

nodejsのバージョンを7にする

何も指定しないとnodejsのバージョンは0.10.48らしいです。これだとyarnが動かない。

Node.js version v0.10.48 does not meet requirement for yarn. Please use Node.js 4 or later.

regionをap-northeast-1にする

regionを指定しないと以下のエラーが出ます。デフォルトのregionはus-east-1らしいです。

The bucket you are attempting to access must be addressed using the specified endpoint.
Please send all future requests to this endpoint. (AWS::S3::Errors::PermanentRedirect)

skip_cleanup: trueを追加

local_dir: distを追加しても、そのままではデプロイ時にはscriptで生成されたdistディレクトリは消えてしまうようです。

出たエラーをググったらこのIssueコメント に行き着いたのでskip_cleanup: trueを追加しました。

/home/travis/.rvm/gems/ruby-2.2.7/gems/dpl-s3-1.9.8/lib/dpl/provider/s3.rb:56:in `chdir': No such file or directory @ dir_chdir - dist (Errno::ENOENT)

できたもの

私のポートフォリオサイトもどきができました。 http://asmsuechan.com/
-> https化しました。 https://asmsuechan.com

ちょっと注意

  • デプロイ専用のIAMユーザーを作りましょう
  • 403が出る方はポリシーの適用を忘れていませんか?ここなどを参考にしてみましょう

参考

S3 Deployment - Travis CI