AWS Lambdaに”TypeScript+コンテナ”をServerless Frameworkでデプロイする方法

この記事はServerless Advent Calendar 2020 23日目の記事です。

先日、AWS re:Invent 2020にてAWS Lambdaの新機能「コンテナイメージのサポート」が発表されました。

AWS Lambda の新機能 – コンテナイメージのサポート | Amazon Web Services ブログ

コンテナサポートの詳細はクラスメソッドさんのブログで詳しく紹介されています。

【速報】Lambdaのパッケージフォーマットとしてコンテナイメージがサポートされるようになりました!! #reinvent | Developers.IO

コンテナを用いた開発環境の構築がスタンダードになり、本番環境においてもコンテナ化が浸透してきた昨今、Lambdaのコンテナサポートを待ち望んでいた方は多いのではないでしょうか。

Serverlessの沼にハマっている僕はもちろんその1人です。

Lambda Functionの開発はServerless Framework、TypeScriptを用いて行っており、これらの開発手法でも十分満足していました。

しかし、今回追加されたコンテナイメージを加えることで、さらに生産性向上が期待できそうだったので、「AWS Lambdaに”TypeScript+コンテナ”をServerless Frameworkでデプロイする方法」を検証してみました。

開発環境

  • macOS : Big Sur 11.1
  • node.js : 14.13.1
  • serverless framework : 2.15.0

作業手順

作業手順は以下の流れで行っていきます。

  1. Serverless Frameworkの初期設定
  2. TypeScript実行用のDockerイメージを作成
  3. ECR作成しServerless FrameworkでLambdaをデプロイ

Serverless Framework初期設定

TypeScriptの実行環境を用意するため、テンプレートにaws-nodejs-typescriptを指定しserverlessコマンドを実行します。

今回はserverless-lambda-typescriptというディレクトリを作成し、そこで作業を行います。


# serverless-lambda-typescriptディレクトリを作成し、hogeディレクトリ内に移動
$ mkdir serverless-lambda-typescript && $_

# serverlessコマンドを実行
$ serverless create --template aws-nodejs-typescript

# ファイルが生成されたことを確認
$ ls -A
.gitignore         .vscode/           handler.ts         package.json       serverless.ts      tsconfig.json      webpack.config.js

ファイルが生成されたことが確認できたらnpm installで必要なパッケージをインストールします。


$ npm i

生成されたserverless.tsはコンテナを利用する設定になっていないため、下記のように修正します。


import type { AWS } from '@serverless/typescript';

const serverlessConfiguration: AWS = {
  service: 'serverless-lambda-typescript',
  frameworkVersion: '2',
  provider: {
    name: 'aws',
    runtime: 'nodejs12.x',
    region: 'ap-northeast-1', // 追記
    apiGateway: {
      minimumCompressionSize: 1024,
    },
    environment: {
      AWS_NODEJS_CONNECTION_REUSE_ENABLED: '1',
    },
  },
  functions: {
    hello: {
      image: 'xxx' // 仮設定
    }
  }
}

module.exports = serverlessConfiguration;

初期設定ではregion: 'ap-northeast-1'が指定されていないので追記します。

修正後の差分は下記のようになります。


