echo("備忘録");

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

【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 (オンライン)

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

【Serverless Framework】Serverless Frameworkのダッシュボード上でCI/CDを実現する方法(各種設定など)

はじめに

前回の記事で、Serverless FrameworkのダッシュボードからGitHubレポジトリ/ブランチと連携して、CI/CDを実現する方法を記載しました。

今回は、前回書ききれなかった詳細設定などについて記載したいと思います。

※Serverless Framework公式サイト www.serverless.com

記載内容

  • プロフィール設定
  • デプロイ結果(を含む各種通知)設定
  • CI/CDでのデプロイ前後の挙動設定(任意処理実行)
  • モニタリングについて
  • まとめ

プロフィール設定

プロフィールは「org settings」(右上のアカウント名をクリックすると表示される)から、「deployment profiles」を選択することで行えます。(なお、デフォルトで「dafault」プロフィールが設定されています))

f:id:Makky12:20200607084951p:plain

プロフィールには下記項目を設定する必要があります。

項目名 説明 備考
settings プロフィールの名前設定&削除 新規作成する場合、まず名前を保存しないと、他の設定が行えない
AWS Account 接続するAWSアカウントの設定 [connect aws]ボタンから、実際に該当のAWSアカウントにログインするか、IAMロールのARNを設定する
safeguards デプロイ時のルール、及び違反した際の挙動(警告orエラー)を設定する 詳細項目については下記「「safeguards」の設定について」参照
parameters serverless.ymlファイルで参照できる環境変数を設定する serverless.ymlに「${param:<variable-key>}」形式で記載すると、ここで設定した環境変数の値を使用できる。(下記「「parameters」について」参照)
「safeguards」の設定について
項目名 説明 備考
name your policy 設定するsafeguard policyの名前
choose a safeguard 設定する項目の種類 詳細は下記「「choose a safeguard」の設定について」参照
Modify the default description and configuration 「choose a safeguard」で設定した項目の説明&ルール 詳細項目については下図参照
Choose an enforcement level 「choose a safeguard」で設定した項目が「Modify the default description and configuration」のルールに違反した際の挙動 下記から選択
・error(デプロイを中止する)
・warning(デプロイは継続するが、警告を表示する)
「choose a safeguard」の設定について

※一部間違いがあるかもしれませんが、だいたいこんな感じだと思います

項目名 説明 備考
allowed-function-namaes 「functions」セクションに定義するLambda関数の名前の規則
allowed-regions デプロイを許可するAWSリージョン
allowed-runtimes デプロイを許可するデプロイ先のLambdaランタイム node12.x、Python3.x等
allowed-stages デプロイを許可するデプロイ先のAPI Gatewayステージ dev, prod等
forbid-s3-http-access 安全でないhttpプロトコルからのデプロイ(先s3バケットへのアクセス)を禁止する これを設定した場合、禁止する
framework-version デプロイを許可するServerless Frameworkのバージョン 範囲指定可能
javascript デプロイ前に、JavaScriptでのテスト&テスト通過を必須にする これを設定した場合、必須にする
no-secret-env-vars Lambda関数に設定する環境変数の値に、AWS Credential形式の値を禁止する ・これを設定した場合、AWS Credential形式の環境変数の値を禁止する
AWS Credentialとは、アクセスキーやシークレットアクセスキー(の値)など
no-wild-iam-role-statement IAM Roleのアクション、リソース等の設定にワイルドカード(*)使用を禁止する これを設定した場合、IAM Roleのアクション、リソース等の設定にワイルドカードの使用を禁止する
require-cfn-role デプロイ時にCloudFormation専用(と想定される)Role(やポリシー)を必須とする(?)
require-description すべてのfunction項目に、description(説明)を必須とする 実際には、descriptionの文字数の範囲(最小/最大)を設定する
require-dlg リストアップしたイベント(http等)で実行されるすべてのfunctionのDLQへのアタッチを必須とする
require-global-vpc すべてのfunctionのVPCへのアタッチを必須とする(?) 実際には、最低限アタッチが必要なサブネットの数を指定
require-stack-tags stackTagオプションに特定のタグ名が含まれることを必須とする 実際には、タグ名だけでなく、その値もチェック可能(正規表現で)
restricted-deploy-times デプロイをしない(禁止する)時間を設ける 実際には、基準日・期間・チェックするインターバルを設定する(下図参照)

