AWS LambdaのCustom Runtimeを使い、Node.js v8などEoLとなったランタイムを動かす

# はじめに

Node.js、バージョンアップの足がかなり早いですよね。
AWS Lambdaにおけるランタイムサポート期間も、これにあわせてハイテンポになっています。

ちゃんとバージョンアップをしろというご意見は重々承知の上ではありますが、
Node.js v8.10でLambda Functionを使い続けざるを得ない場合に、カスタムランタイムを使ってEoLとなったランタイムを動かし延命処置を図ります。

## 動作確認環境

* Arch Linux (2020.04.04)
* Docker 19.03.8-ce
* aws-cli 1.18.36

# カスタムランタイムの使い方

カスタムランタイムの仕様については、[公式ドキュメント](https://docs.aws.amazon.com/ja_jp/lambda/latest/dg/runtimes-custom.html)が詳しいので割愛します。

カスタムランタイムを使用するには、デプロイパッケージあるいはLayerに、`node`実行ファイルと、ハンドラー関数を起動するための`bootstrap`実行ファイルを含める必要があります。
今回は既存のLambda Functionを使用することを想定していますので、関数のデプロイパッケージには手を加えずLambda Layerでランタイムを読み込ませます。

Lambda Layerの作成には、以下のリポジトリを活用させていただきます。
https://github.com/lambci/node-custom-lambda

こちらのリポジトリにはNode v10、v12のファイルが含まれています。（2020/04/04現在）
今回はこのリポジトリをフォークし、v8.10用のファイルを追加することでLayerを作成します。

`bootstrap`については、CとJavascriptで書かれたものがそれぞれ`v12.x/bootstrap.c`、`v12.x/bootstrap.js`にあります。（v10.xも同様）

`bootstrap.c`（をコンパイルした`bootstrap`）がまずAWS Lambdaによって起動され、これが`bootstrap.js`スクリプトをカスタムランタイムのNodeで実行します。
`bootstrap.js`は、[AWS Lambda ランタイムインターフェイス](https://docs.aws.amazon.com/ja_jp/lambda/latest/dg/runtimes-api.html)から関数の呼び出しイベントの受け取り、デプロイパッケージのスクリプト実行、実行結果のPOSTを行います。

上記リポジトリの`bootstrap.js`はNode v8.10でも問題なく動くので、
必要な変更点はLambda Layerに含める`node`実行ファイルをv8.10のものに変更するだけとなります。


# カスタムランタイムの作成

前節で紹介したリポジトリをクローンするところからはじめます。
Dockerが必要となります。

```bash
$ git clone https://github.com/lambci/node-custom-lambda.git
$ cd node-custom-lambda
```

v12.xのディレクトリを元に、v8.10のディレクトリを作成します。

```bash
$ cp -r v12.x v8.10
$ cd v8.10
```

v12.xのLayerファイルを削除しておきます。

```bash
$ rm layer.zip
```

このプロジェクトでは、Docker上で`bootstrap.c`のビルドとNodeのダウンロードを行います。

`config.sh`を編集し、Nodeのバージョンを指定します。

```diff:config.sh
< export NODE_VERSION=12.16.1
---
> export NODE_VERSION=8.10.0
```

ビルドします。

```bash
$ ./build.sh
```

`v8.10/layer.zip`ファイルが出来上がります。
これを解凍すると以下のようなファイルが入っています。

```
layer
├── bin
│   └── node
├── bootstrap
└── bootstrap.js
```

Nodeのバージョンを確認しておきます。

```bash
$ ./layer/bin/node -v
v8.10.0
```

テストが用意されていますので、実行してみます。

```bash
$ ./test.sh
```

以上でカスタムランタイムのLayerが完成しました。


# カスタムランタイムのデプロイ

リポジトリには`publish.sh`が用意されていますが、このスクリプトは全てのリージョンにデプロイされてしまいます。
今回はap-northeast-1にのみデプロイできればよいので、AWS CLIを使って手動でLayerを作成します。

まず、作成したレイヤーのファイル（`layer.zip`）を任意のS3にアップロードします。

```
aws s3api put-object --bucket ${BUCKET_NAME} --key nodejs/8.10.0/layer.zip --body layer.zip --output json
```

次に、Lambda Layerを作成します。
ここではCloudFormationで作成します。

```yml:template.yml
AWSTemplateFormatVersion: 2010-09-09

Parameters:
  S3BucketName:
    Description: A S3 bucket name contains layer.zip
    Type: String

Resources:
  Nodejs8Runtime:
    Type: AWS::Lambda::LayerVersion
    Properties:
      Content:
        S3Bucket: !Ref S3BucketName
        S3Key: nodejs/8.10.0/layer.zip
      Description: Layer for Node.js 8.10.0 Custom Runtime
      LayerName: custom-runtime-nodejs-8

Outputs:
  Nodejs8RuntimeLayerARN:
    Description: A lambda layer ARN of Node.js 8.10.0 Custom Runtime
    Value: !Ref Nodejs8Runtime
    Export:
      Name: !Sub ${AWS::StackName}-runtime-nodejs8
```

Stackを作成。

```
aws cloudformation create-stack --stack-name dev-lambdalayers-nodejs \
--template-body file://template.yml \
--parameter ParameterKey=S3BucketName,ParameterValue=${S3_BUCKET_NAME}
```

Layerができていることを確認します。

```
$ aws lambda list-layer-versions --layer-name custom-runtime-nodejs-8
{
  "LayerVersions": [
    {
      "LayerVersionArn": "arn:aws:lambda:ap-northeast-1:123456789012:layer:custom-runtime-nodejs-8:1",
      "Version": 1,
      "Description": "Layer for Node.js 8.10.0 Custom Runtime",
      "CreatedDate": "2020-04-04T16:59:31.629+0000"
    }
  ]
}
```

# Lambda Functionの作成とテスト

上記で作成したカスタムランタイムをテストします。

Lambda FunctionはServerless Frameworkを使用して作ることにします。
`provider.runtime`に`provided`を指定することでカスタムランタイムを使用できます。
Lambda Layerは、先程のCloudformation StackのOutputをインポートしてARNを指定します。

```yml:serverless.yml
service: test-lambda-function

provider:
  name: aws
  runtime: provided
  stage: dev
  region: ap-northeast-1

functions:
  hello:
    handler: handler.hello
    layers:
      - 'Fn::ImportValue': dev-lambdalayers-nodejs-runtime-nodejs8
```

関数のコードはシンプルに、実行しているNode.jsのバージョンを返すだけです。

```handler.js
module.exports.hello = async event => {
  return process.version;
};
```

これを実行して、`v8.10.0`という文字列が帰ってきたら成功です。

```
$ sls invoke -f hello
"v8.10.0"
```


# 注意点

* AWS公式のNode.jsランタイムには`aws-sdk`が含まれていますが、この方法で作成したカスタムランタイムにはいずれのnpmパッケージも含まれていません。


# おわりに

以上でNode v8を使用するLambda Functionの延命措置ができました。
同様の方法で、Node v6、v4も動かすことが可能です。

しっかりバージョンアップしていくのがベストであることは言うまでもありませんが、
node-gypなどネイティブモジュールはバージョンアップで動かなくなることも多々ありますので、とりあえずの措置には使えるかと思います。

また今回使用したコードはすべて以下リポジトリにアップしています。
https://github.com/uhey22e/node-custom-lambda