AWS Builder Center

Serverless DeepDive #07:関数・version・aliasの状態を比較してみる

同じcodeの別関数・published version・aliasからInvokeし、メモリと/tmpの状態を比較します。同じversionを指すaliasの切替と戻しを追い、局所状態とS3への権限の境界を考えます。

Series: Serverless DeepDive (13 articles)

  1. …
  2. 8
    Serverless DeepDive #07:関数・version・aliasの状態を比較してみる This article
Senior Evangelist

検証構成

同じversionを指す2つのaliasから、同じメモリと/tmpの値を読みました。handlerの外のメモリ変数と/tmpに残る状態を、この稿では局所状態と呼びます。aliasは、関数のversionへ付ける参照名です。名前を変えただけでは、局所状態を分ける入口にはなりませんでした。
#06 では、局所に置くcacheと、次へ引き継ぐ処理結果を分けました。今回は、関数・version・aliasを選んで呼び出します。呼び出し先を分けたとき、どの状態へ届くのかを比べます。
対象条件
regionap-northeast-1(Tokyo)
primaryとcontrol同じNode.jsのcodeを使う別関数
runtimenodejs26.x、実行時v26.8.2、AL2023、public Preview
両関数の設定arm64、512 MB、timeout 5秒、reserved concurrency 1、通常on-demand
published versionprimaryのPROBE_CONFIG_MARK=Aをversion 1、Bをversion 2として発行。表の設定A/Bはこの値
aliasblueと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への通信、ログ出力です。
 
PCからprimaryの各version・aliasとcontrolへ同期Invokeし、局所状態と同じS3 keyを比較する構成

状態を記録する

#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
primary1f903ba85null→primary-07
primary2f903ba85primary-07→primary-07
primary3f903ba85primary-07→primary-07
control131066d6bnull→null
control231066d6bnull→control-07
control331066d6bcontrol-07→control-07
同じcodeでも、controlの初回にprimary-07はありませんでした。両関数とも、値を書いた後の呼び出しでは、その関数の値を読めました。
 
primaryは2・3回目でprimary-07を読み、別関数controlは1・2回目がnull、3回目でcontrol-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 1Adcb2354a1〜3nullv1-07
version 2B9e65301d1〜3nullv2-07
$LATESTB1ae772bd1〜3nulllatest-07
version 2と$LATESTは同じ設定Bでも別UUIDです。各3回では、それぞれの値を引き継ぎました。
 
version 1、version 2、$LATESTを各3回呼び出し、別UUIDと各呼び出し先の値を観測した実測結果
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
blue14v1-07→blue-1
live15blue-1→blue-1
live16blue-1→live-1
blue17live-1→live-1
version 1直指定18live-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へ増えています。
 
同じversion 1を指すblue、live、version 1直指定を3組交互に呼び、同じUUIDと入口を越えて引き継ぐ値を観測した実測結果
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から読みます。
呼び出し先実行versionUUID / 回数書き換え前→後のメモリと/tmp
liveで書込み29e65301d / 7v2-07→live-v2-07
version 2直指定29e65301d / 8live-v2-07→live-v2-07
blue1dcb2354a / 19live-3→live-3
 
liveをversion 2へ変更するとversion 2の局所値を読み、version 1を指すblueの値は変わらなかった実測結果
切替前の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を読みました。
 
liveをversion 1へ戻した後、live、version 1直指定、blueの5回で元のUUIDと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入口で読みました。
読出し先実行versionUUID / 回数その呼び出しのメモリと/tmpS3の値
control$LATEST31066d6b / 4control-07shared-07
version 2直指定29e65301d / 9live-v2-07shared-07
$LATEST$LATEST1ae772bd / 4latest-07shared-07
blue1dcb2354a / 26shared-07shared-07
live1dcb2354a / 27shared-07shared-07
5回とも、保存元のUUIDとRequestIdを含む同じJSONを取得しました。control・version 2・$LATESTは、保存元と別の局所状態から読んでいます。blueとliveは、保存元と同じversion 1のUUIDです。画像のcount 26がblue、27がliveに対応します。
 
version 1のS3保存を、別の局所状態のcontrol・version 2・$LATESTと、version 1の同じ環境のblue・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.json

versionと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_PRIMARY
get-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と追加の入力
1version A、version B、$LATESTへ各3回。$LATESTは--qualifierなしobserve、初回だけvalue=v1-07、v2-07、latest-07。run_id=native-07
2blue、live、live、blue、version A直指定の順で5回を3組observe、各組の最初のblueにblue-N、2回目のliveにlive-Nを渡す。Nは1〜3。run_id=native-07
3liveをversion Bへ変更して3回observe、valueなし。run_id=native-07
4live、version B直指定、blueへ各1回observe、liveにだけvalue=live-v2-07。run_id=native-07
5liveをversion Aへ戻して3回、version A直指定とblueへ各1回observe、valueなし。run_id=native-07
6version A直指定へ1回persist、id=receipt、run_id=storage-07、value=shared-07
7control、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)

  1. …
  2. 8
    Serverless DeepDive #07:関数・version・aliasの状態を比較してみる This article
Any opinions in this article are those of the individual author and may not reflect the opinions of AWS.
Enjoyed reading this content? Let the author know!

Your likes, comments, shares, and saves help creators reach more builders.

Loading recommendations

Loading article