Serverless DeepDive #07:関数・version・aliasの状態を比較してみる
同じcodeの別関数・published version・aliasからInvokeし、メモリと/tmpの状態を比較します。同じversionを指すaliasの切替と戻しを追い、局所状態とS3への権限の境界を考えます。
Series: Serverless DeepDive (13 articles)
- …
- 8Serverless DeepDive #07:関数・version・aliasの状態を比較してみる This article
検証構成
同じversionを指す2つのaliasから、同じメモリと/tmpの値を読みました。handlerの外のメモリ変数と/tmpに残る状態を、この稿では局所状態と呼びます。aliasは、関数のversionへ付ける参照名です。名前を変えただけでは、局所状態を分ける入口にはなりませんでした。
#06 では、局所に置くcacheと、次へ引き継ぐ処理結果を分けました。今回は、関数・version・aliasを選んで呼び出します。呼び出し先を分けたとき、どの状態へ届くのかを比べます。
| 対象 | 条件 |
|---|---|
| region | ap-northeast-1(Tokyo) |
| primaryとcontrol | 同じNode.jsのcodeを使う別関数 |
| runtime | nodejs26.x、実行時v26.8.2、AL2023、public Preview |
| 両関数の設定 | arm64、512 MB、timeout 5秒、reserved concurrency 1、通常on-demand |
| published version | primaryのPROBE_CONFIG_MARK=Aをversion 1、Bをversion 2として発行。表の設定A/Bはこの値 |
| alias | blueとliveをversion 1へ向け、liveだけ2へ変更して1へ戻す |
| 保存先 | 新規S3 bucketの同じ実験prefix。primaryとcontrolの2実行roleへ読み書きを許可。primaryのversion 1・2・$LATESTは同じroleを使う |
| 呼び出し | primaryとcontrolへ各3回、その後にversion・alias・S3の41回。合計47回の同期Invoke。SDKとCLIの自動retryは無効 |
| 別関数の6回の後の41回 | 最初から最後のhandler開始まで約17.4秒。隣り合う開始時刻の差は約0.25〜1.44秒 |
Node.js 26はpublic Preview です。SLAとTechnical Supportの対象外という条件で測りました。reserved concurrency は、関数の同時実行数の上限と予約です。1にしても、実行環境を1つに固定する設定ではありません。
今回は両関数を各1にしました。#06の各2と異なり、47回をすべて順に送りました。並行Invokeは測っていません。
次の図は、検証時の構成です。破線はaliasの参照先を示します。実線はInvokeと応答、S3への通信、ログ出力です。

状態を記録する
#06のprobeを再利用しました。handlerを読み込んだときにUUIDを作り、呼び出し回数、メモリの値、/tmpに書いた値を返します。valueを指定したInvokeだけが、その値を書き換えます。
今回は、context.invokedFunctionArnを記録へ追加しました。Node.jsのcontext では、functionVersionが実行されたversionを示します。invokedFunctionArnは呼び出しに使ったARNです。ARNはAWS resourceを識別する名前で、末尾に指定したversion番号かaliasが入ります。
1
2
version: context.functionVersion,
invoked_function_arn: context.invokedFunctionArn,aliasの名前と、実行されたversionを分けて照合します。AWS CLIの応答にあるExecutedVersionも確かめました。47回とも関数errorはなく、応答のRequestIdと全項目が、CloudWatch Logsのprobe-result 47件と一致しています。
UUIDはmoduleの初期化を記録する値です。表と画像では先頭8桁を使います。同じUUIDと増える回数から、同じ初期化のhandlerが続けて処理したと読みます。これは実測と実行環境のlifecycle からの推測です。物理hostやmicroVMの配置は特定していません。
画像のmemとtmpは、書き換え前の値です。空欄は生出力のnull、つまり保存した値がない状態を表します。
別関数を比較する
最初の6回は、修飾子(qualifier)なしで$LATESTへInvokeしました。primaryへ3回、続けてcontrolへ3回です。primaryの初回だけにprimary-07を渡します。controlでは、初回に値を渡さず、2回目にcontrol-07を渡しました。
| 関数 | 回数 | UUID | 書き換え前→後のメモリと/tmp |
|---|---|---|---|
| primary | 1 | f903ba85 | null→primary-07 |
| primary | 2 | f903ba85 | primary-07→primary-07 |
| primary | 3 | f903ba85 | primary-07→primary-07 |
| control | 1 | 31066d6b | null→null |
| control | 2 | 31066d6b | null→control-07 |
| control | 3 | 31066d6b | control-07→control-07 |
同じcodeでも、controlの初回にprimary-07はありませんでした。両関数とも、値を書いた後の呼び出しでは、その関数の値を読めました。

