echo("備忘録");

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

【Serverless】Serverless Meetup Tokyo #16で発表した感想

経緯

先日、サーバーレスの勉強会イベントである「Serverless Meetup Tokyo #16」がオンラインで開催されました。
serverless.connpass.com

そしてこのイベントで、「AWS x Serverless Framework x QuickSightでIoTデータ可視化」という内容で、発表を行いました。(発表資料はslideshareで公開してます。)

www.slideshare.net

本日は、その「Serverless Meetup Tokyo #16」で登壇した感想についての話です。
※「Serverless Meetup Tokyo #16」の配信内容は、下記「Serverless Community(JP)」の公式ページ内のYoutubeリンクから閲覧できます。

【参考】Serverless Community(JP) について

Serverless Community(JP) はサーバーレスアーキテクチャを用いたアプリケーションの構築における経験の共有を目的とした、コミュニティです。

ServerlessConf Tokyo および Serverless Meetups in Japan の実行組織でもあります。
serverless.connpass.com

開催までの話

  • MS Ignite Tour Osakaで、株式会社Serverless Operationsの堀家さん(Serverless Frameworkのすごい人)のセッションに参加したことがきっかけで、「名古屋でサーバーレスイベントやりましょう」という話になる。
  • では、まず東京のイベント(サーバーレス + IoT)に参加することに
  • 今回ではAzureネタでの発表がほとんどらしいので、 AWSで何か発表しませんか?というお話を頂く
  • じゃあAWS関連で発表しよう&東京のサーバーレス勉強会の雰囲気を肌で感じよう!
  • 翌日は仕事休んで、ゆっくり東京観光でも...

と、思ってたんですけどねえ。

オンライン開催に変更

...まあ、新型コロナウィルスの影響もあり、オンライン開催に変更されました。
(最初は現地開催したいよね...という話もありましたが、最終的にはオンラインになりました)

残念ですが、こればっかりは仕方がないですね。

ちなみに当日ですが、

  • 参加者の方にはYouTubeのURLを連絡し、Youtubeで見てもらう
  • 発表者はZoomでチャット・画面共有などを実施し、いろいろ調整する

という内容でした。(ちなみに、発表者の間で、事前に配信テストなんかも行ってました。)

おかげで(?)当日は大きなトラブルにも見舞われず、イベント自体は(手前味噌ながら)大成功だった、と思っています。

実際にオンラインイベントで発表を行った感想&アドバイス

「聞いている人たちの感想&リアクションがない」ので、不安になる。

オンラインではリアルタイムに聞いている人のリアクションが分からないので「うまくいってるのか?」と、いろいろ不安になります。
(PowerPointを開くと複数ウィンドウがPowerPointで埋まるので、YouTubeとかTwitterを見られないんです)

これに関しては、イベント終了後のお話で「Power BI 王子」こと清水 優吾さんが「一例として、以下の方法がある」とおっしゃっており、実際おすすめです。

使用するアプリの操作が分からないので、何をしていいかわからない

これに関しては、本番でやらかしてしまわないよう、配信に使用するアプリ(今回ならZoom)の基本的な使い方は、あらかじめ押さえておくことをお勧めします。

特に下記あたりの機能は、あらかじめ確認しておいた方が良いです。

  • マイクのミュート/ミュート解除
  • カメラのON/OFF
  • チャット機能
  • 画面共有/共有の解除

オンライン会議の作法が分からない

これに関しては、やればわかる(し、多分経験者が教えてくれる)ので、あまり神経質にならなくてもよいとは思います。
少なくとも、下記の点あたりを押させておけばよいと思います。

  • 他の発表者の方が発表中の時は、マイクはミュートにする。
  • ガタガタしそうな場合(席を外す、物を探す...等)、マイクをミュートにする。
  • マイクのミュート/ミュート解除の勘違いに気を付ける。
    • うっかり何気ないつぶやきや、家族の会話がダダもれになったり...(経験者)
  • 事前に家族に伝えておく。
    • うっかり家族が入ってきたり、完全に油断した格好だったりすると...
  • カメラをONにし続ける場合、何気ない動作もオンラインで映ってたりするので、気を付ける

まあでも「物は試し」というか「経験して慣れる」のが一番だと思います。

参加者(=見る&聴く側の方)へのお願い

YouTubeなど、配信アプリのチャット&Twitterへの(ハッシュタグ付き)ツイートを、バンバン書き込んでください。

  • リアクションが見えるだけで、マジで不安が和らぎます。
  • コメント&ツイートが多いと「反響があった」という点で、本当に嬉しいです。

ディスりはNG、でも意見は大歓迎

  • 発表者も、その内容&関連項目について100%把握してる...とは限りません。
  • むしろ、詳しい方の意見・見解を頂けると、すごく参考&勉強になります。

感想

ただでさえ参加者が多いイベントで、初めての登壇(=発表)がオンラインということで、めちゃくちゃ緊張しましたが、「やってよかった!」と本当に思ってます。

ただ聴く側ではなく、発表を行うことで、いろいろ経験になりますし、何よりアプトプットを行うことで「多くのインプットが身につく」という恩恵もあります。(これは本当にそう思いました)

最近では「DevRel(Developer Relations)」という考えもありますが、自分がアプトプットを行うことで、下記のようなメリットもありますので、本当に登壇・発表を行うことはお勧めだと思いました。

  • 「自分」という技術者のアピール&PR(自分を知ってもらう)
  • 自分を知ってもらうことで、多くの方々と知り合う&関係を持つことができる
  • 知見を共有する&得ることができ、様々なソリューションに繋がる...など

機会があれば、また積極的にチャレンジしたいと思っています。

告知

上記「Serverless Meetup」イベントですが、「Serverless Meetup Tokyo #16」内でも私が発表した通り、名古屋でも開催します!

※名古屋では初めての「Serverless Meetup」イベントになります!
内容がそれなりに固まり次第、connpass上で公開します。

※【3/16 更新】公開しました!

serverless.connpass.com

(個人的には)クラウド&サーバーレスは、まだまだ名古屋ではそこまで浸透していない(オンプレがバリバリ現役)と思っているので、これを機会に名古屋でもクラウド&サーバーレス、及びそのコミュニティを広めたいと思っています。

