echo("備忘録");

IT技術やプログラミング関連など、技術系の事を備忘録的にまとめています。

【VS Code】VS Code Conference Japanで発表しました

はじめに

2020/11/21(土)に、Visual Studio Code5周年記念として、「VS Code Conference Japan」というイベントがオンラインで開催されました。

vscode.connpass.com

この中で、「アプリ開発&チーム管理で役立った拡張機能など」という内容で30分セッションを行いましたので、その内容を含め、VS Code Conference Japanについて書きたいと思います。

※ちなみに、発表資料は↓になります。
URL:https://www2.slideshare.net/MasakiSuzuki3/ss-239362824

www2.slideshare.net

お礼

管理者の皆様、いろいろ手筈を整えていただき、ありがとうございました。

また、当日聴いてくださった皆さん、ありがとうございました。

セッションで紹介した拡張機能について

  • セッションで紹介した拡張機能の概要+αを記載しています。(詳細は先述の発表資料を参照)
  • 個々のURLは載せてませんが、VS Code拡張機能検索やブラウザで名前をそのまま検索すれば、(多分)出てきます。

開発環境編

Setting Sync
  • 複数のPCでVS Code環境の同期を取れる。
  • GitHubアカウント(正確にはアクセストークンとGist ID)さえあればOK。
    • Gist IDはダウンロード時のみ必要
  • やることは「設定のアップロード」と「設定のダウンロード」だけ。

ちなみに、「複数PCでのVS Code環境の同期」機能は、2020/07にVS Codeに標準で搭載された。
ただしまだプレビュー版なので、使用前に警告が出る。

f:id:Makky12:20201123083949p:plain

Live Share
  • VS Code上で複数人で同時にソースコードの編集が行える、Micorsoft製の拡張機能
  • ソースの同期だけではなく、チャット&音声通話も可能。
  • withコロナ時代のリモートワーク全盛の今の時代、ものすごく真価を発揮している拡張機能では

ちなみにLive Shareを実際に動かした様子は、下記「VS Code Meetup#2 -Live Share編-」の動画を参照
https://www.youtube.com/watch?v=4wMlwWCeboQ

サーバーレスアプリ編

Azule Template Viewer
  • AzureのARM(Azure Template Manager。AWSでいうCloudFormationのような、Infrastructure as Codeを実現するリソース)のテンプレートファイルの内容について、事前にデプロイ後の内容を確認できる。
  • てか、マジでめっちゃ便利。
  • マジでCloudFormationにも欲しい...

f:id:Makky12:20201123084425p:plain

Rest Client
  • HTTPリクエスト送信&レスポンス受信をVS Code上で行える。
  • リスエスト定義&送信がめっちゃ簡単。(定義はスクショ参照。送信は[ctrl]+[alt]+[R]だけ)
  • この手の機能の拡張機能で、一番有名なのではないか?

f:id:Makky12:20201123084452p:plain

Swagger Viewer
  • Swagger定義ファイルの内容をプレビューできる。
  • 定義ファイルを変更したら、それがリアルタイムにプレビュー画面に反映されるのが良い。
  • Swagger Editor( https://editor.swagger.io/ )と同等の機能を持つ。

f:id:Makky12:20201123084532p:plain

AWS ToolKit
  • AWSのいくつかのリソース(Lambda, S3, CloudWatchなど)について、参照・AWS上で実行・削除を実施できる。
  • S3はアップロード&ダウンロードを行える。
  • いちいちコンソールを開かないでよい。
  • けど、編集ができないので、そこは今後の課題かな...
  • でも、Step FunctionsのSteta Machineプレビュー機能は便利(これも変更がりリアルタイムで反映される)。あとS3アップロード&ダウンロード機能とか。
  • セッションでも言ったけど、個人的にDynamoDBのCRUDができると、かなりいい感じではないかなと思う。

f:id:Makky12:20201123084604p:plain

コーディング補助編

ESLint
  • JavaScriptの静的検証ツール
  • 構文エラーとか、設定したコーディングルールに反したコードを知らせてくれる。
Prettier
  • コードフォーマッタ
  • コードを整形し、可読性の高いコードにしてくれる。
  • 一部静的検証も行ってくれる。
Grammarly
  • もともとはドキュメントの文法チェッカ
  • スペルが誤っている単語をお知らせしてくれる。
  • 「unofficial」となっているが、使用には全く問題ない。(もともとMSが公式で出していたらしいが、無くなったため)
Code Spell Checker
  • VS Code上でスペルチェックを行ってくれる。
  • スペルが違う単語についてお知らせしてくれて、修正候補も表示してくれる。
  • 「expectVarue」(正しくは「expectValue」)のような、複数の単語がつながった変数などのスペルミスも教えてくれる。

f:id:Makky12:20201123084719p:plain

Gi編

GitLens
  • ブランチ・ファイル・ファイル行単位で履歴を表示する
  • ブランチ/タグ/過去コミット間での差分を表示する
  • ソース内に変更者&コミットコメントを表示する

...など(他にもいろいろ)、「VS CodeでGit扱うなら、とりあえずこれは入れとけ」レベルで便利な拡張機能

f:id:Makky12:20201123084746p:plain

Git Graph
  • Gitリポジトリについて、ブランチ&変更履歴をツリー状に表示してくれる。
    • Backlog Gitの「ネットワーク」みたいな感じ
  • ツリー上から、Gitの各種操作(push/pull/checkoutなど)が可能
  • これも非常に便利だし「とりあえずこれは入れとけ」レベルでおススメ

f:id:Makky12:20201123084817p:plain

その他支援ツール編

Excel Viewer
  • 名前の通り、ExcelVS Code上で閲覧できる。(閲覧のみ。編集は不可)
  • Excelだけではなく、CSVファイルも閲覧可能。
    • というか、CSVビューワーとして非常に便利
  • ソート&フィルタ機能が標準で使用できるのが非常に良い。
  • データの調査&解析に向いている。

f:id:Makky12:20201123085151p:plain

Draw.io Integration
  • Draw.io(現Diagrams.net)のフローチャート作成機能をVS Code上で実行できる。
  • 微妙に違う部分があるかもしれないが、使った限りはさほど気にならないレベル。
  • 本家同様、AWS/Azureなどクラウド関連の図が豊富で、クラウド・サーバーレスのフロー図作成に便利。

f:id:Makky12:20201123084917p:plain

Hex Editor

f:id:Makky12:20201123084948p:plain

おまけ:諸事情で資料に載せられなかった拡張機能

Barcket Lens & Bracket Code Colorizer(2)
  • どちらも、コード内の各種カッコについて、対応するカッコを教えてくれる拡張機能
  • Bracket Code Colorizer(2)は色で、Bracket Lensは文字列で教えてくれる。
  • Bracket Code Colorizerは1と2があるが、基本的な機能は同じ。(2の方が性能が向上している、とのこと)

※左がBracket Lens, 右がBracket Code Colorizer2(なんかこのブログを書いているPCだと、同時使用ができなかった。たまたま?)

indent-rainbow
  • 名前の通り、インデントを深さごとに色分けして、コードを読みやすくしてくれる。
  • 色は設定可能

f:id:Makky12:20201123090320p:plain

余談

  • 最初は「サーバーレスアプリ開発で役立った拡張機能など」というタイトルを予定していましたが、構想を考えてるうちに「これ、ほとんど(Azule Template ViewerとAWS ToolKit以外)サーバーレス関係なくね?」と思い、運営さんにタイトルを変更してもらいました。(ありがとうございました)
  • 実は10月後半からずっと体調不良が続いており、本格的に資料を作成できたのがイベントの2日前からでした。
  • しかも、最初は10分くらいだとずっと思っていたので、イベント3日前のZoomリハーサルで30分と聞いたときは、マジで焦りました。
  • そして「まあリモートだし、始業前とかにも時間あるから、そこで進めるか」と思った時に限って、前日は3週間に一度の出社日というタイミングの悪さ...
  • 発表内容&このブログでも、AWS Toolkitがちょっと辛口だったかな?
  • でも個人的にはAWS Toolkitには期待しているし、実際クラウドの中ではAWSが一番好きなので、期待を込めてという意味で

お知らせ

12月にQiitaさんで行われる「Advent Calendar 2020」において、2記事を担当することになりました。
担当するテーマは、サーバーレスAWS です。

ちなみに、下記のタイトルで記事を書こうかな、と健闘中です。(確定ではない)

  • サーバーレス:Serverless Frameworkを始めよう または Serverless Frameworkでクロススタック参照を行う
  • AWSaws-sdk-mockでAWSのテストを行う

おわりに

ていうか、体調も戻ってきたし、そろそろブログも本格的に再開したいです。
Serverless Frameworkもいろいろ機能やプラグインが追加されましたし、いろいろ触って、いろいろ記事にしたいですね。本当に。
プラグインも自作したいですし。

それにAWSを始め、いろいろ技術が出てきたので、こんな時代でも仕事が絶えないように、時間の許す限りスキルアップしていきたいなと思ってます。

それではまた。

【Serverless Framework】IAM Roleを共有し複数プロジェクトでリソース毎に割り当てする

概要

AWS のIAM Role(≒実行権限)を共通プロジェクトでまとめて作成しておき、後で別の複数プロジェクトで使用する(=共有する)方法です。

経緯

先日Twitterをしていたら、こんなツイートを見つけました。

一つのterraformプロジェクトで一元管理してたリソースを、要望に合わせてIAMとそれ以外のterraformプロジェクトに分けて同じ名前のIAMを作ってから各リソースでPassRoleするようにしたらハマった arnが同じでも内部的には別のKeyを持っていて、destroyされたIAMを参照しようとしてるっぽい

で、それについて僕が書きのRTをしました。

これ、Serverless Frameworkならどうやるかな。親プロジェクトでIAMRole作って、それをOutputに設定して、子プロジェクトでそれをcf:stack変数で参照する感じかな?

そうしたら、元ツイのツイート主の方と色々お話が弾みまして、元ツイのツイート主の方から、下記疑問をいただきました。

今回の要望だと、IAMとそれ以外のプロジェクトに関してapplyできる組織を完全に分けたいという話だったので、slsでやるとしたらIAM用とそれ以外で完全にプロジェクト分けて、それ以外の方はarnをベタで書く感じになりますかね・・(slsでIAMだけdeployできるのかどうかは知らない)

(中略)

slsのIAMって、providerで指定するときにfunctionsにアタッチするpolicyを記述するイメージなので、functionsがない状態でのslsってどういう挙動になるのかは興味ありましね・・知人がslsで実装してるのですが、functionに権限渡してるだけですね・・

このツイートに対して、下記リプを返しました。

多分functionsがない場合、resourcesで普通にCloudFormationみたいに定義する感じですかね。てかslsでIAMだけってそういややったことないですね。たいてい一つはLambdaをデプロイするので…(最悪、ダミーLambdaを作る手はあるかもしれませんが…)

が、上記リプを返した所で、

  • 「そういやfunctionsを一つも作成しないパターン、やったことないな」
  • 「このやり方も実際に動かしたわけじゃないから、動く確証がないな...」
  • 「なら、実際に確かめてみるか!」

というわけで、実際に確かめてみることにしました。

参考

【Serverless Framework】API Gatewayを複数プロジェクト(serverless.yml)で共有する(Share API Gateway and API Resources)

※前回の記事ですが、前回と類似した処理を行いますので、こちらもあわせて参考にされるとよいと思います。

今回作成する成果物構成

プロジェクト

下記2つのプロジェクトを作成します。

プロジェクト名 説明 備考
iam-share-first IAM関連をまとめて作成するプロジェクト 初めにデプロイしておく必要がある
iam-share-second iam-share-firstで作成したIAMを使用するリソースを割り当てるプロジェクト iam-share-firstのデプロイ後にデプロイする必要がある
IAM Roleについて

「iam-share-first」で、下記2つのIAM Roleを作成します。

定義名 説明 備考
IamRoles3GetObject S3のGet系APIのみ実行を許可するIAM Role List系は不可
IamRoles3ListObjects S3のList系APIのみ実行を許可するIAM Role Get系は不可
Lambdaについて

「iam-share-second」で、下記2つのLambdaを作成します。

Lambda名 説明 割り当てるIAM Role 備考
GetObject S3.GetObjectのみ実行される(はず)のLambda IamRoles3GetObject S3.ListObjectsは権限がないからエラーになる(はず)
ListObjects S3.ListObjectsのみ実行される(はず)のLambda IamRoles3ListObject S3.GetObjectは権限がないからエラーになる(はず)

ちなみに、上記2つのLambdaはソースコード自体は全く同じで、その内容は下記になります。

※S3のGetObjectとListObjectsを実施するソースになります。ただ付与しているIAM Roleはそれぞれ異なるので、IAM Roleを付与していない方の処理は権限がなくてエラーになる...という確認です。

※「suzukima-iam-share-test-bucket」には、あらかじめ「I am iamTest.txt」という内容の「iamTest.txt」というテキストファイルを格納しておきます。

'use strict';  
  
const AWS = require("aws-sdk");
const S3 = new AWS.S3();
const BUCKET_NAME = "suzukima-iam-share-test-bucket";
  
module.exports.index = async event => {
  
  console.log(`[event] ${JSON.stringify(event)}`);
  
  try{
    await getObject();
    console.info('GetObject Succeeded');
  } catch(e) {
    console.warn(`[GetObject Error] ${e.message}`);
    console.warn('GetObject Failed');
  }
  
  try{
    await listObjects();
    console.info('listObjects Succeeded');
  } catch(ex) {
    console.warn(`[listObjects Error] ${ex.message}`);
    console.warn('listObjects Failed');
  }
  
  return;  
};
  
async function getObject() {
  const params = {
    Bucket: BUCKET_NAME,
    Key: "iamTest.txt"
  };
  
  const data = await S3.getObject(params).promise();
  console.info(`[GetObject Body] ${data.Body.toString()}`);
  
  return;
}
  
async function listObjects() {
  const params = {
    Bucket: BUCKET_NAME,
  };
  
  const lists = await S3.listObjects(params).promise();
  for(const content of lists.Contents) {
    console.info(`[listObjects Key] ${content.Key}`);
  }
  
  return;
}

iam-share-firstについて

※各プロジェクトのserverless.ymlについては、ブログの最後に載せておきます。
※検証当時はAPI Gatewayも共有すると勘違いして、API Gateway関連のリソースも定義していますが、IAM Roleの共有だけなら不要です。

ただし前回記事と異なり、iam-share-firstではLambdaを一切作成していませんので、その点では参考になると思います。)