Lambdaのsecurity whitepaper は、実行環境を特定の関数version向けにし、別のversion・関数・accountへ再利用しないと説明しています。今回も、別関数で別の初期化と局所状態を観測しました。外部保存先の権限まで分かれるかは、後で別に確かめます。
versionを比較する
前回#06のversion対照 と同じく、versionごとに別の局所状態を観測しました。今回は、その状態へaliasから届くかを比べる準備です。
primaryのcodeを変えず、環境変数PROBE_CONFIG_MARKだけをAからBへ変更しました。Aの状態をversion 1、Bの状態をversion 2として発行します。更新前の環境変数を全件保ち、更新完了を待って発行しました。
この後のprimaryの$LATESTは、設定Bへの更新後の呼び出しです。最初の6回でprimaryが返したf903ba85とは別のUUIDで始まりました。
各versionと、更新できる未発行の版$LATESTへ3回ずつ送りました。初回だけに、それぞれv1-07、v2-07、latest-07を渡します。
| 呼び出し先 | 設定 | UUID | 回数 | 初回の書き換え前 | 2・3回目のメモリと/tmp |
|---|---|---|---|---|---|
| version 1 | A | dcb2354a | 1〜3 | null | v1-07 |
| version 2 | B | 9e65301d | 1〜3 | null | v2-07 |
| $LATEST | B | 1ae772bd | 1〜3 | null | latest-07 |
version 2と$LATESTは同じ設定Bでも別UUIDです。各3回では、それぞれの値を引き継ぎました。

published version は、codeと多くの設定を固定した版です。runtime managementがAutoなら、runtimeの更新は別に起こり得ます。codeと設定を版として選ぶ機能と、実行環境の存続を保証することは分けて考えます。
aliasを比較する
blueとliveを、どちらもversion 1へ向けました。追加のweightは設定せず、参照先を1つにします。blue、live、live、blue、version 1直指定の順に、書込みと読出しを組み合わせました。表は1組目の5回です。
| 呼び出し先 | 実行version | 回数 | 書き換え前→後のメモリと/tmp |
|---|---|---|---|
| blue | 1 | 4 | v1-07→blue-1 |
| live | 1 | 5 | blue-1→blue-1 |
| live | 1 | 6 | blue-1→live-1 |
| blue | 1 | 7 | live-1→live-1 |
| version 1直指定 | 1 | 8 | live-1→live-1 |
5回とも、UUIDはdcb2354aでした。blueで置いたblue-1をliveが読み、liveで置いたlive-1をblueとversion 1の直指定が読みました。呼び出しに使ったARNの末尾は、blue、live、1と変わっています。
値をblue-2/live-2、blue-3/live-3へ変え、同じ順序で合計3組行いました。15回すべてでdcb2354aを観測し、回数は4から18へ増えています。

alias は、発行したversionを指す参照です。参照先のversionを付け替えられます。今回、aliasの名前は局所状態を分けませんでした。別の利用者向けにalias名を増やしても、同じversionへ届く値の扱いと認可は、処理側で決める必要があります。
aliasを切り替える
blueはversion 1のままにして、liveだけをversion 2へ変更しました。変更後のget-aliasで参照先を確認してから、liveへ値を渡さず3回呼びます。
3回とも、UUIDはversion 2の9e65301d、回数は4〜6でした。メモリと/tmpには、version 2の直指定で置いたv2-07が残っています。liveという同じ名前を使っても、version 1のlive-3は引き継ぎませんでした。
続けてliveへlive-v2-07を渡し、version 2の直指定とblueから読みます。
| 呼び出し先 | 実行version | UUID / 回数 | 書き換え前→後のメモリと/tmp |
|---|---|---|---|
| liveで書込み | 2 | 9e65301d / 7 | v2-07→live-v2-07 |
| version 2直指定 | 2 | 9e65301d / 8 | live-v2-07→live-v2-07 |
| blue | 1 | dcb2354a / 19 | live-3→live-3 |