みなさま、機会が合えば、ぜひよろしくお願いします!

  • コロナウィルス(COVID-19)の影響により、connpassページの内容(開催日・開催方法など)が変更になる場合があります。ご了承ください。
  • 変更が発生した場合、connpassページ、およびServerless Community(JP)公式Twitterアカウント(@serverless, ハッシュタグ#serverlessnagoya)より、随時連絡させて頂きます。

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

【PowerApps】PowerAppsアプリで撮影した画像をS3バケットにアップロードする【Serverless Framework】

はじめに

Micorsoft の製品の一つ、Power Platform
中でもPower AppsやPower Automateは、ノーコーディング(またはローコーディング)でかなり本格的なアプリが簡単に作れる、画期的な製品です。

また簡単なアプリならコーディングなしでサクッと作れてしまうので、ITエンジニアにも便利だと思います。

そこでお試しに、(趣味と実用を兼ねて)「カメラで撮影した画像を、AWSのS3バケットに保存する」アプリを作成しました。

なおPower Platformについては、「Power BI 王子」こと清水優吾さんが、素晴らしい資料を公開してますので、ご参照ください。

www.slideshare.net

使うもの

  • Power Apps
    • 上で説明した通りです。
  • カスタムコネクタ
    • Power Platform製品の「コネクタ」(=他のアプリとの連携用の部品)でサポートされていないアプリ・サービスとも連携を可能にする機能。
    • 主に、地先APIを始め、外部Web APIのコールなど
  • AWS Lambda & AWS S3
    • S3にファイルを保存する関係で使います。
  • Serverless Framework
    • もちろん今回も使います。

【注意】カスタムコネクタについて

カスタムコネクタですが、Power Appsで使用する場合「Premium限定機能」となります。(ダイアモンドのアイコンが表示されている)
そのため、試用は可能ですが、一定期間が過ぎると使用不可能(またはサブスクリプションの購入が必要)なので、注意してください。

※あくまで「Power Platformの色々な機能のお試し」として読んでください。

Power Apps側の作成

f:id:Makky12:20200301173858p:plain f:id:Makky12:20200301173941p:plain

Power Apps側の機能ですが、こんな感じになってます。(「Kintai-Upload」は、なんとなく付けただけで、意味はないです)

  • 「撮影」画面で、カメラ機能を使って撮影する。(撮影すると自動で「確認」画面に遷移する)
  • 「確認」画面で「送信する」をクリックすると、S3バケットに撮影した画像をアップロードする。
  • 「やりなおす」をクリックすると、「確認」画面に戻る

で、上記機能の実装方法なんですが、変に説明をするより、マイクロソフトの吉田大貴さんのこの動画を見た方が早いですので、そちらを参照ください。
マジでこの動画を参考にすれば、画像をS3にアップロードする直前までできてしまいます。

www.youtube.com

注意点としては、以下の通りです。

# JSON([画像のバイナリデータ], JSONFormat.IncludeBinaryData) とすると、バイナリデータをBase64形式の文字列に変換できる。    
# [画像のバイナリデータ]には「Image1.Image」など、「画像の表示部品の名前.Image」を指定する。
JSON(Image1.Image, JSONFormat.IncludeBinaryData)

AWS側の設定について

今回、カスタムコネクタ作成にはOpen API(swagger)の定義を利用します。
またOpen API定義はAPI Gatewayからエクスポートできるので、先にAWS側のリソースを作成する必要があります。

というわけで、今回もServerless Frameworkの出番です。

...といっても、Serverless Frameworkは過去記事でもかなり扱ってるので、今回はserverless.ymlの内容だけ。

service: kintai-upload
  
provider:
  name: aws
  runtime: nodejs12.x
  stage: dev
  region: ap-northeast-1
  stackName: ${self:service}
  apiName: api-${self:service}
  profile: default
  
  iamRoleStatements:
    - Effect: "Allow"
      Action:
        - "s3:ListBucket"
        - "s3:PutObject"
        - "s3:GetObject"
      Resource:
        - arn:aws:s3:::${self:service}/*  
  
  environment:
    TZ: Asia/Tokyo
  
functions:
  CreateImage:
    handler: createImage.index
    events:
      - http:
          path: /create
          method: post
          request:
            parameters:
              body:
                imagebinary: false
    environment:
      BUCKET_NAME: ${self:service}
  
  SendSms:
    handler: sendSms.index
    events:
      - s3:
          bucket: ${self:service}
          event: s3:ObjectCreated:*
    environment:
      BUCKET_NAME: ${self:service}

ざっくりまとめると、下記の流れです。(「SendSms」Lambda自体は、今回の記事では未使用)

  • api-kintai-uploadという名前のAPI Gatewayを作って、
  • そこに「CraeteImage」Lambdaを実行するエンドポイントを作成して、
  • 「kintai-upload」という名前のバケットを作成して、そこに「createObject」トリガを定義して
  • 上記トリガで「SendSms」Lambdaを実行する

なお「resourceセクションへのS3バケットの定義(てか、そもそもresourceセクション自体)がないけど」と思ったかもしれませんが、「function」セクションの「events」で「s3」を定義するだけで、下記設定を自動で行ってくれますので、「resources」セクションの定義は不要です。

  • events.s3.bucket に指定した名前のバケット作成
  • events.s3.event に定義したイベントトリガの作成

カスタムコントロールの作成

で、肝心のカスタムコネクタの作成ですが、これはPower Appのホーム画面から[データ]-[カスタムコネクタ]を選択し、右上の「カスタムコネクタの新規作成」をクリックすればOKです。

なお、作成方法がいくつかありますが、今回は「OpenAPIファイルをインポート」で実行します。 f:id:Makky12:20200307085858p:plain

OpenAPIファイルの取得

OpenAPIファイルですが、API Gatewayの場合、該当のAPIを選択した後、下記手順で取得できます。

  • 左のリストから「ステージ」を選択  
  • 「ステージ」で該当ステージを選択後、「エクスポート」タブを選択
  • 「次の形式を選択」で「Swagger」を選択後、ファイル形式に「JSON」を選択する。
    • OpenAPI3、及びYAML形式はまだサポートしてません。

f:id:Makky12:20200307091646p:plain

ファイルをDLしたら、「カスタムコネクタの作成」の「OpenAPIファイルをインポートします」で、DLしたファイルを選択します。
また「コネクタ名」に、一意の名前を入力します。
f:id:Makky12:20200307091806p:plain

で、あとは「全般」「セキュリティ」「定義」などを設定しますが、ここからは「swaggerエディタ」をONにすれば直接OpenAPIファイルの内容を編集できますので、そちらで編集します。
f:id:Makky12:20200307091905p:plain

今回は。こんな感じで定義しました。

swagger: '2.0'
info: {version: '2020-02-29T10:45:32Z', title: api-kintai-upload-to-cloud}
host: *******.amazonaws.com
basePath: /dev
schemes: []
consumes: []
produces: []
paths:
  /create:
    post:
      responses:
        '200': {description: OK}
        '400': {description: Request Error}
        '500': {description: Internal Error}
        default: {description: Something Error}
      parameters:
      - name: imagebinary
        in: body
        description: kintai file binary
        required: true
        schema: {type: string}
      operationId: Kintai-upload-to-cloud-makky12
      description: Kintai-upload-to-cloud用のカスタムコネクタ
      summary: Kintai-upload-to-cloud用のカスタムコネクタ
definitions: {}
responses: {}
securityDefinitions: {}
security: []
tags: []

ちなみに、テストをする場合「テスト」タブで必要な値を入力すればOKです。
(今回の場合、「imagebinary」にbase64文字列に変換した画像のバイナリデータを入力する) f:id:Makky12:20200304201223p:plain

Power Appsに組み込む

で、作成したカスタムコネクタをPower Appsに組み込む方法ですが、これは左のツリーの[データソース]-[コネクタ]から、先程作成したカスタムコネクタを選択すればOKです。

そして、実際にAPIにリクエストを送るには、ボタンの「onSelect」イベントなどで、下記のように設定すればOKです。

# 「api-kintai-upload-to-cloud」は、カスタムコネクタ名。    
# 「Kintaiuploadtocloudmakky12」は、OpenAPIの「operationId」の値。(「定義」タブの「操作ID」で設定可能)  
# imageBase64は、base64文字列に変換した画像のバイナリデータ
'api-kintai-upload-to-cloud'.Kintaiuploadtocloudmakky12(imageBase64);  

S3バケットに保存する。

ここまでこれば、あとはLambda側の処理です。

Lambda側では、特に特別なことはしていません。
よく見かける、画像をs3バケットに保存する処理だと思います。

強いて言えば、注意するのは下記2点です。

  • JSON()関数でbase64文字列にした場合、先頭と末尾に「"」がついているので、「"」を除去する
  • その他、「data:image/png;base64,」という文字列も付与されるので、これを取り除く
// @ts-check
'use strict';

const AWS = require("aws-sdk");
const S3 = new AWS.S3();
const moment = require("moment");

module.exports.index = async event => {
  
  let statusCode = 200;
  let message = "";
  
  try {
    console.info(`[even] ${JSON.stringify(event)})`);
    console.info(`[event body] ${event.body})`);  
  
    const bodyReplaced = event.body.replace(/\"/g, "").replace("data:image/png;base64,", "");
    console.info(`[event replaced] ${bodyReplaced})`)  
  
    const timeStamp = moment().format("YYYYMMDDHHmmssSSS");
    const params = {
      Bucket: process.env.BUCKET_NAME,
      Body: Buffer.from(bodyReplaced, 'base64'),
      Key: `kintai_${timeStamp}.png`,
      ContentType: "image/png"
    };
  
    const data = await S3.putObject(params).promise();
    // console.info(`[S3 data] ${JSON.stringify(data)}`);
  } catch (error) {
    statusCode = 500;
    message = error.message;
    console.error(`[error Message] ${message}`);
  }
  
  return {
    statusCode: statusCode,
    body: JSON.stringify({ message: message }),
  };
};
  

結果

「送信する」ボタンをクリックすると、S3バケットに画像ファイル(*.png)が保存されます。 f:id:Makky12:20200307193524p:plain

そして、ダウンロードして画像ビューワーで見ると、問題なく表示できます。 f:id:Makky12:20200307193617p:plain

まとめ

以上、カスタムコネクタを使用して、画像をS3バケットにアップロードする方法でした。

カスタムコネクタはプレミアム限定機能ですが、こういう感じで、自作APIなどの操作をローコーディングで行えます。
また、自作APIでなくても、プレミアム限定でなくても使用できるAPIもありますので(Twitterとか)、非常に便利です。

Power AppsやPower Automateなど、Power Platform製品、なかなか便利なので、今後も使っていきたいです。

それではまた。

【Serverless Framework】201個以上のリソースをデプロイする方法

はじめに

Infrastructure as Code(IaC)の代表格の1つである、AWS CloudFormation。
僕も業務でよく使うのですが、CloudFormationで1度にデプロイできるリソースの数(=1スタックのリソース数)は、「最大200個」と決められています。

なので当然、Serverless Frameworkでデプロイする際も、1度にデプロイできるリソースの数は200個までです。

でも、プロダクトが大きくなると「200個では足りない!」というケースもあると思います。
今回は、その場合にスタックをネストして、201個以上のリソースをデプロイする方法(+α)についてです。

CloudFormationの制限とスタックのネスト

「CloudFormationで1度にデプロイできるリソース数は、200個が上限」については、公式ページでも明言されています。
(一部ブログを見る限り、昔は明言されてなかった?)
AWS CloudFormation の制限

200個というのも、決して少なくはないのですが、プロダクト(≒リソース)が肥大化してくると、オーバーするケースもあります。(特にロール・ポリシーやアラートが色々増えると)

その解決方法の一つとして、公式サイトでも紹介されているのが「スタックのネスト」で、下記の考え方になります。

  • serverless.yml内で、serverless.ymlのデプロイ先スタック(=スタックA)とは別のスタック(スタックB)を別途定義する。
  • 200個を超えるリソースは、スタックBに紐づくリソースとして定義する。
  • 実際にserverless.ymlをデプロイすると200個を超えるリソースが作成されるが、スタックBに紐づけたリソースはスタックBで「上限200個」に影響するので、スタックAでの「上限200個」には影響しない。
    • もちろん、スタックBのリソースが200個を超えたらNG。
    • スタックB自身は(serverless.ymlに定義されるので)、スタックAの「上限200個」に影響する。

【参考】ネストされたスタックの操作

そこで今回は、Serverless Frameworkで「スタックのネスト」で対応する方法について扱いたいと思います。
※純粋なCloudFormationテンプレート、及びServerless Frameworkの「resources」セクションで扱う方法については、調査完了次第ブログにします。

Serverless Framework公式ページ

「serverless-plugin-split-stacks」という超便利プラグイン

...と、なんかもったいぶって書き方をしましたが、実はServerless Frameworkでスタックをネストする」のは、めちゃくちゃ簡単です。

やり方ですが、下記2つを実行するだけです。

  • 「serverless-plugin-split-stacks」というプラグインをインストールする(下記)
  • serverless.ymlの「plugins」セクションに「serverless-plugin-split-stacks」を追加する。

github.com

plugins:  
  - serverless-plugin-split-stacks  # この定義を追加するだけ
  - ...

これだけ。本当にこれだけ。CloudFormationテンプレートファイルのような「定義ファイルの分割」も一切不要です。(てか、マジで便利すぎ。このプラグイン)

上記作業さえ行っておけば、「deploy」コマンドなどを実施した際に、「serverless-plugin-split-stacks」がリソース数を計算し、200を超えたら自動で定義ファイルを分割してくれますので、こちらの作業は一切不要です。

なお「serverless-plugin-split-stacks」のカスタマイズについては、下記ブログで詳しくまとめられています。

qiita.com

いや、マジで便利すぎます。このプラグイン

デプロイを分割する

今回は「1回のデプロイで作業を完結させる」ために、スタックのネスト方法を記載しましたが、あえて「デプロイ自体を複数回行い、スタック自体を分ける」のも一つの手です。

確かに「何回もデプロイをやるなんて面倒」と思うかもしれませんが、あえて分けることで、以下のようなメリットもあります。

  • デプロイするリソースを分割できる
    • 1ファイルにまとめてしまうと、やたらデプロイに時間がかかるリソースがある場合(CloudFrontなど)、毎回すごい時間がかかってしまう。
    • 「修正(=デプロイ)頻度」「依存関係」などでファイルを分割し、必要なリソースだけデプロイすることで、そういった無駄なデプロイ時間をなくすことができる。
  • 「出力」の参照ができる
    • CloudFormationのスタックには「出力」という参照用項目がある。
      • Serverless Frameworkでは「resources」セクションの「Outputs」で定義できます。
    • 「出力」に値を設定しておけば、後でデプロイする側は、先にデプロイされた側の「出力」の値を参照できる。(ARNなど)

ただし、やはりデプロイを複数回にすると、どうしても管理の煩わしさの問題は発生すると思うので、そこは検討する必要があると思います。

【参考】: serverlessフレームワークで複数スタックに分割して、リスクを軽減する方法 - Qiita

まとめ

  • CloudFormationでは、1スタック(=1デプロイ)200リソースが上限である。
  • スタックのネストを行えば、1デプロイで200個を超えるリソースもデプロイできる。
  • Serverless Frameworkでは、「serverless-plugin-split-stacks」プラグインを使うと、驚くほど簡単にスタックのネストが行える
  • 場合によっては、あえてデプロイ(=スタック)自体を分けるのも一つの手

「リソース管理の煩雑さからの解放」が「冪等性(※)」と並ぶIaCの大きな魅力なので、「スタックのネスト」「スタックの分割」など、いろいろな方法で、極力リソース管理の煩わしさから解放され、注力すべき作業に注力したいですね。

てかそれがAWSやAzureなど、クラウドの大きな魅力の一つでもありますから。

※冪等性(べきとうせい):「誰が何回作業を行っても、必ず毎回同じ結果になる」性質のこと。毎回同じ品質を提供できるというメリットがある。

告知

私事ですが、今週木曜日の27日に開催される「Serverless Meetup Tokyo #16」イベント(下記)にて、スピーカーをさせて頂きます。(オンライン開催です)

serverless.connpass.com

コロナウィルスの影響で、オンライン開催に変更されましたが、発表自体は予定通り行いますので、よろしくお願いいたします。
※当日は、YouTubeでのライブ配信も行われます。

発表では、下記について触れる予定です

  • AWS
  • Infrastructure as Code(IaC)&Serverless Framework(これがメイン...にしたい)
  • AWS Athena&AWS QuickSight

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

【AWS】DynamoDBのキーとソートとフィルタについて

きっかけ

先日、とあるサーバーレスアプリを作っていた際、DynamoDBについて下記のツイートをしました。

そうしたら、下記の突っ込みリプを頂きました。

プライマリーキーではなくパーティションキーなのです
このキーの値によってデータが配置されるノードが決まります。同一ノード内ではある程度検索ができる(レンジキーやローカルインデックスではソートや絞り込みができる)わけですねー
ホットキーがタブーなのも特定のノードに処理が偏るからですね

というか確かに、DynamoDBのキーの扱いについて、認識がイマイチあいまいだなあ...と思う部分があったので、この機会にちょっと調べてみました、

主要なキー群(プライマリインデックス)

DynamoDBの主要なキーには、下記の3種類があります。

  • パーティションキー
    • DynamoDBのデータをどのパーティションに配置するかを決定するキー。
    • キーの形式は「ハッシュ型(HASH)」
    • 使用可能な検索は「完全一致」のみ
    • パーティションキーは必須
  • ソートキー
    • パーティションキーが同一の複数データについて、ソートや範囲検索を行うためのキー
    • キーの形式は「レンジ型(RANGE)」
    • 使用可能な検索は「完全一致」「前方一致(BeginWith)」及び「範囲検索」(値の大小など)
    • ソートキーは任意(なくてもよい)
    • ソートキーだけでの検索は不可(必ずパーティションキーとセット)
      • これが結構面倒だったりする
  • プライマリキー

※DynamoDBの「パーティション」の概念、及び「なぜパーティションキーが"完全一致"固定なのか」などについては、下記公式ページが参考になります。

パーティションとデータ分散(AWS公式ページ)】 https://docs.aws.amazon.com/ja_jp/amazondynamodb/latest/developerguide/HowItWorks.Partitions.html

設計するときに注意すること

上記「パーティションキー」と「ソートキー」を踏まえ、DynamoDBテーブルを設計する際は、下記の点に注意する必要があります。

  • パーティションキー」には、テーブル内の情報の主属性を一意に特定できる値を設定する。
  • 「ソートキー」には、テーブル内の同一主属性のデータについて、並び替え、または範囲指定するような値を指定する

※例:会員制Webサイトのアクセスログを記録するテーブルの場合

てか、DynamoDBはパーティションキーの設計が本当に肝になるので、開発時は慎重に検討する必要があります。

その他のキー群(セカンダリインデックス)

プライマリインデックス(主に「パーティションキー」と「ソートキー」)は上記の通りですが、場合によっては下記のようなケースが出てきます。

  • パーティションキー&ソートキー以外の組み合わせでも検索をかけたい
  • ソートキー1個ではソート条件が足りない

※例えば、先述の「会員制Webサイトのアクセスログを記録するテーブル」の場合、下記のケースが出てくると思います。

  • ある会員がアクセスしたサイト内ページ(「購買履歴」「会員情報閲覧」など)で検索をかけたい
  • アクセスしたデバイスでも並び替えを行いたい

そんな「プライマリインデックスだけじゃ足りない」時に必要になるのが「セカンダリインデックス」です。

セカンダリインデックスには下記の2種類があり、どちらも「プライマリとは別で作成できる、パーティションキー(またはパーティションキーとソートキーのペア)」です。

そしてどちらも「プライマリとは別で作成できる、パーティションキー(またはパーティションキーとソートキーのペア)」です。

では何が違うかというと、以下の点が異なります。

  • グローバルセカンダリインデックス
  • ローカルセカンダリインデックス
    • プライマリとは別で作成できる、パーティションキーとソートキーのペア
      • ソートキーは必須(というか、ソートキーを増やすのが目的)
    • 「プライマリと異なるのはソートキーのみ」という点(=パーティションキーは同じ)
    • ローカルセカンダリインデックスを作成することで、プライマリのソートキーとは別の項目でのソートも可能

具体的な例

例えば下記「GameScores」テーブルがあったとして、この構成だと例えば「各ゲーム別のハイスコアラーの確認(昔のゲームでデモプレイ後に出てくるアレ)」ができません。
パーティションキーは「UserId」、ソートキーは「GameTitle」
f:id:Makky12:20200214202943p:plain

その場合、下記構成のグローバルセカンダリインデックスを作成すれば、それが実現できます。

f:id:Makky12:20200214203012p:plain

また「あるプレーヤーのハイスコアを出したプレイ履歴」を閲覧したい場合は、下記構成のローカルセカンダリインデックスを作成すればOKです。

【参考】
※どちらもAWS公式サイト(上記「GameScores」テーブル画像も、下記サイトのものです)

セカンダリインデックスを作成する際の注意

  • 処理速度の低下
    • セカンダリインデックスを作成すると、プライマリ以外にもセカンダリにも処理を行うため、スループットが低下する(特に書き込み)。
    • RDBで言えば「別途コピーテーブルを作成し、そちらにも各種処理を行う」イメージに近い
    • ストレージ容量も追加で必要になる
  • 個数の制限
    • セカンダリインデックスは、20個までという制限がある。(さすがに20個作成することはなかなかないけど)
    • 大量にセカンダリインデックスが必要な場合は、そもそもテーブル設計を見直す必要がある。(RDBでいう「正規化」)

query()メソッドとsort()メソッド

DynamoDBで複数データの取得によく使われるのが、以下の2つのメソッドです。

  • query() メソッド
  • scan() メソッド

どちらも「DynamoDBからデータを取り出す」点は同じですが、処理内容に下記の違いがあります。

  • query():「KeyConditionExpression」の条件でデータをフィルタリング→条件に合致したデータを取り出す→返却
  • scan():全データを取り出す→「FilterExpression」の条件でデータをフィルタリング→条件に合致したデータを返却

つまり、scan()はフィルタリングの有無に関わらずまず全データを取り出すため、下記のことが言えます。(もちろんデータ量次第ですが)

  • query()メソッドより、処理に時間がかかる
  • query()メソッドより、課金額が多くなる
    • DynamoDBの読み込み時の課金対象は「読み込まれたデータ(上で言う「取り出されたデータ」)」なので

なので、極力下記の方針にした方が良いです。
※公式サイトや各種ブログにも書かれている通りです。

  • DynamoDBからのデータの取り出しは、極力query()メソッドを使う
    • そのために、インデックス(プライマリ/セカンダリ)の構成よく考える
    • セカンダリインデックスが多量に必要になる場合、まずテーブル設計を見直す
  • 決め打ちで1データのみ取得する場合、getItem()メソッドを使う
  • scan()メソッドは、全データの取得が必須な場合のみ使う
    • インデックス構成では賄いきれない
    • 皇族処理の関係上、全データの取得が必須な場合... など

まとめ

  • プライマリキー(パーティションキーとソートキー)の構成を、よく検討する
    • DynamoDBは、本当にこれが重要
  • 場合によってはセカンダリキーの使用も検討する
    • うまく使えば、データの扱いが便利になる。(ただし多用は避ける)
    • セカンダリキーが多量に必要な場合、まずテーブル設計を見直す
  • データ取得は極力query()やgetItem()メソッドで行う。scan()を多用しない。

と言ったとこでしょうか。

てか、これ書きながら自分でも「業務で使ってるあのテーブル、設計ガバガバじゃねえか!」とか思い返してたりするんですけどね...

というか、(DynamoDBに限らず)時間をおいて見返すと「設計がむちゃくちゃ」とか「1から設計し直したい...」と思うものが、本当に多いです。
まだまだスキルアップしないとですね。

告知

私事ですが、下記「Serverless Meetup Tokyo #16」イベントにて、スピーカーをさせて頂くことになりました。
※開催日時:2020/2/27(木) 19:00~

serverless.connpass.com

なお当日は

  • スピーカー・登壇者側は人生初
  • 大トリ
  • 当日は業務終了後に名古屋から直行予定

と、あわただしい感じですが、皆さま、どうぞよろしくお願いします。
※発表では、下記について触れる予定です

  • AWS
  • Infrastructure as Code(IaC)&Serverless Framework
  • Athena&QuickSight

それではまた。

【Serverless Framework】Serverless Frameworkで外部ファイルの値を読み込み(YAML/JSON/JavaScript)

概要

Serverless Frameworkでは、serverless.ymlだけでなく、外部ファイルに定義を設定して読み込むことも可能です。 そのやり方についてです。

なお、読み込み可能なファイルは、以下の通りです。

serverless.com

YAMLJSONファイルの読み込み

YAMLJSONなど「キー:値」形式のファイルは、以下の方法で読み込みます。

※以下はYAMLで読み込んでいますが、JSONも(構造さえ気をつければ)同じです。

# settings.ymlに以下を定義する
hoge: hogehoge
fuga: fugafuga
# serverless.ymlで上記settings.jsonの値を読み込む
functions:
  exampleFunc:
    handler: exampleFunc.handler
    name: exampleFunc
    tags:
      hoge: ${file(./settings.yml).hoge}
      fuga: ${file(./settings.yml).fuga}

上記のように、外部ファイルの値を読み込みたいキーに対して、値を

${file(外部ファイルのパス).値を読み込みたいキー}

と設定すれば、外部ファイルの値を読み込んでくれます。
※上の例の場合、Lambda関数「exampleFunc」のタグに、

  • 名前:hoge, 値:hogehoge
  • 名前:fuga, 値:fugafuga

という2つのタグが登録されます。

また、上記のように「複数のキーを読み込む」場合、「custom」セクションの独自変数をうまく使うことで、読み込み回数を1回にできます。(下記参照)

# 独自変数「settings」に、settings.ymlの定義を格納する。
custom:
  settings: ${file(./settings.yml)}

# 実際に値を参照する場合、上記のcustom.settingsの各キーを参照すれば、
# キーの値を読み込みできる。
functions:
  exampleFunc:
    handler: exampleFunc.handler
    name: exampleFunc
    tags:
      hoge: ${self:custom.settings.hoge}
      fuga: ${self:custom.settings.fuga}
使いどころ

使いどころとしては、例えば下記ケースです。

  • 条件により、設定値を変更したい
    • 環境(開発/本番) など
  • 各環境の設定値自体は不変(Auroraの接続情報 など)

上記の場合、まず下記のようにフォルダ&ファイルを構成します。

【注意】
 ・ [Root]はserverless.ymlがあるフォルダ
 ・ 各「settings.yml」には環境ごとの定義がされている

  • [Root]
    • [dev]
      • settings.yml
    • [prod]
      • settings.yml

次に、serverless.ymlのcustom.settingsを下記のように設定する。
※${opt:env}は「CLIコマンドの引数envの値を参照する」という、
Serverless Frameworkの独自変数

# 独自変数「settings」に、settings.ymlの定義を格納する。
custom:
  settings: ${file(./${opt:env}/settings.yml)}

最後に、デプロイ時のCLIコマンドで、引数「env」に環境に対応したフォルダ名を指定すればOKです。

> sls deploy --env dev

JavaScriptファイルの読み込み

JavaScriptファイルの読み込み(=関数の戻り値を設定)も、大きな違いはないです。
定義としては、下記の書式となります。

キー: ${file(読み込みjsファイルのパス):戻り値を参照する関数名}

# 例:タグ「hoge」に、settings.jsのgetHoge()関数の戻り値  
# (=デプロイ時の日時情報)を格納する。
functions:
  exampleFunc:
    handler: exampleFunc.handler
    name: exampleFunc
    tags:
      hoge: ${file(./settings.js):getHoge}
// settings.jsでは、「module.exports.関数名」で関数を定義し、
// 設定したい値をreturnすればOK
module.exports.getHoge = () => {
    return new Date().toString();
};

なお、

  • 関数に引数を渡すことはできない
  • 戻り値をJavaScriptオブジェクトにして、そのキーを参照することは可能

のようです。

使いどころ

使いどころとしては、例えば下記ケースです。

  • 毎回値が変わる情報を設定したい
    • デプロイ日時情報など(上記の例のように)
  • serverless.ymlに、生の設定値を書きたくない。
    • Auroraの接続パスワード、外部APIのアクセストークンなどの重要情報
    • 本来、SecretManagerで管理するような情報

上記の場合、まずserverless.ymlでは上記同様、JavaScriptファイルの戻り値を設定するように記載して...

functions:
  exampleFunc:
    handler: exampleFunc.handler
    name: exampleFunc
    tags:
      token : ${file(./token.js):getToken}

実際のJavaScriptでは、生の設定値を記載しなくてよいような処理を施しておけばOKです。(例えば下記のような)

// token.jsの内容
module.exports.getToken = () => {
  
    // crypto-jsは暗号化&複合化を行うライブラリ
    const cryptoJs = require("crypto-js");
    const salt = 'set password salt phrase';
  
    // 「encryptedText」には、「tokenに設定する値を暗号化した値」を  
    // 設定する。
    const encryptedText = "encryptedtoken";
  
    // returnする値(=tokenに設定する値)は、  
    //「encryptedTextを復号化した値」を指定する。
    const decryptedText = cryptoJs.AES.decrypt(encryptedText, salt).toString(cryptoJs.enc.Utf8);
  
    return decryptedText;
};

こうすれば、生の設定値を書かなくて済む分、良くなると思います。

※もちろん下記ケースがあるとばれる可能性があるので、そこは注意です。
(.gitignoreに追加するなど、何かしら対応が必要です)

  • token.jsの内容(暗号化した値、復号化処理の内容など)がばれる
  • token.jsをgitHub等で公開している

まとめ

と、久々にServerless Frameworkに関する情報でした。

実は前回から、Serverless Frameworkに限らず、AWS/IaC/クラウドなどのナレッジがかなりたまってきたので、少しずづ公開したいなあと考えています。

また、近々イベントで登壇するかもしれないので、確定したら合わせて公開します。

てか、もう少し定期的にブログを公開したいなあ。

それでは、また次回。

【JavaScript】「とりあえずawait」は卒業しよう

概要

先日、非同期処理に関して、下記のようなツイートをしました。

このツイートに対して、思った以上に反響を頂いた(と思う)ので、今回もう一度async/awaitのことを記事にしました。

ちなみに前回書いたasync/await関連の記事はこちら。
makky12.hatenablog.com

なお今回の記事を書くにあたり、下記ブログを参考にしました。
(ブログを書こうと思ったきっかけもこれを見たから)

「メソッド実行とawaitの分離」の書き方など、役立つソースが多いです。

前提:async/awaitの基本

async/awaitですが、ざっくり書くと

Promiseによる非同期処理を、同期処理っぽく書ける仕組み

です。

これを利用することで、例えば下記のメリットがあります。

  • 同期処理の様にコードが書けるので、ソースが直感的にわかりやすい。
  • Promiseの記載が不要なので、ソースがシンプルになる
    • Promise.then()ネスト(≒コールバック地獄)が不要になる
    • returnが最後に来るので、処理の流れや戻り値が把握しやすい
// 例えば、Promiseだけの場合、非同期処理はこう書くけど...  
// ※axios.get()は、WEB APIなどにgetメソッドでリクエストを送る非同期処理  
function sample() {  
  
    return new Promise(resolve () => {
        axios.get()
        .then((a) => {
            axios.get(a)
            .then((b) => {
                axios.get(b)
                .then((c) => {
                    resolve(c);
                });
            });
        });
    });
}
  
// async/awaitだと、こんな感じでシンプルに書ける。
async function sample() {
  
    const a = await axios.get();
    const b = await axios.get(a);
    const c = await axios.get(b);
  
    return c;

}

本題

で、元の僕のツイートにある

  • 非同期処理A〜Cの3つ
  • 全て成功した場合のみ処理Dを実行
  • A〜Cは依存関係なし

という処理を書く場合、ついつい「とりあえず生中」ならぬ「とりあえずawait」な感じで、条件反射的に下記ソースを書いてしまいがちです。

//  ダメな例
async function sample() {
  
    const a = await axios.get("./a");
    const b = await axios.get("./b");
    const c = await axios.get("./c");
  
    const d = funcD(a, b, c);
    return d;
  
}

が、これだと処理の流れは

  1. 処理Aのリクエストを送り、レスポンス取得まで処理B以降は実施しない
  2. 処理Bのリクエストを送り、レスポンス取得まで処理C以降は実施しない
  3. 処理Cのリクエストを送り、レスポンス取得まで処理Dは実施しない
  4. 処理Dを実施

と直列処理になり結果として処理時間は「A + B + C + D」となってしまいます。

今回の場合、処理A~Cは非同期処理、かつ依存関係はないので、それならば下記の流れで、A~Cは並列に実行すべきです。

1-1. 処理Aのリクエストを実施し、レスポンスを待たずに処理2を実行
1-2. 処理Bのリクエストを実施し、レスポンスを待たずに処理2を実行
1-3. 処理Cのリクエストを実施し、レスポンスを待たずに処理2を実行
2. 処理A~Cのレスポンスをすべて取得するまで待つ
3. 処理Dを実施

これならば、処理時間は「MAX(A, B, C) + D」ですみますので、結果としてA~Cの処理時間に関しては、他の2処理の処理時間分、短い時間で実行可能です。
(例えばCが一番処理時間が長い場合、(A + B)時間、処理時間を短縮できます)

これをコードにすると、下記のようになります。(3パターンのソースを書きました)

なお、「ラーメンで理解するasync/await」にある通り、非同期処理の実行とawaitは、絶対にセットで書かなきゃいけない...なんてことはありません。
下記のように、別々に書いても問題なく動きます。

//  OKな例(async/await使用)
async function sample() {
  
    // とりあえず、リクエストだけは一括で投げておいて...
    const axiosA = axios.get("./a");
    const axiosA = axios.get("./b");
    const axiosC = axios.get("./c");
  
    // awaitするのは、レスポンスの受け取りのみにする。
    // これなら、どれが一番時間がかかっても、待ち時間はMAX(A, B, C)になる
    const a = await axiosA;
    const b = await axiosB;
    const c = await axiosC;
  
    const d = funcD(a, b, c);
    return d;
  
}
  
//  OKな例(Promise.all使用)
function sample() {
  
    return new Promise((resolve) => {
  
      // とりあえず、リクエストだけは一括で投げるのは同じ。
      const axiosA = axios.get("./a");
      const axiosA = axios.get("./b");
      const axiosC = axios.get("./c");
  
      // Promise.all([])は、配列内の全処理のレスポンスが返るまで、
      // then()内の処理の実行を待つメソッド。
      // 逆に、どれか一個でもレスポンスが返ってきたら  
      // then()内の処理を実行する「Promise.race([])」もある
      Promise.all([axiosA, axiosB, axiosC])
        .then((result) => {
          // resultには、Promise.allで指定した処理のレスポンスが配列で
          // 格納される
          const d = funcD(result[0], result[1], result[2]);
          resolve(d);
        });
    })
}
  
//  OKな例(async/awaitとPromise.all併用)
function sample() {
  
    // とりあえず、リクエストだけは一括で投げるのは同じ。
    const axiosA = axios.get("./a");
    const axiosA = axios.get("./b");
    const axiosC = axios.get("./c");
  
    // Promise.all([])とawaitを併用する
    const result = await Promise.all([axiosA, axiosB, axiosC]);
  
    const d = funcD(result[0], result[1], result[2]);
    return d;
  
}

結局、どれで書くべき?

「ラーメンで理解するasync/await」のコメントでも、

  • こう書けばPromise.all()はいらんのか
  • Promise.all()いらんけど、並列実行がわかりにくいから微妙。
  • awaitばっか羅列する書き方()だと、並列実行が分かりにくい

などなど、いろいろ書かれてました。

個人的には、それぞれのメリットとしては、例えば下記だと思います。

  • async/await

    • ソースがシンプルになり、分かりやすい(同期処理っぽくてわかりやすい)
    • 前の非同期処理の結果に依存する非同期処理を実施する場合でも利用可能
      • 「前提:async/awaitの基本」で書いたような、前の非同期処理のレスポンスを利用する、など
  • Promise.all

    • 並列処理ということが一目でわかりやすい
    • awaitの乱用を防げる

ただ、正直どれを使っても大差はない、と思います。
(好みやプロジェクトに応じて使い分ければいいかと)

※個人的には「async/awaitとPromise.all併用」が、一番分かりやすいかな...と思いますが。

まとめ

正直、「結局、どれで書くべき」については、好みやらこだわりやら宗教やらもあるので、どれでも構わないと思います。

ただ「とりあえずawait」で非同期処理を書いていると、今回書いたような「不要な時間をかけてる処理」が出来てしまうので注意しましょう、という話でした。

実装する機能の仕様や設計から、処理フローの構成をしっかり検討することが大切だと思います。

【Serverless Framework】Serverless FrameworkからAWS Lambdaのテストを実施する

前回の更新が2019/10/12...ずいぶん間が空いてしまったなあ。
実際のところ、前回から

  • 本番リリース
  • 遅い夏休み(お遍路結願)
  • 謎の体調不良(1か月ほど)

で、全然ブログもスキルアップ活動も勉強会参加もできませんでした。

が、ようやく回復してきたので、リハビリがてら。

概要

  • Serverless Framework(以下SF)を使って、クラウド上のソースをテストする。
  • 今回はAWS Lambdaのみ
  • 前提として、事前にSFを使用してAWS Lambda(などのAWSリソース)デプロイをしておく。

SFについては、こちらを参照

AWS上のLambdaを実行する

とりあえず、最低限のコマンドがこちら。
※「funcName」には、SFの「functions」セクションで定義したLambda関数の定義名が入ります。

> sls invoke --function funcName

※必須のパラメータは「--function」だけですが、それ以外にも以下のような任意パラメータがあります。(僕が良く使うもの)

オプション名 説明 備考
--stage 実施したい環境 省略時はdev
--region 実施するAWSのリージョン 省略時は--stageの環境に紐づくリージョン(?)
--path イベントハンドラのevent引数に設定するJSONオブジェクトを定義したjsonファイルの相対パス 今回はは割愛したが、--dataだとテキスト形式で指定できる
--log CloudWatchのログをターミナルに出力させるかどうか --logを指定すると、CloudWatchのログをターミナルに出力できる。(これがすごい便利)

--pathオプションについて

--pathオプションに指定するjsonファイルには、ハンドラの「event」引数の中身を定義することができます。

Lambdaに渡す値は、この「event」引数の「queryStringParameters」「body」キーなどに格納されているので、このjsonファイルの中身を変えることで、いろいろな値を指定してテストを行うことができます。

なお、event引数自体にはたくさんのキー&値がありますが、テストする場合は最低限のキー&値を定義すればOKです。

// 例えば、このLambda関数をテストする場合...
exports.handler = async (event, context, callback) => {

  console.log({event});

  let greeting = "No Name";
  if (event && event.queryStringParameters && event.queryStringParameters.greeting) {
    greeting = `${event.queryStringParameters.greeting} Makky12`;
  } 

  let resBody = {
    greeting: greeting,
  }

  const response = {
    "statusCode": 200,
    "headers": {
      "Access-Control-Allow-Origin" : "*"
    },

    "body": JSON.stringify(resBody)
  }

  return response;
}
{
    // jsonファイルは、最低限下記定義さえあればOK。
    "queryStringParameters": {
        "greeting": "Guten"
    },
}
# 下記コマンドでテストすると、以下のようなレスポンスが表示される。  
# (event.jsonに上記json定義があるとする)
> sls invoke --function funcName --path ./event.json 
  
{
    "statusCode": 200,
    "headers": {
        "Access-Control-Allow-Origin": "*"
    },
    "body": "{\"greeting\":\"Guten Makky12\"}"
}

--logオプションについて

通常、テストを実行した場合にターミナルに表示されるのはレスポンスの内容だけですが、「--log」オプションを付けることで、CloudWatchの内容を追加で表示できます。

個人的にはこれがかなり便利な機能だと思っていて、いちいちAWSにアクセスしなくても、エラーの原因がある程度わかりますので、デバッグにもかなり役立ちます。

# --logオプションありだと、レスポンスの下にCloudWatchのログが表示される。  
# ※これがクッソ便利!
> sls invoke --function funcName --path ./event.json --log
  
{
    "statusCode": 200,
    "headers": {
        "Access-Control-Allow-Origin": "*"
    },
    "body": "{\"greeting\":\"Guten Makky12\"}"
}

START RequestId:xxx Version: $LATEST  
2019-12-28 09:07:34.901 (+09:00)   INFO    { event: { queryStringParameters: { greeting: 'Guten' } } }  
END xxx  
REPORT xxx  Duration: 110.36 ms  

ローカルのLambda関数を実行する

先程までは実際にAWS上のLambdaに接続しましたが、ローカルのLambdaをテストすることもできます。

ローカルのLambdaを実行するコマンドはこちら。(「local」が追加されただけですが)

> sls invoke local --function funcName

オプションもAWS上で実行する場合とほとんど同じですが、下記が追加されてます。

オプション名 説明 備考
--env name=value形式で、環境変数を指定できる serverless.ymlで「${opt:env}」(コマンド引数envの値を参照する)定義があるとエラーになる。
この場合省略形(-e)を使用する。
--docker Docker上での動作を可能にする node.js, Ruby, Python, Javaのみ
--docker-arg Dockerで実行する際、Dockerに渡す引数を指定できる

envオプションについて

上記に記載の通り、--envオプションで、Lambdaに定義した環境変数の値を変更できます。

たとえば、先述のLambda関数のresBody.greetingの値に下記の環境変数を定義した場合、下記のような差があります。

変数名
MY_SAMPLE_ENV hogehoge
# AWSのLambda関数を実行した場合
# この場合、--envの指定は無視され、Lambdaで定義した「hogehoge」になる。
> sls invoke --function funcName --path ./event.json --log --env MY_SAMPLE_ENV=fugafuga
  
{
    "statusCode": 200,
    "headers": {
        "Access-Control-Allow-Origin": "*"
    },
    "body": "{\"greeting\":\"hogehoge\"}"
}
START RequestId:xxx Version: $LATEST  
2019-12-28 09:07:34.901 (+09:00)   INFO    { event: { queryStringParameters: { greeting: 'Guten' } } }  
END xxx  
REPORT xxx  Duration: 110.36 ms  
  
# ローカルのLambda関数を実行した場合
# この場合、--envの指定が適用され、--envで指定した「fugafuga」になる。
# なおローカルの場合、--logオプションは無視される
> sls invoke --function funcName --path ./event.json --log --env MY_SAMPLE_ENV=fugafuga
  
{
    "statusCode": 200,
    "headers": {
        "Access-Control-Allow-Origin": "*"
    },
    "body": "{\"greeting\":\"fugafuga\"}"
}

AWS上での直接実行とローカル実行の比較

実行方法 メリット デメリット
直接実行 ・実運用に即したテストができる
・エラー原因が特定しやすい(ログを出力できるので)
・通信費がローカル実行よりかかる
・修正後の動作確認時に、いちいちデプロイや更新を行う必要がある
ローカル実行 ・通信費が直接実行より安い(※)
・修正後の動作確認が簡単
・エラーの特定が直接実行より難しい(ログが出力できないので)
・実運用に即したという点で、直接実行には劣る

※最初「ローカル実行だから、実際のAWSリソース(S3やDynamoDBとか)にはアクセスできない(=エラーになる)」と考えていましたが、実際は普通にSecretManagerやAuroraの値を取得できました。
pluginでローカル実行用の(Mock)プラグインの定義をしなかった場合、普通にリソースの値はAWSに接続して取得するのか?(この辺りは、要調査です)

まとめ

結論としては、結構使いやすいと思います。
やはり「--log」によるログ出力機能が便利だなあ、と感じました。

「ローカル実行時でも、(実運用時と同様に)ハンドラ関数呼び出しによるブラックボックステストしか行えない(privateな内部関数のテストが行えない)」点はありますが、それを差し引いても便利だなあと感じます。
その辺は、Jestなどと併用して解決していけばよいと思います。

体調不良やらいろいろありましたが、何とか年内にブログを書けて良かった。

それでは、良いお年を。