😇

Lambda Container Image Runtime 躓きメモ

2022/02/15に公開

はじめに

おことわり

本記事の正確性等は一切保証できませんが、理解の誤りや、誤植・手順漏れ等、コメント・ご指摘に関しては何でも頂ければ幸いです。マサカリは優しく投げてください。

書くこと

  • やったこと
  • トラブルシューティング
  • 自己流の解釈

書かないこと

  • 正解の解釈
  • 具体的なコード

参考

https://docs.aws.amazon.com/ja_jp/lambda/latest/dg/images-create.html

背景・課題

背景

実現したいことはシンプルで,ただデータベース(AWS DocumentDB)とECSサービス数を,毎日夜間帯に実行される定期バッチ処理のためにスケジュールドスケーリングさせたかった.

https://docs.aws.amazon.com/ja_jp/documentdb/latest/developerguide/db-cluster-manage-performance.html

しかしながら,(調べた限り)宣言的なスケーリングの機能はDocumentDBには備わっていないので, 手段は以下の二つを検討していた.

  1. AWS CLI(またはAWS SDK with Node.js等)によるAWS DocumentDB APIを介したスケーリング実行をLambda+EventBridgeから実行
  2. AWS CDKによるCloudFormationを介したスケーリング実行をLambda+EventBridgeから実行

採用案としては,正直どちらでもよかった(というよりはもっと上手い方法があればそうしたかった)が,今回対象とするシステムの全てはAWS CDKを使って管理されていることもあり,一貫性を取るために2.案を採用した.

結論から言えば,普通にAWS SDK等を使ってマネージドランタイムを用いたLambda上で実行するほうが楽だったかも.

本文

Lambda Containerの動作原理解釈

実際には,Lambda Imageの中には以下3つのスクリプトが配置されており(bashカスタムランタイムの場合),以下図に示した順番で実行される.

lambda-entrypoint.sh
#!/bin/sh
# Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved.

set -euo pipefail

printenv

if [ $# -ne 1 ]; then
  echo "entrypoint requires the handler name to be the first argument" 1>&2
  exit 142
fi
export _HANDLER="$1"

RUNTIME_ENTRYPOINT=/var/runtime/bootstrap
if [ -z "${AWS_LAMBDA_RUNTIME_API}" ]; then
  exec /usr/local/bin/aws-lambda-rie $RUNTIME_ENTRYPOINT
else
  exec $RUNTIME_ENTRYPOINT
fi
bootstrap
#!/bin/sh

set -euo pipefail

# Initialization - load function handler
source $LAMBDA_TASK_ROOT/"$(echo $_HANDLER | cut -d. -f1).sh"

# Processing
while true
do
  HEADERS="$(mktemp)"
  # Get an event. The HTTP request will block until one is received
  EVENT_DATA=$(curl -sS -LD "$HEADERS" -X GET "http://${AWS_LAMBDA_RUNTIME_API}/2018-06-01/runtime/invocation/next")

  # Extract request ID by scraping response headers received above
  REQUEST_ID=$(grep -Fi Lambda-Runtime-Aws-Request-Id "$HEADERS" | tr -d '[:space:]' | cut -d: -f2)

  # Run the handler function from the script
  RESPONSE=$($(echo "$_HANDLER" | cut -d. -f2) "$EVENT_DATA")

  # Send the response
  echo "${RESPONSE}" | curl -s -X POST "http://${AWS_LAMBDA_RUNTIME_API}/2018-06-01/runtime/invocation/$REQUEST_ID/response" -d @-
done
function handler () {
  EVENT_DATA=$1
  echo "$EVENT_DATA" 1>&2;
  RESPONSE="Echoing request: '$EVENT_DATA'"

  SCENARIO=$(echo "$EVENT_DATA" | jq -r .scenario)
  STEP=$(echo "$EVENT_DATA" | jq -r .step)

  echo "ENV: ${APP_ENV}" 1>&2;
  echo "SCENARIO: ${SCENARIO}" 1>&2;
  echo "STEP: ${STEP}" 1>&2;

  # 処理
  
  echo $RESPONSE
}
FROM public.ecr.aws/lambda/provided:al2 as build

ENV HOME="/var/task"
ENV NVM_DIR="${HOME}/.nvm"
ENV APP_HOME="${HOME}/App"

...

EXPOSE 8080

CMD [ "function.handler" ]

この時,注意点は以下3点

1. 権限

ローカルで編集したbootstrap, function.sh, lambda-entrypoint.shの権限は, 別ユーザーが実行できるような権限になっていなければならない.

Lambda上でContainerが実行される際には, sbx_userXXXというユーザー名で実行される.
そのため, 上記ファイルはsbx_userXXXが触れる場所・権限設定が付与されている必要がある.

-rwxr-xr-x  1 xxx  yyy  850 Jan 24 13:39 bootstrap
-rwxr-xr-x  1 xxx  yyy  425 Jan 24 13:39 lambda-entrypoint.sh
-rwxr-xr-x  1 xxx  yyy  676 Jan 27 14:01 function.sh

2. 配置場所

読み取り・実行を行うスクリプト等の配置場所は,/var/task配下が丸い.
ここ以外でもうまくいくかもしれないが,別ディレクトリを指定するだけだと実行時に当該ディレクトリが見つからなかったりしてエラーで死ぬ.

自分は作業ディレクトリ・ホームディレクトリ自体を/var/task配下に切り替えるようにDockerfileを記述し,各種Install等も/var/task配下になるように指定した.

FROM public.ecr.aws/lambda/provided:al2 as build

ENV HOME="/var/task"
ENV NVM_DIR="${HOME}/.nvm"
ENV APP_HOME="${HOME}/App"

RUN yum update -y && yum install -y tar gzip

RUN cd ${HOME} \
  && curl -o- https://raw.githubusercontent.com/creationix/nvm/master/install.sh | bash \
  && export NVM_DIR=${NVM_DIR} && [ -s "${NVM_DIR}/nvm.sh" ] && \. "${NVM_DIR}/nvm.sh" \
  && nvm install v14.15.0 && nvm use v14.15.0 && npm install -g yarn

RUN cd ${HOME} \
  && export NVM_DIR="${NVM_DIR}" && \. "${NVM_DIR}"/nvm.sh \
  && nvm use v14.15.0 \
  && npm install -g aws-cdk@1.139.0
  
...

3. 書き出し場所

実行結果の書き出し場所は,/tmp配下のみ許容されている.
CDKだとsynth結果が吐き出されることがあるため, /tmp配下へ出力するようなOptionを設定する必要がある.

function.sh
function handler () {
  EVENT_DATA=$1
  echo "$EVENT_DATA" 1>&2;
  RESPONSE="Echoing request: '$EVENT_DATA'"

  cd ${APP_HOME}/
  cdk deploy -c args=$EVENT_DATA --output /tmp/cdk.out

  echo $RESPONSE
}

4. 環境変数名

Lambda実行時,Containerに与える環境変数としてENVという環境変数名は予約されているのか,使用すると不安定になるので使えない.
実行環境dev/prodを区別する意味で利用したかったが,APP_ENV等にして回避.

Discussion