LoginSignup
25
22

More than 5 years have passed since last update.

AWS Lambda アプリケーションのチェーン呼び出しを単一のコードで書けるライブラリを書いてみた

Posted at

これは何でしょう

AWS Lambda におけるチェーン呼び出しするような一連のLambda関数を動的にデプロイすることで単一のコードで書けるライブラリを書いてみましたという記事です。名付けて"Mizukiri"。

image

コードなど

ことば

ここでは AWS Lambda 上にデプロイされるアプリケーションのことをLambda関数と呼びます。AWSの公式ドキュメントでもそのように呼称されています。別の意味に解釈されそうな言葉ですがしょうが無いです。

概要

Mizukiriはnpmパッケージで、コンセプト実証のコードです。Lambda関数のチェーン呼び出しを単一コードで実現します。
Mizukiriでmap的なコードを書くと、その場で関数をシリアライズしてnpm installして、zipで固めてAWS Lambdaにデプロイして、それを非同期でinvokeします。

いわゆる"やってみた"

実際のところは、こういうコードがあるとして、mapに引き渡された関数一つ一つがLambda関数として省労力で開発できるライブラリがあったら面白いなと思って作ったコンセプト実証ライブラリです。つまり、"やってみた"ってやつです。

[1, 2, 3, 4] // ES2015
  .map(item => item + 1)
  .map(item => item + 2)
  .map(item => item + 3) // => [7, 8, 9, 10]

特長

  1. Mizukiriによりチェーンを構成するひとつひとつのLambda関数を個別に管理する必要がなくなります。特に簡単なチェーン呼び出しをするアプリケーションの場合管理や開発の負担を大幅に減らせます。
  2. (未実装ですが)チェーン部分をただのmap処理と入れ替えれば自動テストが容易になります。
  3. モノリシックなコードベースの特性を生かしながら、Lambda関数のチェーンによる恩恵を受けることが出来ます。

だめなところ

  1. コンセプト実証コードレベルです。
  2. 筆者の妄想をコードに起こしただけなので各識者ツッコミに耐えられない可能性が高いです。
  3. 自動生成コードに対する自動テストがありません。
  4. 単純なアプリほど処理全体のスループットはそう良くならないと思います。

例を使った説明

