既存のLambda FunctionにLambda Layersを導入し、AWS SAMで管理する(Node.js)

PUBLISHED ON 2019-02-07 — AWS, AWS LAMBDA, AWS SAM, JAVASCRIPT, NODE.JS, SERVERLESS, SLACK

Summary

  • axiosなどのモジュールに依存した既存のNode.js Lambda Functionがある
  • このfunctionで使用しているモジュールをAWS Lambda Layersに移行し、aws-sam-cliでデプロイする
  • 既存functionで個別にnpm installしていたモジュールを削除し、デプロイしたLayerを使用するように変更する
  • Layer移行後も既存functionが sam local invoke コマンドでローカルで実行できる事を確認する
  • Layerを使うように変更した既存functionをデプロイする
  • 移行した結果
    • 既存functionのサイズを半分以下に削減できた
    • 既存functionの実行時間を半分以下に削減できた
  • AWS SAM 便利
    • Lambda Layerも便利、だけど、チーム開発でCIの仕組み構築まで考え始めると一工夫必要かも?

前提条件

# aws-cliのバージョン
$ aws --version
aws-cli/1.16.80 Python/3.7.1 Darwin/18.2.0 botocore/1.12.70

# aws-sam-cliのバージョン
$ sam --version
SAM CLI, version 0.10.0

既存Functionで使用中のモジュールをLayersに移行する

Layer用のプロジェクトディレクトリを作成し、Layer化したいnpmモジュールをインストールします。 ディレクトリの構成は公式ドキュメントの記載 の通り nodejs/node_modules という構成になるようにします。

$ cd <PROJECT_ROOT>
$ mkdir nodejs

$ cd nodejs
$ npm init -y
$ npm install [必要なモジュール群]

ここまで完了すると、PROJECT_ROOTの下には下記のディレクトリとファイルが作成された状態になります

.
└── nodejs
    ├── node_modules
    ├── package-lock.json
    └── package.json

zipでアーカイブ

成果物をnodejs ディレクトリがトップディレクトリとなるようにアーカイブします。

$ cd <PROJECT_ROOT>
$ zip -r layer.zip ./

zipをS3にアップロード

作成したzipをS3にアップロードします。バケットが別途必要であれば事前に作成しておきます。

# BUCKET, REGION, FOLDERは適宜各自の環境の内容に置き換え

# バケットを作成(必要であれば)
$ aws s3 mb s3://${S3_BUCKET} --region ${S3_REGION}

# この例では、zipを指定バケット・指定フォルダの下にアップロード
$ aws s3 cp layer.zip s3://${S3_BUCKET}/${S3_FOLDER}/

AWS SAMのテンプレートを作成

デプロイテンプレートを作成します。テンプレートの形式はSAMの公式ドキュメント を参考にします。ContentUri がS3にアップロードしたzipのURLになっている事がポイントです。

$ vi template.yaml
AWSTemplateFormatVersion: '2010-09-09'
Transform: AWS::Serverless-2016-10-31
Description: >
  My Node.js modules Layer

Resources:
  MyLayer:
    Type: AWS::Serverless::LayerVersion
    Properties:
      LayerName: my-layer
      Description: My Node.js modules layer
      ContentUri: 's3://S3_BUCKET/S3_FOLDER/layer.zip'
      CompatibleRuntimes:
        - nodejs8.10
      LicenseInfo: 'Available under the MIT-0 license.'
      RetentionPolicy: Retain

作成したらaws-sam-cliでyamlのvalidationをしておくと良いです。

$ sam validate

デプロイ/公開

aws-sam-cliでデプロイします。

$ sam deploy --template-file ./template.yaml --stack-name layer-stack --capabilities CAPABILITY_IAM

Waiting for changeset to be created..
Waiting for stack create/update to complete
Successfully created/updated stack - layer-stack

既存functionをLayerを使用する形式に変更する

今回筆者が対象にしたfunctionは下記のようなディレクトリ構成で、API Gatewayをeventトリガーとして処理を行うfunctionです。

.
├── Makefile
├── README.md
├── hello_world
│   ├── app.js
│   ├── lib
│   ├── node_modules
│   ├── package-lock.json
│   ├── package.json
│   └── tests
│       └── unit
│           └── test_handler.js
├── serverless-output.yaml
└── template.yaml

Layer移行により node_modules に含まれている大部分のmoduleがこのfunctionでは不要になる想定です。これを念頭に、

  • Layer移行済モジュールを削除
  • samテンプレートを修正
  • ローカルでfunctionを実行して正常動作するか確認する
  • デプロイ&動作確認

という流れで移行作業を進めていきます。

Layer移行済のモジュールを削除

まずLayerに移行したモジュールを対象のfunctionから削除します。package.jsonのdependenciesから不要モジュールの記述を削除し、npm install を再実行します。

例えば私のfunctionを例にすると、aws-sdk, axios, cheerioの3モジュールを全てLayerに移行したので、下記のdependenciesは全て不要になるので削除しました。

# CODE_URIは適宜各自の環境の内容に置き換え

$ cd <FUNCTION_ROOT>
$ vi ${CODE_URI}/package.json
  "dependencies": {
    "aws-sdk": "^2.387.0",
    "axios": "^0.18.0",
    "cheerio": "^1.0.0-rc.2"
  },
  :
