Lambda Web Functions の実行環境を調べてみた

Lambda Web Functions の実行環境を調べてみた

AWS Lambda Web Functions の実行環境を調べました。CPU は Graviton3 世代の 2 vCPU で、OpenSSL の性能は c7g.large 相当でした。従来の Lambda、Lambda MicroVMs、EC2 と比較し、CPU 使用率が低いほど費用面で有利になる試算結果も紹介します。
2026.10.16

はじめに

2026年10月に GA となった AWS Lambda Web Functions は、1 つの実行環境に 2 GB のメモリと 2 vCPU を割り当てます。

https://docs.aws.amazon.com/lambda/latest/dg/web-functions-runtimes.html

この記事では、実行環境から確認できる範囲で、CPU の世代と OpenSSL のベンチマーク性能を調べました。あわせて、従来の Lambda(Event Functions)、Lambda MicroVMs、EC2 の c6g、c7g、c8g インスタンスと比較しました。

検証内容

検証には、Node.js 24(arm64)で動作する診断アプリを使用しました。固定のエンドポイントから /proc と /sys を読み取り、許可リストにあるコマンドを実行します。また、/tmp への書き込みをテストし、結果を JSON で返します。環境変数は取得しません。診断データは、東京リージョンの 1 つの実行環境から取得しました。コードは記事末尾に掲載しています。

実行環境のスペック

Lambda Web Functions の実行環境から確認できた情報は、以下のとおりです。

CPU とメモリ

項目 値
Node.js v24.21.0(arm64)
カーネル 6.18.51-120.163.amzn2023.aarch64.microvm
OS Amazon Linux 2023.12.20260930
vCPU 2(os.availableParallelism()、/proc/cpuinfo の件数、Cpus_allowed_list のいずれも 2 CPU)
CPU CPU part 0xd40、CPU variant 0x1、BogoMIPS 2100.00
メモリ(公式ドキュメント) 2 GB
メモリ(/proc/meminfo の MemTotal) 2460024 kB(約 2.35 GiB)。os.totalmem() は 2519064576 バイト、スワップは 0
os.cpus() の model unknown

公式のメモリ割り当て量は 2 GB ですが、実行環境の /proc/meminfo では、MemTotal が約 2.35 GiB と表示されました。

権限とサンドボックス

/proc/self/status の権限に関する行です。

Uid:        993     993     993     993
Gid:        990     990     990     990
CapInh:     0000000000000000
CapPrm:     0000000000000000
CapEff:     0000000000000000
CapBnd:     0000000000000000
CapAmb:     0000000000000000
NoNewPrivs: 1
Seccomp:    2
Seccomp_filters:    8

アプリは root ではない一般ユーザーで動作しており、dmesg の実行は拒否されました(後述)。ps -eo pid,ppid,user,comm では、PID 1 は root の init、PID 2 は sbx_user… の node でした。lsns では、time、user、net、mnt、uts、ipc、pid、cgroup の 8 種類の名前空間を確認しました。

ファイルシステムと /tmp

実行環境の df -h の出力です。

Filesystem      Size  Used Avail Use% Mounted on
/dev/vde        1.6G  1.1G  436M  71% /
tmpfs            64M     0   64M   0% /dev
/dev/vdc        3.8M  3.8M     0 100% /var/task
/dev/vdd        525M   11M  503M   3% /tmp
/dev/root       9.8G  854M  8.9G   9% /etc/passwd
/dev/vdb        1.5G  1.3M  1.5G   1% /etc/resolv.conf

/ は ext4 の ro,nosuid,nodev、/var/task は squashfs の ro,nosuid,nodev でマウントされていました。/tmp は ext4 の rw,relatime で、noexec は付いていませんでした。/tmp は読み書き可能で、アプリからの書き込みにも成功しました。/dev/shm は存在せず、アクセスすると ENOENT になりました。

/tmp に 64 MiB ずつ書き込むと、ファイルが 526,385,152 バイト(約 502 MiB)になった時点で ENOSPC になりました。書き込み前の空き容量は 527,192,064 バイトで、ファイルの削除後には同じ値まで回復しました。statfs が返すファイルシステムの容量は 525 MiB でしたが、一時ファイルに使える量の目安は約 502 MiB です。

