ちょっと話題の記事

AWS Lambda ( Typescript ) の Lambda Layers 活用、開発、デプロイ考察

2019.01.18

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

動機が2つあります。

  • Lambda Function を TypeScript で実装したい
  • 2018年の re:Invent で発表された AWS Lambda Layers を使ってみたい

TypeScript で

これまでサーバーレスの業務では、Lambda Function をPythonで開発することが多かったのですが、どうしても型が欲しくなりました。特に、Lambda Function は AWS SDK を使うシーンが多いため、このパラメータや戻り値の型情報をドキュメントから得るか、コード補完できるかは、大きな違いです。いくつか選択肢がありますが TypeScript を選びました。

AWS Lambda Layers

ちょっと触ってみたかったというのが正直なところですが、ライブラリを Lambda Function に含めるとどうしてもデプロイパッケージが肥大化します。この記事でやってみたのは、node_modules のproductionパッケージを AWS Lambda Layers に含める というものです。

全体概略

arch.jpg

  • ①: アプリケーション全体で利用するパラメータをパラメータストアで一元管理します
  • ②: AWS SAM を使って Lambda Function をデプロイします
  • ③: AWS SAM を使って Lambda Layers に node_modules をデプロイします
  • ④: CloudFormation を使って AWS リソースをデプロイします

もくじ

  • Cloud Development Kit スケルトンの利用
  • パラメータストアの利用
  • Lambda Function を開発する
  • AWS Lambda Layers への node_modulesのデプロイ
  • Lambda Functionのデプロイ
  • AWS Lambda 以外の AWSリソースを定義してデプロイ
  • Makefileにする

バージョン情報

  • Lambda Function: nodejs 8.10
aws --version
aws-cli/1.16.90 Python/3.6.5 Darwin/18.2.0 botocore/1.12.80
┌── archiver@3.0.0  # node_modules の zip で利用
├── aws-cdk@0.16.0  # プロジェクトテンプレートの作成で利用
├── aws-sdk@2.384.0 # Lambda Function の開発で利用
├── commander@2.19.0# スクリプトでコマンドライン引数を取得するために利用
├── js-yaml@3.12.1  # パラメータストアのリストからCloudFormationテンプレートを作るのに利用
├── luxon@1.9.0     # サンプルアプリケーションで利用
├── ngeohash@0.6.3  # サンプルアプリケーションで利用
├── source-map-support@0.5.9 # Lambda Function のエラーを追跡するために導入
├── ts-loader@5.3.3 # webpack で TypeScriptソースからのバンドル作成に利用
├── ts-node@7.0.1   # TypeScript のスクリプトを直接実行するのに利用
├── tslint@5.12.0
├── typescript@3.2.2
├── webpack@4.28.3
├── webpack-cli@3.2.1
└── webpack-node-externals@1.7.2  # Lambda Function のバンドル時 node_modules を除外するために利用

Cloud Development Kit 初期化コマンドの利用

CDKはTypeScriptもサポートしています。プロジェクトテンプレートをイチから作成するのも大変だなと思ったので、初期化のためだけにCDKを使うことにしました。

npm i -g aws-cdk
cdk init app --language=typescript

TypeScript で開発するにあたり基本となる設定ファイルが追加されます。これをベースにしていろいろ追加していきます。

パラメータストアの利用

サーバーレスアプリケーションを開発する上での課題のひとつがこれだと思っています。パラメータとひとくちににいっても、環境ごとの違い、CloudFormationに埋め込むもの、スクリプトで利用するもの…など、誰がどう使うかわかりません。

CloudFormationでは、パラメータの参照先としてパラメータストアを指定することができます。これを受け、

  • 利用したいパラメータをパラメータストアにPUTする
  • パラメータストアのデータを適切な形式に変換して利用する

という大方針を立てました。まずは利用したいパラメータをパラメータストアへアップロードすることを考えてみます。

PUT元になるパラメータファイルを用意してPUTする