$ cd hello_world
$ npm install

template.yamlの修正

デプロイしたLayerのARNを下記コマンドで確認します。 --layer-name にはLayerのtemplate.yamlで設定した LayerName を指定します。正常終了すると下記形式の結果が返ってきます。(REGION と ACCOUNT_ID は適宜ご自身の環境に読み替えてください)

$ aws lambda list-layer-versions --layer-name my-layer | grep LayerVersionArn

            "LayerVersionArn": "arn:aws:lambda:REGION:ACCOUNT_ID:layer:my-layer:1",

確認したARNを使用して、functionのテンプレートファイルをLayerを使用する形式に修正します。こちらのドキュメント に記載の通り、functionのリソース定義のProperties指定に Layers というセクションを追加し、そこにLayerのARNを設定します。

$ cd <FUNCTION_ROOT>
$ vi template.yaml
Resources:
  HelloWorldFunction:
    Type: AWS::Serverless::Function # More info about Function Resource: https://github.com/awslabs/serverless-application-model/blob/master/versions/2016-10-31.md#awsserverlessfunction
    Properties:
      CodeUri: hello_world/
      Handler: app.lambdaHandler
      Runtime: nodejs8.10

      (中略)

      Layers:
        - arn:aws:lambda:ap-northeast-1:123456789012:layer:my-layer:1

ローカルでfunctionの動作確認

Layerに移行した&移行済モジュールを削除したので、ローカルでの実行がこれまで通り行えるかを確認しておきます。ローカルでの実行コマンドはLayer移行前から変える必要は無く、これまで通り sal local invoke コマンドを使用します。

1点挙動が異なるのは、Layer移行後初回の実行時にデプロイ済のLayer Functionのダウンロードが実行される事です。デフォルトではダウンロード結果は ${HOME}/.aws-sam/layers-pkg/ 下に保存(キャッシュ)され、Layerのバージョンアップを行わない限りは2回目以降の実行時はキャッシュが利用されます。

$ sam local invoke -e event.json HelloWorldFunction

Downloading arn:aws:lambda:ap-northeast-1:123456789012:layer:my-layer:1  [####################################]  7298732/7298732
:

注意点: Layers設定時のハマりどころ

こちらのドキュメントの例では、template.yamlの Layers セクションで組み込み関数 Fn::Sub の短縮形 !Sub が使われているのですが、これはlocalでの実行時は機能しません。この内容で sam local invoke を行うと、Fn::Sub 関数で期待している変数置換が実行されずLayerのARNが正しい内容で処理されない為、Layer Functionのダウンロードが実行されません。その結果「layerに含まれているモジュールが見つからない」という旨のエラーになってしまいます。localでの実行も考慮するなら、LayersのARNは文字列のみで記載するか、あるいは Ref 関数で「文字列のみで記載された別リソースを参照する」という手段で記述します。

デプロイ/公開

aws-sam-cliでデプロイします。

# BUCKET, FOLDER, STACKは適宜各自の環境の内容に置き換え

$ sam package --template-file ./template.yaml --output-template-file ./serverless-output.yaml --s3-bucket ${S3_BUCKET} --s3-prefix ${S3_FOLDER}
Successfully packaged artifacts and wrote output template to file ./serverless-output.yaml.

$ sam deploy --template-file ./serverless-output.yaml --stack-name ${STACK} --capabilities CAPABILITY_IAM

Waiting for changeset to be created..
Waiting for stack create/update to complete
Successfully created/updated stack - layer-stack

Layerの更新

node moduleの追加/削除などでLayerの更新が必要になった場合も作業の流れはほとんど変わりません。

まずzipアーカイブとS3へのアップロードを再度実行します。

$ cd <PROJECT_ROOT>
$ zip -r layer.zip ./

$ aws s3 cp layer.zip s3://${S3_BUCKET}/${S3_FOLDER}/

更新デプロイは sam deploy ではなく aws lambda publish-layer-version コマンドで行います。

$ aws lambda publish-layer-version \
  layer-name my-layer \
  description "My Node.js modules Layer" \
  license-info "Available under the MIT-0 license." \
  content S3Bucket=${S3_BUCKET},S3Key=${S3_FOLDER}/layer.zip \
  compatible-runtimes nodejs8.10

上記コマンドでLayerの更新が完了したら、Layerのバージョンが更新された旨が処理結果として返ってきます。利用側functionのtemplate.yamlのLayersバージョンも合わせて更新し、functionの再デプロイが必要になります。

$ cd <FUNCTION_ROOT>
$ vi template.yaml
Resources:
  HelloWorldFunction:
    Type: AWS::Serverless::Function # More info about Function Resource: https://github.com/awslabs/serverless-application-model/blob/master/versions/2016-10-31.md#awsserverlessfunction
    Properties:
      CodeUri: hello_world/
      Handler: app.lambdaHandler
      Runtime: nodejs8.10

      (中略)

      # my-layer:2 にバージョンアップ
      Layers:
        - arn:aws:lambda:ap-northeast-1:123456789012:layer:my-layer:2

ブックマーク