/tmp などローカルのファイルは、実行環境が再利用される間は残ることがあります。ただし、実行環境の寿命は保証されません。

https://docs.aws.amazon.com/lambda/latest/dg/web-functions-env-lifecycle.html

/proc と /sys

パス 結果
/sys/class/dmi/id ENOENT
/sys/hypervisor ENOENT
/sys/fs/cgroup ENOENT
/proc/self/cgroup 読める
/proc/self/mountinfo 読める(7 マウント、sysfs なし)
/proc/cpuinfo、/proc/meminfo、/proc/self/status、/proc/self/limits 読める
/proc/cmdline EACCES

cgroup の CPU とメモリの上限、DMI、ハイパーバイザーの情報は、これらのパスから取れませんでした。CPU 数と実行環境から見えるメモリ情報は、/proc/cpuinfo、/proc/meminfo、Node.js の os モジュールから取得できました。

外部コマンド

バイナリの回収と同梱

診断に使用したコマンドのうち、実行環境に最初から存在したのは df、uname、coreutils だけでした。それ以外のコマンドは、AL2023 の arm64 コンテナ(public.ecr.aws/amazonlinux/amazonlinux:2023、linux/arm64)からバイナリと共有ライブラリを回収し、デプロイパッケージの bin/ と lib/ に同梱しました。同梱版は util-linux 2.37.4、procps-ng 3.3.17、coreutils 8.32 でした。

collect-binaries.sh として保存しました。

#!/bin/bash
# AL2023 (linux/arm64) コンテナ内で util-linux / procps-ng / coreutils のバイナリと依存ライブラリを bin/ lib/ に回収する。
set -euo pipefail
OUT="${1:-/out}"
mkdir -p "$OUT/bin" "$OUT/lib"
dnf install -y -q util-linux procps-ng openssl >/dev/null 2>&1 || dnf install -y util-linux procps-ng openssl
cat /etc/os-release | head -3
# df / uname は coreutils-single の multi-call バイナリ(/usr/bin/coreutils)への shebang スクリプトなので、本体を回収する
for c in lscpu lsblk lsmem findmnt lsns dmesg free ps coreutils openssl; do
  p=$(command -v "$c" || true)
  if [ -z "$p" ]; then echo "MISSING $c"; continue; fi
  cp -L "$p" "$OUT/bin/$c"
  { ldd "$p" 2>&1 || true; } | awk '/=> \//{print $3}' | while read -r so; do cp -Ln "$so" "$OUT/lib/" 2>/dev/null || true; done
  echo "OK $c $p"
done
rpm -q openssl openssl-libs util-linux procps-ng coreutils-single
# ローダー本体は lib/ に置く
cp -L /lib/ld-linux-aarch64.so.1 "$OUT/lib/" 2>/dev/null || cp -L /lib64/ld-linux-aarch64.so.1 "$OUT/lib/"
ls -l "$OUT/bin" "$OUT/lib"
[ -n "${HOST_UID:-}" ] && chown -R "$HOST_UID:${HOST_GID:-$HOST_UID}" "$OUT" || true

回収は次のコマンドで実行しました。diag-bin に bin/ と lib/ が出力されました。

docker run --rm --platform linux/arm64 -e HOST_UID=$(id -u) -e HOST_GID=$(id -g) -v "$PWD:/work" -v "$PWD/diag-bin:/out" public.ecr.aws/amazonlinux/amazonlinux:2023 bash /work/collect-binaries.sh /out

df と uname は shebang スクリプトで、実体は multi-call バイナリの coreutils でした。coreutils を同梱し、--coreutils-prog=<name> を付けて呼びました。それ以外のコマンドは、同梱したローダーにライブラリパスを渡す次の形で実行しました。

<DST>/lib/ld-linux-aarch64.so.1 --library-path <DST>/lib <DST>/bin/<cmd>

診断アプリでは <DST> を /tmp/diag-bin にしています。

