Lambda Container Image Runtime 躓きメモ
はじめに
おことわり
本記事の正確性等は一切保証できませんが、理解の誤りや、誤植・手順漏れ等、コメント・ご指摘に関しては何でも頂ければ幸いです。マサカリは優しく投げてください。
書くこと
- やったこと
- トラブルシューティング
- 自己流の解釈
書かないこと
- 正解の解釈
- 具体的なコード
参考
背景・課題
背景
実現したいことはシンプルで,ただデータベース(AWS DocumentDB)とECSサービス数を,毎日夜間帯に実行される定期バッチ処理のためにスケジュールドスケーリングさせたかった.
しかしながら,(調べた限り)宣言的なスケーリングの機能はDocumentDBには備わっていないので, 手段は以下の二つを検討していた.
- AWS CLI(またはAWS SDK with Node.js等)によるAWS DocumentDB APIを介したスケーリング実行をLambda+EventBridgeから実行
- AWS CDKによるCloudFormationを介したスケーリング実行をLambda+EventBridgeから実行
採用案としては,正直どちらでもよかった(というよりはもっと上手い方法があればそうしたかった)が,今回対象とするシステムの全てはAWS CDKを使って管理されていることもあり,一貫性を取るために2.
案を採用した.
結論から言えば,普通にAWS SDK等を使ってマネージドランタイムを用いたLambda上で実行するほうが楽だったかも.
本文
Lambda Containerの動作原理解釈
実際には,Lambda Imageの中には以下3つのスクリプトが配置されており(bash
カスタムランタイムの場合),以下図に示した順番で実行される.
#!/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
#!/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 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