resources.Resources

[resources.Resources]内で、先程の「IamRoles3GetObject」及び「IamRoles3ListObjects」の2つのIAM Roleを作成します。

そしてそれぞれに、(今回の対象バケットである)「suzukima-iam-share-test-bucket」に対する「S3.Getxxx」及び「S3.Listxxx」系のアクションを許可します。

またこの2つのIAM Roleは共にLambda関数に割り当てますが、そのLambda関数ではログを出力したいので、CloudWatch Logsに関するすべてのアクションも許可しています。

resources.Outputs

[resources.Outputs]では、[resources.Resources]で定義した2つのIAM Roleについて、「出力」タブに出力するように設定を行っています。
前回同様、こうすることによって、この2つのIAM RoleのARNをiam-share-secondで参照できるようにしています。

なお、 iam-share-first内で使用している「serverless-pseudo-parameters」プラグインですが、これはServerless FrameworkにてCloudFormationの(「AWS::AccountId」などの)疑似パラメータ参照を可能にするプラグインです。

www.serverless.com

CloudFormationの疑似パラメータ参照については、こちらを参照。

docs.aws.amazon.com

iam-share-secondについて

functions

iam-share-secondでは、[functions]で定義するLambda関数(「GetObject」及び「ListtObjects」)の「role」に、iam-share-firstで作成したIAM RoleのARNを指定するだけでOKです。

また、ここではServerless Frameworkの「${cf:[スタック名].[キー名]}」変数を使用して、iam-share-firstスタックの「出力」タブから各IAM RoleのARNを取得しています。

動作確認

実際に動作確認した結果(ログ)が、下記2つの画像になります。

GetObject関数では、S3.getObject()は正常に実施され、「iamTest.txt」の中身の「I am iamTest.txt」というログが表示されていますが、S3.listObjects()は実行権限がないため「Access Denied」となっています。

逆にListObjects関数では、S3.listObjects()は正常に実施され、バケット内の「iamTest.txt」がログに表示されていますが、S3.getObjects()は実行権限がないため「Access Denied」となっています。

このことからも、IAM Role、およびそのLambda関数への割り当てが正常に動作しているといえます。

GetObject

f:id:Makky12:20201006194537p:plain

ListObjects

f:id:Makky12:20201006194555p:plain

まとめ

以上が「IAM Roleを共有し複数プロジェクトでリソース毎に割り当てする」方法になります。

「なんでそんな面倒くさい事するの?」と思われるかもしれませんが、前回の記事でも書いた通り、プロジェクト自体が大きくなって、リソース管理やライフサイクルなどでのリソース分割を本格的に考えなければならなくなった際、リソース共有やプロジェクト分割は非常に重要になってきます。

というか、それらを本格的に考えなければならなくなった時には、すでに手遅れだったりする場合もあるので、リソース共有やプロジェクト分割は余裕のあるうちから考慮・対処をしておくのがおすすめです。(自分も痛い目にあっていますので...)

それでは、今回はこの辺で。

serverless.ymlについて

iam-share-first
  
service: iam-share-first  
provider:
  name: aws
  runtime: nodejs12.x
  
  apiGateway:  
    restApiResources:
      /s3:
        Ref: SuzukimaRestApiResourceS3
  
  stage: dev
  region: ap-northeast-1
  profile: default
  
plugins:
  - serverless-pseudo-parameters
  
custom:
  projectName: iam-share-test
  
