89
78

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.

AWS LambdaをWebから非同期で呼ぼうとした話

Last updated at Posted at 2016-06-28

AWSをいじるようになりLambdaに手を出したところ、盛大にハマった話です。

どこでハマったか

API Gatewayを利用し、Webから非同期でLambdaを呼びだそうとしたところ、盛大にハマりました。

まずはわかる人向けの結論

API GatewayからLambdaを呼び、そのLambda上からLambdaを非同期呼出することで解決。

本編

Lambdaって?

  • ランバダ?
  • ラムダと読みます
  • コードをAWSクラウド上で実行してくれる
    • 対応言語はNodeJS, Java, Python
  • AWSクラウド上の色々な所で実行トリガーを設定できる
    • 例えばS3へのファイルアップロード
    • DynamoDBのテーブル更新時 (insert, update, ...)

例えば

  • S3に画像がアップロードされた瞬間、サムネイル画像を生成する
  • S3にログファイルがアップされたら、即座にファイルの解析を行う
  • DynamoDBにレコードがインサートされたら何かをする~~(利用シーン思いつかず)~~

デメリット

  • 300秒以上かかる処理はできない(2016/06月現在)
  • メモリ1.5G以上消費する処理はつらい
  • Javaが遅い(らしい)

メリット

  • EC2いらない
  • 安い
  • スケーラブル
    • 関数に対して割り当てるメモリを調整できる(128M ~ 1536M)
    • CPUはメモリに比例して勝手に強くなる
  • オンデマンド(https経由)からも呼べる

何ができるのか

  • クライアント(ブラウザ等)が直接Lambdaを起動できる
  • GET, POST等でデータを送信できる
  • そしてそのデータ(Payload)に対して/応じてごにょごにょできる

但し、HTTPS経由でのLambda関数の起動はAWS Lambda単体では実現できないのでAWS API Gatewayと連携します。

API Gatewayとは

  • LambdaをHTTPS経由で呼び出せるようにするやつ
  • 詳細はこちら
  • これとLambdaをうまく組み合わせて準備完了

で、何をしたいのか

  • ブラウザからPOSTしたデータをS3に書き出したい
  • けど書き出している時間は待ちたくない
  • 要はポストするだけして即座に"200 OK"が欲しい
    • 厳密にいうと202

理想のイメージ

c9f84b4d5cd16bc88144831885cc0f45.png

まずは普通にやってみる

今回はNode.jsを採用しました。exports.handler = function() {}に渡されているeventにPOSTされたpayloadが入ってます。

lambda.js
'use strict';

var AWS_ACCESS_KEY_ID = 'ACCESS_KEY_HERE';
var AWS_SECRET_ACCESS_KEY = 'SECRET_ACCESS_KEY_HERE';
var S3_BUCKET_NAME = 'target.bucket.name';

var AWS = require('aws-sdk');

AWS.config.update({
    accessKeyId: AWS_ACCESS_KEY_ID,
    secretAccessKey: AWS_SECRET_ACCESS_KEY,
    region: 'ap-northeast-1'
});

exports.handler = function(event, context, callback) {

    var params = {
        Bucket: S3_BUCKET_NAME,
        Key: 'payload.json',
        Body: JSON.stringify(event),
        ContentType: 'application/json; charset=utf-8',
        ACL: 'public-read'
    };

    s3.putObject(params, function(err, data) {
        if (err) {
            context.done(err);
        } else {
            context.done(null, 'Successfully uploaded data.');
        }
    });
};

2003623849da2011afd35d8dab1d2e80.png

上記のコード管理画面内にある「API endpoints」タブよりエンドポイントURLを作成することができるので、そこから生成したURLをレストクライアント等から叩いてみます。

270b88515a9e28e7beeed0d3726c8e7e.png

無事成功したので、ここから非同期化を目指します。

非同期化へのアプローチ

  • 無思考的にsetTimeout()を試す
  • Node.jsのchild_processを使う
  • API Gatewayの統合リクエストヘッダーにX-Amz-Invocation-Typeを追加する
  • LambdaからLambdaを呼び出す

setTimeout();

lambda.js
setTimeout(function() {
    
    s3.putObject(params, function(err, data) {
        if (err) {
            context.done(err);
        } else {
            context.done(null, 'Successfully uploaded data.');
        }
    });
    
}, 0);
  • S3へのアップロード部分の実行を遅延させてみる
  • setTimeout()は実行キューへの登録が遅延されるだけで、結局同一スレッドで実行されるので意味なし
  • (おさらい)Javascriptはシングルスレッド

ちなみに

setTimeout()後にプロセスを強制終了してみると...プロセスが一瞬で終わります。
当然setTimeout()内の処理も実行されません。

lambda.js
setTimeout(function() {
    
    s3.putObject(params, function(err, data) {
        if (err) {
            context.done(err);
        } else {
            context.done(null, 'Successfully uploaded data.');
        }
    });
    
}, 0);