f:id:Makky12:20200607090130p:plain

「parameters」について

たとえば、こんな感じで設定した場合...

f:id:Makky12:20200607081113p:plain

serverless.ymlで、下記のように記載します。

# serverless.yml内で組織とアプリを「org」「app」キーで紐づけて...
org: my-org-name
app: my-app-name  
  
# (中略)  
  
# 例えばトップ階層のenvironmentに下記設定を行うと、全Lambda関数にキー「hogehoge」、値「fuga」という環境変数が追加される。  
# ※もちろんserverless.yml内のどのキーの値にも設定できる。
environment:  
  hogehoge: ${param:hoge}

デプロイ結果(を含む各種通知)設定

登録したアプリをデプロイを実施した際に(CI/CD、CLIからのデプロイ共に)、デプロイについての通知を設定できます。
ダッシュボードの「apps」から該当のアプリをクリックした後で、「app settings」-「notifications」を選択すると設定できます。

f:id:Makky12:20200607090417p:plain

設定項目は下記のとおりです。(デプロイに関係ないものは除外)

項目名 説明 備考
Select notification type 通知方法(Email/Slack/SNS/WebHookから選択) 選択後、項目毎に通知先を設定します。(メールアドレス、SNSのARNなど)
stages デプロイを通知するステージ 全部or特定のステージ([app settings]-[stages]で作成したステージ、複数選択可)
services デプロイを通知するサービス 選択したアプリ内の全サービスor特定のサービス(複数選択可)
alerts 通知するデプロイのイベント(開始/成功/失敗) 複数選択可

※実際には、下記のようなデプロイ以外の通知にもこの設定は適用されます。

CI/CDでのデプロイ前後の挙動設定(任意処理実行)

Serverless FrameworkダッシュボードでのCI/CDデプロイ時には、内部的に

  • npm install
  • npm test
  • serverless deploy

の3コマンドを順に実施します。

そして、これらのコマンドの実施前後に独自の処理を実施することができます。

【参考】:Serverless Dashboard - CI/CD Custom Scripts

npm install/npm testの前後に処理を挟む

npm install、またはnpm testコマンドの前後に独自処理を挟む場合、package.jsonの「scripts」内に下記コマンドを追加します。

{  
  "name": "xxxx",  
  "version": "1.0.0",  
  "scripts": {  
  
     // preinstall: npm install実行前に実行される処理
    "preinstall": "echo \"preinstall command executed\""  
  
    // postinstall: npm install実行後に実行される処理  
    "postinstall": "echo \"postinstall command executed\""  
  
    // pretest: npm test実行前に実行される処理    
    "pretest": "echo \"pretest command executed\""  
  
    // posttest: npm test実行後に実行される処理    
    "posttest": "echo \"posttest command executed\""
  }
}

ちなみに前回の記事で、「「node_modules」フォルダ(=npmモジュール群)をgit管理下に含めないと、Lambda実行時に「cannnot find module」エラーが出る」件について、

ここはうまい方法があるかもしれないので(それこそCodeBuildの「buildspec」ファイルみたいな機能を使うとか)、そのあたりも調べて、別記事で書きたいです。

ということを書きましたが、「postinstall」コマンドを使う事で解決できます。

例えば、ルート直下の「lambda」フォルダ内でLambda関数&使用するnpmモジュールを管理している場合、下記定義をpackage.jsonに追加すればOKです。
(ルートフォルダは最初のnpm installでインストールできるので問題ない)

{  
  "name": "xxxx",  
  "version": "1.0.0",  
  "scripts": {  
    // これを追加  
    "postinstall": "cd lambda && npm i && cd ../ && exit 0"
  }
}
serverless deployの前後に処理を挟む

serverless deployコマンドの前後に処理を挟むことは、package.jsonの設定では実行できません。

これは「serverless-plugin-scripts」というプラグインをインストール後、serverless.ymlに下記定義を行うことで実現できます。(つまりCLIからのserverless deployコマンドでも可能)