$ git diff
diff --git a/serverless.ts b/serverless.ts
index f51a061..e04e0a7 100644
--- a/serverless.ts
+++ b/serverless.ts
@@ -3,14 +3,6 @@ import type { AWS } from '@serverless/typescript';
 const serverlessConfiguration: AWS = {
   service: 'hoge',
   frameworkVersion: '2',
-  custom: {
-    webpack: {
-      webpackConfig: './webpack.config.js',
-      includeModules: true
-    }
-  },
-  // Add the serverless-webpack plugin
-  plugins: ['serverless-webpack'],
   provider: {
     name: 'aws',
     runtime: 'nodejs12.x',
@@ -23,15 +15,7 @@ const serverlessConfiguration: AWS = {
   },
   functions: {
     hello: {
-      handler: 'handler.hello',
-      events: [
-        {
-          http: {
-            method: 'get',
-            path: 'hello',
-          }
-        }
-      ]
+      image: 'xxx'
     }
   }
 }

TypeScript実行用のDockerイメージを作成

次にTypeScriptを実行するためDockerイメージを作成します。

AWSからLambda用のイメージが配布されていますが、TypeScriptに対応したものはないので、JSにトランスパイルしたファイルをコンテナ内に配置します。

TypeScriptからJavaScriptへのトランスパイルはwebpackコマンドで行うのでwebpack-cliをインストールします。


$ npm i -D webpack-cli

次にDockerfileを作成し、下記のように記載します。


FROM amazon/aws-lambda-nodejs:12
COPY handler.ts package*.json tsconfig.json webpack.config.js ./
RUN npm i
RUN npx webpack
CMD [ "handler.hello" ]

コンテナ作成時にwebpack.config.jsの設定を元にnpx webpackを実行するようにしています。

webpack.config.jsもコンテナを利用する前提になっていないため、下記のように修正します。


const path = require('path');
const nodeExternals = require('webpack-node-externals');

module.exports = {
  entry: './handler.ts',
  devtool: 'source-map',
  resolve: {
    extensions: ['.mjs', '.json', '.ts'],
    symlinks: false,
    cacheWithContext: false,
  },
  output: {
    libraryTarget: 'commonjs',
    path: __dirname,
    filename: 'handler.js',
  },
  optimization: {
    concatenateModules: false,
  },
  target: 'node',
  externals: [nodeExternals()],
  module: {
    rules: [
      // all files with a `.ts` or `.tsx` extension will be handled by `ts-loader`
      {
        test: /\.(tsx?)$/,
        loader: 'ts-loader',
        exclude: [
          [
            path.resolve(__dirname, 'node_modules'),
            path.resolve(__dirname, '.serverless'),
            path.resolve(__dirname, '.webpack'),
          ],
        ],
        options: {
          transpileOnly: true,
          experimentalWatchApi: true,
        },
      },
    ],
  },
  plugins: [
    // new ForkTsCheckerWebpackPlugin({
    //   eslint: true,
    //   eslintOptions: {
    //     cache: true
    //   }
    // })
  ],
};

修正前後の差分は下記のようになります。


$ git diff
diff --git a/webpack.config.js b/webpack.config.js
index 83dd851..e7bddc7 100644
--- a/webpack.config.js
+++ b/webpack.config.js
@@ -1,13 +1,9 @@
 const path = require('path');
-const slsw = require('serverless-webpack');
 const nodeExternals = require('webpack-node-externals');
-const ForkTsCheckerWebpackPlugin = require('fork-ts-checker-webpack-plugin');

 module.exports = {
-  context: __dirname,
-  mode: slsw.lib.webpack.isLocal ? 'development' : 'production',
-  entry: slsw.lib.entries,
-  devtool: slsw.lib.webpack.isLocal ? 'eval-cheap-module-source-map' : 'source-map',
+  entry: './handler.ts',
+  devtool: 'source-map',
   resolve: {
     extensions: ['.mjs', '.json', '.ts'],
     symlinks: false,
@@ -15,8 +11,8 @@ module.exports = {
   },
   output: {
     libraryTarget: 'commonjs',
-    path: path.join(__dirname, '.webpack'),
-    filename: '[name].js',
+    path: __dirname,
+    filename: 'handler.js',
   },
   optimization: {
     concatenateModules: false,

ローカル環境で実行

ここまでの手順を終えるとローカル環境でコンテナを起動してテストすることが可能になります。

Docker CLIを利用してコンテナイメージを構築します。


$ docker build -t serverless-lambda-typescript-container .

コンテナが正常に機能しているか確認するには、Lambda Runtime Interface Emulator を使用して、コンテナイメージをローカルで起動します。


$ docker run -p 9000:8080 serverless-lambda-typescript-container:latest

起動したコンテナに対してcURLで呼び出してテストします。


$ curl -XPOST "http://localhost:9000/2015-03-31/functions/function/invocations" -d '{}'
{"statusCode":200,"body":"{\n  \"message\": \"Go Serverless Webpack (Typescript) v1.0! Your function executed successfully!\",\n  \"input\": {}\n}"}

無事に成功すると上記のようにレスポンスが返ってきます。

ECR作成しServerless FrameworkでLambdaをデプロイ

ローカル環境でのテストが通ったら、いよいよLambdaにデプロイします。

はじめに作成したDockerイメージを管理するECRを作成します。


$ aws ecr create-repository --repository-name serverless-lambda-typescript-container

ECRを作成したらDockerイメージをpushします。
※ xxxはAWSアカウントによって異なりますので、正しいコマンドはAWSマネジメントコンソール上で確認してください


# 認証トークンを取得し、レジストリに対してDockerクライアントを認証
$ aws ecr get-login-password --region ap-northeast-1 | docker login --username AWS --password-stdin xxx.dkr.ecr.ap-northeast-1.amazonaws.com

# Dockerイメージを作成
$ docker build -t serverless-lambda-typescript-container .

# イメージにタグ付け
$ docker tag serverless-lambda-typescript-container:latest xxx.dkr.ecr.ap-northeast-1.amazonaws.com/serverless-lambda-typescript-container:latest

# ECRにpush
$ docker push xxx.dkr.ecr.ap-northeast-1.amazonaws.com/serverless-lambda-typescript-container:latest
:
latest: digest: sha256:qwertyuiopasdfghjklzxcvbnm123456789 size: 2212

ECRにpush後に出力されるdigestはLambdaをデプロイする際に必要なので控えておきます。

最後にServerless FrameworkでLambdaにデプロイします。

serverless.tsに仮設定しておいたimageプロパティを下記のように書き換えます。


$ git diff
diff --git a/serverless.ts b/serverless.ts
index e04e0a7..2918832 100644
--- a/serverless.ts
+++ b/serverless.ts
@@ -15,7 +15,7 @@ const serverlessConfiguration: AWS = {
   },
   functions: {
     hello: {
-      image: 'xxx'
+      image: 'xxx.dkr.ecr.ap-northeast-1.amazonaws.com/serverless-lambda-typescript-container@sha256:qwertyuiopasdfghjklzxcvbnm123456789'
     }
   }
 }

imageの指定方法は{account}.dkr.ecr.{region}.amazonaws.com/{repository}@{digest}となります。

注意したいのがイメージの指定はタグではできず、digestでの指定が必須となる点です。

digestにはECRにpushした際に控えておいた文字列を指定します。

imageの指定ができたら、下記コマンドを実行しLambda Functionを作成しましょう。


$ serverless deploy

処理が通ったらAWSコンソール上でLambda Functionがパッケージタイプimageで作成されていることが確認できます。

AWSコンソール上でテストしてみます。

無事に処理が通ることを確認することができました。

おわりに

AWS Lambdaがコンテナサポートされたことによって、本番環境と同じ環境で開発を行うことが可能になりました。

普段から利用しているtype safeなTypeScriptでも、その恩恵に預かれることができました。

とは言え、WebpackでのトランスパイルやServerless Frameworkでのデプロイに手間がかかる点を考えるとまだまだ改善の余地はあるでしょう。

特にデプロイについては

  1. プログラムファイルを修正
  2. ECRにDockerイメージをpush
  3. serverless.tsのimage指定を2のdigestに更新
  4. serverless deployでLambdaを更新

とファイルを変更したりと多少の無理矢理感は否めないので、実運用に耐えれるかは今後も検証を重ねたいです。

ではまた。

最新情報をチェックしよう!