package:  
  exclude:
    - node_modules
    - node_modules/**
  
# you can add CloudFormation resource templates here
resources:
  Resources:
    SuzukimaRestApi:
      Type: AWS::ApiGateway::RestApi
      Properties:
        Name: ${self:custom.projectName}-RestApi
    SuzukimaRestApiResourceS3:
      Type: AWS::ApiGateway::Resource
      Properties:
        RestApiId: !Ref SuzukimaRestApi
        ParentId:
          'Fn::GetAtt': [ SuzukimaRestApi, RootResourceId ]
        PathPart: 's3'
      DependsOn:
        - SuzukimaRestApi
    SuzukimaBucket:
      Type: AWS::S3::Bucket
      Properties:
        BucketName: suzukima-${self:custom.projectName}-bucket
    IamRoles3ListObjects:
      Type: AWS::IAM::Role
      Properties:
        AssumeRolePolicyDocument:
          Version: "2012-10-17"
          Statement:
            -
              Effect: Allow
              Principal:
                Service:
                  - lambda.amazonaws.com
              Action:
                - sts:AssumeRole
        Path: "/"
        Policies:
          -
            PolicyName: ${self:custom.projectName}-s3ListObjectsPolicy
            PolicyDocument:
              Version: "2012-10-17"
              Statement:
                -
                  Effect: Allow
                  Action:
                    - s3:List*
                  Resource:
                    'Fn::GetAtt': [SuzukimaBucket, Arn]
                -
                  Effect: Allow
                  Action:
                    - logs:*
                  Resource:
                    'Fn::Sub': arn:aws:logs:ap-northeast-1:#{AWS::AccountId}:log-group:/aws/lambda/*
        RoleName: ${self:custom.projectName}-s3ListObjectsRole
      DependsOn:
        - SuzukimaBucket
    IamRoles3GetObject:
      Type: AWS::IAM::Role
      Properties:
        AssumeRolePolicyDocument:
          Version: "2012-10-17"
          Statement:
            -
              Effect: Allow
              Principal:
                Service:
                  - lambda.amazonaws.com
              Action:
                - sts:AssumeRole
        Path: "/"
        Policies:
          -
            PolicyName: ${self:custom.projectName}-s3GetObjectPolicy
            PolicyDocument:
              Version: "2012-10-17"
              Statement:
                -
                  Effect: Allow
                  Action:
                    - s3:Get*
                  Resource:
                    'Fn::Join':
                      - "/"
                      - - 'Fn::GetAtt': [SuzukimaBucket, Arn]
                        - "*"
                -
                  Effect: Allow
                  Action:
                    - logs:*
                  Resource:
                    'Fn::Sub': arn:aws:logs:ap-northeast-1:#{AWS::AccountId}:log-group:/aws/lambda/*
        RoleName: ${self:custom.projectName}-s3GetObjectRole
      DependsOn:
        - SuzukimaBucket  
  
  Outputs:
    apiGatewayRestApiId:
      Value:
        Ref: SuzukimaRestApi
      Export:
        Name: ${self:custom.projectName}-restApiId-${self:provider.stage}
    apiGatewayRestApiRootResourceId:
      Value:
        'Fn::GetAtt': [SuzukimaRestApi, RootResourceId]
      Export:
        Name: ${self:custom.projectName}-rootResourceId-${self:provider.stage}
    apiGatewayResourcesS3:
      Value:
        Ref: SuzukimaRestApiResourceS3
      Export:
        Name: ${self:custom.projectName}-ResourcesS3-${self:provider.stage}
    iamRoleS3ListObjectsArn:
      Value:
        'Fn::GetAtt': [IamRoles3ListObjects, Arn]
      Export:
        Name: ${self:custom.projectName}-iamRoleS3ListObjectsArn-${self:provider.stage}
    iamRoleS3GetObjectArn:
      Value:
        'Fn::GetAtt': [IamRoles3GetObject, Arn]
      Export:
        Name: ${self:custom.projectName}-iamRoleS3GetObjectArn-${self:provider.stage}
iam-share-second
service: iam-share-second  
  
provider:
  name: aws
  runtime: nodejs12.x
  
  stage: dev
  region: ap-northeast-1
  profile: default
  
  apiGateway:
    restApiId: ${cf:${self:custom.parentStackName}.apiGatewayRestApiId}  
    restApiRootResourceId: ${cf:${self:custom.parentStackName}.apiGatewayRestApiRootResourceId}  
    restApiResources:
      /s3: ${cf:${self:custom.parentStackName}.apiGatewayResourcesS3}
  
custom:
  parentStackName: iam-share-first-dev
  projectName: iam-share-test

package:
  exclude:
    - node_modules
    - node_modules/**
    - restClient.txt

functions:
  GetObject:
    handler: iamTest.index
    name: GetObject
    role: ${cf:${self:custom.parentStackName}.iamRoleS3GetObjectArn}
    events:
      - http:
          path: /s3/get
          method: get
  
  ListObjects:
    handler: iamTest.index
    name: ListObjects
    role: ${cf:${self:custom.parentStackName}.iamRoleS3ListObjectsArn}
    events:
      - http:
          path: /s3/list
          method: get

【Serverless Framework】API Gatewayを複数プロジェクト(serverless.yml)で共有する(Share API Gateway and API Resources)

今回の内容

Serverless Frameworkを使用して、API Gatewayを複数プロジェクト(serverless.yml)で共有する(=あるプロジェクトで作成したAPI Gatewayを別のプロジェクトでも使いまわす)方法です。

Serverless Framework公式ページ

詳細

Serverless Frameworkでは、デフォルトではプロジェクト(serverless.yml)ごとに「functions」セクションで定義したLambdaのエンドポイントとなるAPI Gatewayが自動で作成されるため*1、「既存のAPI Gatewayを共有する」ことはできません。

しかし、状況によっては「既存のAPI Gatewayを共有したい(=今回もエンドポイントとして使用したい)」ケースが発生します。

それをServerless Frameworkで実現する方法です。

API Gatewayを共有する必要性

始めのうちはともかく、だんだんプロジェクトが大きくなると、デプロイ時に下記問題が発生します。

  • デプロイにとても時間がかかる
  • 1リソース(Lambdaなど)を追加するだけで、全リソースをフルデプロイしなきゃならない
    • 必要最小限だけデプロイすればいい
    • (例) Lambdaをデプロイするだけなら、S3やDynamoDBのデプロイは不要
  • リソース数が多くなると、CloudFormationの制限に引っかかる
    • CloudFormationでは、1スタックのリソースは最大200個まで

そのため、上記の解決策として下記のような「スタック分割」が行われます。

  • スタックのネスト
  • ライフサイクル(≒更新頻度)によるスタック分割
  • (マイクロサービス的な)一定のサービス単位によるリソース分割

この場合、

  • API GatewayとLambdaを別スタック(serverless.yml)に定義する
  • 別スタックで作成したAPI GatewayをLambdaのエンドポイントにする

ということがあります。(この場合「共有」ではないですが、「既存のAPI Gatewayを使いまわす」という意味では同じになります)

そのため、「既存のAPI Gatewayを共有する」必要が発生してくるわけです。

※この「スタック分割」については、別の機会に書こうと思ってます。

参考サイト

API Gatewayの共有の仕方

具体的なAPI Gatewayの共有の仕方ですが、これは実際のserverless.ymlを見ながら説明します。

ここでは「splitdeployfirst」「splitdeploysecond」という、2つのプロジェクト(=スタック)を使用します。(各serverless.ymlの内容を、末尾に掲載してあります)

「splitdeployfirst」(=先にデプロイする方のserverless.yml)

先にデプロイする「splitdeployfirst」ですが、こちらは特にAPI Gateway共有だからといって、大きく変わる部分はないと思います。(「apiGateway.restApiId」や「apiGateway.restApiRootResourceId」の定義も不要です)

ただ一点、大きく違うのは「resouces.Outputs」で、「apiGatewayRestApiId」「apiGatewayRestApiRootResourceId」という2つの項目のValueにそれぞれ

  • Ref: ApiGatewayRestApi
  • 'Fn::GetAtt': [ApiGatewayRestApi, RootResourceId]

を指定しています。(実際には必須ではないですが、こうしたほうが便利です」)

こうすることで、作成されるAPI Gatewayの「restApiId」及び「restApiRootResourceId」の値を取得できるので、それを「出力」に表示し「splitdeploysecond」プロジェクトから参照できるようにしています。

「ApiGatewayRestApi」について

※「apiGatewayRestApiId」及び「apiGatewayRestApiRootResourceId」のValueの指定で「ApiGatewayRestApi」という、serverless.ymlのどこにも定義していないキーを指定しています。

これなのですが、Serverless FrameworkではAPI Gatewayを作成する際に、「Type=AWS::ApiGateway::RestApi」のリソースを「ApiGatewayRestApi」という項目名で作成します。
ですのでこれらは「こう書くものなんだ」と思ってください。

※「apiGatewayResourcesSplitDeploy」については、後述します。

「splitdeploysecond」(=後でデプロイする方のserverless.yml)

後でデプロイする(=既存のAPI Gatewayを共有する)「splitdeploysecond」ですが、こちらで「restApiId」「restApiRootResourceId」など、API Gatewayの共有に必要な「provider.apiGateway」項目の定義を行います。

「restApiId」及び「restApiRootResourceId」の値について、「${cf:[スタック名].[キー名]}」という変数を使用しています。
Serverless Frameworkでは、この記載をすることで、CloudFormationスタックの「出力」タブ内の「キー」に該当する「値」を取得することができます。

※実際に取得できるのは「resources.Outputs」内の各項目の「Value」の値

こうしておくと、値を直書きした場合と違い、下記のメリットがあります。(splitdeployfirstで「resources.Outputsの定義は必須ではないが、こうしたほうが便利」と書いたのは、そのためです)

  • 環境を変えても(dev/stg/prodなど)、値を書き直す必要がない
  • spritdeployfirstをremove→再度デプロイしても、値を書き直す必要がない

【参考】https://www.serverless.com/framework/docs/providers/aws/guide/variables/#reference-cloudformation-outputs

※ちなみに「restApiId」及び「restApiRootResourceId」の値は、AWSコンソールからも確認できます。
API Gatewayから該当のAPI Gatewayを選択し、左のリストの「リソース」を選択すると、画面左上に表示されます。(下の画像では消してますが)

f:id:Makky12:20200901195527p:plain

restApiResourcesの指定

ところで、プロジェクト「spritdeployfirst」「spritdeploysecond」に定義したLambda(hello, guten)は、共にルートパス「splitdeploy」を参照しています。

この場合、それぞれパス「splitdeploy/hello」及び「splitdeploy/guten」のリソースがなんの問題もなくAPI Gatewayに作成されて欲しいところです。

しかし、Serverless Framework公式ページにも下記の記載があることからわかるように、実際は「spritdeploysecond」のデプロイ時にエラーになります。

The above example services both reference the same parent path /posts. However, Cloudformation will throw an error if we try to generate an existing path resource.

これは「spritdeploysecond」の「splitdeploy」を「spritdeployfirstで作成した「splitdeployと同じである」ことを認識してくれないため、下記の現象が発生し、エラーとなります。

  • 「spritdeploysecond」のデプロイ時に、ルートパス「splitdeploy」を持つリソースを新規に作成しようとする
  • しかしルートパス「splitdeploy」を持つリソースはすでに存在する
  • すでにルートパス「splitdeploy」を持つリソースは存在するのに、ルートパス「splitdeploy」を持つリソースを新規作成しようとしている
  • 重複エラー

そのため、既存のAPI Gatewayを共有する場合、パスの一部が共通のLambdaを作成する場合「パスsplitdeployは、既存のパスsplitdeployの事だよ」と教えてあげる必要があります。

そして、それを行っているのが下記の部分で、「restApiResources」に「/splitdeploy」、及びそのrestApiResourceIdを指定することで「このserverless.ymlのfunctionsのパスで指定している/splitdeployは、既存の/splitdeployの事だよ」と教えています。

こうすることで、既存のパス「/splitdeploy」を持つLambda(guten)があっても、正常にデプロイできるようになります。

provider:  
  apiGateway:
    restApiResources:
      /splitdeploy: ${cf:${self:custom.parentStackName}.apiGatewayResourcesSplitDeploy}
apiGatewayResourcesSplitDeployの内容

ところで、上記「/splitdeploy」の値を見て「splitdeployfirst(のserverless.yml)のresources.Outputs.SplitDeployFirstAuthorizerArn.Valueの値を取得している」ことは何となく分かると思います。

しかし、実際にそこに指定されている「ApiGatewayResourceSplitdeploy」という項目は、serverless.ymlのどこにもありません。

では、どこに指定されているのでしょうか?

結論を言いますと、Serverless Frameworkでは、定義内容をCloudFormation構文に変換する(serverless-state.jsonに書き込む)際、「events=http」であるLambdaのpathの内容について、区切り(/)ごとに「Type=AWS::ApiGateway::Resource」のリソースを作成します。
そしてそのキー名は「ApiGatewayResource + ルートパスからのpathの区切りごとの名前」となります。(先頭のみ大文字)

例えば、pathが「/splitdeploy/hello」の場合、キー名が下記の2つのリソースが作成されます。

  • ApiGatewayResourceSplitdeploy
  • ApiGatewayResourceSplitdeployHello

なので、それを見越して「Ref:ApiGatewayResourceSplitdeploy」と記載しているわけです。

Lambdaオーサライザの指定

API Gatewayにオーソライザを設定する場合があると思います。

Serverless Frameworkでは、Lambdaオーソライザを使用する場合、各Lambdaの「event.http」の子要素として「authorizer」を設定します。(splitdeploysecondのserverless.ymlを参照)

そしてここでは、splitdeployfirstにオーサライザ用のLambdaのみ設定し(AuthorizerLambda)、gutenでは「authorizer」にそのARNを指定しています。

この場合、今までのようにsplitdeployfirstのリソースの値(今回はARN)を参照する場合、resources.Outputsについつい下記定義をしようとしてしまいます。

resources:
  Outputs.
    SplitDeployFirstAuthorizerArn:
      Value:
        'Fn::GetAtt': [AuthorizerLambda, Arn]
      Export:
        Name: ${self:service}-SplitDeployFirstAuthorizerArn-${self:provider.stage}

しかし上記定義だと、デプロイ時に「AuthorizerLambdaなんて項目ねえよ」とエラーになってしまいます。ちなみに、Cognitoをオーソライザにした場合は、このエラーは発生しません。*2
なぜでしょうか?

これまたServerless Frameworkの仕様なのですが、Serverless Frameworkでは、定義内容をCloudFormation構文に変換する際、Lambda関数のキー名には、末尾に「LambdaFunction」というサフィックスを付けてserverless-state.jsonに書き込みます。

ですので「AuthorizerLambda」はserverless-state.jsonには「AuthorizerLambdaLambdaFunction」というキー名で書き込まれます。*3

「ApiGatewayRestApi」しかり「ApiGatewayResourceSplitdeploy」しかり、こういうServerless Framework独自の仕様を知らないと、思わぬところでハマるので注意が必要です。(serverless-state.jsonを追っていけばそのうち分かりますけどね)

まとめ

以上、長くなってしまいましたが、API Gatewayを共有する方法(別プロジェクトで使いまわす方法)でした。

始めのうちは気にする必要はないかもしれませんが、アプリが大きくなったり、運用面を考えると「スタックのネスト」「デプロイ分割」を考える必要があり、その際にAPI Gatewayの共有を考える必要が出てきます。

スタック(=serverless.yml)の再構成というのは、アプリがそれなりの規模になればなるほど困難になってしまうので、たとえ面倒でも始めのうちにしっかり考えて、最適な構成にしておきたいものです。

てか次回はその「スタックのネスト」「デプロイ分割」あたりについて書こうかな、と考えています。

それでは、長くなってしまいましたが、今回はこの辺で。

備考:「splitdeployfirst」「splitdeploysecond」のserverless.ymlの内容

# splitdeployfirstのserverless.yml
service: splitdeployfirst
  
provider:
  name: aws
  runtime: nodejs12.x
  
# you can overwrite defaults here  
  stage: dev
  region: ap-northeast-1
  profile: default
  
functions:
  hello:
    handler: handler.hello
    events:
      - http:
          path: /splitdeploy/hello
          method: get
  
  AuthorizerLambda:
    handler: handler.authorizer
  
resources:
  # Resources:
  Outputs:
    apiGatewayRestApiId:
      Value:
        Ref: ApiGatewayRestApi
      Export:
        Name: ${self:service}-restApiId-${self:provider.stage}
    apiGatewayRestApiRootResourceId:
      Value:
        'Fn::GetAtt': [ApiGatewayRestApi, RootResourceId]
      Export:
        Name: ${self:service}-rootResourceId-${self:provider.stage}
    apiGatewayResourcesSplitDeploy:
      Value:
        Ref: ApiGatewayResourceSplitdeploy
      Export:
        Name: ${self:service}-ResourcesSplitDeploy-${self:provider.stage}
    SplitDeployFirstAuthorizerArn:
      Value:
        'Fn::GetAtt': [AuthorizerLambdaLambdaFunction, Arn]
      Export:
        Name: ${self:service}-SplitDeployFirstAuthorizerArn-${self:provider.stage}
#splitdeploysecondのserverless.yml 
service: splitdeploysecond
custom:
  parentStackName: splitdeployfirst-dev
  
provider:
  name: aws
  runtime: nodejs12.x
  
  apiGateway:
    restApiId: ${cf:${self:custom.parentStackName}.apiGatewayRestApiId} 
    restApiRootResourceId: ${cf:${self:custom.parentStackName}.apiGatewayRestApiRootResourceId} 
    restApiResources:
      /splitdeploy: ${cf:${self:custom.parentStackName}.apiGatewayResourcesSplitDeploy}
 
# you can overwrite defaults here
  stage: dev
  region: ap-northeast-1
  profile: default
    
functions:
  guten:
    handler: handler.guten
    events:
      - http:
          path: /splitdeploy/guten
          method: get  
          authorizer: ${cf:${self:custom.parentStackName}.SplitDeployFirstAuthorizerArn} 

*1:event=httpのLambdaがある場合

*2:その場合Cognitoはresources.Resourcesに指定すると思いますが、resources.Resourcesに指定したキー名はserverless-state.jsonにもそのままのキー名で書き込まれるため

*3:明らかに「Lambda」がかぶってるのはどうなの?という感じですね...

【Serverless Framework】Serverless Jest Pluginで始めるLambdaテストの第一歩

はじめに

前々回の【AWS】単体テストを考慮したLambdaの構成を考えたで、単体テストについて少し触れました。

で、今回はそこから一歩踏み込んで「実際にテストを導入してみよう!」という内容になります。(なぜ前回書かなかったのかは置いといて)

また、どうせならServerless Framework経由で利用できるように「Serverless Jest Plugin」という、Serverless Frameworkのプラグインにて行う内容になっています。

Serverless Jest Pluginとは?

名前の通り、Serverless Framework上でJestを使用したテスト(テスト駆動開発:TDD)を行うためのプラグインになります。
www.serverless.com

ちなみに(今回は扱いませんが)、上記のMocha版である「Serverless Mocha Plugin」というのもあります。
www.serverless.com

このServerless Jest Pluginを使用して、以下のことができます。

  • テストの作成
  • テストの実行

インストール&設定

インストールですが、他のプラグイン同様、以下を行うだけです。

  • npm installの実施
  • serverless.ymlの「plugins」セクションに追加
# npm installを実施する  
> npm i serverless-jest-plugin --save-dev  
  
# serverless.ymlのpluginsセクションに追加する  
plugins:
  - serverless-jest-plugin

また「custom」セクションに「jest」というキーを設定し、その子要素に「Jestの設定項目:その値」というKey:Valueを用意することで、その項目の設定をそのまま反映することができます。

# 設定例  
# (例)下記の記載をすると、Jestのテスト時間を30秒に延ばすことができる。(デフォルトは5秒)  
custom:
  jest:
    testTimeout: 30000

テストの作成

で、まず行う「テストの作成」ですが、これは下記コマンドを実施すればOKです。

# Lambda関数作成と同時にテストを作成する場合  
> sls create function -f myFunction --handler ./index.handler --path tests
  
# 既存のLambda関数に対して、テストだけ作成する場合
> sls create test -f myFunction--path tests  

パラメータの意味は、以下の通りです。

パラメータ 意味 設定値 備考
-f Lambda定義名 serverless.ymlのfunctionsセクションに定義する(した)Lambda定義名
--handler ハンドラ関数名 Lambdaのハンドラ関数のパス(ファイル名&関数名) serverless.ymlのfunctionsセクションのhandlerに設定される値
--path テストファイルを格納するフォルダのパス ・未指定の場合、「__tests__」というフォルダを自動作成し、その中に格納する。
・「{function}」という値を指定すると、フォルダは作成せず、元のLambda関数と同じ階層にテストファイルを作成する
「{function}」は、実際のLambda定義名に置き換える...などではなく、「{function}」という固定の文字列

例えば、以下のコマンドを実行した場合、結果は下記の通りになります。

> sls create function -f pow--handler ./pow.index --path {function}
# serverless.ymlのfunctionsセクションに、以下の定義が追加される
functions:  
  pow:  
    handler: ./pow.index  

フォルダ:

  • <ルートフォルダ>
    • node_modules
    • pow.js
    • pow.test.js

また、作成されたテストファイル(pow.test.js)の中身は下記の通りです。

'use strict';

// tests for pow
// Generated by serverless-jest-plugin  
  
const mod = require('./pow');  
  
const jestPlugin = require('serverless-jest-plugin');  
const lambdaWrapper = jestPlugin.lambdaWrapper;  
const wrapped = lambdaWrapper.wrap(mod, { handler: 'index' });  
  
describe('pow', () => {  
  beforeAll((done) => {  
//  lambdaWrapper.init(liveFunction); // Run the deployed lambda  
  
    done();  
  });  
  
  it('implement tests here', () => {  
    return wrapped.run({}).then((response) => {  
      expect(response).toBeDefined();  
    });  
  });  
});  

テストの実行

もう一つの「テストの実行」ですが、これは下記コマンドを実施すればOKです。

> sls invoke test -f pow--stage dev --region ap-northeast-1

パラメータの意味は、以下の通りです。(すべて任意項目です)

パラメータ 意味 設定値 備考
-f テストするLambdaの定義名 serverless.ymlのfunctionsセクションに定義したLambda定義名 未指定の場合、全テストが実施される
--stage 実施するステージ Lambdaのテストを実施するステージを指定したい場合、設定する 不要なら未指定でもOK
--region 実施するリージョン Lambdaのテストを実施するリージョンを指定したい場合、設定する 不要なら未指定でもOK

実際に先程のテスト(pow.test.js)を実施すると、以下の結果になります。
f:id:Makky12:20200816133241p:plain

また、下記のようにテストを変更すると、ちゃんとテスト結果はfailになります。

// pow.test.jsに下記テストを追加する  
it('statusCode test', () => {
    return wrapped.run({}).then((response) => {
      expect(response.statusCode).toBe(200);
   });
});  
  
// pow.jsのハンドラ関数(index)を下記の通りに変更する  
module.exports.index = (event, context, callback) => {
  const response = {
    statusCode: 400,  // statusCodeを200→400に変更する
    body: JSON.stringify({
      message: 'Go Serverless v1.0! Your function executed successfully!',
      input: event,
    }),
  };

  callback(null, response);
};

f:id:Makky12:20200816133722p:plain

その他の事項

で、その他の事項ですが...公式ページのドキュメントに書いてあるのは、これで以上です。
おそらくは「Lambdaのハンドラ関数のテスト用プラグイン」という前提なんででしょう。

なので、簡単な使い方を説明するのは以上ですが、ちょっと調査したことを羅列したいと思います。

ローカル関数のテストは可能?

結論から言うと「可能」です。
ただし、ローカル関数が以下の条件を満たす場合のみです。

  • exportされている(まあこれは当然)
  • 非同期関数(async function)である

例えば、pow.jsを以下の通りに変更します。(event.numの2乗の数値をbody.powResultに設定してレスポンスを返すLambda関数です。)

'use strict';
  
const index = async (event, context, callback) => {
  
  const powResult = await main(event.num);
  
  const response = {
    statusCode: 200,
    body: JSON.stringify({
      message: 'Go Serverless v1.0! Your function executed successfully!',
      input: event,
      powResult: powResult
    }),
  };
  
  callback(null, response);
};
  
const main = async num => {
  return await funcPow(num);
}
  
const funcPow = async num => {
  return Math.pow(num, 2);
}
  
module.exports = {
  index: index,
  main: main,
  funcPow: funcPow
}

で、pow.test.jsに下記テストを設定します。

'use strict';
  
// tests for pow
// Generated by serverless-jest-plugin
  
const mod = require('./pow');
  
const jestPlugin = require('serverless-jest-plugin');
const lambdaWrapper = jestPlugin.lambdaWrapper;
const wrapped = lambdaWrapper.wrap(mod, { handler: 'index' });
const wrappedfuncPow = lambdaWrapper.wrap(mod, { handler: 'funcPow' });  // これを追加
  
it('main Function test', async () => {
  // ローカル関数funcPowのテスト
  const result = await wrappedfuncPow.run(4);
  expect(result).toBe(16);
});

上記テストを実施すると、ちゃんとローカル関数funcPow()のテストができます。

f:id:Makky12:20200816141353p:plain

しかし、以下のようにfuncPow()を同期関数に書き換えて(「async」を削除)テストを実施すると...

// pow.jsのfuncPowを同期関数に変更する
const funcPow = num => {
  console.log('funcPow Called');
  return Math.pow(num, 2);
}  
  
// pow.test.jsでfuncPowを呼ぶ
it('funcPow Function test', () => {
  const result = wrappedfuncPow.run(4);
  expect(result).toBe(16);
});

結果、テストは失敗します。
f:id:Makky12:20200820192818p:plain

というか、戻り値が空オブジェクトになっています。
また「funcPow Called」というログが出力されているので、funcPow関数自体は呼ばれていることが分かると思います。

これですが、元となるlambdaWrapperに下記のような処理がなされているため、戻り値がPromiseでないと、正しく処理されないためです。

if (this.handler) {
  const handlerResult = this.handler(event, lambdaContext, callback);
  // Check if the result itself is a promise
  if (handlerResult && handlerResult.then) {
    handlerResult.then(function(data) {
      // Avoid Maximum call stack size exceeded exceptions
      return setImmediate(function() {
        callback(null, data);
      });
    }).catch(function(err) {
       // Avoid Maximum call stack size exceeded exceptions
       return setImmediate(function() {
        callback(err);
      });
    });
  }
} else {  
// 以下略
}

この場合は、無理やりlambdaWrapper.wrapを使わず、素直に以下のような、Jest標準の記載をすればOKです。

// pow.jsのfuncPowを同期関数に変更する
const funcPow = num => {
  console.log('funcPow Called');
  return Math.pow(num, 2);
}  
  
// pow.test.jsでfuncPowを呼ぶ
it('funcPow Function test', () => {
  const result = mod.funcPow(4);
  expect(result).toBe(16);
});

この点からも、Serverless Jest Pluginは「Lambdaのハンドラ関数のテスト用プラグイン」という前提なんでしょう。

まとめ

というわけで、Serverless Jest Pluginについて簡単に説明しました。

決して「これ一つで劇的にテスト環境が改善する」とか「至れり尽くせり」ではないですが、Lambdaテスト導入の第一歩としては、悪くないツールだと思います。

こういうツールをきっかけに、テストについてある程度詳しくなって、そこから次のステップに進むのが良いのかな、と個人的に感じました。

それでは、今回はこの辺で。

【Serverless Framework】Serverless Step FunctionsプラグインでStep Functionsを定義する

本題

皆さん、Step Functions使ってますか?

...僕は使ってません。(仕事では)

でも、プライベートでは使っておりまして「これは一連の処理(=ワークフロー)を実行するのに、すごく便利な機能だ!」と思ってます。(てか、業務に導入したい...)

で、今回はそのStep Functionsを、Serverless Frameworkで定義しちゃおう!という内容です。

※Serverless Framework公式ページ www.serverless.com

Step Functionsとは?

AWS公式サイトの説明では、下記のとおりです。

AWS Step Functions は、AWS Lambda 関数および AWS の複数のサービスを、ビジネスに不可欠なアプリケーション内に簡単に配列することができるサーバーレスの関数オーケストレーターです。アプリケーションの状態を維持する、チェックポイント化されたイベント駆動型の一連のワークフローを、視覚的なインターフェイスを使って作成および実行することができます。1 つのステップの結果が、次のステップへのインプットとして機能します。アプリケーション内の各ステップは、ユーザーが定義したビジネスロジックに基づいて、順番通り、計画通りに実行されます。

まとめると、ざっとこんな感じでしょうか?

  • 複数のステップ(Lambda実行など)で構成された一連の処理(=フロー)を管理・制御するための仕組み
  • イベントドリブンなフローを構成することができ、前ステップのアウトプットを次ステップのインプットにできる。
  • 前の状態(ステート)により、次のステップの実行を変化させることができる
    • 「ステート」とは、例えば「正常終了/異常発生」だったり、あるいはアウトプットの値...など

利点

個人的には、例えば下記のような点が利点なのかな、と思っています。

  • あるステップの結果(正常終了/異常終了)により、処理を変化させられる
    • 例えば「異常終了時、エラー種別によって次のステップの内容を変える」なんてことができる。
    • CloudFormationのLambda.failrueと違い、細かい条件指定も可能
  • 前ステップのアウトプットを次ステップのインプットにできる
    • 引数など、必要な情報の受け渡しが簡単になる。
    • アウトプットの内容により、次のステップの処理を変えることも可能
  • Lambda関数の場合、各ステップのLambda間に依存関係が発生しない
    • 上記2点は、やろうと思えばLambdaでできるが(Invokeやモジュール化)、そうすると依存関係が発生してしまう。
    • Step Functionにするとそういう依存関係が発生しなくなるので、保守・改修がやりやすい

ちなみに個人的には、AzureのDurable Functionsに近い立ち位置なのかな?と思ってます。

今回の前提

Step Functionの詳細(定義の仕方、文法など)は、以下「Step Functions の詳細」を参照してください。
(「状態」及び「入力および出力処理」あたりが参考になると思います)

今回は、そのあたりについての詳細な解説は省きます。
docs.aws.amazon.com

また、今回使用するステートマシンの定義は、以下の通りになります。

※概要を説明すると、下記の通りです。

  • State1~State7では、それぞれStepFunc1~StepFunc7というLambdaを実行します。
    • Stete5、State6がないのは、諸事情で欠番になったからです。
  • State1は戻り値として「Even」か「Odd」という値を「num_type」というキーで返却します。
  • 「ChoiceStepFunc1ResponseEvenOrOdd」で「num_type」の値を判定し、下記の処理を行います。
    • 「num_type」が「Even」の場合、State2を実施します。
    • 「num_type」が「Odd」の場合、State3とState4を並列(Pararell)に実施します。
  • どちらの場合も、最後に「State7」を実施して、処理を終了します。

f:id:Makky12:20200810185742p:plain

そして、その定義(JSON)はこちらになります。(各プロパティの意味は、「Step Functions の詳細」を参照。)

{
  "Comment": "test of creating StepFunctions from Serverless Framework.",
  "StartAt": "State1",
  "States": {
    "State1": {
      "Type": "Task",
      "Resource": "(StepFunc1のARN)",
      "Next": "ChoiceStepFunc1ResponseEvenOrOdd"
    },
    "ChoiceStepFunc1ResponseEvenOrOdd": {
      "Type": "Choice",
      "Choices": [
        {
          "Variable": "$.num_type",
          "StringEquals": "Even",
          "Next": "State2"
        },
        {
          "Variable": "$.num_type",
          "StringEquals": "Odd",
          "Next": "OddParallelStepFunc3And4"
        }
      ],
      "Default": "State2"
    },
    "State2": {
      "Type": "Task",
      "Resource": "(StepFunc2のARN)",
      "Next": "State7"
    },
    "OddParallelStepFunc3And4": {
      "Type": "Parallel",
      "Next": "State7",
      "Branches": [
        {
          "StartAt": "State3",
          "States": {
            "State3": {
              "Type": "Task",
              "Resource": "(StepFunc3のARN)",
              "End": true
            }
          }
        },
        {
          "StartAt": "State4",
          "States": {
            "State4": {
              "Type": "Task",
              "Resource": "(StepFunc4のARN)",
              "End": true
            }
          }
        }
      ]
    },
    "State7": {
      "Type": "Task",
      "Resource": "(StepFunc7のARN)",
      "End": true
    }
  }
}

Step FunctionsをServerless Frameworkで定義する

で、本題の「Step FunctionsをServerless Frameworkで定義する」方法ですが、Step Functions自体はCloudFormationでサポートされてますので、serverless.ymlのresources.ResourcesにCloudFormation構文で定義を書けますし、デプロイすれば普通に作成されます。

docs.aws.amazon.com

が、Serverless Frameworkには、本題にある「Serverless Step Functions」という、Setp Functionsを定義するためのとても便利なプラグインがあるので、そちらを紹介します。

「Serverless Step Functions」プラグインを使う

「Serverless Step Functions」は、堀家 隆宏さんが開発した、Step Functions用のServerless Frameworkプラグインで、これを使うと、resources.ResourcesにCloudFormation構文を定義せずとも、独自にStepFunctionsの定義が行えます。

www.serverless.com

インストール&使用法

まずは、上記公式サイトにあるように、以下を実施します。

# npmコマンドで、Serverless Step Functionsのインストールを実施 
> npm install --save-dev serverless-step-functions  
  
# serverless.ymlの「plugins」に、Serverless Step Functionsを追加する。  
plugins:
  - serverless-step-functions  

その後、serverless.ymlのルート階層(「service」や「functions」と同じ階層)に「stepFunctions.stateMachines」というキーを用意します。
実際のステートマシン定義は、この「stateMachines」の要素として書きます。

stepFunctions:  
  stateMachines:  
    # ここにStateMachineの定義を書く

実際に書いてみた

で、上記のステートマシンの定義は、下記の感じになります。

stepFunctions
  stateMachines:
    stateMachinetTest1:  
      # retainをtrueに定義すると、スタックの更新・削除をしてもステートマシンを残せる。
      retain: true
      events:
        - http:
            path: stepfunc
            method: GET  
      # ステートマシンの名前を定義できる
      name: MyStateMachineTest1
      definition:
        StartAt: State1
        States:
          State1:
            Type: Task
            Resource:
              Fn::GetAtt: [StepFunc1, Arn]
            Next: ChoiceStepFunc1ResponseEvenOrOdd
          ChoiceStepFunc1ResponseEvenOrOdd:
            Type: Choice
            Choices:
              - Variable: "$.num_type"
                StringEquals: "Even"
                Next: State2
              - Variable: "$.num_type"
                StringEquals: "Odd"
                Next: OddParallelStepFunc3And4
            Default: State2
          State2:
            Type: Task
            Resource:
              Fn::GetAtt: [StepFunc2, Arn]
            Next: State7
          OddParallelStepFunc3And4:
            Type: Parallel
            Next: State7
            Branches:
            - StartAt: State3
              States:
                State3:
                  Type: Task
                  Resource:
                    Fn::GetAtt: [StepFunc3, Arn]
                  End: True
            - StartAt: State4
              States:
                State4:
                  Type: Task
                  Resource:
                    Fn::GetAtt: [StepFunc4, Arn]
                  End: True
          State7:
            Type: Task
            Resource:
              Fn::GetAtt: [StepFunc7, Arn]
            End: True  
      ## CloudFormation同様に「dependsOn」を使用することもできる。
      dependsOn:
        - StepFunc1
        - StepFunc2
        - StepFunc3
        - StepFunc4
        - StepFunc7

上記の通り、先程のステートマシンのJSON定義そのままの感覚で、定義を書くことができます。
CloudFormation定義と違い、シンプルにステートマシンの定義さえすればOKというのが、このプラグインの魅力だと思います。

また、以下のようなステートマシンの制御を行うプロパティがそのまま使用できる...というのも分かると思います。(もちろん他のプロパティについても同様)

  • StringEquals
  • Branches
  • Next/End

つまり、元のStepFunctionの構文を知っていれば、それをそのままServerless Frameworkで定義できる、ということです。

ファイルの分割

(公式サイトの説明が非常に詳しいので)簡単な説明は以上ですが、ステートマシンの定義はどうしても長くなりがちで、それに合わせてserverless.yml自体も大きくなってしまいがちです。

そういったこともあり(?)、ステートマシンの定義のみ別ファイルに分割するということが可能です。

ファイル分割する場合は、以下のように定義します。

# serverless,ymlでは「stepFunctions」キーのみ定義し、その値にステートマシンの定義ファイルのパスを記載する。  
stepFunctions:
  ${file(./statemachines.yml)}  
# statemachines.ymlファイルには「stateMachines」キー、及び実際のステートマシンの定義を記載する。  
stateMachines:  
  stateMachinetTest1: ...  
  

# ※以下、「stateMachinetTest1」以降の内容は「dependsOn」以外、上記のserverless.ymlと同じなので省略。  
# dependsOnだけはstatemachines.ymlに定義がないので、コメントアウトなどするなどの対処が必要。

その他

その他にも、以下のようなことが可能です。

...が、さすがに全部は説明しきれないので、詳細は先述のServerless Step Functionsプラグイン公式ページを参照してください。

  • Cloudwatch Alarmでの通知
  • Cloudwatch Notificationでの別リソースへの通知
    • SNS, SQS, Kinesis, Firehose, Lambdaなど
    • 別のStep Functionsを起動することも可能
  • blue-green deploymentへの対応 など

まとめ

というわけで、「Serverless Step Functions」プラグインでStep Functionsを定義する方法をざっと説明しました。
駆け足になってしまいましたが、なんとなくServerless Frameworkで比較的簡単にStep Functions定義を行える、ということを分かっていただけたかと思います。

というか、こういう便利なプラグインを利用して、どんどんAWSの機能を現場でも導入していきたいですね。

そして、サーバーレスアプリをもっと使いやすいものにできるよう、そいうった便利なを活用していきたいものです。

それでは、今回はこの辺で。

【AWS】単体テストを考慮したLambdaの構成を考えた

概要

Lambdaのソースコードを書いた際に、ローカル環境で単体テストを実施する場合があると思います。

実は今まで業務では、そこまで単体テストを書いていなかったのですが*1、一度書いてみたら「こりゃ便利だ」と感じ、本腰を入れて単体テストを勉強し始めました。

が「単体テストを考慮してLambdaのソースコードを書く」となる際に、いくつかの問題点があったので、自分なりに考えてみました。

今回はそんな内容です。

※今回使用するソースは、末尾の「サンプルソース」がベースになります。(前回の【JavaScript】awaitの使い方を復習するのソースを少し改変したものです)

よくあるLambdaソースでの問題点

僕が今まで関わったLambdaソースを思い出した際に「単体テスト」という観点でぱっと思いつく問題点といえば、ざっとこんな感じです。(本当は、多分もっとあるでしょうが)

  • メイン処理の詳細(仕様通りの処理が行われたかどうか)が分からない
  • ローカル関数の単体テストができない

上記2点について、書きたいと思います。

メイン処理の詳細が分からない

これのケースでよくあるのが、「ハンドラ関数にメイン処理を全部記載している」というもの。
例えば、まさに末尾の「サンプルソース」に書いたようなコードです。(てか、前回の記事もそうなってますけどね)

テストフレームワーク(MochaやJestなど)の多くは「関数の戻り値」でテスト結果を判別しますが、ハンドラ関数に全部処理を書くと、戻り値からは「成功/失敗しか分からない」ケースがあります。

まだ「クライアントにレスポンスで詳細なデータを返す」ような場合はレスポンスから判定ができますが、そうではない場合、詳細な処理結果を確認するのは困難です。*2

対処方法

対処方法ですが、AWS公式のAWS Lambda 関数を使用する際のベストプラクティスにもある通り、「ハンドラ関数をメインロジックから分離する」のが良いと思います。

サンプルソースで言えば、こんな感じでしょうか。

'use strict';

module.exports.hello = async event => {  
  
  console.info(`[event] ${JSON.stringify(event)}`);  
  const users = event.users;  
  const result = await main(users);  
  
   return {
    statusCode: 200,
    body: JSON.stringify(
      {
        message: 'OK',
      }
    ),
  };  
}  
    
async function main(users) {
  const promises = [];  
  
  for(const user of users) {
    const promise = asyncFuncA(user);
    promises.push(promise);
  }
  
  const result = await Promise.all(promises);
  return result;
};
  
// (以下省略)

ローカル関数の単体テストができない

これですが、Lambda関数は大抵exportするのはイベントハンドラだけです。(てか、それ以外exportする必要ない)
つまり、標準ではテストソースから単体テストを実施できるのはイベントハンドラだけです。

もちろん本来はそれでOKですが、単体テストを行う場合、やはりローカル変数もテストしておきたいところです。

対処方法

対処方法ですが、ちょっと考えたんですが、最終的には単純に「全関数exportするのが一番早いのかも」と思いました。

ソースにすると、こんな感じですかね。

'use strict';

// ここではmodule.exportsを付けない
async function hello(event) {  
  // (途中省略)
}  
    
async function main(users) {
  // (途中省略)
};
  
// (途中省略)  
  
module.exports = {
    hello:hello,  
    asyncFuncA: asyncFuncA,
    asyncFuncB: asyncFuncB
}

ただ「公開する必要がないのにexportするのが気になる」という懸念もあるので、下記のように条件付けexportする感じでしょうか?

// (途中省略)  
  
  const exportFunc = {  
    hello:hello
  }  
  
  // 環境変数ENVが'local'の時のみ、ローカル変数をexportする。  
  // テストを実施するローカルPCで、環境変数ENVの値を'local'にしておく。
  if(process.env.ENV === "local") {  
    exportFunc["main"] = main;
    exportFunc["asyncFuncA"] = asyncFuncA;  
    exportFunc["asyncFuncB"] = asyncFuncB;
  }
  
  module.exports = exportFunc;
}

ただし正直これについては、私もベストアンサーがまだ分からないので、もしも良いやり方があったら、ぜひ教えて頂ければ...と思っています。*3

ローカル変数のスタブ化

これで「よくあるLambdaソースでの問題点」の問題は一通り洗い出しましたが、追加で一点。

ローカル関数が時間がかかるような処理を行う場合、ハンドラ関数の単体テストを行う際に面倒なことがあります。
例えばサンプルソースでも、asyncFuncAが値「0」をresolveするのに2秒かかってしまいます。

このような場合、ハンドラ関数の単体テストでは、さっさと値「0」をresolveしてほしい...となります。(つまり、ローカル関数をスタブ化したい)

で、単体テストについて調べていた時に、それについて下記サイトで説明されており、「なるほど!すごい!」と思いました。
これなら別にnpmモジュールのインストールなどもすることなく、ローカル関数のスタブ化ができます。

てか、頭のいい人はいるんですねえ...

qiita.com

対処方法

対処方法は上記サイトの通りなんですが、実際にソースにしてみました。

かいつまんで説明すると、

  • ローカル変数を直接exportするのではなく、hookポイント(≒hook用の変数)を設ける。
  • ローカル変数は、上記hookポイントの配下に配置する。
  • exportするのは、(ハンドラ関数と)hookポイントのみ

となります。

サンプルソース」で説明すると、以下の通りになります。

  // hookポイント用の変数  
  const hook = {};  
  
  // hookポイントにローカル変数を格納する。  
  hook["main"] = main;
  hook["asyncFuncA"] = asyncFuncA;  
  hook["asyncFuncB"] = asyncFuncB;
  
  // (ハンドラ関数は省略)  
  
  async function main(users) {  
  
    // ローカル関数の呼び出しを、hookポイントからの呼び出し(=hook変数の参照)にする。  
    const promise = asyncFuncA(user);  
    const promise = hook.asyncFuncA(user);  
  }  
  
  // (asyncFuncA, asyncFuncB関数は省略)   
  
  // exportするのは、イベントハンドラとhookポイントのみ
  const exportFunc = {  
    hello:hello,  
    hook: hook
  }  
  
  module.exports = exportFunc;
}

で、テストソースはこんな感じにします。

const { hello, hook } = require("./handler.js");
const event = {  
  users:["userA", "userB", "userC"]
};  
  

describe('handler.js tests', () => {
  describe('hello test', () => {
    it('sample', async () => {
        
      // asyncFuncAの書き換え
      // hook['asyncFuncA'] = async function() { return 1; };
      const response = await hello(event);
      const message = JSON.parse(response.body).message;
      expect(message).toBe("OK");
    });
  })
});

上記では11行目をコメントしていますが、この状態だとサンプルソースの通り、hook['asyncFuncA']は2秒待って値「0」をresolveします。
しかし11行目のコメントを外し、hook['asyncFuncA']の内容を書き換えると、実際にテストを実施した際、asyncFuncAは即座に値「1」をresolveします。

つまり、テスト関数内でローカル関数をスタブ化することも可能になるわけです。

また上記サイトにある通り「明示的に準備したstub関数以外が誤って呼び出されたら undefined 等でエラーにする」なんてテストも可能になったりします。

これを使えば、単体テストが便利になるかもしれないですね。

まとめ

と、ざっとちょっとLambdaの単体テストを触ってみて分かった問題点&対処をまとめてみました。

まだまだ単体テストについては分からないことだらけですが、いろいろ便利なことがたくさんありそうなので、これからも勉強していきたいと思います。

てか、まだまだ分からない点があることもあり、もしかしたら違っていることやアンチパターン的なことを書いてしまっているかもしれませんので、その際は教えて頂ければ非常にありがたいです。

それでは、今回はこの辺で。

告知

私が共同管理者をしている、下記LTイベントが公開されています。

まだまだ参加者を募集中ですので、みなさまぜひご参加ください!

connpass.com

参考:サンプルソース

'use strict';

module.exports.hello = async event => {  
  
  console.info(`[event] ${JSON.stringify(event)}`);  
  const promises = [];  
  
  for(const user of event.users) {
  
    const promise = asyncFuncA(user);
    promises.push(promise);
  }
  
  await Promise.all(promises);
  
  return {
    statusCode: 200,
    body: JSON.stringify(
      {
        message: 'OK',
      }
    ),
  };
};
  
async function asyncFuncA(user) {
  await asyncFuncB(user);
  return 0;
}
  
function asyncFuncB(user) {
  return new Promise(resolve => {
    setTimeout(() => {
      resolve();
    }, 2000);
  });
}

*1:Serverless Frameworkのinvokeコマンド&eventオブジェクトを定義したJSONファイルの指定でやりくりしていた

*2:CloudWatch/DynamoDB/S3などのトリガで起動するLambda関数の場合、詳細なデータを返さないケースが多いと思います。

*3:「exportしてないローカル変数もテストソースから参照できるようにする」npmモジュールがあったような気がしたのですが...詳細分かったら、後日追記します。

【JavaScript】awaitの使い方を復習する

はじめに

JavaScriptの非同期処理(Promiseとかasync/await)について、今年の1月に、下記のブログを書きました。
makky12.hatenablog.com

が、最近AWS Lambdaの非同期処理をJavaScriptで書いていて、ちょっとこんがらがってしまったことがあったので、備忘録的に記事にしました。

復習:awaitとは

awaitとは、非同期処理について、Promiseの結果が返ってくるまで処理を待機する(ように記載できる)構文です。

例えば、下記の2つのソースは、同じ動作をします。

// awaitを使わない場合、Promiseの結果が返った来た後の処理は  
// then()内に記載する。
function noAwaitFunc() {
    someAsyncFunc()
    .then(data => {
        console.log(data);
    });  
}
  
// awaitを使う場合、Promiseの結果が返った来た後の処理を  
// awaitから下のソースに記載する。  
// つまり、awaitから下の処理は全てPromiseの結果が返った来た後に  
// 実施される。(thenの中身と同じ)
// なおawaitを使う関数は「async」を定義し、非同期関数にする必要がある。    
async function withAwaitFunc() {
    const data = await someAsyncFunc();
    console.log(data);
}

awaitを使う事で、下記のメリットがあります。

  • 非同期処理をシンプルに書くことができ、ソースの可読性などが上がる
    • コールバック地獄のような記載がなくなる
  • 非同期処理を同期処理っぽく書くことができ、分かりやすくなる

で、依存関係がない複数の非同期処理について、処理時間を考える意味で「取りあえずawait」はやめよう...というのを書いたのが、先述のブログの内容です。

// asyncFuncA~asyncFuncCは全て非同期処理を行う関数とする。  
  
// 処理時間がasyncFuncA:5秒、asyncFuncB:10秒、asyncFuncC:15秒として、  
// 下記の書き方だと全て終わらせるのに5+10+15=30秒かかってしまう。
const a = await asyncFuncA();
const b = await asyncFuncB();
const c = await asyncFuncC();
  
// 下記のようにPromise.allをawaitし、3処理を並列処理すれば、  
// 処理時間はmax(5, 10, 15)=15秒で済む。     
const a = asyncFuncA();
const b = asyncFuncB();
const c = asyncFuncC();
const results = Promise.all([a, b, c]);

なおawaitについて、たまに「Promiseの結果が返ってくるまで処理を待つ」みたいなニュアンスの記載をしている記事がありますが、これは誤りです。

※あくまで「そのように記載できる」だけで、実際に「Promiseの結果が返ってくるまで処理を待つ」わけではないです。(先程「ように記載できる」と書いたのはそのため)

本題

で、僕がこんがらがったのは下記ソース。

例えば下記ソースでは、

  • 非同期関数hello()(=ハンドラ関数)で、someAsyncFuncAを呼び出している
  • 非同期関数someAsyncFuncAで、someAsyncFuncBを呼び出している
  • 非同期関数someAsyncFuncBで、別の非同期処理(ここではsetTimeOut)を呼び出す
  • 非同期関数someAsyncFuncA、someAsyncFuncBをawaitしている
  • 非同期関数hello()は、全ユーザーのsomeAsyncFuncAがresolveされるまで、returnしない(Promise.all)

なのですが、

  • someAsyncFuncAはawait使うけど、awaitより下はPromiseの結果が返るまで実行されない
    • returnまでの処理は、全てPromiseの結果が返った後にしか実施されない
    • return以後のソースは実施されない(=到達不可)なので、Promiseの結果が返らないとsomeAsyncFuncA自身を抜けることができない?
  • 結局、someAsyncFuncAがreturn(=resolve)されるまで、someAsyncFuncAの呼び出しから後の処理は実施されない(=待たされる)のか?

という疑問が浮かび、こんがらがってしまいました。

module.exports.hello = async event => {    
    const promises = [];
    // usersには、ユーザーの配列が格納されてるものとする  
    for(const user of users) {
         const promise = someAsyncFuncA(user);
         promises.push(promise);
    }  
    
    await Promise.all(promises);
    return;
}  
  
async function someAsyncFuncA(user) {  
    // usersには、ユーザーの配列が格納されてるものとする
    await someAsyncFuncB(user)
    return;
}  
  
async function someAsyncFuncB(user) {  
    // 何でもいいから、なんか非同期処理をするものとする  
    return new Promise(resolve => {
        setTimeout(() => {
            console.info('user is' + user);
            resolve();
        }, 2000);
  });  
}

もちろん、実際は「復習:awaitとは」の内容でなんとなくわかる通り、そんなことはなかったわけですが。

あれこれ言わず、手を動かして確認する

てなわけで、実際にソースを動かして確認しました。(Lambdaで実行)

'use strict';
const moment = require("moment");

const users = ["userA", "userB", "userC"];
  
// ソースが見にくくなるので省きましたが、実際はconsole.infoで  
// 実施時刻も出力しています。
module.exports.hello = async event => {  
  
  console.info(`[event] ${JSON.stringify(event)}`);  
  const promises = [];  
  
  for(const user of users) {
    console.info(`[asyncFuncA beforeCall] ${user}`);  
  
    // asyncFuncAの内容を2パターン用意し、1パターンずつ  
    // 実施して実施順を確認する。
    const promise = asyncFuncA(user);
    console.info(`[asyncFuncA afterCall] ${user}`);
    promises.push(promise);
  }
  
  console.info(`[Promise.all waiting]`);  
  
  // 3人のユーザーすべての処理が終わるまで待機
  await Promise.all(promises);
  console.info(`[Promise.all resolved]`);
  
  return {
    statusCode: 200,
    body: JSON.stringify(
      {
        message: 'Go Serverless v1.0!...(以下略)',
      }
    ),
  };
};
  
// パターンA:awaitを使用する場合
async function asyncFuncA(user) {
  console.info(`[asyncFuncA called] ${user}`);
  await asyncFuncB(user);
  console.info(`[asyncFuncA resolve] ${user}`);
  return;
  
  console.info(`[asyncFuncA finished] ${user}`);
}
  
// パターンB:awaitを使わず、Promise.then()を使用する場合
async function asyncFuncA(user) {
  
  return new Promise(resolve => {
     asyncFuncB(user)
     .then(() => {
       console.info(`[asyncFuncA resolve] ${user}`);
       resolve();
     });
  
     console.info(`[asyncFuncA finished] ${user}`);
  });
}  
  
// asyncFuncAから呼ばれる非同期処理を実施する関数。  
// ここではsetTimeOutを実施し、2秒後にresolveする。
function asyncFuncB(user) {
  
  console.info(`[asyncFuncB called] ${user}`);
  return new Promise(resolve => {
    setTimeout(() => {
      console.info(`[asyncFuncB resolve] ${user}`);
      resolve();
    }, 2000);
  
    console.info(`[asyncFuncB finished] ${user}`);
  });
}

結果をログで確認する

その結果のログが以下。

結論から言ってしまえば、処理順序はパターンAとBで全く同じ。
先述の「asyncFuncAがreturn(=resolve)されるまで待たされる」なんてことはありません。

唯一、赤枠の「asyncFuncA finished」のログの有無だけが違います。(が、awaitとPromise.thenの仕様を考えれば、当然の結果といえます)

つまり、「呼び出す非同期関数内でのawait/Promise.thenによる挙動の違いは全くないので、awaitによる待たされなどの考慮は一切不要」となるわけです。
(もちろん、Promiseの結果が返る前の処理は別)

またログを見ると分かる通り、どちらもasyncFuncAのresolveの前に次のログ(「asyncFuncA afterCall」や次のユーザーのasyncFuncAの呼び出し)が行われています。
「復習:awaitとは」で、「Promiseの結果が返ってくるまで処理を待つ」正確にはは誤り」と記載しましたが、このことからも「awaitでも別に処理を待つわけではない」ことが分かります。

ただし、「全て直列で処理を実行する」場合は、その考え方でも差し支えないと思います。 (並列で処理する場合、上記を考慮する必要がある)

※パターンA:awaitを使用する場合 f:id:Makky12:20200724183446p:plain

※パターンB:awaitを使わず、Promise.then()を使用する場合 f:id:Makky12:20200724183800p:plain

2020/7/25追記:Lambdaハンドラ関数をasync/awaitを使わずに書く

上記までで関数asyncFuncAについて、awaitとPromise.thenで、処理の違いは全くないことが分かりました。
でも、関数asyncFuncAはあくまで「サブ関数(=サブルーチン)」です。

では、メイン関数であるLambdaハンドラ関数(今回ならhello)をasync/awaitを一切使わず書いた場合、結果はどうなるのか?...というのを調べました。

実際AWS公式ドキュメントでも、ハンドラ関数はほぼ非同期関数(async function)で書いてあり、そうではない形式で書かれているなんて、まずないと思います。

で、実際に先述のhello関数をasync/awaitを一切使用せずに書くと、こうなります。

'use strict';  
  
const moment = require("moment");  
const users = ["userA", "userB", "userC"];
  
// 先程と同様、ソースは省きましたが、実際はconsole.infoで  
// 実施時刻も出力しています。  
  
// 先述のソースのLambdaハンドラ関数を、async/awaitを  
// 一切使わずに記載した例
module.exports.hello = event => {
  
  return new Promise(resolve => {
    console.info(`[event] ${JSON.stringify(event)}`);
    const promises = [];
  
    for(const user of users) {
      console.info(`[asyncFuncA beforeCall] ${user}`);
      const promise = asyncFuncA(user);
      console.info(`[asyncFuncA afterCall] ${user}`);
      promises.push(promise);
    }
  
    console.info(`[Promise.all waiting]}`);
    Promise.all(promises)
    .then(() => {  
  
      // ここでresponseを返す
      console.info(`[Promise.then resolved]`);
      resolve({
        statusCode: 200,
        body: JSON.stringify(
          {
            message: 'Go Serverless v1.0! Your function executed successfully!',
          }
        ),
      });
    })
    console.info(`[handler finished]`);
  });
};
  
function asyncFuncA(user) {
  
  return new Promise(resolve => {
    asyncFuncB(user)
    .then(() => {
      console.info(`[asyncFuncA resolve] ${user}`);
      resolve();
    });
  
    console.info(`[asyncFuncA finished] ${user}`);
  });
}
  
function asyncFuncB(user) {
  
  console.info(`[asyncFuncB called] ${user}`);
  return new Promise(resolve => {
    setTimeout(() => {
      console.info(`[asyncFuncB resolve] ${user}`);
      resolve();
    }, 2000);
  
    console.info(`[asyncFuncB finished] ${user}`);
  });
}

で、上記ソースをRest Clientで実行した結果が下図。(余計な箇所は消してます)
f:id:Makky12:20200725183004p:plain

結果から言えば、async/await使用時と同様、何ら問題なく実行できました。

つまりイベントハンドラでも、awaitとPromise.thenで、挙動は全く同じです。
逆に言えば、挙動が全く同じということは、ソースの可読性・保守性・サイズなどを考えると、あえてPromise.thenで書く理由はあまりなさそうです。

IEでも動作させるクライアント側の処理を書く場合」など、何か特別な理由がない限りは、ですが...

※参考までに、CloudWatchのログも張り付けておきます。(内容は先述のパターンBのログと全く同じですが...)
f:id:Makky12:20200725183218p:plain

まとめ

以上、JavaScriptの非同期処理(Promiseとかasync/await)について発生した些細な疑問、及びその結果でした。
分かったつもりでいましたが、まだまだ非同期処理は厄介で、分かりにくい部分があるので、いろいろ調べないといけません。

というか、前回のブログから2か月近く空いてしまいました。

実は体調を崩してしまっていたのもあり、なかなかブログまで手が回りませんでした。(7月は毎週月曜を休みにしてもらったくらいですし...)

ただ、体調はちょっとづつ回復しており、先日(2020/7/15)には下記「VS Code Meetup #6」で「VS Code + Serverless FrameworkによるAWS環境構築&デプロイ確認」という内容で登壇もさせて頂いたので、これから少しづつブログの方も書いていこうと思います。


VS Code Meetup #6 (オンライン)

それでは、今回はこの辺で。