アップロードしたいデータをJSON形式で用意します。以下サンプルです。環境ごとのファイルを用意し、対応するパラメータストアのパスにPUTするイメージです。例えば、開発環境itgのパラメータEnvは、パラメータストアの/itg/lambda/Env というキー名で保存します。こうすることで、パラメータトアの「パスを指定して一覧を抽出する」というコマンドラインオプション(--path)を使い、開発環境のパラメータ一覧を抽出できます。

environments/itg-ssm-variables.json

[
  {
    "Name": "NameSpace",
    "Value": "cm"
  },
  {
    "Name": "Env",
    "Value": "itg"
  },
  {
    "Name": "DynamoDBEndpoint",
    "Value": "https://dynamodb.ap-northeast-1.amazonaws.com/"
  },
  {
    "Name": "NoteDeviceTableName",
    "Value": "${env}-note-device"
  },
  {
    "Name": "LocationTypeRcu",
    "Value": "1"
  },
  {
    "Name": "LocationTypeWcu",
    "Value": "1"
  },
  {
    "Name": "LocationPagerLimit",
    "Value": "2"
  }
]

スクリプトでPUTします。

jq -c --arg env itg 'def addenv(f): f as $value | "/" + $env + "/lambda/" + $value; .[] |  {Name:addenv(.Name), Value:.Value, Type:"String"} | tostring'  environments/itg-ssm-variables.json |\
awk -v env=itg -v ns=cm '{ print "aws ssm put-parameter --overwrite --cli-input-json " $1}' |\
sh

これで以下を実行することになります。

aws ssm put-parameter --overwrite --cli-input-json "{\"Name\":\"/itg/lambda/NameSpace\",\"Value\":\"cm\",\"Type\":\"String\"}"
aws ssm put-parameter --overwrite --cli-input-json "{\"Name\":\"/itg/lambda/Env\",\"Value\":\"itg\",\"Type\":\"String\"}"
aws ssm put-parameter --overwrite --cli-input-json "{\"Name\":\"/itg/lambda/DynamoDBEndpoint\",\"Value\":\"https://dynamodb.ap-northeast-1.amazonaws.com/\",\"Type\":\"String\"}"
aws ssm put-parameter --overwrite --cli-input-json "{\"Name\":\"/itg/lambda/NoteDeviceTableName\",\"Value\":\"${env}-note-device\",\"Type\":\"String\"}"
aws ssm put-parameter --overwrite --cli-input-json "{\"Name\":\"/itg/lambda/LocationTypeRcu\",\"Value\":\"1\",\"Type\":\"String\"}"
aws ssm put-parameter --overwrite --cli-input-json "{\"Name\":\"/itg/lambda/LocationTypeWcu\",\"Value\":\"1\",\"Type\":\"String\"}"
aws ssm put-parameter --overwrite --cli-input-json "{\"Name\":\"/itg/lambda/LocationPagerLimit\",\"Value\":\"2\",\"Type\":\"String\"}"

反映されていることを確認します。

aws ssm get-parameters-by-path --path /itg/lambda

