Lambdaファンクションの関数インスタンスは再利用される

Lambdaで環境変数っぽいことを実現するためにエイリアス名を使う方法があります。
http://qiita.com/mashiro/items/ab4cab5e623397d6b29d

しかし、ハマりどころがあるので書いておきます。

## サンプル

devステージとproステージでDynamoDBのテーブルを切り替える例です。

* [configモジュール](http://qiita.com/_daisuke/items/0d3a76a1290f08f4fea5)を使用
* 環境変数`NODE_ENV`にdev,proという値を設定して切りかえ
    * Lambdaのコンテキスト変数からエイリアス名を取り出す

```shell-session
envtest/                                                                                                                                         
├── config                                                                                                                                       
│   ├── dev.js                                                                                                                                   
│   └── pro.js                                                                                                                                   
├── db.js                                                                                                                                        
├── index.js                                                                                                                                     
├── node_modules                                                                                                                                 
│   ├── co                                                                                                                                       
│   └── config                                                                                                                                   
└── package.json           
```
```js:index.js
"use strict";
const co = require("co");
const db = require("./db");

exports.handle = function(e, ctx, cb) {
  const aliaseName = ctx.invokedFunctionArn.match(/([^:]+)$/)[1];
  process.env.NODE_ENV = aliaseName;
  console.log("NODE_ENV: ", process.env.NODE_ENV);
  co(function*(){
    return yield db.getFromDB();
  }).then(result => {
    cb(null, result);
  }).catch(err => {
    cb(err);
  });
};
```
```js:db.js
const config = require("config");

const AWS = require("aws-sdk");
AWS.config.setPromisesDependency(Promise);
const DynamoDB = new AWS.DynamoDB.DocumentClient();

module.exports.getFromDB = function*() {
  console.log(`get from table ${config.tablePrefix}.Table`);
  return yield DynamoDB.get({
    TableName: `${config.tablePrefix}.Table`,
    Key: {
      key: "hogehoge"
    }
  }).promise();
};
```

## 動作

一見動きそうですが、このようなエラーになります。

```shell-session
$ apex invoke envtest -L -a dev                                                                      
START RequestId: d7c859e0-7284-11e6-92b7-4568188a1126 Version: $LATEST                                                                           
2016-09-04T09:49:16.384Z        d7c859e0-7284-11e6-92b7-4568188a1126    NODE_ENV:  dev                                                           
2016-09-04T09:49:16.384Z        d7c859e0-7284-11e6-92b7-4568188a1126    get from table undefined.Table                                           
2016-09-04T09:49:16.488Z        d7c859e0-7284-11e6-92b7-4568188a1126    {"errorMessage":"Requested resource not found","errorType":"ResourceNotFo
undException"
```

なぜでしょう？ これは`require("config")`されるタイミングにあります。

```js:index.js
const db = require("./db");
```

という部分がありますが、これにより芋づる式に、

```js:db.js
const config = require("config");
```

ココが呼ばれます。

これはhandle関数の下記の部分よりも先に呼ばれてしまうのです。

```js:index.js
exports.handle = function(e, ctx, cb) {
  const aliaseName = ctx.invokedFunctionArn.match(/([^:]+)$/)[1];
  process.env.NODE_ENV = aliaseName;
  console.log("NODE_ENV: ", process.env.NODE_ENV);
```

つまり、NODE_ENVにdevだとか、proだとかがセットされる前です。

## 修正案

これを解消するには、

```js:index.js
"use strict";
const co = require("co");

exports.handle = function(e, ctx, cb) {
  const aliaseName = ctx.invokedFunctionArn.match(/([^:]+)$/)[1];
  process.env.NODE_ENV = aliaseName;
  console.log("NODE_ENV: ", process.env.NODE_ENV);
  co(function*(){
    const db = require("./db");     //ココに持ってくる
    return yield db.getFromDB();
  }).then(result => {
    cb(null, result);
  }).catch(err => {
    cb(err);
  });
};
```

こうなります。

```shell-session
$ apex invoke envtest -L -a dev
START RequestId: 8a0fbd26-7286-11e6-8438-af4d75d76e02 Version: $LATEST
2016-09-04T10:01:24.967Z	8a0fbd26-7286-11e6-8438-af4d75d76e02	NODE_ENV:  dev
2016-09-04T10:01:24.967Z	8a0fbd26-7286-11e6-8438-af4d75d76e02	get from table Sample.DEV.Table
END RequestId: 8a0fbd26-7286-11e6-8438-af4d75d76e02
REPORT RequestId: 8a0fbd26-7286-11e6-8438-af4d75d76e02	Duration: 67.24 ms	Billed Duration: 100 ms 	Memory Size: 128 MBMax Memory Used: 26 MB	
{"Item":{"value":"DEV環境","key":"hogehoge"}}
```

できた気がしますね。

## しかし、Lambdaファンクションの関数インスタンスは再利用されるのです

話はまだ終わりません。ここからが本当に書きたかったことです。

### devもproもバージョン9を向ける

下記のようにdevもproもバージョン9を向けるようにしてみましょう。

```shell-session
aws lambda list-aliases --function-name sample_envtest
{
    "Aliases": [
        {
            "FunctionVersion": "9",
            "Name": "dev",
        },
        {
            "FunctionVersion": "9",
            "Name": "pro",
        }
    ]
}
```

### 実行結果

proを実行してみます。ふむ、期待通りですね

```shell-session
$ apex invoke envtest -L -a pro
START RequestId: b45a8038-7287-11e6-91a6-e3f26108a1b5 Version: 9
2016-09-04T10:09:45.389Z	b45a8038-7287-11e6-91a6-e3f26108a1b5	NODE_ENV:  pro
2016-09-04T10:09:45.390Z	b45a8038-7287-11e6-91a6-e3f26108a1b5	get from table Sample.PRO.Table
END RequestId: b45a8038-7287-11e6-91a6-e3f26108a1b5
REPORT RequestId: b45a8038-7287-11e6-91a6-e3f26108a1b5	Duration: 77.82 ms	Billed Duration: 100 ms 	Memory Size: 128 MBMax Memory Used: 26 MB	
{"Item":{"value":"PRO環境","key":"hogehoge"}}
```

続いてdevを実行すると・・・

```shell-session
$ apex invoke envtest -L -a dev
START RequestId: dad8792f-7287-11e6-8961-2d198a82fe02 Version: 9
2016-09-04T10:10:49.968Z	dad8792f-7287-11e6-8961-2d198a82fe02	NODE_ENV:  dev
2016-09-04T10:10:49.969Z	dad8792f-7287-11e6-8961-2d198a82fe02	get from table Sample.PRO.Table
END RequestId: dad8792f-7287-11e6-8961-2d198a82fe02
REPORT RequestId: dad8792f-7287-11e6-8961-2d198a82fe02	Duration: 134.01 ms	Billed Duration: 200 ms 	Memory Size: 128 MBMax Memory Used: 26 MB	
{"Item":{"value":"PRO環境","key":"hogehoge"}}
```

おやおや？ `Sample.PRO.Table`を読んでしまっている！
`NODE_ENV:  dev`と出ているのに！

### 関数インスタンスの再利用問題

一度requireされたモジュールはキャッシュされるのはNode.jsを使う方なら周知の事実でしょう。
だからdb.jsでrequire("config")された内容はキャッシュされます。

私はLambdaはCGIのように実行されるたびにプロセス実行をするものだと思っていました。
しかし、この結果を見るに１回目にproとして実行されたプロセスが再利用されているように見えます。

これが「関数インスタンスが再利用される」というやつですね

### devを$LATESTに向けてみる

```shell-session
aws lambda list-aliases --function-name sample_envtest
{
    "Aliases": [
        {
            "FunctionVersion": "$LATEST",
            "Name": "dev",
        },
        {
            "FunctionVersion": "9",
            "Name": "pro",
        }
    ]
}
```

実はこの環境では`$LATEST==9`なのですが、ともかくやってみます。

```shell-session
$ apex invoke envtest -L -a dev
START RequestId: b5ad53ae-7289-11e6-99c6-e397f9b62998 Version: $LATEST
2016-09-04T10:24:06.605Z	b5ad53ae-7289-11e6-99c6-e397f9b62998	NODE_ENV:  dev
2016-09-04T10:24:06.605Z	b5ad53ae-7289-11e6-99c6-e397f9b62998	get from table Sample.DEV.Table
END RequestId: b5ad53ae-7289-11e6-99c6-e397f9b62998
REPORT RequestId: b5ad53ae-7289-11e6-99c6-e397f9b62998	Duration: 101.35 ms	Billed Duration: 200 ms 	Memory Size: 128 MBMax Memory Used: 27 MB	
{"Item":{"value":"DEV環境","key":"hogehoge"}}

$ apex invoke envtest -L -a pro
START RequestId: b7771774-7289-11e6-a3ca-55d16d901553 Version: 9
2016-09-04T10:24:09.589Z	b7771774-7289-11e6-a3ca-55d16d901553	NODE_ENV:  pro
2016-09-04T10:24:09.589Z	b7771774-7289-11e6-a3ca-55d16d901553	get from table Sample.PRO.Table
END RequestId: b7771774-7289-11e6-a3ca-55d16d901553
REPORT RequestId: b7771774-7289-11e6-a3ca-55d16d901553	Duration: 46.22 ms	Billed Duration: 100 ms 	Memory Size: 128 MBMax Memory Used: 26 MB	
{"Item":{"value":"PRO環境","key":"hogehoge"}}
```

なるほど。どうやらLambdaファンクションの関数インスタンスはバージョンごとに再利用されるようです。
ログに、

```shell-session
START RequestId: b5ad53ae-7289-11e6-99c6-e397f9b62998 Version: $LATEST
START RequestId: b7771774-7289-11e6-a3ca-55d16d901553 Version: 9
```

という出方をしているので、$LATESTと最新バージョは違うものとして扱われるようです。

## まとめ

この挙動は注意しておかないとハマりそうです。Lambdaファンクションの内部実装がコンテナで、コンテナ起動時間の問題を軽減するためにある程度の単位で再利用されるというのは小耳に挟んでいました。

https://aws.amazon.com/jp/lambda/faqs/
> Q: AWS Lambda は関数インスタンスを再利用しますか?
>
> パフォーマンス向上のため、AWS Lambda は新しく関数のインスタンスを作成するのではなく、関数のインスタンスを保持してその後のリクエストに対応することがあります。ただし、常にインスタンスを再利用するわけではありません。

が、こうなるというところまで頭が回っていませんでした。
回避するためにはconfigの内容がキャッシュされない方法で、configを毎回評価する必要があるでしょう。

大域変数である`process.env.NODE_ENV`にdev,proをセットするのも怪しくなってきました。
同時invokeとLambdaファンクションのプロセスの関係がどうなるのかがまだわかりませんが、スレッドセーフでは無いかもしれません。

## 雑惑

apexには環境変数っぽいことをする機能があります。
`apex deploy`するとこっそりとこんなファクションがかまされています。

```js
try {
  var config = require('./.env.json')
  for (var key in config) {
    process.env[key] = config[key]
  }
} catch (err) {
  // ignore
}

exports.handle = require('./index').handle
```

これを素直に使ったほうが良いのかなと思うのですが、この機能を使うと、

|バージョン番号|      |
|-----------|------|
|10         |proの最新|
|9          |devの最新|

となってしまうのが嫌なのですよね。。。