概要

AWS Lambdaは、複数のプログラミング言語に対応したマネージドランタイムを提供しています。 公式にサポートされていない言語を使用したい場合にはOS専用ランタイムを利用することができます。

OS専用ランタイムでは、カスタムランタイムを作成して、AWS LambdaランタイムAPIに従った動作を自前で実装することでLambda関数として好きな言語を使えるようになります。

公式のチュートリアルでは実行時のイベントをそのままLambdaのレスポンスとして返却する処理を以下のようなbootstrapで記述しています。

このbootstrapでは冒頭にset -e を設定しています。 set -e を記載するとコマンドの途中でエラーが発生した場合に、即座にそのスクリプト全体を異常終了とすることができます。 しかし、異常終了するとwhileループを抜けてしまうので、次回Lambdaを実行しようとすると初期化処理から始まってしまいコールドスタートとなってしまいます。

チュートリアル: カスタムランタイムの構築 - AWS Lambda

#!/bin/sh

# -e: エラー時にそれ以降の処理を実施しない
# -u: 未定義の変数を使用できない
# -o pipefail: パイプの途中で失敗してもエラー扱いにする
set -euo pipefail

# $_HANDLERで function.handler と指定すると . で区切り functionのみ取り出す
# 結果としてfunction.shが読み込まれる
source $LAMBDA_TASK_ROOT/"$(echo $_HANDLER | cut -d. -f1).sh"

# イベント駆動のためループで実行され続ける
while true
do
  # HTTPResopnseを受け取るための一時ファイルを作成する
  # HEADERSに作成された一時ファイルの場所が代入される (ex. /tmp/XXXX.tmp)
  HEADERS="$(mktemp)"

  # EVENT_DATAにeventを代入する
  # 同時にあとで必要なレスポンスヘッダをあらかじめ作成したファイルに格納する
  # このcurlでHTTP通信するエンドポイントは処理がリクエストされるまで待ち続ける
  EVENT_DATA=$(curl -sS -LD "$HEADERS" "http://${AWS_LAMBDA_RUNTIME_API}/2018-06-01/runtime/invocation/next")

  # レスポンスヘッダから Lambda-Runtime-Aws-Request-Idを取り出す
  REQUEST_ID=$(grep -Fi Lambda-Runtime-Aws-Request-Id "$HEADERS" | tr -d '[:space:]' | cut -d: -f2)

  # $_HANDLERで function.handler と指定すると . で区切り handlerのみ取り出す
  # 結果としてfunctions.shで読み込んだhandlerが実行される
  # 第1引数としてEVENT_DATAを渡している
  # handlerの出力がRESPONSEに格納される
  RESPONSE=$($(echo "$_HANDLER" | cut -d. -f2) "$EVENT_DATA")

  # Lambdaのレスポンスを返す
  curl "http://${AWS_LAMBDA_RUNTIME_API}/2018-06-01/runtime/invocation/$REQUEST_ID/response"  -d "$RESPONSE"
done
function handler () {
  # EVENT_DATAを取り出す
  EVENT_DATA=$1

  # EVENT_DATAの標準出力を標準エラーにリダイレクトする
  # CloudWatchLogsには標準出力と標準エラーの両方が出力されるため、
  # 標準出力は返却値、標準エラーはログ出力用として使い分けていると思われる
  echo "$EVENT_DATA" 1>&2;

  # レスポンスとしてEVENT_DATAとして標準出力する(最終的にLambdaのレスポンスとなる)
  RESPONSE="Echoing request: '$EVENT_DATA'"
  echo $RESPONSE
}

コールドスタートを防ぐ

set -eを設定しつつ、ifで個別にエラー処理を個別に実施することによってコールドスタートを防ぎます。

sourceコマンドでhandlerを読み込んで実行していましたが、ifの中で直接 function.sh を実行するように変更しました(ハンドラ設定は全無視しています)。 ifで終了コードを判定し成功していればこれまでどおり、失敗していれば呼び出しエラー時のAPIを叩くようにしています。 これにより、whileから抜けずに次の処理に進むことができます。

#!/bin/sh

set -euo pipefail

while true
do
  HEADERS="$(mktemp)"
  EVENT_DATA=$(curl -sS -LD "$HEADERS" "http://${AWS_LAMBDA_RUNTIME_API}/2018-06-01/runtime/invocation/next")
  REQUEST_ID=$(grep -Fi Lambda-Runtime-Aws-Request-Id "$HEADERS" | tr -d '[:space:]' | cut -d: -f2)

  if RESPONSE=$(./function.sh $EVENT_DATA)
  then
    curl "http://${AWS_LAMBDA_RUNTIME_API}/2018-06-01/runtime/invocation/$REQUEST_ID/response"  -d "$RESPONSE"
  else
    ERROR="{\"errorMessage\" : \"実行中にスクリプトが異常終了しました: ステータス:$?\", \"errorType\" : \"StatusCodeException\"}"
    curl "http://${AWS_LAMBDA_RUNTIME_API}/2018-06-01/runtime/invocation/$REQUEST_ID/error" -d "$ERROR" --header "Lambda-Runtime-Function-Error-Type: Function.StatusCodeError"
  fi
done

function.shはhandlerとしてではなく、直接実行内容を記載するよう変更しています。 ここでも set -e を設定することでエラー時にその場で異常終了するようにしました。 標準出力にLambdaのレスポンス、標準エラーにCloudWatchLogsの内容を出力する点は変えていません。

#!/bin/sh
set -euo pipefail

EVENT_DATA=$1

echo "$EVENT_DATA" 1>&2;

RESPONSE="Echoing request: '$EVENT_DATA'"
echo $RESPONSE

以上の変更によりコールドスタートを防ぐことができましたが、既存の資産をそのまま移行したいなどの理由がなければbashを使わずに各種プログラミング言語で実装したいです。

リンク