Mizukiriを使った次のようなLambda関数用のコードをデプロイして invoke すると、 demo-func-1, demo-func-2, demo-func-3 という3つのAWS Lambda アプリケーションが AWS Lambda 上で生成されデプロイされます。デプロイ後自動的に最初のLambda関数であるdemo-func-1 に引数として [1, 2, 3, 4, 5, 6, 7] が渡された上で実行され、demo-func-2以降並列に各Lambda関数で次々とチェーン処理されます。
サンプルアプリケーションはGitHubに配置しております( https://github.com/takeshinoda/mizukiri-demo )。

import Mizukiri from 'mizukiri' // ES2015

exports.handler = (event, context) => {
  new Mizukiri({ 'lodash': '3.10.1' },
               { lambdaConfig:
                 {
                   region: 'us-west-2',
                   role: 'arn:aws:iam::111111111111:role/lambda-test-1',
                   timeout: 300
                 }
               })
    .entry((line) => {
             console.log(line)
             return line + 1
           },
           { name: 'demo-func-1', require: { '_': 'lodash' } }) 
    .chain((line) => {
             console.log(line)
             return line + 1
           },
           { name: 'demo-func-2' }) 
    .chain(line => console.log(line) ,
           { name: 'demo-func-3' })
    .deployApplications()
    .exec([1, 2, 3, 4, 5, 6, 7])
    .then((value) => {
      context.succeed()
    })
    .catch(e => context.fail(e))
}

実行の概要図

上記のコードは下図のような形で実行されます。矢印そばの説明文の番号順に実行されます。mizukiri-demo 上でアプリケーションを生成して、デプロイし、mizukiri-demoから最初の invoke が実施されます。

image

AWS Lambda とチェーン呼び出し is 何

AWS Lambda?

AWSにより提供されているサービスです1。超簡単に説明すると関数だけデプロイして、イベント駆動的に実行されます。イベントはAWSが提供している各種サービスにより発火することもありますし、独自のアプリケーションからAPIによりデプロイされた関数を同期もしくは非同期で呼び出すことができます。どこのサーバでどのような構成で実行されているかはまさに雲の中です。
インフラ的な管理対象はメモリサイズや単位時間あたりの呼び出し回数程度で、関数のみのデプロイというアプリケーションに対する強烈な制約を課す代わりに、インフラ管理をかなりのレベルまでコモディティ化することに成功しています。
これはドキュメント記載の入門コードです2
入力を { key1: 'hello', key2: 'hoge', key3: 'mage' } として、同期呼び出しすると 'hoge' が返されます。

exports.handler = function(event, context) { // ES5
    console.log('value1 =', event.key1);
    console.log('value2 =', event.key2);
    console.log('value3 =', event.key3);
    context.succeed(event.key1);  // Echo back the first key value
    // context.fail('Something went wrong');
};

チェーン呼び出しについて

AWS Lambda を下図のように連続的に非同期で呼び出します。EC2部分などトリガーとなるプロセスによりキックされ、最終的に何かのストレージなどに処理結果を書き込むパターンです。

image

なにを解決しようとしてチェーン呼び出しなのか

上の図だけだとすべての処理を最初のプロセスで実施してRDBに書き込めばよいように思えます。しかし、単純なデータ変換処理が幾重にも重なったり分岐するようなアプリケーションを、ビジネスの長期運用の間に拡張していくとスケーラビリティや複雑性の問題が徐々に顕在化していくことがあります。
そのような問題に直面した時のLambda利用の具体的な事例について以前発表をしましたので、スライドを参照してみてください ( => API Gateway / Lambda / Kinesis を使ったストリーミングなバッチ実行基盤の実装)。

多段呼び出しによる並列性

呼び出しを非同期にすることで並列で動くmap処理のような事が出来ます。それぞれの横棒がLambdaにデプロイされたLambda関数の実行を示しており、色はLambda関数の種類を示しています。

image

さらに多段呼び出し。
image

アプリケーションの拡張性

単一のプロセスでデータ変換処理をしてデータストアに書き込むパターンの場合に、各種変換処理への分岐を書くのはデータの配管コードを多く書くことになりがちで、ビジネスロジックだけに集中することが徐々に難しくなってきます。また、スケーラビリティを確保するのも容易ではありません。Lambda関数を多段に下図のようにつなぐことで変換処理内容そのもののの改善や拡張性を高めることが出来きます。
image

Mizukiri

コンセプト

DSLだけでLambda関数のチェーン呼び出しを実現できるようにします。

Mizukiri利用のサンプルコードを再掲します。これ自身もLambda関数です。Lambda関数として実行されると、コメントに記されている関数1〜3それぞれの関数がLambda関数としてそれぞれデプロイされ、自動的にチェーン処理の対象となります。

import Mizukiri from 'mizukiri' // ES2015

exports.handler = (event, context) => {
  new Mizukiri({ 'lodash': '3.10.1' },
               { lambdaConfig:
                 {
                   region: 'us-west-2',
                   role: 'arn:aws:iam::111111111111:role/lambda-test-1',
                   timeout: 300
                 }
               })
    .entry((line) => { // 関数1
             console.log(line)
             return line + 1
           },
           { name: 'demo-func-1', require: { '_': 'lodash' } }) 
    .chain((line) => { // 関数2
             console.log(line)
             return line + 1
           },
           { name: 'demo-func-2' }) 
    .chain(line => console.log(line) , // 関数3
           { name: 'demo-func-3' })
    .deployApplications()
    .exec([1, 2, 3, 4, 5, 6, 7])
    .then((value) => {
      context.succeed()
    })
    .catch(e => context.fail(e))
}

デプロイされるLambda関数概要

entry()chain()メソッドに設定された関数をシリアライズしてソースコードを生成し、Lambda関数としてzipで圧縮してデプロイします。

下に示されるコードは、Mizukiriにより生成されデプロイされるコードを擬似的に示したものです。配列で受け取ったデータは要素毎に処理され、都度非同期で次のLambda関数に引き渡されます。

次のLambda関数に配列を送信すればその配列分だけ1回のinvokeで処理されます。

var func = function(item) {
  // some code
  return hoge;
};

exports.hander = function(event, context) {
  var environment = unserialize({ 'a': 1 }),
      items = Array.isArray(event.data) ? event.data : [event.data],
      promises;

  environment.__context = context;
  promises = items.map(function(item) {
    var result = func.call(environment, item);
    return invokeNextLambdaAsync(result);
  });

  Promise
    .all(promises)
    .then(function() { context.suceed() })
    .catch(function(e) { context.fail(e) })
};

API

constructor

Mizukiri(packages ={}, options ={})
コンストラクタです。チェインされるLambda関数の集まりを管理します。

packages : デプロイされるアプリケーションのdependencyパッケージです。
options : チェイン全体の設定になります。

optionsサンプル:

{
  lambdaConfig: {
    accessKeyId: <access key id>,  // optional
    secretAccessKey: <secret access key>,  // optional
    profile: <shared credentials profile name>, // optional for loading AWS credientail from custom profile
    region: 'us-east-1',
    handler: 'index.handler',
    role: <role arn>,
    timeout: 10,
    memorySize: 128
  }
}

entry

entry(lambdaEntry, overrideOptions = null)
最初に呼び出されるLambda関数を設定します。

lambdaEntry : Lambda関数から呼び出される関数を設定します。 function(data) です。data には入力値のうち、一つ一つの要素が設定されて呼び出されます。
lambdaEntry : Lambda関数固有の設定をします。
return : 自分自身を返します。

{
  name: 'func-name', // require
  requires:          // option
    { 'aws': 'aws-sdk' }, // キーを変数名、値をrequireする。 var aws = require('aws-sdk'); としてデプロイする。
  binding: {},        // option. 関数はこのオブジェクトをthisとして呼ぶ
}

chain

entry(lambdaEntry, overrideOptions = null)
2つめ以降に呼び出されるLambda関数を設定します。function(data) です。data には入力値のうち、一つ一つの要素が設定されて呼び出されます。
lambdaEntry : Lambda関数固有の設定をします。
return : 自分自身を返します。

{
  name: 'func-name', // require
  requires:          // option
    { 'aws': 'aws-sdk' }, // キーを変数名、値をrequireする。 var aws = require('aws-sdk'); としてデプロイする。
  binding: {},        // option. 関数はこのオブジェクトをthisとして呼ぶ
}

deployApplications

deployApplications()
entry()chain()によって設定されたLambda関数をデプロイします。
return : promiseです。promiseにはexec()メソッドが設定されており、そのままexec()を呼び出せます。

exec

exec(payload)
entry()に設定されたLambda関数を非同期invoke(Event呼び出し)します。
payload : entryに引き渡される値です。
return : promiseです。

これから

戻り値の形式で次段の多重度を制御したり、reduce処理や、間にkinesisを自動的に挟んで分流できるようにできるようにできれば面白いかなと考えてます。

おわりに

アドベントカレンダーに無理矢理押し込まれたので、良い機会だと以前から妄想していたのをコードに起こしてみました。nodejsは普段はフィールドとしていないので意外に大変でした。
これヤバい、ネタ終了と思ったのは、Lambda上でchild_process.exec('npm i'...)したら Command not found.って返ってきた時です。真っ青になって調べてみると、npmはパッケージとしても提供されていて、require('npm')してnpmのAPIをたたくことでnpmコマンドを動かせることが分かったので、アンドキュメンテッドな感じでしたがなんとか実装できました。

25
22
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
25
22