Serverless Frameworkのserverless-layersプラグインを使って超お手軽にnode_modulesをAWS Lambda Layers化する

Serverless Frameworkで手軽にLambda Layersを扱えないかなということで探していたところ、serverless-layersというプラグインを見つけました。このプラグインを利用することで、依存モジュールを簡単にLambda Layersとしてデプロイできます。
2019.10.02

この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。

こんにちは。サービスグループの武田です。

AWS Lambdaを使ってサーバーレスなアプリケーションを開発する際に、Serverless Frameworkが非常に便利で重宝しています。

さてLambdaをNodeで開発する場合、依存モジュール(node_modules)の扱いにはいくつか選択肢があります。その中で、現在ではLambda Layersを使うというのが候補の筆頭になるでしょうか。Serverless Frameworkで手軽にLambda Layersを扱えないかなということで探していたところ、serverless-layersというプラグインを見つけました。このプラグインを利用することで、依存モジュールを簡単にLambda Layersとしてデプロイできたのでシェアします。

検証環境

次のような環境で検証しています。

$ sw_vers
ProductName:	Mac OS X
ProductVersion:	10.14.6
BuildVersion:	18G103

$ node -v
v10.16.3

$ npm ls serverless serverless-layers
example-serverless-js@1.0.0 /path/to/example-serverless-js
├── serverless@1.53.0
└── serverless-layers@1.4.2

Lambdaのランタイムはnodejs10.xです。またAWSにデプロイするためのIAMなどは用意されていることとします。

やってみた

プロジェクト作成

何はともあれデプロイするアプリケーションを作っていきます。またnode_modulesがないと話が進みませんので、適当なモジュールとして今回はmomentをインストールしました。

$ npx -p serverless sls create -t aws-nodejs -p example-serverless-js
$ cd $_ && npm init -y && npm install -D serverless
$ npm install moment

またデプロイ先を東京リージョンにするため、provider.regionを追記しておきます。

serverless.yml

provider:
  name: aws
  runtime: nodejs10.x
  region: ap-northeast-1

次にインストールしたモジュールを使うようにhandler.jsを少し改造します。

handler.js

'use strict';

const moment = require('moment');