process.exit();

// 即オワタ\(^o^)/

child_process

  • Node.jsはchild_processモジュールを通じて親プロセス(メインストリーム)から子プロセスを生成することができる
  • そして子プロセスは親プロセスから切り離す事ができる!

何が嬉しいのか

  • 子プロセスを親から切り離すことによって、親プロセスが死んでも子プロセスは動き続ける事ができるようになる。
  • 更にこの状態で子が親に対して、自分の処理終了を待たないように宣言することができる。
    • 通常は切り離したとしても親は子の終了を待つ

実際のコード

lambda.js
'use strict';

exports.handler = function(event, context, callback) {

    var spawn = require('child_process').spawn;
    
    spawn('node', ['longlongProcess.js'], {
        stdio: 'ignore',
        detached: true
    }).unref();

}
  • spawnでOSコマンドを実行できる
  • detached: trueが切り離し命令
  • unref()することで子の終了を待機しなくなる
  • これで勝つる!(フラグ)

...それLambdaで出来無いよ

Lambdaではメインストリームの処理が終了した瞬間、派生する処理は全て殺されます。なので前述の例でいうとunref()が実行された瞬間、親プロセスは以降の行に処理が無いのでそこで終了します。

=子も終了します

なので

下記の様なコードにしても同様、意図した動作になりません。
Lambdaでない環境であれば、detachしてる子プロセスは生き残り処理が続行されますが、Lambdaではprocess.exit()によって子も殺されます。無念。。

lambda.js
'use strict';

exports.handler = function(event, context, callback) {

    var spawn = require('child_process').spawn;
    
    // unref()せず、直後に親を殺してみる
    spawn('node', ['longlongProcess.js'], {
        stdio: 'ignore',
        detached: true
    });
    
    process.exit();
}

X-Amz-Invocation-Type

AWS API Gatewayの公式ドキュメントに下記のような記述があります。

a386475ecd013e5f7108ccd2c4ad15b0.png

これは要するに、API Gatewayの

682a3d430391b553ac3ac8cf91e63f65.png

a7431a349eab98338ca55d84c49ddf0a.png

追加して下さいということです。

しかし

やってはみるものの一向に非同期にならず、10秒スリープするようなスクリプトを呼ぶと、ご丁寧に10秒待たされる。
API GatewayのLambdaに関連しそうなドキュメントをひたすら読むも謎は解けず...

\ここで盛大にハマる/

一応試したことリスト

  • メソッドリクエストから統合リクエストに対してヘッダーマッピング式をつかってマッピングしてみる
  • ヘッダーのキーの大文字、小文字等を試してみる
    • X-Amz-Invocation-Type
    • XAmzInvocationType
    • x-amz-invocation-type
    • xaMziNvOCatIoNtYpE (やけくそ)
  • InvocationTypeのEventやらDryRunやらを試してみる
  • POSTだのGETだのリクエストメソッドを変えてみる

時は流れ

ふと初心に戻りLambdaの公式ドキュメントを読み返していた所、衝撃の事実を発見

045118173de07dd93be3297366b4cc8d.png

Amazon API Gateway を使用して HTTPS 経由で Lambda 関数を呼び出す場合、Lambda は常に RequestResponse **(同期)**呼び出しタイプを使用します。

/(^o^)\

ということで最終手段

Lambda => Lambda

LambdaからLambdaを呼びます。
イメージ図はこんな感じです。

35e12d61a47bb9799d7000b48483ba3d.png

実際のコード

  • Lambda上でaws-sdkを取得し、そこから更にLambdaを非同期呼出
  • lambda.invokeAsync()がポイント
  • InvokeArgsには受け取ったpayload(event)をそのままぶん投げる
lambda.js
'use strict';

var AWS_ACCESS_KEY_ID = 'ACCESS_KEY_HERE';
var AWS_SECRET_ACCESS_KEY = 'SECRET_ACCESS_KEY_HERE';
var FUNCTION_NAME = 'nextLambdaFunction';

var AWS = require('aws-sdk');

AWS.config.update({
    accessKeyId: AWS_ACCESS_KEY_ID,
    secretAccessKey: AWS_SECRET_ACCESS_KEY,
    region: 'ap-northeast-1'
});

exports.handler = function(event, context, callback) {

    var lambda = new AWS.Lambda();

    var params = {
        FunctionName: FUNCTION_NAME,
        InvokeArgs: JSON.stringify(event)
    };

    lambda.invokeAsync(params, function(err, data) {
        if (err) {
            context.done(err);
        } else {
            context.done(null, data);
        }
    });
};

上記の構成、コードにてようやく非同期化に成功しました。

まとめ

  • Lambdaをうまく活用すればEC2いらずで経済的!
  • HTTPS経由の非同期呼び出しは工夫すればなんとかなる!

最後に

ドキュメント読もう。ちゃんと。

89
78
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
89
78

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?