AWS SAMによるLambda Layers & Layers利用Functionの作成手順と運用観点での注意点

16 min read読了の目安(約14700字

Summary

  • Node.jsのLambdaで、レイヤー用のスタックと、それを利用するFunctionスタックのAWS SAMによる作成を以下の手順で実施
    1. レイヤーのスタックをデプロイ
    2. レイヤーを利用するLambda Functionのスタックで、sam local invoke によるローカル実行で動作確認
    3. レイヤーを利用するLambda Functionのスタックをデプロイ
  • レイヤーのスタックでは、<テンプレートのContentUriで指定したフォルダ>/nodejs/node_modules というフォルダにライブラリがインストールされるようにフォルダを構成する
  • sam deploy--guided オプション付きで実行するとデプロイ成果物配置用のS3バケットを自動で作成(既にあれば再利用)してくれるが、このバケットは "バージョニング有・ライフサイクル未設定" で作成されるので、samconfig.tomlによって2回目以降のデプロイ手順を簡略化する事が出来る恩恵とバージョニングの恩恵 vs コスト面の課題 のメリデメを判断して利用する
    • --guided オプションと --s3-bucket オプションでの固有バケット指定を併用すると、--s3-bucket オプションの内容は無視される

前提条件

  • Summaryに記載の通り、スタックがレイヤー用とそれを利用するLambda Functionで別々になっている、というケースを想定しています
# aws-cliのバージョン
$ aws --version
aws-cli/2.1.28 Python/3.9.2 Darwin/19.6.0 source/x86_64 prompt/off

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

# Dockerのバージョン
$  docker version | grep Version | head -1
Version:           20.10.2  # Docker Desktop for Mac Ver 3.1.0

# nodeのバージョン
$ node -v
v12.18.4

レイヤーのスタックを構築・デプロイ

レイヤー用スタックのフォルダ構成

下記構成で用意します。テンプレートファイルと、レイヤーに含めるライブラリ群を配置するフォルダ(例では node-base-layer )、という構成です。

.
├── .node-version
├── node-base-layer
│   └── nodejs
│       ├── node_modules
│       ├── package-lock.json
│       └── package.json
└── template.yaml

template.yaml

例えば下記のような内容になります。

---
AWSTemplateFormatVersion: 2010-09-09

Description: Lambda Layer for Node.js

Transform: AWS::Serverless-2016-10-31

Parameters:

  NodeBaseLayerNameParam:
    Description: Layer Name for NodeBaseLayer
    Type: String
    Default: node-base-layer

Resources:

  NodeBaseLayer:
    Type: AWS::Serverless::LayerVersion
    Properties:
      Description: Base Layer for Node.js
      LayerName: !Ref NodeBaseLayerNameParam  # デフォルト = "node-base-layer"
      ContentUri: node-base-layer/
      RetentionPolicy: Retain
      CompatibleRuntimes:
        - nodejs12.x

  NodeBaseLayerPermission:
    Type: AWS::Lambda::LayerVersionPermission
    Properties:
      Action: lambda:GetLayerVersion
      LayerVersionArn: !Ref NodeBaseLayer
      Principal: !Ref AWS::AccountId

Outputs:
  Name:
    Description: Stack Name.
    Value: !Ref AWS::StackName
    Export:
      Name: !Ref AWS::StackName

  NodeBaseLayerArn:
    Description: Published NodeBaseLayer ARN.
    Value: !Ref NodeBaseLayer
    Export:
      Name: !Sub ${AWS::StackName}-layer-arn

package.json

例えば下記のような内容になります。※ 後ほどの動作確認用にライブラリとして cheerio だけを含めています。

{
  "name": "nodejs",
  "version": "1.0.0",
  "description": "Lambda Layer for Node.js",
  "main": "index.js",
  "dependencies": {
    "cheerio": "^1.0.0-rc.5"
  },
  "devDependencies": {},
  "scripts": {},
  "keywords": [],
  "author": "",
  "license": "ISC"
}

npm install → デプロイ

公式ドキュメントで言及されているフォルダ配置のルール に則ってレイヤーに含めるライブラリ(Nodeの場合は node_modules )を配置し、テンプレートの ContentUri で指定するローカルパスもそれに合わせたものにする必要があります。今回の例では node-base-layer/nodejs/node_modules というパスにライブラリがあり、テンプレートの ContentUri には node-base-layaer/ を指定します。

結果的に <テンプレートのContentUriで指定したフォルダ>/nodejs/node_modules というフォルダがライブラリ配置先と合致していればOK です。

公式リポジトリに用意されているNode.js用の雛形プロジェクト の場合では、lib/nodejs/node_modules というフォルダをライブラリ配置先にしてあります 。 (なので ContentUri にはlib/が指定されています

実際の運用は

  1. package.jsonを編集してライブラリを追加なり更新なり削除し
  2. npm install して
  3. sam deploy する

という流れになります。今回はバケット自動作成のお試しも兼ねて --guided オプション付きでデプロイしてみます。

# ライブラリのインストール
$ cd node-base-layer/nodejs
$ npm install

# デプロイ
$ cd ../..
$ sam deploy --template template.yaml --region <YOUR_REGION> --stack-name layer-stack --capabilities CAPABILITY_IAM --guided

Configuring SAM deploy
======================

        Looking for config file [samconfig.toml] :  Not found

        Setting default arguments for 'sam deploy'
        =========================================
        Stack Name [layer-stack]: 
        AWS Region [ap-northeast-1]: 
        #Shows you resources changes to be deployed and require a 'Y' to initiate deploy
        Confirm changes before deploy [y/N]: y
        #SAM needs permission to be able to create roles to connect to the resources in your template
        Allow SAM CLI IAM role creation [Y/n]: Y
        Save arguments to configuration file [Y/n]: Y
        SAM configuration file [samconfig.toml]: 
        SAM configuration environment [default]: 


Deploy this changeset? [y/N]: y

デプロイ結果確認

デプロイが正常に完了すれば、AWS::Lambda::LayerVersion リソースと AWS::Lambda::LayerVersionPermission リソースがそれぞれ作成されます。aws lambda list-layer-versions コマンドでそのARNを確認できればOKです。

# 出力結果の末尾の数値はバージョン番号なので、バージョンが増える度に表示される結果も増えます
$ aws lambda list-layer-versions --layer-name node-base-layer | jq '.LayerVersions[].LayerVersionArn'

"arn:aws:lambda:ap-northeast-1:<自身のAccountId>:layer:node-base-layer:1"

注意点: --guided オプション付きでデプロイした際に自動作成されるS3バケットについて

ところで、sam deploy--guided オプション付きで実行すると samconfig.toml(=デフォルトのファイル名) というファイル が作成されます。

version = 0.1
[default]
[default.deploy]
[default.deploy.parameters]
stack_name = "layer-stack"
# バケット
s3_bucket = "aws-sam-cli-managed-default-samclisourcebucket-xxxxxxxxxxxx"
# プレフィクス
s3_prefix = "layer-stack"
region = "ap-northeast-1"
confirm_changeset = true
capabilities = "CAPABILITY_IAM"

この中の s3_bucket が、--guided オプション付きでのデプロイ時にSAMが自動で作成してくれるデプロイ成果物配置用バケットの名前です。実は2年前に同テーマの記事を書いた当時はレイヤーをzipアーカイブ→S3にアップロード という操作は自前で行う必要があったのですが、2019年11月リリースのバージョンからバケット自動作成に対応されました。これによりレイヤー用スタックの管理運用作業が「ライブラリの追加や更新」→「sam deploy」という操作だけで済むようになりました。

作成されたバケットには、デプロイされたレイヤーのテンプレートとパッケージングされたライブラリ群の2種類のオブジェクトが存在しています。

$ aws s3api list-objects --bucket aws-sam-cli-managed-default-samclisourcebucket-xxxxxxxxxxxx | jq '.Contents[].Key'

"layer-stack/xxxxxxxxxxxxxxx.template"
"layer-stack/yyyyyyyyyyyyyyyyyyyy"

この自動作成されるバケットの注意点は、バージョニングが有効になっている、且つ、ライフサイクルが未設定 な事です。つまりこのバケットを配置先としてデプロイを実行するとその都度成果物が新バージョンで登録され続ける&それらが残り続ける事になります。--guided オプション付きでのデプロイによりsamconfig.tomlに(デフォルトの)設定を出力しておく事によって2回目以降のデプロイ手順を簡略化する事が出来るという恩恵+バージョニングの恩恵は得られるものの、コスト面の注意は払っておいた方が良いかも知れません。
例えば sam deploy にはバケット明示用の --s3-bucket やプレフィクス指定用の --s3-prefix というオプションも用意されていますので、これらを利用して自動作成バケットの利用を回避する、という策も検討の余地がありそうです(ちなみにSAM 1.19では、--guided--s3-bucket を両方指定すると --s3-bucket の方が無視される。という挙動になっています)

レイヤーを利用するFunctionのスタックで、sam local invoke によるローカル実行

Function用スタックの準備

続いてレイヤーを利用するFunctionを準備します。デプロイ済のレイヤーに含まれているライブラリを参照できるという事だけを確認したいので、とりあえず sam init で単純なプロジェクトを作っておきます。

$ sam init

Which template source would you like to use?
        1 - AWS Quick Start Templates
        2 - Custom Template Location
Choice: 1
What package type would you like to use?
        1 - Zip (artifact is a zip uploaded to S3)
        2 - Image (artifact is an image uploaded to an ECR image repository)
Package type: 1

Which runtime would you like to use?
        1 - nodejs14.x
        2 - python3.8
        3 - ruby2.7
        4 - go1.x
        5 - java11
        6 - dotnetcore3.1
        7 - nodejs12.x
        8 - nodejs10.x
        9 - python3.7
        10 - python3.6
        11 - python2.7
        12 - ruby2.5
        13 - java8.al2
        14 - java8
        15 - dotnetcore2.1
Runtime: 7

Project name [sam-app]:

Cloning app templates from https://github.com/aws/aws-sam-cli-app-templates

AWS quick start application templates:
        1 - Hello World Example
        2 - Step Functions Sample App (Stock Trader)
        3 - Quick Start: From Scratch
        4 - Quick Start: Scheduled Events
        5 - Quick Start: S3
        6 - Quick Start: SNS
        7 - Quick Start: SQS
        8 - Quick Start: Web Backend
Template selection: 1

    -----------------------
    Generating application:
    -----------------------
    Name: sam-app
    Runtime: nodejs12.x
    Dependency Manager: npm
    Application Template: hello-world
    Output Directory: .

    Next steps can be found in the README file at ./sam-app/README.md

sam-app というプロジェクトが作成されますので、この中のFunctionをデプロイ済レイヤーを参照・利用するよう変更します。

template.yaml

「パラメータでレイヤーのARNを指定し、それを対象FunctionのLayersに設定」するように修正しておきます(以下差分)

@@ -5,6 +5,11 @@

   Sample SAM Template for sam-app

+Parameters:
+  LayerArnParam:
+    Description: Layer ARN
+    Type: String
+
 # More info about Globals: https://github.com/awslabs/serverless-application-model/blob/master/docs/globals.rst
 Globals:
   Function:
@@ -23,6 +28,8 @@
           Properties:
             Path: /hello
             Method: get
+      Layers:
+        - !Ref LayerArnParam

 Outputs:
   # ServerlessRestApi is an implicit API created out of Events key under Serverless::Function```

関数コード app.js

「レイヤーに含まれているライブラリをrequireする」というコードを追加しておきます。

@@ -1,5 +1,6 @@
 // const axios = require('axios')
 // const url = 'http://checkip.amazonaws.com/';
+const cheerio = require('cheerio')
 let response;

 /**

sam local invoke でローカル実行

npm installsam buildsam local invoke の順で、正常に実行されるか確認してみます。

# sam-app のpackage.jsonにもデフォルトでいくつかライブラリを含んでいるのでそれをインストールしておく
$ cd hello-world
$ npm install


# build (--parameter-overrides の LayerArnParam でデプロイしたレイヤーのARNを指定)
$ cd ..
$ sam build --template template.yaml --region <YOUR_REGION> --parameter-overrides LayerArnParam=<DEPLOYED_LAYER_ARN>

Build Succeeded

Built Artifacts  : .aws-sam/build
Built Template   : .aws-sam/build/template.yaml

Commands you can use next
=========================
[*] Invoke Function: sam local invoke
[*] Deploy: sam deploy --guided


# local invoke (レイヤーのARNは --parameter-overrides オプションのパラメータで設定)
$ sam local invoke HelloWorldFunction --template template.yaml --region <YOUR_REGION> --parameter-overrides LayerArnParam=<DEPLOYED_LAYER_ARN>

{"statusCode":200,"body":"{\"message\":\"hello world\"}"}

requireしたライブラリがレイヤーから読み込めていれば正常終了します。

補足: デプロイ済レイヤーを sam local invoke 実行時に参照する仕組み

公式ドキュメントに解説がありますが、ザックリまとめると、

  • ${HOME}/.aws-sam/layers-pkg/<LAYER_NAME>-<LAYER_VERSION_NUMBER>-xxxxxxxxxx というパスにレイヤーがダウンロード&キャッシュされる
    • Node.jsの場合、${HOME}/.aws-sam/layers-pkg/<LAYER_NAME>-<LAYER_VERSION_NUMBER>-xxxxxxxxxx/nodejs/node_modules というフォルダ構成になっていればOK
  • sam local invoke の実行環境であるDockerイメージ = "samcli/lambda" イメージがビルドされる。この際にこのDockerイメージ内へダウンロード&キャッシュ済のレイヤーが取り込まれる
  • レイヤーは実行環境の /opt ディレクトリ下に展開される ので、つまりNode.jsの場合は /opt/nodejs/node_modules 下に展開されていればOK
    • ローカルの"samcli/lambda" Dockerイメージでも同様のフォルダが展開されていればOK。実際に/opt 下にどんなパスで展開されたか?は docker run でコンテナに入って確認できる(公式でも同様の確認手順が紹介されている
      • ※逆に、/opt/nodejs/node_modules に展開できていなかったりすると、sam local invoke 時にレイヤーに含まれているライブラリを参照しようとしたタイミングで "Cannot find module" なエラーが発生します

レイヤーを利用するFunctionのスタックのデプロイ

レイヤーの初回デプロイ時と同様に、Functionスタックの初回デプロイも --guided オプション付きで実行してみます。

$ sam deploy --template template.yaml --region <YOUR_REGION> --stack-name sam-app --capabilities CAPABILITY_IAM --guided --parameter-overrides LayerArnParam=<DEPLOYED_LAYER_ARN>

Configuring SAM deploy
======================

        Looking for config file [samconfig.toml] :  Not found

        Setting default arguments for 'sam deploy'
        =========================================
        Stack Name [sam-app]: 
        AWS Region [ap-northeast-1]: 
        Parameter LayerArnParam [arn:aws:lambda:ap-northeast-1:<自身のAccountId>:layer:node-base-layer:1]: 
        #Shows you resources changes to be deployed and require a 'Y' to initiate deploy
        Confirm changes before deploy [y/N]: y
        #SAM needs permission to be able to create roles to connect to the resources in your template
        Allow SAM CLI IAM role creation [Y/n]: Y
        HelloWorldFunction may not have authorization defined, Is this okay? [y/N]: y
        Save arguments to configuration file [Y/n]: Y
        SAM configuration file [samconfig.toml]: 
        SAM configuration environment [default]: 


Deploy this changeset? [y/N]: y

レイヤーデプロイ時と同様に生成される samconfig.toml の内容を見てみると、レイヤーデプロイ時に自動作成されたバケットが(特に実行時に指定したわけでも無いのに)再利用されている事が分かります。

# レイヤーデプロイ時に作成されたバケット
s3_bucket = "aws-sam-cli-managed-default-samclisourcebucket-xxxxxxxxxxxx"

sam deploy--guided オプションは、

  • まず「"aws-sam-cli-managed-default" というスタックと、そのスタックで作成する "aws-sam-cli-managed-default-samclisourcebucket-xxxxxxxxxxxx" という形式の名前のS3バケット」の取得を試みる
  • 存在していなければ、その作成処理が実行される

という仕様になっており(参考: ソースコード = samcli/lib/bootstrap/bootstrap.py の manage_stack 関数 )、今回のケースではレイヤーデプロイ時にスタック&バケットが既に作成済であった為、Functionのデプロイ時にはそれが自動で再利用されました。

繰り返しになりますが、このバケットは バージョニングが有効になっている、且つ、ライフサイクルが未設定 なので、レイヤー・Function共にこのバケットを利用する場合はそれぞれの成果物が削除されずデプロイの度に新バージョンが登録され続ける事になります。samconfig.tomlによる2回目以降のデプロイ簡略化の恩恵+バージョニングの恩恵 vs コスト面の課題、これらのメリデメを判断して利用した方が良さそうです。

ブックマーク