module.exports.hello = async event => {
  return {
    statusCode: 200,
    body: JSON.stringify(
      {
        message: 'Go Serverless v1.0! Your function executed successfully!' + moment().format(),
        input: event,
      },

これで準備ができましたので、ローカル実行してみましょう。

$ npx sls invoke local -f hello
{
    "statusCode": 200,
    "body": "{\n  \"message\": \"Go Serverless v1.0! Your function executed successfully!2019-10-01T12:00:00+09:00\",\n  \"input\": \"\"\n}"
}

問題なく実行できました。

Layer化せずにデプロイ

Layer化した後とのサイズが比較できるように、一度そのままデプロイしてみます。

$ npx sls deploy

...
Serverless: Uploading service example-serverless-js.zip file to S3 (890.28 KB)...

$ npx sls invoke -f hello
{
    "statusCode": 200,
    "body": "{\n  \"message\": \"Go Serverless v1.0! Your function executed successfully!2019-10-01T12:00:00+00:00\",\n  \"input\": {}\n}"
}

890.28 KB というサイズでした。実行も問題なくできています。マネジメントコンソールにアクセスしてデプロイされたファイルを確認してみたところ、次のようになっていました。

serverless-layersを導入してデプロイ

続いてserverless-layersを導入していきます。次のステップで導入します。

  1. Lambda Layersデプロイ用のS3バケットを用意
  2. serverless-layersプラグインをインストール
  3. 設定ファイルに設定を追加

1の手順は省略します。ここではバケット名をlambda-layers-deploy-123456789012としておきます。

次にプラグインのインストールです。npm installでも可能ですが、sls plugin installの方がプラグインの設定もしてくれるため少し便利です。

$ npx sls plugin install --name serverless-layers

最後に先ほど作成したバケットを設定ファイルに追加します。

serverless.yml

custom:
  serverless-layers:
    layersDeploymentBucket: lambda-layers-deploy-123456789012

Gitなどのリポジトリで管理する場合、バケット名が設定ファイルに含まれていると都合が悪いこともあるでしょう。その場合はバケット名を外部ファイルに移し、serverless.ymlからはそれを参照するようにしておくとよいかと思われます。

それではデプロイしてみます。

$ npx sls deploy
Serverless: [LayersPlugin]: Downloading package.json from bucket...
Serverless: [LayersPlugin]: package.json does not exists at bucket...
Serverless: [LayersPlugin]: Dependencies has changed! 

...
Serverless: Uploading service example-serverless-js.zip file to S3 (54.85 KB)...

$ npx sls invoke -f hello
{
    "statusCode": 200,
    "body": "{\n  \"message\": \"Go Serverless v1.0! Your function executed successfully!2019-10-01T12:00:00+00:00\",\n  \"input\": {}\n}"
}

自動的にpackage.jsonをロードしてLayerとしてアップロードしていることが確認できます。またLambda本体は 54.85KB とかなり軽量化されていました。マネジメントコンソールでファイルを確認すると次のようになっていました。node_modulesがなくなっています。

さらにスリム化

本筋ではないのですが、package.jsonが少し邪魔に見えたので除外設定もしてみました。

serverless.yml

exclude:
  - package.json
  - package-lock.json

これで再度デプロイします。

$ npx sls deploy
Serverless: [LayersPlugin]: Downloading package.json from bucket...
Serverless: [LayersPlugin]: Comparing package.json dependencies...
Serverless: [LayersPlugin]: Not has changed! Using same layer arn: arn:aws:lambda:ap-northeast-1:123456789012:layer:example-serverless-js-dev:1

...
Serverless: Uploading service example-serverless-js.zip file to S3 (421 B)...

$ npx sls invoke -f hello
{
    "statusCode": 200,
    "body": "{\n  \"message\": \"Go Serverless v1.0! Your function executed successfully!2019-10-01T12:00:00+00:00\",\n  \"input\": {}\n}"
}

421B !! めっちゃ軽くなりましたね!ついでにpackage.jsonを比較して変更がなかったため、Lambda Layersも更新していないことが確認できました。デプロイされたのはhandler.jsだけです。

依存パッケージを追加してみる

先ほどの手順で、依存関係に変更がなければLambda Layersも変更しないことを確認しました。最後に、依存関係に変更があった場合に、Lambda Layersも更新することを確認しましょう。追加のモジュールとしてunderscore.stringを追加してみます。

$ npm install underscore.string

次にこのモジュールを使用するようにhandler.jsを改造します。

handler.js

'use strict';

const moment = require('moment');
const s = require('underscore.string');

module.exports.hello = async event => {
  return {
    statusCode: 200,
    body: JSON.stringify(
      {
        message: 'Go Serverless v1.0! Your function executed successfully!' + s.prune(moment().format(), 12),
        input: event,
      },

ローカルで実行してみます。

$ npx sls invoke local -f hello
{
    "statusCode": 200,
    "body": "{\n  \"message\": \"Go Serverless v1.0! Your function executed successfully!2019-10-01T...\",\n  \"input\": \"\"\n}"
}

特に問題がなければ、最後にデプロイして確認します。

$ npx sls deploy
Serverless: [LayersPlugin]: Downloading package.json from bucket...
Serverless: [LayersPlugin]: Comparing package.json dependencies...
Serverless: [LayersPlugin]: Dependencies has changed! Re-installing...

...
Serverless: Uploading service example-serverless-js.zip file to S3 (446 B)...

$ npx sls invoke -f hello
{
    "statusCode": 200,
    "body": "{\n  \"message\": \"Go Serverless v1.0! Your function executed successfully!2019-10-01T...\",\n  \"input\": {}\n}"
}

依存関係の変更を検知してLambda Layersを更新していることが確認できました。またLambda本体も446Bとコンパクトなままです。

まとめ

Serverless Frameworkのプラグインを利用することで、Lambda Layersを簡単に導入できました。意識することなく依存モジュールを扱え非常に便利ですね!