{
    "Parameters": [
        {
            "Name": "/itg/lambda/Env",
            "Type": "String",
            "Value": "itg",
            "Version": 11,
            "LastModifiedDate": 1546935822.918,
            "ARN": "arn:aws:ssm:ap-northeast-1:xxxxxxxxxxxxx:parameter/itg/lambda/Env"
        },
...

これを使って CloudFormation テンプレートなどへ応用していきます。

Lambda Function を開発する

サンプルアプリケーションを使います。

src
└── lambda
    ├── domains
    ├── handlers
    ├── infrastructures
    └── modules

このようなフォルダ構成にしました。Lambda Function は、エントリポイントのパラメータを受けつけさえすれば、あとはその中で別のクラスを呼び出したりライブラリを使ったりは自由です。サンプルアプリケーションでは Kinesis Streams から受け取ったデータを DyanmoDB へ保存しています。

handlers/location-transfer.ts

exports.transfer = async (event: any) => {
    console.log(event);

    const dispatchPromises = event.Records.map((record: any) => {
        const payloadString = new Buffer(record.kinesis.data, 'base64').toString('utf-8');
        const payload = JSON.parse(payloadString);
        console.log(payload);
        return LocationTransferController.update(payload); // ここで計算したりDynamoDBへ保存したり
    });
    return Promise.all(dispatchPromises);
};

この Lambda Function を AWS Lambda で実行できるようにしましょう。 

AWS Lambda Layers への node_modulesのデプロイ

開発した Lambda Function をデプロイする前に、node_modules の扱いについて考えます。Lambda Function で外部ライブラリを使っている場合、当然 Function から参照できる必要があります。そこで、

  1. Lambda Function の実行環境にプリインストールされているライブラリを使う
  2. Lambda Funciton にライブラリのコードを含める

いずれかの手段をとることになります。1は実行環境にないライブラリを使おうと思った瞬間に詰むので、2を選びたいです。が、AWS SDK を筆頭に、ライブラリを含めるとどうしてもサイズが膨らみます。そこで、2018年の re:Invent で発表された AWS Lambda Layers を使って、この課題の解決を試みます。なお、node_modules を Lambda Layers に入れて使う方針は実際に試した方もいらっしゃるようで、以下の記事が参考になります:

もうちょっと踏み込んで開発サイクルに組み込んでいくとすると、以下のようなことも考慮する必要がありそうです。

  • package.json のうち production のものだけ zip する
  • zip、s3アップロード、Layer へのデプロイ
  • デプロイしたLayerのARNを Lambda Function のデプロイテンプレートから参照できるようにする

package.json のうち production のものだけデプロイ用にインストールする

npm installすると devDependencies も含めてインストールされますが、Lambda Function で実行時に必要になるのは dependencies のもののみです。開発時にインストールしたものはdevDependenciesも入っているので、単にnode_modulesをzipしたのでは不要なものが多すぎます。開発で利用している node_modulesとは別に、改めてインストールしなおすことにします。このときnpm install でインストール先パスを指定できると嬉しかったのですが、どうも難しいみたいですね。

ダーティですが package.json を作業用フォルダにコピーし、そこで本番用ライブラリだけインストールすることにしました。

mkdir -p layer_modules 
cp package.json layer_modules 
npm install --production --prefix layer_modules

layer_modules/node_modules にインストールされるのでこれをzip、S3アップロード、Layerデプロイしていきます。

zip、s3アップロード、Layer へのデプロイ

ドキュメント によると、zipを展開したときに nodejs/node_modules/aws-sdk のような構成になっている必要があるようです。zipするスクリプトを書きました。

node-modules-archives.ts

import * as archiver from 'archiver';
import * as fs from 'fs';

export class NodeModulesArchives {
    public static archive(): void {
        const modules = './layer_modules';
        const output = fs.createWriteStream('./lambda_layer.zip');
        const archive = archiver('zip', {zlib: {level: 9}});
        archive.pipe(output);
        archive.directory(modules, 'nodejs');
        archive.finalize();
    }
}
NodeModulesArchives.archive();

package.json に実行コマンドを追加します。

package.json

...
    "scripts": {
        "build": "tsc",
        "watch": "tsc -w",
        "cdk": "cdk",
        "archive-library": "ts-node node-modules-archives.ts",
        "build-lambda": "webpack",
        "gen-params": "ts-node environments/template-parameters-generator.ts"
    },
...

これでzipします。

npm run archive-library

できあがったzipをS3にアップロードします。デプロイ用のS3バケットを用意してそこへアップします。

aws s3 mb s3://cm-itg-note-lambda-deploy
aws s3 cp lambda_layer.zip s3://cm-itg-note-lambda-deploy/

最後に、SAMテンプレートを作成して、デプロイします。

layer.yaml

AWSTemplateFormatVersion: '2010-09-09'
Transform: 'AWS::Serverless-2016-10-31'
Resources:
  NoteNodeModulesLayer:
    Type: AWS::Serverless::LayerVersion
    Properties:
      LayerName: itg-note-library-layer
      Description: node_modules
      ContentUri:
        Bucket: cm-itg-note-lambda-deploy
        Key: lambda_layer.zip
      CompatibleRuntimes:
        - nodejs8.10
      LicenseInfo: MIT
      RetentionPolicy: Retain
aws cloudformation deploy \
	--template-file layer.yaml \
	--stack-name itg-lambda-layer  \
	--capabilities  CAPABILITY_NAMED_IAM CAPABILITY_IAM \
	--no-fail-on-empty-changeset;

Layer に node_modules をデプロイすることができました。これで Layer 側の準備は完了です。

Lambda Functionのデプロイ

次は Lambda Function をデプロイします。ここで、最初にパラメータストアへパラメータをPUTしていることを思い出してください。

SAMないしCloudFormation のテンプレートを複数にわける場合、どうしてもパラメータの共有が課題になっていました。例えば DynamoDB をデプロイするテンプレートとLambda Functionをデプロイするテンプレートをわける場合、それぞれのテンプレートに同じパラメータを記述することになります。これではテーブル名に変更が入った場合などのメンテナンス性がよくありません。また、そういったパラメータはE2Eテストなど、デプロイテンプレート以外からも使われるシーンがあるため、「一元管理したものを必要に応じて展開する」という考え方にいきつきました。

パラメータストアから CloudFormation テンプレートの Parameters を生成する

というわけで、冒頭の概略図に乗せたような、パラメータストアで一元管理し、それをCloudFormationのParametersとして展開、各テンプレートにくっつける という方針にします。以下のようなスクリプトを使いました。

template-parameters-generator.ts(抜粋)

public async generateCfnParametersFile(): Promise<void> {

    // パラメータストアから一覧取得
    const ssmParameters = await this.getSsmParameters(env);

    // CloudFormation の Parameters の形へ変換
    const cfnParametersFromFile: ICfnKeyValue[] = fixedParameters.map(this.convertFixedParameterToCfnParameter);
    const cfnParametersFromSsm: ICfnKeyValue[] = ssmParameters.map(this.convertSsmParameterToCfnParameter);
    const cfnParameters = {
        Parameters:
            cfnParametersFromFile.concat(cfnParametersFromSsm).reduce((map: ICfnParameter, obj) => {
                map[obj.Key] = obj.Value;
                return map;
            }, {}),
    };

    // ファイル出力
    const outPath = `templates/${env}_lambda_common_parameters.yaml`;
    const outData = jsyaml.safeDump(cfnParameters);
    console.log(outData);
    await util.promisify(fs.writeFile)(outPath, outData, 'utf8');
}

※ 全文はこちら

実行すると以下のようなファイルができあがります。

itg_lambda_common_parameters.yaml

Parameters:
  DeployBucketName:
    Type: String
    Default: deploy
  Env:
    Type: 'AWS::SSM::Parameter::Value<String>'
    Default: /itg/lambda/Env
  NameSpace:
    Type: 'AWS::SSM::Parameter::Value<String>'
    Default: /itg/lambda/NameSpace
  LocationPagerLimit:
    Type: 'AWS::SSM::Parameter::Value<String>'
    Default: /itg/lambda/LocationPagerLimit
  NoteDeviceTableName:
    Type: 'AWS::SSM::Parameter::Value<String>'
    Default: /itg/lambda/NoteDeviceTableName
  LocationTypeRcu:
    Type: 'AWS::SSM::Parameter::Value<String>'
    Default: /itg/lambda/LocationTypeRcu
  DynamoDBEndpoint:
    Type: 'AWS::SSM::Parameter::Value<String>'
    Default: /itg/lambda/DynamoDBEndpoint

Lambda Layers のテンプレートを修正する

Lambda Function のデプロイを行う前に、環境名などベタ書きしていた Lambda Layers の SAM テンプレートを修正します。生成したパラメータ情報を使うようにしましょう。

layer.yaml

AWSTemplateFormatVersion: '2010-09-09'
Transform: 'AWS::Serverless-2016-10-31'
Resources:
  NoteNodeModulesLayer:
    Type: AWS::Serverless::LayerVersion
    Properties:
      LayerName: !Sub ${Env}-note-library-layer
      Description: node_modules
      ContentUri:
        Bucket: !Ref DeployBucketName
        Key: lambda_layer.zip
      CompatibleRuntimes:
        - nodejs8.10
      LicenseInfo: MIT
      RetentionPolicy: Retain

デプロイする前にパラメータファイルと合体します。

cat templates/layer.yaml templates/itg_lambda_common_parameters.yaml > layer.yaml

これで、パラメータを利用したデプロイが可能です。

Lambda Function のテンプレートを作成する

  • Lambda Layers の ARN を指定する
  • 生成されたパラメータファイルを合体する

ことを意識します。

lambda_note.yaml

AWSTemplateFormatVersion: '2010-09-09'
Transform: AWS::Serverless-2016-10-31
Resources:
  NoteTransferLocationLambda:
    Type: AWS::Serverless::Function
    Properties:
      FunctionName: !Sub ${Env}-note-transfer-location
      Role: !GetAtt NoteLambdaRole.Arn
      KmsKeyArn: !GetAtt NoteLambdaKmsKey.Arn
      Handler: location/index.transfer    # Lambda Function のハンドラとなるパスを指定
      Runtime: nodejs8.10
      CodeUri: dist/
      Timeout: 5
      Layers:
        - !Sub arn:aws:lambda:${AWS::Region}:${AWS::AccountId}:layer:${Env}-note-library-layer:${LayerVersion}
      Environment:
        Variables:
          ENV: !Ref Env
          NOTE_DEVICE_TABLE_NAME: !Ref NoteDeviceTableName

  NoteLambdaKmsKey:
    Type: AWS::KMS::Key
    Properties:
      Description: Note application KMS key.
      KeyPolicy:
        Id: key-consolepolicy-3
        Version: '2012-10-17'
        Statement:
        - Sid: Enable IAM User Permissions
          Effect: Allow
          Principal:
            AWS: !Sub arn:aws:iam::${AWS::AccountId}:root
          Action: kms:*
          Resource: "*"
        - Sid: Allow use of the key
          Effect: Allow
          Principal:
            AWS:
            - !GetAtt NoteLambdaRole.Arn
          Action:
          - kms:Encrypt
          - kms:Decrypt
          - kms:ReEncrypt*
          - kms:GenerateDataKey*
          - kms:DescribeKey
          Resource: "*"
        - Sid: Allow attachment of persistent resources
          Effect: Allow
          Principal:
            AWS:
            - !GetAtt NoteLambdaRole.Arn
          Action:
          - kms:CreateGrant
          - kms:ListGrants
          - kms:RevokeGrant
          Resource: "*"
          Condition:
            Bool:
              kms:GrantIsForAWSResource: true
  NoteLambdaKmsKeyAlias:
    Type: AWS::KMS::Alias
    Properties:
      AliasName: !Sub 'alias/${Env}/note/lambda'
      TargetKeyId: !Ref NoteLambdaKmsKey
  NoteLambdaRole:
    Type: 'AWS::IAM::Role'
    Properties:
      RoleName: !Sub ${Env}-note-lambda-role
      ManagedPolicyArns:
      - 'arn:aws:iam::aws:policy/AmazonDynamoDBFullAccess'
      - 'arn:aws:iam::aws:policy/AmazonKinesisFullAccess'
      - 'arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole'
      - 'arn:aws:iam::aws:policy/AmazonS3FullAccess'
      Policies:
      - PolicyName: KmsDecryptPolicy
        PolicyDocument:
          Version: '2012-10-17'
          Statement:
            Effect: Allow
            Action:
            - kms:Encrypt
            - kms:Decrypt
            Resource:
            - !Sub 'arn:aws:kms:*:${AWS::AccountId}:key/*'
      - PolicyName: PinpointFullAccess
        PolicyDocument:
          Version: '2012-10-17'
          Statement:
            Effect: Allow
            Action:
            - mobiletargeting:*
            - mobileanalytics:*
            Resource: "*"
      - PolicyName: PermissionToPassAnyRole
        PolicyDocument:
          Version: '2012-10-17'
          Statement:
            Effect: Allow
            Action:
            - iam:PassRole
            Resource: !Sub arn:aws:iam::${AWS::AccountId}:role/*
      AssumeRolePolicyDocument:
        Version: '2012-10-17'
        Statement:
        -
          Effect: 'Allow'
          Principal:
            Service:
            - 'lambda.amazonaws.com'
          Action:
          - 'sts:AssumeRole'

webpack で バンドルからnode_modules を除外する

Lambda Function をデプロイするために、バンドルを作ります。ポイントは、node_modules は バンドルに含めないこと です。webpack.config.js を以下のようにしました。webpack-node-externalsというライブラリを使っています。

webpack.config.js

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

module.exports = {
    mode: 'development',
    target: 'node',
    entry: {
        // entry を複数指定することで、Lambda Function ごとのJSバンドルを作ることができます
        'location': path.resolve(__dirname, './src/lambda/handlers/location/location-transfer.ts'),
    },
    // webpack-node-externals を使って除外します
    externals: [nodeExternals()],
    output: {
        filename: '[name]/index.js',
        path: path.resolve(__dirname, 'dist'),
        libraryTarget: 'commonjs2',
    },
    devtool: "inline-source-map",
    module: {
        rules: [
            {
                test: /\.ts$/,
                use: [
                    {
                        loader: 'ts-loader'
                    }
                ]
            }
        ]
    },
    resolve: {
        extensions: ['.ts', '.js']
    }
};

デプロイ

あとはコマンドを実行してデプロイします。

npm run build-lambda # `webpack` を実行しているだけ
cat templates/lambda_note.yaml templates/itg_lambda_common_parameters.yaml > lambda_note.yaml
aws cloudformation package \
	--template-file lambda_note.yaml \
	--s3-bucket cm-itg-note-lambda-deploy \
	--output-template-file packaged-note.yaml
aws cloudformation deploy \
	--template-file packaged-note.yaml \
	--stack-name itg-note-lambda  \
	--capabilities  CAPABILITY_NAMED_IAM CAPABILITY_IAM \
	--no-fail-on-empty-changeset \
	--parameter-overrides \
		DeployBucketName=cm-itg-note-lambda-deploy \
		LayerVersion=1

これで Lambda Function 周りがデプロイできました。

AWS Lambda 以外の AWSリソースをデプロイ

Lambda Function 以外でも、考え方は同じです。CloudFormation テンプレートを定義して、パラメータファイルと合体します。DynamoDB リソースをデプロイするテンプレートを書いてみます。

infra_dynamodb_tables.yaml

AWSTemplateFormatVersion: '2010-09-09'
Resources:
  NoteDeviceTable:
    Type: AWS::DynamoDB::Table
    Properties:
      TableName: !Ref NoteDeviceTableName
      AttributeDefinitions:
      - AttributeName: endpointId
        AttributeType: S
      - AttributeName: geoHash5
        AttributeType: S
      - AttributeName: dispatchAt
        AttributeType: N
      KeySchema:
      - AttributeName: endpointId
        KeyType: HASH
      ProvisionedThroughput:
        ReadCapacityUnits: !Ref LocationTypeRcu
        WriteCapacityUnits: !Ref LocationTypeWcu
      GlobalSecondaryIndexes:
      - IndexName: geoHash5-index
        KeySchema:
        - AttributeName: geoHash5
          KeyType: HASH
        - AttributeName: dispatchAt
          KeyType: RANGE
        Projection:
          ProjectionType: ALL
        ProvisionedThroughput:
          ReadCapacityUnits: !Ref LocationTypeRcu
          WriteCapacityUnits: !Ref LocationTypeWcu

デプロイします。

cat templates/infra_dyanmodb_tables.yaml templates/itg_lambda_common_parameters.yaml > infra_dynamodb_tables.yaml
aws cloudformation deploy \
	--template-file infra_dynamodb_tables.yaml \
	--s3-bucket cm-itg-note-lambda-deploy \
	--stack-name itg-dynamodb-tables-resource  \
	--capabilities CAPABILITY_NAMED_IAM \
	--no-fail-on-empty-changeset ;

これで Lambda Function が実行できます。テストデータを流してみましょう。

{
  "Records": [
    {
      "kinesis": {
        "data": "eyJkZXZpY2VUb2tlbiI6IiIsImxhdGl0dWRlIjoiMzUuNzAyMDY5MSIsImxvbmdpdHVkZSI6IjEzOS43NzUzMjY5IiwiZW5kcG9pbnRJZCI6IlhYWFgtMDcwMC00QTVFLVhYWFhYWFgiLCJkYXRlIjoxNTQ3MTk2ODI0ODMwLjMwNDJ9"
      }
    }
  ]
}

lambda_test.jpg

Lambda Function のコンソールから実行します。

dynamodb_result.png

GeoHashの計算されたデータが投入されていれば成功です。

Makefileにする

環境名やテンプレート名は変わるものなので、タスクランナー的なものがあると良いですね。これまでに書いたスクリプトを Makefile として実行できるようにしました。

以下の要領で実行できます。

swrole # あらかじめデプロイしたい環境にスイッチロール
make push-params env=itg ns=cm # 名前空間と環境名を指定してパラメータストアへPUT
make layer env=itg ns=cm layer=0 # Lambda Layer の zip とデプロイ
make deploy-note env=itg ns=cm layer=0 # Lambda Function のデプロイ
make infra-dynamodb_tables env=itg ns=cm # CloudFormation テンプレートによるデプロイ

まとめ

Lambda Layers を利用してみてどうだったか

まず、劇的な効果があったのは Lambda Function のバンドルサイズです。

  • node_modules を含めた場合: 6.8MB
  • node_modules を除外した場合: 66KB

歴然ですね。少なくとも Lambda Function のみをデプロイする点においてはサイクルの高速化が望めそうです。Lambda Layers を利用した場合のパフォーマンスについては岩田の記事を参考にしてください。

上記の記事でも言及がありますが、現状、Lambda Layers のバージョンが上がった場合、依存する Lambda Function をすべて再デプロイする必要があります。常に Lambda Layers の最新版を参照するようなオプションがあると嬉しいと思いました。ただ、これは思わぬ事故を発生させる要因にもなる(ライブラリの破壊的アップデートでアプリケーションが動かなくなるなど)ので、利便性との兼ね合いではありますね。

TypeScript で開発してみてどうだったか

俄然良いです。 ts-loader を導入したり、ソースマップを利用したりと、動かし始めるまでのステップはやや多いですが、一番うれしかったのは AWS SDK のパラメータと戻り値に型定義があり、IDEで補完することで入出力がわかるため、リファレンスドキュメントを見る時間が激減した 点です。

また、TypeScriptというよりはJavaScriptの話ですが、AWSとやりとりするコードは何かとリストや配列を使います(サンプルの Kinesis Streams のレコードを使う Lambda Function もそうですね)。forEachmapfilterreduceといった操作が用意されているため、効率的にロジックを組むことができます。

総括

この記事では パラメータストアを組み合わせて Lambda Layers、Lambda Function、AWSリソースをデプロイする例を示しました。SAM を筆頭に、 AWSサーバーレスアプリケーションのデプロイ周りはとても便利になっていますが、全体をひとつのアプリケーションとして扱いたい場合、どうしてもまだ一工夫必要そうです。参考例としてどなたかの助けになれば幸いです。

参考

ソースコード