zip にした bin/ と lib/ の 24 ファイルは、すべてモード 755 のまま /var/task に展開されました。/var/task は noexec ではなく、/var/task/bin/lscpu は直接実行できました。lscpu は、直接実行、ローダー経由、/tmp にコピーしてからの実行のいずれでも、同じエラーで終了コード 1 になりました。次の表は、/tmp にコピーしてローダー経由で実行した結果です。

実行結果

コマンド 同梱版の結果
lscpu exit 1: failed to determine number of CPUs: /sys/devices/system/cpu/possible: No such file or directory
lsblk exit 1: failed to access sysfs directory: /sys/dev/block: No such file or directory
lsmem exit 1: cannot open /sys/devices/system/memory: No such file or directory
findmnt exit 0
lsns exit 0
dmesg exit 1: read kernel buffer failed: Operation not permitted
free -m exit 0
ps exit 0
df -h exit 0(実行環境の /usr/bin/df と同一の出力)
uname -a exit 0(実行環境の /usr/bin/uname と同一の出力)

失敗したのは、/sys を読む lscpu、lsblk、lsmem と、権限が要る dmesg でした。/proc で足りる findmnt、lsns、free、ps は、同梱すれば動きました。実行環境の /usr/bin は 175 エントリで、df、uname、coreutils 以外の対象コマンドは /usr/bin、/bin、/usr/sbin、/sbin、/usr/local/bin のどこにもありませんでした。

CPU の世代と性能

Graviton3 世代の CPU

Linux カーネルの CPU part 定義では、0xd40 は Neoverse V1 に対応します。AWS Graviton の技術ガイドでは、Graviton3(E) のコンパイルオプションが -mcpu=neoverse-v1 で、Graviton2 は neoverse-n1(0xd0c)、Graviton4 は neoverse-v2(0xd4f)です。

https://aws.github.io/graviton/c-c++.html

Web Function と、c6g.large、c7g.large、c8g.large の EC2 を比べた CPU の識別情報です。

項目 c6g.large c7g.large c8g.large Web Function
CPU part 0xd0c 0xd40 0xd4f 0xd40
CPU variant / revision 0x3 / 1 0x1 / 1 0x0 / 1 0x1 / 1
BogoMIPS 243.75 2100.00 2000.00 2100.00
lscpu の BIOS Model name AWS Graviton2 AWS Graviton3 AWS Graviton4 —(/sys が無く lscpu が失敗)
Features の SVE 系 なし sve、svebf16、svei8mm(sve2 なし) sve2、sveaes、svesha3 など sve、svebf16、svei8mm(sve2 なし)
OPENSSL_armcap 0xbd 0x13ffd 0x179fd 0x13ffd

Web Function の CPU part、CPU variant、BogoMIPS、Features、OPENSSL_armcap は、いずれも c7g.large と一致しました。Graviton2 にない SVE 系の機能を備える一方、Graviton4 が備える SVE2 はありません。以上から、Web Function には Graviton3 世代の CPU が使われていると推測できます。ただし、Graviton3 と Graviton3E は CPU part だけでは区別できません。また、Web Function では機種名の文字列を取得できなかったため、この判断は各値の一致に基づきます。

OpenSSL speed の比較

同梱した openssl 3.5.8 で、Web Function(1 つの実行環境)と 3 機種の EC2(各 1 台)に、同じ引数の openssl speed を実行しました。EC2 には dnf install で同じ版を入れ、ap-northeast-1a の AL2023 で SSM Run Command から実行しました。測定コマンドの形は次のとおりです。

openssl speed -seconds 2 -bytes {64,16384} [-multi 2] -evp {aes-128-gcm,aes-256-gcm,sha256,sha512,sha3-256,chacha20-poly1305}
openssl speed -seconds 2 [-multi 2] {rsa2048,ecdsap256,ecdhx25519}

1 プロセスで測定した 17 項目のうち、代表的な 4 項目を示します。各値は 3 ラウンドの中央値です。