www.serverless.com

# pluginsに「serverless-plugin-scripts」を定義する。
plugins:  
  - serverless-plugin-scripts    
  
# (中略)  
  
# 「custom」に「scripts.hooks」キーを定義し、その中でServerless Frameworkのライフサイクルイベント&処理内容を定義する。  
# ライフサイクルイベントは配列形式で記載可能。  
  
# (例)デプロイ前イベント(before:deploy:deploy)とデプロイ後イベント(deploy:finalize)を定義  
custom:
  scripts:
    hooks:
      - ‘before:deploy:deploy': <before deploy script>  
      - ‘deploy:finalize’: <deploy finalize script>  
    

※Serverless Frameworkのライフサイクルイベントについては、下記ページに詳しく載っています。

Serverless Lifecycle Cheat Sheet · GitHub

例えば、package.jsonとserverless.ymlに下記定義を行った場合、

{    
  // 「scripts」以外は省略
  "scripts": {
    "preinstall": "echo \"preinstall command executed\" && exit 0",
    "postinstall": "cd lambda && npm i && cd ../ && exit 0"
  }
}
plugins:  
  - serverless-plugin-scripts    
  
# (中略)  
  
custom:
  scripts:
    hooks:
      - 'before:deploy:deploy': ${file(./beforedeploy.js):beforeDeploy}
      - 'deploy:finalize': ${file(./finalizedeploy.js):finalizeDeploy}
    
// beforedeploy.jsの内容。(ただログを出力するだけ)  
// return 0してるのは、何か値を返さないとデプロイ時にエラーになるため。(voidではNG)  
// なおfinalizedeploy.jsの内容は、「beforeDeploy」が「finalizeDeploy」になっている以外は全く同じ
module.exports.beforeDeploy = () => {
    console.log("beforeDeploy Executed");
    return 0;
}  

デプロイログは、下記の通りになります。(ちゃんと定義したイベントが走っています)

f:id:Makky12:20200607090702p:plain
f:id:Makky12:20200607090834p:plain

モニタリングについて

CI/CDとは直接関係でないですが、アプリを選択後「service」から該当のサービスを選択すると、そのサービスのモニタリングを行えます。

モニタリングでは、例えば以下の事を確認できます。

  • API呼び出し回数&実行時間
  • 各Lambda関数の実行回数&実行時間
  • エラー情報
  • メモリ使用量 など

f:id:Makky12:20200607091229p:plain
f:id:Makky12:20200607091242p:plain
f:id:Makky12:20200607091254p:plain

まとめ

以上が、ダッシュボードでCI/CDを行う際の、大まかな設定になります。

必須項目/任意項目色々ありますが、これらの設定をうまく使う事で、例えば

  • safeguardで「no-wild-iam-role-statement」を設定することで、不要なリソースへのアクセスやアクションの実行を禁止し、想定外の事態を避ける
  • alertsの「deployment」のチェックをONにすることで、
    • デプロイ失敗時にそれを検知し、素早く対処する(≒迅速に新機能を提供できるようにする)
    • 意図しないデプロイが発生した際、なぜ起こったのかを素早く調査する(セキュリティインシデントの可能性もあるので)
  • モニタリングを使用して、Cloudwatch Eventsの日時バッチ的な処理が失敗した際、素早く調査する

のようなことも可能になり、運用面でも大いに有効活用できると思います。

ぜひこれらの設定を有効活用していきたい&業務を楽にしたいですね。

ちなみに、Serverless FrameworkでのCI/CDについては、Serverless Framework公式Youtubeチャンネルにも動画があるので、そちらも参考にしてください。

https://www.youtube.com/watch?v=mB0owYlJgKs&t=506s


CI/CD with Serverless Framework

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

【Serverless Framework】Serverless Frameworkのダッシュボード上でCI/CDを実現する方法

はじめに

AWSにしろAzureにしろ、クラウドベースの開発でよく「CI/CD」(継続的インティグレーション/継続的デリバリー)が取り入れられていると思います。

そして、Git連携(=gitの特定リポジトリ/ブランチにpushしたら、連動してデプロイが実施される)を導入するケースも多いです。