切替前のversion 2直指定の最後はv2-3、回数3です。そこから切替後のlive初回(回数4)までの開始時刻差が約6.65秒でした。この間隔では、以前の状態を読めました。aliasの変更を、局所状態を消して始める操作とは扱えません。切替先のcodeは、cacheの有無に左右されず処理を始められるようにします。
liveをversion 1へ戻す
liveをversion 1へ戻すと、UUIDは再びdcb2354aでした。liveの3回は回数20〜22、続くversion 1の直指定とblueは23・24です。5回とも、メモリと/tmpからlive-3を読みました。

version 1のblueの読出しから、戻したliveの初回までの開始時刻の差は約1.05秒です。今回は空き時間を置かずに呼びました。#06の空き時間の対照 では別UUIDになっています。versionへ戻すことはメモリの復元機能ではありません。実行環境のlifecycle も、環境の存続を前提にしないよう説明しています。業務データを戻す場合は、永続データの扱いも別に決めます。
S3から読み出す
前回#06のS3対照 と同じく、別versionから同じ保存結果を読めました。今回は2つのaliasを加え、実行roleの範囲も確かめます。
version 1の直指定でshared-07をS3へ保存し、PutObjectの成功を待って返しました。同じrun_idとidから決まるkeyを、次の5入口で読みました。
| 読出し先 | 実行version | UUID / 回数 | その呼び出しのメモリと/tmp | S3の値 |
|---|---|---|---|---|
| control | $LATEST | 31066d6b / 4 | control-07 | shared-07 |
| version 2直指定 | 2 | 9e65301d / 9 | live-v2-07 | shared-07 |
| $LATEST | $LATEST | 1ae772bd / 4 | latest-07 | shared-07 |
| blue | 1 | dcb2354a / 26 | shared-07 | shared-07 |
| live | 1 | dcb2354a / 27 | shared-07 | shared-07 |
5回とも、保存元のUUIDとRequestIdを含む同じJSONを取得しました。control・version 2・$LATESTは、保存元と別の局所状態から読んでいます。blueとliveは、保存元と同じversion 1のUUIDです。画像のcount 26がblue、27がliveに対応します。