項目 Web Function c6g.large c7g.large c8g.large
sha256 64B(kB/s) 287,673 225,301 287,460 367,102
aes-128-gcm 16KB(kB/s) 5,949,915 2,393,760 5,287,231 6,333,563
rsa2048 sign/s 1,032 294 1,045 1,187
ecdsap256 sign/s 41,629 18,754 41,980 48,936

Web Function は多くの項目で c7g.large に近い結果でした。

次に、2 プロセス(-multi 2)の処理性能を、1 プロセスに対する倍率で示します。17 項目 × 3 ラウンドの 51 件で集計しています。

環境 中央値 最小〜最大
Web Function 1.84 1.809〜1.856
c6g.large 2.000 1.954〜2.016
c7g.large 1.997 1.861〜2.022
c8g.large 1.998 1.801〜2.003

EC2 の 3 機種は 2.00 倍前後で、Web Function は 2 vCPU でも 1.84 倍でした。

Lambda MicroVMs・従来の Lambda との比較

Web Functions、Lambda MicroVMs、従来の Lambda で、同じ診断を実行しました。MicroVMs はベースライン 4 GB / 2 vCPU、従来の Lambda は arm64 / 3,538 MB に設定しています。

項目 Web Functions Lambda MicroVMs 従来の Lambda
見える CPU 数 2 8(ピーク) 3
MemTotal 2460024 kB 16437604 kB(ピーク) 4510668 kB
実行ユーザーと権限 uid 993、CapEff 0、Seccomp フィルター 8 個 uid 0、CapEff 14 個、Seccomp 0 uid 993、CapEff 0、Seccomp フィルター 6 個
/sys なし sysfs(読み取り専用)。DMI と hypervisor はなし なし
lscpu、lsblk、lsmem exit 1 exit 0 exit 1
dmesg 拒否 exit 0 拒否
/tmp 約 502 MiB で ENOSPC 1 GiB まで書けた(テストの上限) 約 495 MiB で ENOSPC(既定の 512 MB)
CPU part 0xd40 0xd40(sve 系なし、1 台) 0xd0c と 0xd40
openssl speed の 2 プロセスの伸び 1.84 2.00 1.69

1 つの実行環境で測定した MicroVMs では、root で動作し、/sys も参照できたため、lscpu、lsblk、lsmem、dmesg はすべて成功しました。openssl speed は 8 プロセスで 7.996 倍(51 件の中央値)になりました。

従来の Lambda は、uid、ケーパビリティ、/sys の可視性、および対象コマンドの成否が Web Functions と同じでした。見える CPU は 3 でしたが、openssl speed の合計は 2 プロセスで 1.686 倍、3 プロセスで 1.698 倍、4 プロセスで 1.696 倍と、約 1.7 倍で頭打ちになりました。同じ設定の従来の Lambda でも、実行環境によって CPU は Graviton2 と Graviton3 に分かれました。OpenSSL の性能測定には、Graviton3 の実行環境を使用しています。

1 時間あたりの費用の試算

実測した請求ではなく、料金表の単価から計算した試算値です。前提は次のとおりです。

  • 単価: 東京リージョン、Arm、USD で、2026-10-01 発効の Price List API の値を使いました。コンピュート料金だけを対象にしています。
  • CPU 使用率: 使った CPU 時間 ÷(2 vCPU × 3,600 秒)です。2 vCPU の構成を 1 時間動かし続けるものとします。
  • Web Functions: vCPU の使用時間とメモリ使用量に課金されるものとして計算しました。メモリ使用量は測定していないため、1 GB と仮定しています。
  • MicroVMs: ベースライン 4 GB / 2 vCPU の範囲内として計算しました。
CPU 使用率 EC2 c7g.large Web Functions(消費メモリ 1 GB) Lambda MicroVMs(4 GB / 2 vCPU)
100% $0.0910 $0.2475 $0.2936
75% $0.0910 $0.1895 $0.2936
50% $0.0910 $0.1314 $0.2936
25% $0.0910 $0.0734 $0.2936

使用率が約 33% を下回ると、Web Functions の試算額が EC2 を下回ります。

計算式
  • Web Functions
