AWS Lambdaで手軽にNode.jsコンテナ稼働

Node.jsなLambdaを、お手軽にコンテナ化して運用したい。

分かりやすいメリットとしては、以下のようなものがある。

* [Lambda Layers](https://docs.aws.amazon.com/ja_jp/lambda/latest/dg/chapter-layers.html)等で頑張って導入していた外部ライブラリを全てイメージ内に同梱できるため、取り回し易い
  * [Lambda Runtime同梱のAWS-SDK](https://docs.aws.amazon.com/ja_jp/lambda/latest/dg/lambda-runtimes.html)と異なるバージョンのAWS-SDKを使いたい場合も、これに該当する
* [コードパッケージサイズ上限が10GiB](https://docs.aws.amazon.com/ja_jp/lambda/latest/dg/gettingstarted-limits.html#function-configuration-deployment-and-execution)であるため、コードサイズに関して普段ほぼ気にしなくてよくなる

## サンプルアプリ

環境変数 `NAME` を出力するLambdaである「`my-app`」を例として話を進めていく（あくまで例なので、処理自体に深い意味は無い）。

### index.js

環境変数を読み込むために、外部ライブラリとして [`env-var`](https://www.npmjs.com/package/env-var) を使用している。

```js
const env = require('env-var');
const NAME = env.get('NAME').required().asString();
exports.handler = async (event) => {
    console.debug('Say hello to CloudWatch Logs');
    return `It's me, ${NAME}!`;
};
```

### package.json

おそらくこんな感じ。
`env-var` をインストールしたからには `package-lock.json` も生成されているのであろう。

```json
{
  "name": "my-app",
  "version": "1.0.0",
  "private": true,
  "main": "index.js",
  "dependencies": {
    "env-var": "^7.4.1"
  }
}
```

## コンテナ化

[公式の導入手順](https://docs.aws.amazon.com/ja_jp/lambda/latest/dg/nodejs-image.html#nodejs-image-instructions)と、以下の情報を、合わせて眺めていくと分かり易いはず。

### Dockerfile

Dockerfileを作成する。
[AWSベースイメージ](https://gallery.ecr.aws/lambda/nodejs)を土台として、手元の構成を再現すればよい。

```dockerfile
FROM public.ecr.aws/lambda/nodejs:18

COPY index.js ./
COPY package.json ./
COPY package-lock.json ./

RUN npm clean-install

CMD ["index.handler"]
```

### Amazon ECR

イメージの保存先であるECRリポジトリを、Lambdaと同リージョンにて作成する。
Lambdaサービスがイメージを読み込むことになるため、その動作を許可しておくこと。

以下はCloudFormationでの設定例。

```yaml
AWSTemplateFormatVersion: '2010-09-09'
Transform: AWS::LanguageExtensions
#
# ...略...
#
Resources:
  EcrRepository:
    Type: AWS::ECR::Repository
    Properties:
      RepositoryName: my-app
      #
      # ...略...
      #
      RepositoryPolicyText:
        Fn::ToJsonString:
          Version: '2012-10-17'
          Statement:
            - Effect: Allow
              Principal:
                Service: lambda.amazonaws.com
              Action:
                - ecr:BatchGetImage
                - ecr:GetDownloadUrlForLayer
```

### イメージの作成とプッシュ

イメージを作成してECRリポジトリにプッシュする。

以下はコマンド例。

```console
docker build . --tag my-app:local
```

```console
aws ecr get-login-password | docker login --username AWS --password-stdin 111122223333.dkr.ecr.ap-northeast-1.amazonaws.com
docker tag my-app:local 111122223333.dkr.ecr.ap-northeast-1.amazonaws.com/my-app:v1.0.0
docker push 111122223333.dkr.ecr.ap-northeast-1.amazonaws.com/my-app:v1.0.0
```

### Lambdaでのイメージ使用

Lambdaの設定において、プッシュしたイメージを指定する。

以下はCloudFormationでの設定例。

```yaml
AWSTemplateFormatVersion: '2010-09-09'
#
# ...略...
#
Resources:
  LambdaFunc:
    Type: AWS::Lambda::Function
    Properties:
      PackageType: Image
      Code:
        ImageUri: 111122223333.dkr.ecr.ap-northeast-1.amazonaws.com/my-app:v1.0.0
      #
      # ...略...
      #
      Environment:
        Variables:
          NAME: Red
```

### 以上

このように、AWSベースイメージを元に簡単にコンテナ運用することができる。

## TypeScript組み込み例

TypeScriptにした場合でも基本は変わらず、トランスパイルしたものをイメージに組み込む、という処理をそれっぽく行えばよい。

以下は組み込み例。

### package.json (TypeScript)

`esbuild` した成果物を `dist` ディレクトリに出力するような `build` を設定する。

```json
{
  "name": "my-app",
  "version": "1.0.0",
  "private": true,
  "main": "index.js",
  "dependencies": {
    "env-var": "^7.4.1"
  },
  "devDependencies": {
    "esbuild": "^0.19.8"
  },
  "scripts": {
    "build": "rm -rf dist && esbuild index.ts --bundle --minify --sourcemap --platform=node --target=node18 --outdir=dist"
  }
}
```

### Dockerfile (TypeScript)

ベースイメージにて用意されている環境変数 `LAMBDA_TASK_ROOT` （ベースイメージの `WORKDIR` でもある）を利用して、ビルド出力物である `dist` ディレクトリの中身だけをイメージに組み込む。

```dockerfile
FROM public.ecr.aws/lambda/nodejs:18 AS builder
COPY index.ts ./
COPY package.json ./
COPY package-lock.json ./
COPY tsconfig.json ./
RUN npm clean-install
RUN npm run build

FROM public.ecr.aws/lambda/nodejs:18
COPY --from=builder ${LAMBDA_TASK_ROOT}/dist/* ./
CMD ["index.handler"]
```

## その他メモ

* Lambdaサービスのコンテナ起動スピードは良好
  * [キャッシュ](https://aws.amazon.com/jp/blogs/news/optimizing-lambda-functions-packaged-as-container-images/)に多少時間がかかる場合はある
* LambdaサービスからのECRリポジトリコンテナイメージダウンロード料金は[無料](https://aws.amazon.com/jp/lambda/pricing/)
* コンテナ以外のデプロイパッケージ（CFnで言うところの `PackageType`）で稼働していた既存Lambda関数リソースを、そのままコンテナ利用に切り替えることはできない
  * デプロイパッケージを途中で切り替えることはできない、というLambdaサービス仕様のため。大人しく新リソースを作成するんだ……