同じS3 prefixの読み書きは、primaryとcontrolの2実行roleへ許可しています。primaryのversion 1・2・$LATESTは同じroleを使います。実行時のS3 SDKは3.1105.0です。画像のpersistedとfoundの1は、trueを表します。
S3へのアクセスは、IAMとkeyの範囲で決めます。aliasを利用者別の入口にしても、データへの権限は分かれません。Invokeを送る利用者が、そのS3 keyを読み書きしてよいかは別に確かめます。各roleには必要な範囲だけを許可します。
関数やversionを分ける管理に加え、外部データの認可とcacheの再作成を扱う代償が残ります。今回、tenantの認可や本番のIAM構成までは測っていません。
測定を試す
検証には、共通labのisolation-boundaries scenarioを使いました。次のhandlerは、実測したcodeです。index.jsとしてzip直下へ置き、2つの検証用Lambdaへ同じcodeを配備します。handler名はindex.handlerです。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
const { randomUUID } = require("node:crypto");
const { readFileSync, writeFileSync } = require("node:fs");
const { setTimeout: sleep } = require("node:timers/promises");
// These identify module initialization and local state, not a physical host or microVM.
const environmentId = randomUUID();
const initializedAtMs = Date.now();
const tmpPath = "/tmp/lambda-lab-node-probe.json";
let calls = 0;
let memoryValue = null;
let s3Client;
const modes = ["observe", "persist", "read", "fail", "sleep"];
console.log(
JSON.stringify({ phase: "probe-init", environmentId, initializedAtMs, node: process.version }),
);
function identifier(value, field) {
if (typeof value !== "string" || !/^[a-z0-9-]{1,64}$/.test(value))
throw new TypeError(`${field} must match [a-z0-9-]{1,64}`);
return value;
}
function temporaryState() {
try {
return JSON.parse(readFileSync(tmpPath, "utf8"));
} catch (error) {
if (error.code !== "ENOENT") throw error;
return { environmentId, value: null };
}
}
function storage() {
if (!s3Client) {
const { S3Client } = require("@aws-sdk/client-s3");
s3Client = new S3Client({ maxAttempts: 1 });
}
return s3Client;
}
exports.handler = async (event, context) => {
if (!event || typeof event !== "object" || Array.isArray(event))
throw new TypeError("event must be a JSON object");
const mode = event.mode ?? "observe";
if (!modes.includes(mode)) throw new TypeError("unknown mode");
const id = identifier(event.id ?? "observe", "id");
const runId = identifier(event.run_id ?? "manual", "run_id");
const delayMs = event.delay_ms ?? 0;
if (!Number.isInteger(delayMs) || delayMs < 0 || delayMs > 30000)
throw new TypeError("delay_ms must be between 0 and 30000");
if (event.value !== undefined && (typeof event.value !== "string" || event.value.length > 128))
throw new TypeError("value must be a string of at most 128 characters");
const tmp = temporaryState();
const before = { memoryValue, tmpValue: tmp.value };
if (event.value !== undefined) {
memoryValue = event.value;
tmp.value = event.value;
}
writeFileSync(tmpPath, JSON.stringify(tmp));
calls += 1;
const record = {
id,
run_id: runId,
mode,
request_id: context.awsRequestId,
environment_id: environmentId,
initialized_at_ms: initializedAtMs,
count: calls,
memory_before: before.memoryValue,
memory_value: memoryValue,
tmp_environment_id: tmp.environmentId,
tmp_before: before.tmpValue,
tmp_value: tmp.value,
version: context.functionVersion,
invoked_function_arn: context.invokedFunctionArn,
config_mark: process.env.PROBE_CONFIG_MARK ?? "A",
node: process.version,
started_ms: Date.now(),
};
console.log(JSON.stringify({ phase: "probe-enter", ...record }));
await sleep(delayMs);
if (mode === "persist" || mode === "read") {
const bucket = process.env.LAB_EVIDENCE_BUCKET;
const prefix = process.env.LAB_EVIDENCE_PREFIX;
if (!bucket || !prefix) throw new Error("storage is not configured");
const key = `${prefix}${runId}/${id}.json`;
const { PutObjectCommand, GetObjectCommand } = require("@aws-sdk/client-s3");
record.sdk = require("@aws-sdk/client-s3/package.json").version;
if (mode === "persist") {
await storage().send(
new PutObjectCommand({ Bucket: bucket, Key: key, Body: JSON.stringify(record) }),
);
record.persisted = true;
} else {
try {
const response = await storage().send(new GetObjectCommand({ Bucket: bucket, Key: key }));
record.saved = JSON.parse(await response.Body.transformToString());
record.found = true;
} catch (error) {
if (error.name !== "NoSuchKey") throw error;
record.found = false;
}
}
}
record.finished_ms = Date.now();
console.log(JSON.stringify({ phase: "probe-result", ...record }));
if (mode === "fail") throw new Error("intentional failure after observation");
return record;
};Node.jsのzip配備 の手順で、nodejs26.x、arm64、512 MB、timeout 5秒、reserved concurrency 1を両関数へ設定します。runtime同梱のSDKを使うため、上のcodeではSDKを同梱しません。Previewである点も今回の条件です。
同じregionに検証用のS3 bucketを1つ作り、public accessをblockします。両関数の環境変数へPROBE_CONFIG_MARK=A、LAB_EVIDENCE_BUCKET=作成したbucket名、LAB_EVIDENCE_PREFIX=evidence/dd07/を設定します。2つの実行roleへCloudWatch Logsの書込みと、次のS3権限を付けます。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": ["s3:PutObject", "s3:GetObject"],
"Resource": "arn:aws:s3:::YOUR_BUCKET/evidence/dd07/*"
},
{
"Effect": "Allow",
"Action": "s3:ListBucket",
"Resource": "arn:aws:s3:::YOUR_BUCKET",
"Condition": {"StringLike": {"s3:prefix": "evidence/dd07/*"}}
}
]
}最初はprimaryへ3回、controlへ3回を送ります。primaryの初回にだけvalue=primary-07、controlの2回目にだけvalue=control-07を指定します。mode=observe、run_id=baseline-07は全6回で共通です。
AWS CLIの同期Invoke では、出力fileを毎回変え、関数errorがないこととRequestId、値を確かめます。profileと関数名は、自分の検証用の値へ置き換えます。
1
2
3
4
5
6
7
AWS_MAX_ATTEMPTS=1 aws lambda invoke \
--profile YOUR_PROFILE --region ap-northeast-1 \
--function-name YOUR_PRIMARY \
--invocation-type RequestResponse --log-type Tail \
--cli-binary-format raw-in-base64-out \
--payload '{"id":"primary-1","run_id":"baseline-07","mode":"observe","value":"primary-07"}' \
primary-1.jsonversionとaliasを作る
6回の後で、primaryの設定Aを発行します。返されたVersionの番号を使い、1と決めつけません。
1
2
3
AWS_MAX_ATTEMPTS=1 aws lambda publish-version \
--profile YOUR_PROFILE --region ap-northeast-1 \
--function-name YOUR_PRIMARYget-function-configurationで最新の環境変数を取り、PROBE_CONFIG_MARKだけをBへ変えます。ほかの変数は全件保ちます。更新のLastUpdateStatusがSuccessfulになってから再度発行し、返された番号を使います。設定更新のRevisionId を指定すると、そのIDが一致した場合だけ更新します。指定する場合は、発行や更新の後に最新の設定から取得し直します。
次に、blueとliveを最初のversionへ向けます。VERSION_Aには、発行結果の番号を指定します。同じcommandのnameをliveにして、もう1つ作ります。
1
2
3
4
AWS_MAX_ATTEMPTS=1 aws lambda create-alias \
--profile YOUR_PROFILE --region ap-northeast-1 \
--function-name YOUR_PRIMARY --name blue \
--function-version VERSION_A番号を指定するInvokeには--qualifier VERSION_Aを付けます。aliasの場合は--qualifier blueまたはliveです。次の順で送り、ここまでの6回と合わせて47回になります。
| 順序 | 呼び出し先・回数 | modeと追加の入力 |
|---|---|---|
| 1 | version A、version B、$LATESTへ各3回。$LATESTは--qualifierなし | observe、初回だけvalue=v1-07、v2-07、latest-07。run_id=native-07 |
| 2 | blue、live、live、blue、version A直指定の順で5回を3組 | observe、各組の最初のblueにblue-N、2回目のliveにlive-Nを渡す。Nは1〜3。run_id=native-07 |
| 3 | liveをversion Bへ変更して3回 | observe、valueなし。run_id=native-07 |
| 4 | live、version B直指定、blueへ各1回 | observe、liveにだけvalue=live-v2-07。run_id=native-07 |
| 5 | liveをversion Aへ戻して3回、version A直指定とblueへ各1回 | observe、valueなし。run_id=native-07 |
| 6 | version A直指定へ1回 | persist、id=receipt、run_id=storage-07、value=shared-07 |
| 7 | control、version B、$LATEST、blue、liveへ各1回 | read、id=receipt、run_id=storage-07、valueなし |
aliasの変更は、次のupdate-aliasを使います。変更後にget-aliasでFunctionVersionを確認し、追加のweightを設定しない条件を保ちます。
1
2
3
4
AWS_MAX_ATTEMPTS=1 aws lambda update-alias \
--profile YOUR_PROFILE --region ap-northeast-1 \
--function-name YOUR_PRIMARY --name live \
--function-version VERSION_B今回のUUIDや再利用先が、次の測定でも同じになるとは限りません。accountのquotaによってreserved concurrencyを設定できない場合もあるため、変えた条件は記録します。証拠を回収した後は、検証用のaliasとversion、関数、ログ、role、S3のobjectとbucketを削除し、同じregionで不在を確認します。
まとめ
同じcodeの別関数と別versionでは、別の局所状態を観測しました。一方、同じversionを指すblueとliveは、逐次の15回でメモリと/tmpの値を共有していました。aliasの名前で、利用者ごとの状態を分ける設計にはできません。
aliasの参照先を変えると、切替先のversionの状態へ届きました。元へ戻した今回のInvokeでは以前の値を読めましたが、業務結果の復元は永続データの扱いで決めます。
同じ呼び出し名で公開versionを切り替えるには、aliasを使えます。Lambdaのbest practices は、利用者データの局所保存を避け、可変の状態が必要なら別関数か別versionを検討するよう示しています。
Lambdaを選ぶ理由は、実行環境の準備・再利用・終了をAWSへ任せられることです。局所状態が消えてもcacheを再作成できる処理を、その採用条件にします。同じversionを指すaliasで値を引き継いだため、利用者別の認可とcacheに残す値は処理側で決めます。S3 keyへのIAM権限も、呼び出し先の選択とは別に決めます。
Series: Serverless DeepDive (13 articles)
- …
- 8Serverless DeepDive #07:関数・version・aliasの状態を比較してみる This article
Enjoyed reading this content? Let the author know!
Your likes, comments, shares, and saves help creators reach more builders.
Loading recommendations
Loading article