vCPU  7,200 vCPU-秒 × $0.0000322421 = $0.2321
メモリ 3,600 GB-秒 × $0.0000042688 = $0.0154
合計(使用率 100%)= $0.2475 / 時間

EC2 と同額になる使用率
($0.0910 − $0.0154) ÷ $0.2321 = 0.326(32.6%)
  • 従来の Lambda(メモリ 3,538 MB)を 1 時間実行した場合($0.1658)
3,538 MB ÷ 1,024 = 3.455 GB
3.455 GB × 3,600 秒 = 12,438 GB-秒
12,438 GB-秒 × $0.0000133334 = $0.1658

まとめ

今回の検証は、Lambda Web Functions の 1 つの実行環境を対象としたものです。確認した環境は Amazon Linux 2023 上で動作する 2 vCPU 構成で、CPU の識別情報とベンチマーク結果から、Graviton3 世代の CPU が使われていると推測できます。

メモリなどの条件を揃えた比較ではありませんが、DB や外部 API の応答待ちが多い Web サーバーでは、CPU 使用率に応じた課金が有利になる可能性があります。

従来の Lambda と比べると、現時点では利用できるランタイムや構成に制約があります。一方、CPU 使用率が低い Web ワークロードでは有力な選択肢になり得ますので、ぜひお試しください。

診断アプリのコード(app-src/diag.ts)
import http from 'node:http';
import fs from 'node:fs';
import os from 'node:os';
import path from 'node:path';
import { spawnSync } from 'node:child_process';

// Diagnostic app for a Lambda Web Function. Read-only except for /tmp.
// - Only fixed paths and a fixed command allowlist are used. No request data reaches a path or a command.
// - Environment variables and /proc/self/environ are never read or returned.
const port = Number(process.env.LOCAL_PORT ?? 3000);
const HOSTNAME = os.hostname();
const SECRET_NAME = /serial|uuid/i; // values of these files are replaced by their length

type FileResult = { path: string; ok: boolean; bytes?: number; text?: string; truncated?: boolean; error?: string };

const readCapped = (p: string, cap = 16384): FileResult => {
  let fd: number | undefined;
  try {
    fd = fs.openSync(p, 'r');
    const buf = Buffer.alloc(cap + 1);
    const n = fs.readSync(fd, buf, 0, cap + 1, null);
    const truncated = n > cap;
    const text = buf.subarray(0, Math.min(n, cap)).toString('utf8');
    if (SECRET_NAME.test(path.basename(p))) return { path: p, ok: true, bytes: n, text: `<redacted by app: ${n} bytes>` };
    return { path: p, ok: true, bytes: n, text, truncated };
  } catch (e) {
    return { path: p, ok: false, error: (e as NodeJS.ErrnoException).code ?? String(e) };
  } finally {
    if (fd !== undefined) try { fs.closeSync(fd); } catch { /* ignore */ }
  }
};

// Read the regular files directly under `dir`; list (but do not read) sub-entries. Optionally recurse.
const dumpDir = (dir: string, depth: number, readLimit = 200): unknown => {
  let names: string[];
  try { names = fs.readdirSync(dir).sort(); } catch (e) { return { dir, ok: false, error: (e as NodeJS.ErrnoException).code ?? String(e) }; }
  const files: FileResult[] = [];
  const subdirs: string[] = [];
  const links: Record<string, string> = {};
  const children: unknown[] = [];
  for (const name of names) {
    const full = path.join(dir, name);
    let st: fs.Stats;
    try { st = fs.lstatSync(full); } catch (e) { files.push({ path: full, ok: false, error: (e as NodeJS.ErrnoException).code ?? String(e) }); continue; }
    if (st.isSymbolicLink()) { try { links[name] = fs.readlinkSync(full); } catch { links[name] = '?'; } continue; }
    if (st.isDirectory()) {
      subdirs.push(name);
      if (depth > 0) children.push(dumpDir(full, depth - 1, readLimit));
      continue;
    }
    if (files.length < readLimit) files.push(readCapped(full, 4096));
  }
  return { dir, ok: true, subdirs, links, files, children };
};