実際にAWSなら、「AWS CodeBuild」「AWS CodePipeline」のようなCI/CD支援サービスも提供されています。

今回のブログは、そのCI/CD&Git連携を、Serverless Frameworkのダッシュボードのみで実現する方法です。

www.serverless.com

前提

  • Serverless Frameworkの1.48.0以降のバージョンが必要なので、必要に応じて事前にバージョンアップをしてください。(最新版なら問題なし)
  • 公式ページ(https://serverless.com)から、サインアップを済ませておいてください。(右上の「Sign-Up-Free」から)
  • 事前にGit連携対象のGitHubリポジトリ&ブランチを作成しておいてください。(ブランチは「master」でもOK)

初期設定

初回ログイン時、まずは作成するアプリの設定を行います。
下記3種類から選びます。(今回は「Serverless Framework Existing Project」前提で進めます)

項目 説明 備考
Node.js Rest API Node.jsのRest APIプロジェクトの新規作成
Python Rest API PythonRest APIプロジェクトの新規作成
Serverless Framework Existing Project 既存のServerless Frameworkプロジェクトから選択 新規作成せず、既存のプロジェクトを使用する場合はこちら。(まだAWSにデプロイしていなくてもOK)

f:id:Makky12:20200530195714p:plain

また、初回のみ「GitHub連携」及び「AWSアカウント連携」の設定を行う必要があります。

それぞれ、ボタンをクリックするとGitHub/AWSのログインページが表示されますので、GitHubのレポジトリ連携/AWSサインインを行えば、自動でダッシュボード画面に戻ります。

f:id:Makky12:20200531190750p:plain

アプリを登録する

まず、CI/CD導入対象のアプリを登録する必要があります。 まずダッシュボードにサインインします。

公式ページ右上の「Sign-in」からサインインすると、自分のダッシュボードが表示されます。

※下記CLIコマンドでサインイン画面を開くことができます。(もちろん自分でブラウザを開いても問題なし)

> serverless login

※その他、一度サインインすれば、CLIから下記コマンドでダッシュボードを開くことができます。

> serverless dashboard

ダッシュボードが表示されたら、[apps] - [add app]から、必要項目を設定して、[add app]ボタンをクリックすると、アプリを登録できます。(設定できる項目は以下の通り)

項目 説明 備考
name アプリの名前
profile デプロイに使用するprofile ※1

※1:profileは右上の[アカウント名]-[org settings]-[DEPLOYMENT PROFILES]から設定できます。(細かくは別記事で書きます)
とりあえずは「default」でOK

f:id:Makky12:20200530195849p:plain

その後、登録した[apps]から登録したアプリ名をクリックすると、「Deploy Your Service」項目が表示されますが、まずは先に下記の「デプロイステージの登録」を行います。

デプロイステージの登録

[app settings]-[stage]を選択すると、デプロイ先のステージを登録できます。 デフォルトで「default」が登録されていますが、「dev」「stg」など、明確にステージ名が決まっている場合、ここで事前に登録しておきます。

なお、設定項目は下記2つです。

項目 説明 備考
stage name ステージ名
deployment profile デプロイに使用するプロファイル

f:id:Makky12:20200530200628p:plain

CD/CDの選択

ここまで設定したら、登録した[apps]から登録したアプリ名をクリック後、下記いずれかの方法でCI/CDの設定を行います。

  • 「Deploy Your Service」画面で「Deploy Your GitHub」をクリックする※
  • [ci/cd settings]タブをクリックする

※ちなみに「Deploy From the CLI」を選択すると、CLIから登録したアプリをデプロイする方法が表示されます。

この方法ではGitHubレポジトリ・ブランチ連携デプロイは出来ませんが、登録したアプリのモニタリング&デプロイ履歴の確認などは出来ます。(この辺も別記事で書きます)

※「Deploy From the CLI」の場合、ログイン後、下記コマンドで登録したアプリと表示中のプロジェクトの紐づけを行うことができます。

> serverless --org (アカウント名) --app (登録したアプリ名)
# 例  
> serverless --org makky12 --app my-app  

設定できる項目は、以下の通りです。

項目 説明 備考
connection 連携するGitHubレポジトリ名 GitHubリポジトリを未設定の場合、まずGitHub側でリポジトリ設定が必要(ここで設定可能)
preview deploys テスト&デプロイ対象のブランチ(?) ※1
branch deploys デプロイトリガ元のブランチ&デプロイステージ
region デプロイ先のAWSリージョン ここからは「advanced settings」の項目※2
service デプロイ対象のサービス名 ※3
runtime デプロイ対象のプログラムランタイム 「node.js」「python」など※3
trigger directions デプロイトリガ元のブランチの中で、実際にトリガを起動するフォルダ・ファイル 「Always trigger a deployment」にチェックを入れると、該当ブランチをpushしたらデプロイが実行される。
チェックを入れなかった場合、指定したフォルダ・ファイルの変更がpushされた場合のみデプロイが実行される。
preview deploys プレビュー対象のステージ(?) [app settings]-[stage]で設定したステージ、または「use branch name as stage」を選択。
後者を選んだ場合、ブランチ名がそのままステージ名になる。
また「Destroy stage and resources when branch is deleted」で「ブランチを削除したら、該当ステージ&リソースを削除するかどうか」を設定できる。※1

※1:てか、なんで同じ名前なの...(誤記ではなく、本当に同じ名前)
※2:「advanced settings」とあるけど、リージョンってかなり重要なような...
※3:初期設定で「Serverless Framework(Existing Project)」を選択した場合、設定済&指定不可

f:id:Makky12:20200531191131p:plainf:id:Makky12:20200531191142p:plain

実行

ここまで設定したら、実際に設定したレポジトリ・ブランチに該当プロジェクトをpushしてみます。

設定に問題なければ、pushに連動してデプロイが実行されるはずです。 ※左上の「ci/cd」の隣に青い●マークがつき、クリックすると「実行中」みたいな感じで円がグルグル回っているはずです。

f:id:Makky12:20200531191320p:plain

デプロイ終了すると、結果に応じたアイコンが表示されるので、クリックするとデプロイのレポート・及びログを確認できます。

f:id:Makky12:20200531191451p:plain f:id:Makky12:20200530200424p:plain

あとは実際に動かしてみて、正しくデプロイされている&変更が反映されている事を確認してください。

デプロイ失敗理由(僕の場合)

  • serverless.ymlの変数値の設定に必要なファイルがpushされていなかった
  • node_modulesフォルダがpushされていなかった

しょうもない理由といえばしょうもない理由なんですが、ログを追って原因が分かった時、「あ、そうか、CI/CDだとそうなんだ」と思いました。

というのは、(前者はともかく後者は)今の業務で開発しているプロジェクトだと、node_modulesフォルダ一式はGitの管理対象外にしてたからです。(そうそう自分で直接変更なんてしないし、事前にnpm installすればOKなので)

でもCI/CDだと、当然Gitにpushしないと動かないので(Lambda実行時に「●●のモジュールがない」とエラーが出る)、Gitの管理対象にしないといけないんだ、と分かりました。

とはいえ、ここはうまい方法があるかもしれないので(それこそCodeBuildの「buildspec」ファイルみたいな機能を使うとか)、そのあたりも調べて、別記事で書きたいです。

まとめ

というわけで、Serverless FrameworkのダッシュボードだけでCI/CDを実現する方法でした。

AWS CodeBuildやAWS CodePipeline、及び各種CI/CD用ツールはありますが、Serverless Frameworkのダッシュボードだけでも、簡単なCI/CDを実現できますので、有効に活用していきたいですね。

また、今回記載できなかった

  • プロファイル設定
  • ビルドコマンド設定
  • アプリのモニタリング&各種通知設定

なんかについても、後日別記事で書きたいと思います。

それでは、かなり久々な記事でしたが、今回はこの辺で。

【Serverless Framework】Lambda Destinations機能をServerless Frameworkで実装する

はじめに

昨年11月末にAWSから「Lambda Destinations(Lambda非同期呼び出しの宛先指定)」機能が発表されました。

そしてこの機能について、3月中旬に「Serverless Frameworkが正式サポート」したと公式ブログで発表がありました。
そこで、早速この機能をServerless Frameworkで使ってみました。

serverless.com

前提:「Lambda Destinations(Lambda非同期呼び出しの宛先指定)」って?

概要としては、以下の機能になります。

  • Lambda実行結果に従って、次のアクション(=実行リソース)を指定できる。
    • 正常終了(Success)/異常終了(Failure)で個別に指定可能
  • 指定可能なアクション(=実行リソース)は、以下の通り
    • Simple Queue Service(SQS)
    • Simple Notification Service(SNS)
    • Lambda
    • Event Bridge
  • 非同期実行のLambda(=各種イベントで実行されるLambda)のみ設定可能
    • API Gateway(≒Rest API)経由で実行されるような、同期実行のLambdaでは実行不可(※)

API Gateway経由Lambdaでも、非同期で実施することは可能です。

https://d2908q01vomqb2.cloudfront.net/1b6453892473a467d07372d45eb05abc2031647a/2019/11/25/lambda-destinations1.png

【参考】

実践:Serverless Frameworkで実装してみる

※前提として、前掲のServerless Framework公式ブログに記載の通り、Serverless Frameworkのバージョンを1.66.0以上にしてください。

serverless.ymlの「functions」セクションで、以下のようなLambda関数を定義します。 (今回はSuccess/Failure共に、アクションはLambda関数にしています)

functions:  
  Challange:  
    handler: challange.index  
    events:  
      # API Gatewayは通常は同期実行(Lambda Destination対象外)だけど、テスト用に実装
      - http:  
          path: /challange  
          method: get  
      ## 実際はこんな感じで、何かのイベントで実行するように実装する(元リソースの定義は省略)  
      - schedule: rate(10 minutes)  
    # 「onSuccess」/「onFailure」に成功時/失敗時の定義名を記載する
    destinations:  
      onSuccess: Success  
      onFailure: Failure  
  
  # 成功時のリソースの定義を記載(Lambdaの場合はハンドラを定義)  
  Success:  
    handler: succeeded.index  
  
  # 失敗時のリソースの定義を記載(Lambdaの場合はハンドラを定義)
  Failure:  
    handler: failed.index  

定義としては、以上です。

なお上記関数ですが、下記の内容となります。(シンプルすぎるので内容は省略)

  • challange.index:onSuccess検証時は何もせずにreturnするだけ、onFailure検証時は何かErrorをthrowする。
  • succeeded.index/failed.index:console.log()でログを1行書き出すだけ

invokeでテスト実行する

上記serverless.ymlをデプロイしたら、実際にテストをしてみます。
※デプロイ成功していれば、「Challange」Lambda内に下記の通り「非同期呼び出し」ソースが二つ追加されます。
f:id:Makky12:20200418175220p:plain

デプロイを確認したら「invoke」コマンドでテストを行います。

ここで気を付けるのは、invoke」コマンドを非同期(=イベント)で呼び出すには「--type」オプションに「Event」オプションを指定しないといけない点です。

これを忘れると「--type=RequestResponse」(=同期呼び出し)として実行されるため、Lambda Destinationは正常に機能しません。
(堀家さん、ありがとうございます。)

# test.jsonはchallange.index実行時のevent引数の内容をそのまま指定  
# 「--type Event」を忘れないこと!
> slss invoke --function Challange --path test.json --type Event

正常に実行されると、特に何も表示されずにコマンドが終了しますので、CloudWatch Logを確認します。

CloudWatch Logを確認すると、成功時/エラー発生時共に正しくアクション先Lambdaが実施され、ログが書き出されていることが分かります。

  • 上が成功時、下がエラー発生時
  • 画像は1つにまとめてますが、実際はどちらもchallange.index、及びsucceeded.index(またはfailed.index)のログです。
  • エラー発生時にchallange.indexのログが3つあるのは、2回リトライ処理を実施しているためです。

f:id:Makky12:20200418185809p:plain
f:id:Makky12:20200418185830p:plain

なお、Serverless Framework公式の下記動画でも、Serverless FrameworkによるLambda Destinationsの実装が詳しく紹介されています。 www.youtube.com

まとめ

以上がServerless FrameworkでLambda Destinationsを実装する方法です。

Lambda Destinationsですが、成功時/失敗時の処理を個別に切り出せるので、下記のようなメリットがあると思います。

  • 成功時&エラー時の処理を、同一ソースに書かなくてよい。
  • 上記により、ソースの可視性&保守性の向上につながる
  • リトライ処理を考慮した成功時&エラー時の処理を実装できる

こういう便利な機能は、どんどん活用していきたいですね。

告知

4/24(金)にオンラインで開催される「Infra Study Meetup #1 ~Infrastructure as Code~」に「Infrastructure as Codeを導入して良かった点」という内容でオンライン登壇させて頂きますので、よろしくお願いいたします。(てか4/18(土)時点で、参加者2000人超えそうな勢いなんだけど...)

forkwell.connpass.com

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

【Serverless Framework】メモリ不足でデプロイができない場合の対処方法

概要

Serverless Frameworkでパッケージングやデプロイを行う際に、メモリ不足で失敗することがあります。その場合の対象方法です。

serverless.com

現象

先日、業務で開発したサーバーレスSPAをServerless Frameworkでデプロイしようとしたら、パッケージング時に下記エラーが発生し、デプロイできませんでした。

Ineffective mark-compacts near heap limit Allocation failed - Javascript heap out of memory

アプリやServerless Frameworkの設定を見直したり、「VS Codeが原因かな?」と思い下記記事の内容を色々試しましたが、現象改善しませんでした。

その後、コマンドプロンプトでも同様の現象が発生したので、単純にエラーメッセージの通り、Node.jsのメモリが足りないのが原因と分かりました。
※PCのメモリは十分な容量があったので、そちらはではないと考えました。(てか、そっちならほぼ詰みだった)

対応方法

対応方法ですが、Node.jsのメモリを増やしてあげれば解決します。

下記の通り、環境変数「NODE_OPTIONS」の「max_old_space_size」パラメータにメモリサイズ(MB)を指定することで、Node.jsのメモリを設定可能です。

# Node.jsのメモリサイズを指定する。(MBで)
# 下記だと、メモリサイズを4GBにする。
> set NODE_OPTIONS=--max_old_space_size=4096

なお「node」コマンドならば「max_old_space_size」パラメータの値をコマンド毎に指定できますが、Serverless Frameworkの「serverless(sls/slss)」コマンドではダメでした。(少なくとも僕の環境では)
(余談ですが、「serverless」コマンドのエイリアス、「sls」以外に「slss」でもOKなんですね。最近知りました。これでPowerShellでもエイリアスが使えます。(「Select-String」のエイリアス「sls」とかぶるから、「sls」はPowerShellでは使えない))

  • Node.jsのデフォルトのメモリサイズは1GBです。
  • あまり増やすと、今度は別アプリの動作などに影響がある場合があるので、注意してください。

上記コマンドを実施した後、先述のサーバーレスSPAをServerless Frameworkでデプロイしたところ、問題なくデプロイできました。

なお、実際のNode.jsのメモリサイズを確認する方法については、下記動画で詳しく説明されています。

まとめ

以上、Serverless Frameworkでメモリ不足が発生した場合の対象方法でした。

もちろん、この方法では対処出来ない場合もあるので、その場合はアプリ構成を考えたり(肥大化が原因の可能性がある)、場合によってはPC環境のスペックアップも考える必要があると思います。

ただ、暫定的な対応には使えると思うので、一度試すのもありかと思います。

告知

4/7(火)にオンラインで行われた「VS Code Meetup #4」にて、「開発チーム管理で役立ったVSCode拡張機能」という内容でオンライン登壇をさせて頂きました。
また、その時の登壇資料も公開していますので、よろしければご参照ください。

vscode.connpass.com

www.slideshare.net

その他、4/24(金)に同じくオンラインで開催される「Infra Study Meetup #1 ~Infrastructure as Code~」にも「Infrastructure as Codeを導入して良かった点」という内容でオンライン登壇させて頂くことになりましたので、よろしくお願いいたします。(てか4/12(日)時点で、参加者1400人超えてる...)

forkwell.connpass.com

それでは、今回はこのへんで。