Serverless + TypeScript + AWS Lambdaの開発環境構築
概要
Serverless FrameworkとはLambda関数などを開発・デプロイするためのツールです。今回はServerless + TypeScript + AWS Lamdbaのローカル開発環境の導入手順について書きます。
プロジェクトの作成
はじめにServerlessの環境構築をします。今回はAWS LamdbaのNode.jsを利用するため、templateはaws-nodejs
で作成します。pathのサービス名は適宜変更してください。
$ npx serverless create --template aws-nodejs --path my-service _______ __ | _ .-----.----.--.--.-----.----| .-----.-----.-----. | |___| -__| _| | | -__| _| | -__|__ --|__ --| |____ |_____|__| \___/|_____|__| |__|_____|_____|_____| | | | The Serverless Application Framework | | serverless.com, v1.36.0 -------'
ドキュメントにはnpm install -g serverless
と書かれていますが、npxを使用することをオススメします。
このときのserverlessのバージョンはv1.36.0
でした。
次にnpm packageを作成します。Serverlessのライブラリは必須ではないですが、CLIのバージョン固定するためにインストールしておきます。
$ cd my-service $ yarn init $ yarn add -D serverless
ローカル開発環境の構築
次にserverless-offline
のライブラリでローカル環境の構築を行います。
$ yarn add -D serverless-offline
serverless.yml
にserverless-offline
とテスト用にhelloを以下のように追記しておきます。
plugins: - serverless-offline functions: hello: handler: handler.hello events: - http: path: hello method: GET
起動して以下のログが出れば成功です。
$ yarn sls offline start Serverless: Offline listening on http://localhost:3000
package.jsonにスクリプトを追加しておくと便利ですね。
curlで叩いてみます。
$ curl http://localhost:3000/hello {"message":"Go Serverless v1.0! Your function executed successfully!",... }
上記のログが出れば設定完了です。
デプロイ
次にAWS Lambdaへデプロイします。serverless.yml
に以下を追記します。region、profile名を適宜変更してください。
provider: name: aws runtime: nodejs8.10 region: us-west-2 profile: my-service
AWSの認証キーは必要に応じて~/.aws/credentials
に追記します。
[my-service] aws_access_key_id = <your-key-here> aws_secret_access_key = <your-secret-key-here>
以下のコマンドでデプロイします。
$ yarn sls deploy
AWSコンソール上に表示されていれば完了です。
TypeScriptの設定
次にTypeScriptの設定を行います。設定はとてもシンプルです。
$ yarn add -D typescript serverless-plugin-typescript
offlineと同様に以下の記述を追記します。
plugins: - serverless-offline - serverless-plugin-typescript
handler.js
をhandler.ts
に変更し、以下のように書き換え、ローカルサーバが正常に立ち上がれば完了です。
export const hello = async (event, context) => ({ statusCode: 200, body: JSON.stringify({ message: 'Go Serverless v1.0! Your function executed successfully!', input: event, }), });
まとめ
まだ使い始めたばかりですが、Serverless FrameworkでAWS Lambdaの開発・テストがローカル環境で行うことができとても便利です、ぜひ試してみてください。
リンク
MetabaseをElastic Beanstalkにデプロイする
概要
Metabaseを導入したので、それの手順についてまとめます。
Metabaseのデプロイ
こちらのリンクからAWSにデプロイするボタンがあるのですが、うまく動かなかったため今回は手動でセットアップします。
まずはじめにElastic Beanstalkのサーバを構築します。プラットフォームは事前設定済みプラットフォームのDockerを選択します。
次にアプリケーションコードをアップロードします。ソースコードは既にS3上に上がっているためS3のURLを指定します。
https://s3.amazonaws.com/downloads.metabase.com/v0.31.1/metabase-aws-eb.zip
最新バージョンはこちらで確認できます、適宜URLのバージョン部分を書き換えてください。
リージョンのエラーが出る場合はソースコードをローカルにダウンロードし、ローカルファイルからダウンロードしたソースコードを選択しアップロードします。 「更にオプションを追加」をクリックし適宜設定し、環境を構築します。
Auroraの設定
今回は既存のAurora上にMetabase用のDBを作成します。新しくDBを作成する場合はこちらを参考にしてください。
ルートユーザでログインし、Aurora上にMetabase用のデータベースとユーザを作成します。
CREATE DATABASE metabase CHARACTER SET utf8mb4; CREATE USER 'metabase.admin'@'%' IDENTIFIED BY 'password'; GRANT SELECT ON *.* TO 'metabase.admin'@'%' WITH GRANT OPTION;
必要に応じて、対象となるデータベース上にReadonlyユーザを作成してください。
環境変数の設定
スタートガイドに従ってElastic Beanstalk上に環境変数を設定します。上記で生成したユーザを設定してください。
設定を更新して以下の画面が表示されれば準備完了です。
まとめ
Metabaseとても便利そうなのでぜひ使ってみてください。
ソース
VM moduleを利用してLambdaのような機能を提供する
概要
Express + VMモジュールを使用し、Lambdaのような機能を提供するサーバの構築方法、またVMの注意点についてまとめます。
vmとは
VMとは同プロセス上で別のNodeスクリプトの実行をサポートするモジュールです。eval
に似ていますが、Contextの値を外から代入できる大きな違いがあります。
また同プロセス上で動くので値の共有が可能です。以下が実行例です。
const vm = require('vm'); const code = `() => num + 1`; const func = vm.runInNewContext(code, { num: 1 }); func(); // 2
このようにVM上で生成された関数をVM外で使用することができます。
サーバ概要
下図がざっくりとしたサーバ構成図です。
クライアントがリクエストとともにクライアントバージョンを送信し、そのバージョンとマッチしたVMスクリプトをDBから取得します。また一度取得したスクリプトはパフォーマンス向上のためオンメモリに保持しています。
そのスクリプトをVM上で起動し、サーバのAPIをContext経由で渡すことでスクリプト上からサーバへのリクエストを可能にします。 以下が実行例です。
// script1.js handlers.getData = async () => ({ a: await apis.getDataA(), b: await apis.getDataB(), c: await apis.getDataC(), });
// vm.js const vm = require('vm'); const fs = require('fs'); const path = require('path'); const script = fs.readFileSync(path.join(__dirname, 'script1.js')); // 実際はDBから取得 const apis = { async getDataA() { return 'a'; }, async getDataB() { return 'b'; }, async getDataC() { return 'c'; }, }; const handlers = {}; const context = { apis, handlers, }; vm.runInNewContext(script, context); (async () => { const res = await handlers.getData(); console.log(res); // { a: 'a', b: 'b', c: 'c' } })();
$ node vm.js
このようにVM上で作成されたhandlersをVM外で実行することにより、一つのクラスタで複数のバージョンの異なるクライアントをサポートしています。
使用上の注意
Objectの扱い
JavaScriptでは全てのオブジェクトはObject型を継承します。例えばArrayはObject型を継承しているため、以下のようにinstanceof
がtrue
になることが確認できます。
const array = []; console.log(array instanceof Array); // true console.log(array instanceof Object); // true
しかしVM上ではVM外とは異なるObject型が生成され、VM内の全てのObject型はVM外とは異なるObject型を継承します。
const vm = require('vm'); const code = `() => []`; const func = vm.runInNewContext(code, {}); const res = func(); console.log(res instanceof Array); // false console.log(res instanceof Object); // false
逆も同様な問題があります。
const vm = require('vm'); const code = `() => array instanceof Array`; const func = vm.runInNewContext(code, { array: [] }); func(); // false
Arrayの場合はArray.isArray
やObject.prototype.toString
を使用することで、Arrayであるかどうかの判別が可能です。
console.log(Array.isArray(res)); // true console.log(Object.prototype.toString.call(res)); // [object Array]
しかしVM上で生成された値をサードパーティのライブラリに代入する際、内部でinstandeof
等を使用している可能性があるためこの方法が安全とは言えません。
エラーハンドリング
エラーハンドリングでも同様な問題があります。
const vm = require('vm'); const code = `() => { throw new TypeError() }`; const func = vm.runInNewContext(code, {}); try { func(); } catch (e) { if (e instanceof TypeError) { console.log('type error!'); } else if (e instanceof Error) { console.log('common error!'); } else { console.log('unkown error!'); } } // unknown error!
つまりVM上で生じたTypeError
はinstnaceof
では判別することができません。
util.inheritsを使った解決方法
util.inherits
とは、Node上でprototype関数を継承するために使われる関数です。
現在はextendsシンタックスを使用してclassを継承することが主流ですが、classの実態はprototype関数であり、util.inherits
を使用して拡張することができます。
基本的な使い方は以下の通りです。
const util = require('util'); // prototype function function MyError1() {} new MyError1() instanceof Error; // false util.inherits(MyError1, Error); new MyError1() instanceof Error; // true // class class MyError2 {} new MyError2() instanceof Error; // false util.inherits(MyError2, Error); new MyError2() instanceof Error; // true
これを踏まえVM上のclassの継承関係を解決していきます。以下のように呼び出し元のclassを渡し、そのclassをVM内で継承することにより解決することができます。
const util = require('util'); const inherit = 'util.inherits(Array, classMap.Array);'; const code = `() => []`; const func = vm.runInNewContext(`${inherit}${code}`, { util, classMap: { Array } }); const res = func(); console.log(res instanceof Array); // true console.log(res instanceof Object); // true
他のclassも同様な問題があるため、こちらから適宜追加してください。
まとめ
NodeでLambdaのような機能を提供するサーバの構築方法を紹介しました。 750KBくらいのスクリプトを使用していますが、VMによるオーバーヘッドは5ms程度で無視できるレベルかと思います。
VMは様々な用途に使用でき、Nodeの幅が広がる面白いモジュールです。 VM使ったライブラリも公開してます、ぜひ試してみてください。
リンク
Node.js with TypeScriptのCode Coverageを計測する
概要
Node.js with TypeScriptでcode coverageを計測できるようにしたので、その導入手順について書きます。
NYCのセットアップ
はじめにNYCのセットアップします。 NYCとはIstanbulのコマンドラインツールで、Istanbulとはcode coverageを計測してくれるツールです。
まずはnyc
とts-node
をinstallします。
$ npm i -D nyc ts-node mocha
次にnyc
のtaskをpackage.json
のscriptに追加します。今回はtestファイルをtest directory配下に置いています。
// package.json { ... "scripts": { "test": "nyc mocha test/*" }, ... }
次にnyc
のconfigをpackage.json
に設定します。必要に応じて設定を変更してください。
// package.json { ... "nyc": { "include": [ "lib/**/*.ts" ], "extension": [ ".ts" ], "require": [ "ts-node/register" ], "reporter": [ "text", "text-summary", "html" ], "sourceMap": true }, ... }
opn-cliの設定(オプション)
coverage計測後、ぱっと見れるようにopn-cli
の設定も一緒にpackage.json
に追加します。
$ npm i -D opn-cli
// package.json { ... "scripts": { "test": "nyc mocha test/*", "open:cov": "opn coverage/index.html" }, ... }
codecovの設定(オプション)
codecovで可視化したいのでcodecov
の設定も追加します。codecov
はCircleCIから自動的に走らせています。
$ npm i -D codecov
// package.json { ... "scripts": { "test": "nyc mocha test/*", "codecov": "nyc report --reporter=lcovonly && codecov", "open:cov": "opn coverage/index.html" }, ... }
実行
後はnpm testを実行すると結果を見ることができます。nyc
のHTMLはbranchの詳細まで見れるのでとても便利です。
$ npm test
$ npm run open:cov
まとめ
CodeのCoverageが全てではないですが、テストを書く習慣・目標値の設定としてはとても良い指標だと思います。 これに加え、質の良いテストがかけるように心がけていきたいです。
ちなみにNYCはNew York Cityだそうです。
リンク
https://istanbul.js.org/ https://github.com/istanbuljs/nyc http://azimi.me/2016/09/30/nyc-mocha-typescript.1.html
direnvを使ってgithubのアカウントを複数設定する
はじめに
仕事のGithubアカウントと個人のGithubアカウントが別のため、direnvを使用してディレクトリごとに別のアカウントを使用できるようにしました。
direnvとは
direnvとはディレクトリごとに.envrc
を設定することによりディレクトリごとに環境を変更することができます。Githubのsshだけでなく色々な用途に使えます。
direnvのインストール
ドキュメントに従って以下を実行します。
$ git clone https://github.com/direnv/direnv
$ cd direnv
$ make install
私の環境では最新版のv2.14.0
がうまく動かなかったため、v2.7.0
をインストールしました。
$ git clone https://github.com/direnv/direnv
$ cd direnv
$ git checkout v2.7.0
$ make install
.envrc
の設定
まずそれぞれのアカウント用に別のssh keyを用意します、ここでは仮に~/.ssh/id_rsa_github1
, ~/.ssh/id_rsa_github2
とします。
direnvではディレクトリ配下全ての設定を書き換えることができるので、作業ディレクトリをそれぞれ別に~/work
, ~/personal
とします。
次にそれぞれのディレクトリにssh keyを設定します。
GIT_SSH_COMMANDの環境を設定することで、gitコマンドを入力した際に自動的にこのコマンドが呼ばれるため.envrc
に以下のコードを追加します。
# ~/work/.envrc export GIT_SSH_COMMAND="ssh -i ~/.ssh/id_rsa_github1"
# ~/personal/.envrc export GIT_SSH_COMMAND="ssh -i ~/.ssh/id_rsa_github2"
direnv: error .envrc is blocked. Run `direnv allow` to approve its content.
のエラーが出た際には
$ direnv allow
を実行すると有効にすることができます。
まとめ
複数のGithubアカウントを扱うことができるようになりましたが、芝が分散してしまうのでとても悲しいです。個人のアカウントが使える時代が来ることを祈っております。
リンク
Async HooksとAsync Resourcesの導入
概要
この記事はNode.js Advent Calendar 2017 15日目の記事です。
カナダから非同期で失礼します。
Async hooksとはNode.jsの非同期イベントをトレースすることができるネイティブライブラリです。まだ試験段階なのでAPIが変わる可能性がありますが、Async Hooksに入門したので、Async HooksとAsync Resouresの使い方、実際の使用例の考察をまとめました。
Node.jsのバージョンはv9.3.0を使用しています。
Async Hooksの基本的な使い方
まずは下記のコードを見てみます。このコードはAsync Resourcesが1秒ごとに生成され、破棄される様子を見ることができます。
const fs = require('fs'); const util = require('util'); const asyncHooks = require('async_hooks'); const hooks = { // Resourceが生成されるときに呼ばれる init(asyncId, type, triggerAsyncId, resource) { const obj = { asyncId, type, triggerAsyncId, resource }; fs.writeSync(1, `init\n${util.format(obj)}\n`); }, // Resourceのcallbackが呼ばれる直前に呼ばれる before(asyncId) { fs.writeSync(1, `before\t${util.format({ asyncId })}\n`); }, // Resourceのcallbackが終了したときに呼ばれる after(asyncId) { fs.writeSync(1, `after\t${util.format({ asyncId })}\n`); }, // Resourceが破棄されるときに呼ばれる destroy(asyncId) { fs.writeSync(1, `destroy\t${util.format({ asyncId })}\n`); }, // Promiseのresolveが呼ばれるタイミングで呼ばれる promiseResolve(asyncId) { fs.writeSync(1, `promiseResolve\t${util.format({ asyncId })}\n`); } }; // Hookの生成 const asyncHook = asyncHooks.createHook(hooks); // Hookを有効にする asyncHook.enable(); // 1秒おきにResourceの生成 (function start() { // 実行中のasyncId const eid = asyncHooks.executionAsyncId(); // 呼び出し元のasyncId const tid = asyncHooks.triggerAsyncId(); console.log(`start\teid: ${eid} tid: ${tid}`); setTimeout(start, 1000); })(); // 10秒後にhookを無効にする setTimeout(() => asyncHook.disable(), 10000);
createHook
に任意の関数を渡すことで、非同期イベントの検知が簡単にできるようになります。特別難しい機能は無いので、それぞれの用語については下記の表にまとめました。
async_hooks
用語 | 型 | 説明 |
---|---|---|
createHook | Function | Hookの作成 |
executionAsyncId | Function | 実行中のasyncIdの取得 |
triggerAsyncId | Function | 呼び出し元のasyncIdの取得 |
createHook
用語 | 型 | 説明 |
---|---|---|
init | Function | Resoureが生成される時に呼ばれる |
before | Function | Resourceがcallbackを呼ぶ直前で呼ばれる |
after | Function | Resoureがcallbackを呼び終えた直後に呼ばれる |
destroy | Function | Resoureが破棄される時に呼ばれる |
promiseResolve | Function | PromiseのResourceでresolve が呼ばれる時に呼ばれる |
asyncId | number | Async Resourceに割り当てられたid |
triggetAsyncId | number | 呼び出し元のasyncId |
enable | Function | Hookの有効化 |
disable | Function | Hookの無効化 |
⚠使用上の注意⚠
createHook
の関数内でエラーが発生した際はuncaughtException
でキャッチすることはできず、スタックトレースを残してプロセスが終了されます。
またconsole.log
, process.stdout.write
は非同期オペレーションなためAsync Resourceが生成されます。Hook内の各関数ではfs.writeSync
を使用してください。
スタックトレースを遡る
何か面白いことできないかなぁと探っていたところ、興味深いコードを見つけたので、これをベースにAsync Resouresの誕生から終焉までの軌跡を表示するようにしてみました。 実際のログはこちらです。コードは以下のとおりです。
const delay = util.promisify(setTimeout); const DELAY = 1000; const map = new Map(); function init(asyncId, type, triggerAsyncId, resource) { const obj = { asyncId, type, triggerAsyncId, resource }; Error.captureStackTrace(obj, init); map.set(asyncId, [obj.stack, triggerAsyncId]); fs.writeSync(1, `init\n${util.format(obj)}\n`); } function destroy(asyncId) { fs.writeSync(1, `destroy\tasyncId: ${asyncId}\n`); showStackTrace(asyncId); map.delete(asyncId); } function showStackTrace(asyncId) { const array = map.get(asyncId); if (!array) { return; } const [stack, tid] = array; fs.writeSync(1, `stack: \tasyncId: ${asyncId} triggetAsyncId: ${tid}\n${stack.replace(/(.*)\n/, '')}\n`); showStackTrace(tid); } asyncHooks.createHook({ init, destroy }).enable(); let promise = delay(DELAY); for (let i = 0; i < 10; i++) { promise = promise.then(() => delay(DELAY)); }
スタックトレースを遡ることができるので、デバッグに使えるかもしれません。
メモリリークの検知
以下のコードはAsync Resousesが生成されたものの、リソースが破棄されない例です。map
のサイズが次第に大きくなっていくことが一目でわかるので、メモリリークの検知の早期発見に使えるかもしれません。
const map = new Map(); function init(asyncId) { map.set(asyncId, 1); fs.writeSync(1, `init\tasyncId: ${asyncId} mapSize: ${map.size}\n`); } function destroy(asyncId) { map.delete(asyncId); fs.writeSync(1, `destroy\tasyncId: ${asyncId} mapSize: ${map.size}\n`); } asyncHooks.createHook({ init, destroy }).enable(); const queue = []; (function start() { new Promise(resolve => queue.push(resolve)); setTimeout(start, 100); })();
Async Resourcesの基本的な使い方
AsyncResource
クラスを使用することで、任意の非同期イベントを作成することが可能です。emitBefore
とemitAfter
はセットなので発火する際には必ず両方を呼ぶ必要があります。
const { AsyncResource } = require('async_hooks'); // 自動的にinitが呼ばれる const asyncResource = new AsyncResource('Async'); // Callbackを呼ぶ直前に呼ぶ asyncResource.emitBefore(); // Callbackが終了した直後に呼ぶ asyncResource.emitAfter(); // AsyncResourceが破棄される時に呼ぶ asyncResource.emitDestroy(); // AsyncResourceのインスタンスに割り当てられたasyncIdを返す asyncResource.asyncId(); // 呼び出し元のasyncIdを返す asyncResource.triggerAsyncId();
Async Resourcesの導入
こちらのBluebird
のPRによると、メモリリークの問題も解決し安定してきたようなので、実際にAigle
のライブラリに適用してみようと思います。コミットはこちら。
まず、Nodeのバージョンがv9.3.0以上推奨とのことでv9.3.0以上のみを有効にしました。
const node = typeof process !== 'undefined' && process.toString() === '[object process]'; const supportAsyncHook = node && (() => { const support = '9.3.0'; const [smajor, sminor] = support.match(/\d+/g); const version = process.versions.node; const [vmajor, vminor] = version.match(/\d+/g); return vmajor > smajor || vmajor === smajor && vminor >= sminor; })();
次にconstructor
でAsyncResource
を生成します。
class Aigle { constructor(executor) { ... this._resource = supportAsyncHook && new AsyncResource('PROMISE'); ... } }
今回はPromiseライブラリなので、Native Promiseと同じtypeを使用しました。
これでAigle
インスタンスが生成されるときにinit
が呼ばれるようになります。
次に、onFulfilled
またはonRejected
が呼ばれる直前にemitBefore
を、直後にemitAfter
を追加します。
簡単な方法としては、
const original = onFulfilled; onFulfilled = value => { this._resource.emitBefore(); try { return original(value); } catch (e) { throw e; } finally { this._resource.emitBefore(); } };
以上のように追加するのが簡単ですが、パフォーマンスを考慮して別の関数にしました。callbackが呼ばれる直前・直後に追加すれば問題ありません。
AsyncResource
の問題点としてパフォーマンスが著しく低下するため、デフォルトではAsync Resourcesを無効にしています。有効にする方法は、
Aigle.config({ asyncResource: true });
で使用することができます。
パフォーマンスについては後日調査してまた記事を書こうと思います。
まとめ
Async Hooksを使って非同期イベントのトレースを簡単にすることができるようになりました、デバッグにとても便利そうなライブラリです。
またAsync Resourcesを使うことでカスタムな非同期イベントを生成できますが、パフォーマンスに難がありそうです。Immediate
イベントは自動的に発行されるので特別Async Resourcesを使う必要はないのかなぁとも思いましたが、あまり詳しくないのでわかりません。もし詳しい方いたらぜひシェアしていただけたらうれしいです。
今後も最新の情報収集とパフォーマンスの調査を行っていきたいと思います。
ソース
Sidekiqのベストプラクティス
概要
Sidekiqのベストプラクティスに沿って、メッセージ機能を実装していきます。
0. Sidekiqとは
Sidekiqとはbackgroundでタスクを処理してくれるライブラリです。日付指定でセットして実行したりもできます、便利そうです。
ベストプラクティスに沿ってWorker用のテーブルを用意します。
# messages class CreateMessages < ActiveRecord::Migration[5.1] def change create_table :messages do |t| t.integer "sender_id" t.integer "recipient_id" t.string "body" t.timestamps end end end
# message_tasks class CreateMessageTasks < ActiveRecord::Migration[5.1] def change create_table :message_tasks do |t| t.integer "sender_id" t.integer "recipient_id" t.string "body" t.timestamps end end end
今回はミニマムのデモのため構造は全く同じですが、実際は送信するユーザが複数だったりプッシュ通知をしたりするかもしれません。
1. ジョブのパラメータを小さく・シンプルに
上記のMessageTasks
のテーブルを用意することで、引数をtask_id
のみにすることができます。
class MessageWorker include Sidekiq::Worker def perform(task_id) end end
また呼び出し側では以下のようになります。
task = MessageTask.create(sender_id: sender_id, recipient_id: recipient_id, body: body) MessageWorker.perform_async(task.id)
2. 冪等性とトランザクション
perform
内でエラーが発生した場合、自動リトライ機能が働きます。デフォルトでは25回のリトライ後(おおよそ21日後)そのジョブは削除されマニュアルで対応しないといけません。
それを防ぐためにもMessageTasks
とトランザクションを使用することで、より安全にデータを扱うことができます。
class MessageWorker include Sidekiq::Worker def perform(task_id) task = MessageTask.find(task_id) if task.nil? return end ActiveRecord::Base.transaction do Message.create(sender_id: task.sender_id, recipient_id: task.recipient_id, body: task.body) task.destroy end end end
3. Concurrencyの設定
Concurrencyを設定することによりsidekiqの処理を並列化することが可能です。デフォルト値は25スレッドで推奨値は50未満。100以上設定した場合、安定性の問題があるとの。
config/database.yml
を変更することで変えることができます。この値は可変ではないので、用途に応じて変更する必要があるようです。
production: adapter: mysql2 database: foo_production pool: 25
まとめ
Sidekiqを使うことで同期処理が必要がないタスクは非同期で処理させることができます。同期処理させる必要ないものは非同期にしてレスポンス速度を改善できそうですね。