const statfs = (p: string) => {
  try {
    const s = fs.statfsSync(p, { bigint: true });
    const bs = s.bsize;
    return {
      path: p, type: `0x${s.type.toString(16)}`, bsize: Number(bs), blocks: Number(s.blocks), bfree: Number(s.bfree), bavail: Number(s.bavail),
      files: Number(s.files), ffree: Number(s.ffree),
      totalBytes: Number(s.blocks * bs), freeBytes: Number(s.bfree * bs), availBytes: Number(s.bavail * bs),
    };
  } catch (e) { return { path: p, error: (e as NodeJS.ErrnoException).code ?? String(e) }; }
};

const modeInfo = (p: string) => {
  try { const st = fs.lstatSync(p); return { path: p, mode: (st.mode & 0o7777).toString(8), uid: st.uid, gid: st.gid, size: st.size, symlink: st.isSymbolicLink() ? fs.readlinkSync(p) : undefined }; }
  catch (e) { return { path: p, error: (e as NodeJS.ErrnoException).code ?? String(e) }; }
};

// ---- A. files read directly by Node ----
const routeA = () => ({
  dmi: dumpDir('/sys/class/dmi/id', 0),
  hypervisor: dumpDir('/sys/hypervisor', 3),
  cpuinfo: readCapped('/proc/cpuinfo', 65536),
  cgroup: dumpDir('/sys/fs/cgroup', 0),
  cgroupSelf: readCapped('/proc/self/cgroup'),
  mountinfo: readCapped('/proc/self/mountinfo', 65536),
  status: readCapped('/proc/self/status'),
  extra: ['/proc/version', '/proc/meminfo', '/proc/self/limits', '/proc/self/uid_map', '/proc/self/gid_map', '/proc/sys/kernel/osrelease', '/etc/os-release', '/proc/cmdline', '/proc/sys/kernel/hostname']
    .map((p) => readCapped(p, 8192)),
});

// ---- C. what Node itself reports ----
const routeC = () => ({
  node: { version: process.version, arch: process.arch, platform: process.platform, versions: { v8: process.versions.v8, openssl: process.versions.openssl } },
  os: {
    arch: os.arch(), machine: os.machine(), platform: os.platform(), type: os.type(), release: os.release(), version: os.version(),
    totalmem: os.totalmem(), freemem: os.freemem(), uptimeSec: os.uptime(), loadavg: os.loadavg(),
    availableParallelism: os.availableParallelism(), cpuCount: os.cpus().length,
    cpus: os.cpus().map((c) => ({ model: c.model, speed: c.speed })),
    hostnameLength: HOSTNAME.length,
  },
  process: { pid: process.pid, uid: process.getuid?.(), gid: process.getgid?.(), groups: process.getgroups?.(), cwd: process.cwd(), dirname: __dirname, execPath: process.execPath, memoryUsage: process.memoryUsage() },
  statfs: ['/', '/tmp', '/var/tmp', '/dev/shm', __dirname].map(statfs),
  bundledModes: ['bin', 'lib'].flatMap((d) => { try { return fs.readdirSync(path.join(__dirname, d)).map((n) => modeInfo(path.join(__dirname, d, n))); } catch { return []; } }),
});

// ---- /tmp write test (only /tmp; file removed afterwards) ----
const routeTmpWrite = () => {
  const file = '/tmp/diag-fill.bin';
  const chunk = Buffer.alloc(64 * 1024 * 1024, 0x61);
  const capBytes = 2 * 1024 * 1024 * 1024;
  const before = statfs('/tmp');
  const started = Date.now();
  let written = 0;
  let stopped = 'cap';
  let error: string | undefined;
  try {
    const fd = fs.openSync(file, 'w');
    try {
      while (written < capBytes) {
        fs.writeSync(fd, chunk, 0, chunk.length);
        written += chunk.length;
      }
    } catch (e) { stopped = 'error'; error = (e as NodeJS.ErrnoException).code ?? String(e); }
    try { fs.fsyncSync(fd); } catch { /* ignore */ }
    fs.closeSync(fd);
  } catch (e) { stopped = 'open-error'; error = (e as NodeJS.ErrnoException).code ?? String(e); }
  let fileSize: number | undefined;
  try { fileSize = fs.statSync(file).size; } catch { /* ignore */ }
  const during = statfs('/tmp');
  try { fs.unlinkSync(file); } catch { /* ignore */ }
  return { capBytes, chunkBytes: chunk.length, chunksWritten: written / chunk.length, writtenBytes: written, fileSizeAfterBytes: fileSize, stopped, error, ms: Date.now() - started, statfsBefore: before, statfsDuringFileExists: during, statfsAfterDelete: statfs('/tmp') };
};

