AWS Lambda ( Typescript ) の Lambda Layers 活用、開発、デプロイ考察
動機が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 に含める というものです。
全体概略
- ①: アプリケーション全体で利用するパラメータをパラメータストアで一元管理します
- ②: 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
)を使い、開発環境のパラメータ一覧を抽出できます。
[ { "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 へ保存しています。
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 から参照できる必要があります。そこで、
- Lambda Function の実行環境にプリインストールされているライブラリを使う
- 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するスクリプトを書きました。
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 に実行コマンドを追加します。
... "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テンプレートを作成して、デプロイします。
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として展開、各テンプレートにくっつける という方針にします。以下のようなスクリプトを使いました。
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'); }
※ 全文はこちら
実行すると以下のようなファイルができあがります。
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 テンプレートを修正します。生成したパラメータ情報を使うようにしましょう。
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 を指定する
- 生成されたパラメータファイルを合体する
ことを意識します。
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というライブラリを使っています。
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 リソースをデプロイするテンプレートを書いてみます。
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 Function のコンソールから実行します。
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 もそうですね)。forEach
、map
、filter
、reduce
といった操作が用意されているため、効率的にロジックを組むことができます。
総括
この記事では パラメータストアを組み合わせて Lambda Layers、Lambda Function、AWSリソースをデプロイする例を示しました。SAM を筆頭に、 AWSサーバーレスアプリケーションのデプロイ周りはとても便利になっていますが、全体をひとつのアプリケーションとして扱いたい場合、どうしてもまだ一工夫必要そうです。参考例としてどなたかの助けになれば幸いです。