// ---- B. external commands (fixed allowlist) ----
const SPECS: Record<string, string[]> = {
  lscpu: [], lsblk: [], lsmem: [], findmnt: [], lsns: [], dmesg: [],
  free: ['-m'], ps: ['-eo', 'pid,ppid,user,comm'], df: ['-h'], uname: ['-a'],
};
const MULTICALL = new Set(['df', 'uname']); // coreutils-single: /usr/bin/coreutils --coreutils-prog=<name>
const SRC_BIN = path.join(__dirname, 'bin');
const SRC_LIB = path.join(__dirname, 'lib');
const DST = '/tmp/diag-bin';
const LOADER = 'ld-linux-aarch64.so.1';

const prepare = () => {
  const r: Record<string, unknown> = { dst: DST };
  try {
    fs.mkdirSync(path.join(DST, 'bin'), { recursive: true });
    fs.mkdirSync(path.join(DST, 'lib'), { recursive: true });
    for (const [src, sub] of [[SRC_BIN, 'bin'], [SRC_LIB, 'lib']] as const) {
      for (const n of fs.readdirSync(src)) {
        const to = path.join(DST, sub, n);
        fs.copyFileSync(path.join(src, n), to);
        fs.chmodSync(to, 0o755);
      }
    }
    r.ok = true;
  } catch (e) { r.ok = false; r.error = (e as NodeJS.ErrnoException).code ?? String(e); }
  return r;
};

const run = (variant: string, argv: string[]) => {
  const t = Date.now();
  const r = spawnSync(argv[0], argv.slice(1), { timeout: 30000, maxBuffer: 2 * 1024 * 1024, env: { PATH: '/usr/bin:/bin' }, encoding: 'utf8' });
  const cap = (s: string | null, n: number) => (s && s.length > n ? `${s.slice(0, n)}\n...[truncated ${s.length - n} chars]` : s ?? '');
  return {
    variant, argv: argv.map((a) => a.replace(DST, '<DST>').replace(__dirname, '<TASK>')),
    exitCode: r.status, signal: r.signal, spawnError: r.error ? `${(r.error as NodeJS.ErrnoException).code ?? ''} ${r.error.message}`.trim() : undefined,
    stdout: cap(r.stdout, 16384), stderr: cap(r.stderr, 4096), ms: Date.now() - t,
  };
};

// ---- openssl speed (fixed argument lists only; the number of processes is fixed per route) ----
const OSSL = path.join(DST, 'bin', 'openssl');
const osslArgv = (args: string[]) => [path.join(DST, 'lib', LOADER), '--library-path', path.join(DST, 'lib'), OSSL, ...args];
const cpuStat = () => (readCapped('/proc/stat', 4096).text ?? '').split('\n').filter((l) => /^cpu\d* /.test(l));
const BULK_ALGS = ['aes-128-gcm', 'aes-256-gcm', 'sha256', 'sha512', 'sha3-256', 'chacha20-poly1305'];
const BULK_SIZES = ['64', '16384'];

const osslSuite = (kind: 'bulk' | 'pk', multi: number) => {
  const prep = prepare();
  const multiArgs = multi > 1 ? ['-multi', String(multi)] : [];
  const argLists: string[][] = kind === 'bulk'
    ? BULK_ALGS.flatMap((a) => BULK_SIZES.map((b) => ['speed', '-seconds', '2', '-bytes', b, ...multiArgs, '-evp', a]))
    : ['rsa2048', 'ecdsap256', 'ecdhx25519'].map((a) => ['speed', '-seconds', '2', ...multiArgs, a]);
  const statBefore = cpuStat();
  const started = Date.now();
  const runs = argLists.map((args) => run(`openssl-${kind}-multi${multi}`, osslArgv(args)));
  return { prepare: prep, kind, multi, totalMs: Date.now() - started, cpuStatBefore: statBefore, cpuStatAfter: cpuStat(), runs };
};
const routeOsslInfo = () => {
  const prep = prepare();
  return { prepare: prep, version: run('openssl-version', osslArgv(['version', '-a'])), cpuStat: cpuStat() };
};

const routeWhich = () => {
  const names = [...Object.keys(SPECS), 'coreutils'];
  const dirs = ['/usr/bin', '/bin', '/usr/sbin', '/sbin', '/usr/local/bin'];
  const found: Record<string, unknown[]> = {};
  for (const n of names) found[n] = dirs.map((d) => path.join(d, n)).filter((p) => fs.existsSync(p)).map(modeInfo);
  let usrBinCount: number | undefined;
  try { usrBinCount = fs.readdirSync('/usr/bin').length; } catch { /* ignore */ }
  return { pathVar: process.env.PATH, dirs: dirs.map((d) => modeInfo(d)), usrBinCount, found, osRelease: readCapped('/etc/os-release', 4096) };
};

const routeCmds = () => {
  const prep = prepare();
  const results: unknown[] = [];
  for (const [name, args] of Object.entries(SPECS)) {
    const inner = MULTICALL.has(name)
      ? [path.join(DST, 'bin', 'coreutils'), `--coreutils-prog=${name}`, ...args]
      : [path.join(DST, 'bin', name), ...args];
    results.push(run('bundled', [path.join(DST, 'lib', LOADER), '--library-path', path.join(DST, 'lib'), ...inner]));
    for (const d of ['/usr/bin', '/bin', '/usr/sbin', '/sbin']) {
      const p = path.join(d, name);
      if (fs.existsSync(p)) { results.push(run(`builtin:${d}`, [p, ...args])); break; }
    }
  }
  // Does the executable bit survive the zip? Run one bundled command straight from the code directory.
  const inplace = run('bundled-inplace', [path.join(SRC_LIB, LOADER), '--library-path', SRC_LIB, path.join(SRC_BIN, 'lscpu')]);
  const inplaceDirect = run('bundled-inplace-direct', [path.join(SRC_BIN, 'lscpu')]);
  return { prepare: prep, results, inplace, inplaceDirect };
};

const ROUTES: Record<string, () => unknown> = { '/a': routeA, '/c': routeC, '/which': routeWhich, '/cmds': routeCmds, '/tmpwrite': routeTmpWrite,
  '/ossl-info': routeOsslInfo, '/ossl-bulk-1': () => osslSuite('bulk', 1), '/ossl-bulk-2': () => osslSuite('bulk', 2), '/ossl-pk-1': () => osslSuite('pk', 1), '/ossl-pk-2': () => osslSuite('pk', 2) };

const server = http.createServer((req, res) => {
  const p = (req.url ?? '/').split('?')[0];
  const started = Date.now();
  try {
    const handler = ROUTES[p];
    const body = handler ? handler() : { endpoints: Object.keys(ROUTES) };
    // Never print the host name (an identifier of the execution environment).
    const text = JSON.stringify({ path: p, elapsedMs: Date.now() - started, body }, null, 2).split(HOSTNAME.length >= 4 ? HOSTNAME : '\u0000').join('<HOSTNAME>');
    res.writeHead(200, { 'content-type': 'application/json' });
    res.end(text);
  } catch (e) {
    res.writeHead(500, { 'content-type': 'application/json' });
    res.end(JSON.stringify({ error: String((e as Error).name), message: String((e as Error).message) }));
  }
});

server.listen(port, () => console.log(`listening on port ${port}`));

この記事をシェアする

AWSのお困り事はクラスメソッドへ

関連記事