Skip to main content

In this article

2024年に使い始めたいNode.jsの最新ランタイム機能10選

著者
feature java dto

2024年5月29日

0 分で読めます
10 Node.js runtime features you SHOULD be using in 2024

サーバーサイドJavaScriptのランタイムをめぐっては、BunがNode.js APIとの互換性を高めるなど、さまざまな革新が続いています。一方、Node.jsランタイムも豊富な標準ライブラリとランタイム機能を備えています。

2024年を迎える今、Node.jsランタイムの最新機能や機能性を把握するには絶好の機会です。最新情報を追うのは、単に「時代についていく」ためではありません。最新APIを活用して、より効率的で高性能かつセキュアなコードを書くためです。

この記事では、2024年にすべての開発者が使い始めたいNode.jsの最新ランタイム機能を10個紹介します。最新のAPIから、BunやDenoのような新興ランタイムが提供する注目機能まで幅広く取り上げます。

前提条件:Node.jsのLTSバージョン

最新機能を試す前に、Node.jsのLTS(長期サポート)バージョンを使用していることを確認してください。この記事の執筆時点で、最新のNode.js LTSバージョンはv20.14.0です。

Node.jsのバージョンを確認するには、次のコマンドを使います。

node --version

現在LTSバージョンを使用していない場合は、fnmやnvmなどのバージョン管理ツールを使うと、Node.jsの各バージョンを簡単に切り替えられます。

Node.js 20の新機能

以降のセクションでは、最近のNode.jsバージョンで導入された新機能をいくつか紹介します。安定版の機能もあれば、まだ実験段階のものもあります。また、以前から利用できたものの、まだ知られていない機能もあります。

以下のトピックを取り上げます。

  1. Node.jsテストランナー

  2. Node.js標準のモック機能

  3. Node.js標準のテストカバレッジ

  4. Node.jsウォッチモード

  5. Node.js Corepack

  6. Node.js .envローダー

  7. __dirnameと__fileに使えるNode.js import.meta.file

  8. Node.js標準のタイマープロミス

  9. Node.jsパーミッションモジュール

  10. Node.jsポリシーモジュール

1. Node.js標準のテストランナー

Node.jsのランタイムにテストランナーが導入される前は、どうしていたでしょうか。これまでは、node-tap、jest、mocha、vitestなどの人気ツールを使っていた方も多いでしょう。 

Node.js標準のテストランナーを開発ワークフローで活用する方法を見ていきましょう。まず、次のようにNode.jsからテストモジュールをテストファイルにインポートします。

import { test } from 'node:test';

それでは、Node.jsテストランナーの使い方を順に見ていきましょう。

node:testで単一のテストを実行する

単一のテストを作成するには、テスト名とコールバック関数を渡してtest関数を呼び出します。コールバック関数内にテストロジックを記述します。

import { test } from "node:test";
import assert from "node:assert";
import { add } from "../src/math.js";

test("should add two numbers", () => {
  const result = add(1, 2);
  assert.strictEqual(result, 3);
});

test("should fail to add strings", () => {
  assert.throws(() => {
    add("1", "2");
  });
});

このテストを実行するには、node --testコマンドの後にテストファイル名を指定します。

node --test tests/math.test.js

Node.jsテストランナーは、プロジェクト内のテストファイルを自動検出して実行できます。慣例として、ファイル名の末尾は.test.jsにしますが、この命名規則は必須ではありません。

テストファイルを位置引数で指定しない場合、Node.jsテストランナーはヒューリスティックとglobパターンマッチングを使ってテストファイルを探します。たとえば、test/やtests/フォルダ内のすべてのファイル、test-で始まるファイル、または.testで終わるファイルが対象になります。

たとえば、globパターンでテストファイルを照合できます。

node --test '**/*.test.js'

node:assertを使ったテストアサーション

Node.jsテストランナーでは、組み込みのassertモジュールを使ってアサーションを実行できます。assert.strictEqualなど、さまざまなメソッドでテスト結果を検証できます。

import assert from 'node:assert';

test('Test 1', () => {
  assert.strictEqual(1 + 1, 2);
});

Node.js標準のテストランナーでテストスイートとテストフックを使う

describe関数を使うと、関連するテストをテストスイートにまとめられます。テストを整理し、管理しやすくできます。

import { test, describe } from "node:test";

describe('My Test Suite', () => {
  test('Test 1', () => {
    // Test 1 logic
  });

  test('Test 2', () => {
    // Test 2 logic
  });
});

テストフックは、テストの前後に実行される特別な関数です。テスト環境のセットアップやクリーンアップに役立ちます。

test.beforeEach(() => {
  // Runs before each test
});

test.afterEach(() => {
  // Runs after each test
});

test.skip関数を使ってテストをスキップすることもできます。特定のテストを一時的に実行しない場合に便利です。

test.skip('My skipped test', () => {
  // Test logic
});

さらに、Node.jsテストランナーには、テスト結果をさまざまな形式で整形・表示するレポーターが用意されています。--reporterオプションでレポーターを指定できます。

node --test --test-reporter=tap

Jestを使うのはやめるべき?

JestはNode.jsコミュニティで人気のテストフレームワークですが、いくつかの欠点があり、Node.js標準のテストランナーのほうが魅力的な選択肢となる場合があります。

Jestを開発依存関係としてインストールするだけで、MIT、Apache-2.0、CC-BY-4.0、ライセンス不明が1つを含む、さまざまなライセンスの推移的依存関係が277個追加されます。ご存じでしたか?

Jestの推移的依存関係グラフ。
  • Jestはグローバル変数を変更するため、テストで予期しない動作が起きることがあります。

  • instanceof演算子は、Jestでは常に期待どおりに動作するとは限りません。

  • Jestを導入するとプロジェクトの依存関係が大幅に増え、サードパーティ依存関係を最新に保つのが難しくなります。また、開発時の依存関係に関するセキュリティ問題などを不必要に管理することにもなります。

  • Jestはオーバーヘッドがあるため、Node.js標準のテストランナーより遅くなる場合があります。

Node.js標準のテストランナーには、サブテストや並行テストの実行など、ほかにも優れた機能があります。サブテストでは、各test()コールバックがcontext引数を受け取り、context.testを使って入れ子のテストを作成できます。並行テストは、適切に扱い、競合状態を避ける方法を理解していれば便利な機能です。テストスイートのdescribe()関数に2つ目の引数としてconcurrency: trueオブジェクトを渡すだけです。

テストランナーとは何ですか?

テストランナーは、開発者がコードの自動テストを管理・実行するためのソフトウェアツールです。Node.jsのテストランナーはNode.jsとのシームレスな連携を目的としたフレームワークで、Node.jsアプリケーションのテストを作成・実行するための豊富な環境を提供します。

2. Node.js標準のモック機能

モックは、テスト対象のコードを分離するために開発者が使う手法の1つです。Node.jsランタイムには標準のモック機能が導入されており、開発者が理解し、効果的に使うことが重要です。

Jestのjest.spyOnやmockResolvedValueOncelなど、ほかのテストフレームワークのモック機能を使ったことがあるでしょう。HTTPリクエストやファイルシステムAPIなど、テスト中に実際のコードを実行したくないときに便利です。こうした処理をスタブやモックに置き換え、後から検証できます。

watchやcoverage機能など、Node.jsランタイムのほかの機能とは異なり、モック機能は実験的機能とされていません。ただし、Node.js 18で導入されたばかりの新機能であるため、今後変更される可能性があります。

import { mock } from 'node:test'を使ったNode.js標準のモック機能

実践的な例を使って、Node.js標準のモック機能の使い方を見てみましょう。テストランナーとモジュールのモック機能は、Node.js 20 LTSで安定版として利用できます。

環境変数を.envファイルから読み込むユーティリティモジュールdotenv.jsを使います。また、dotenv.jsモジュールをテストするファイルdotenv.test.jsも使います。

こちらが、社内で作成したdotenvモジュールです。

// dotenv.js
import fs from "node:fs/promises";

export async function loadEnv(path = ".env") {
  const rawDataEnv = await fs.readFile(path, "utf8");
  const env = {};
  rawDataEnv.split("\n").forEach((line) => {
    const [key, value] = line.split("=");
    env[key] = value;
  });

  return env;
}

dotenv.jsファイルには、loadEnvという非同期関数があります。この関数はfs.readFileメソッドでファイルを読み込み、内容をキーと値のペアに分割します。Node.js標準のファイルシステムAPIであるfsを使っていることがわかります。

それでは、Node.js標準のモック機能を使って、この関数をテストする方法を見てみましょう。

// dotenv.test.js
import { describe, test, mock } from "node:test";
import assert from "node:assert";
import fs from "node:fs/promises";

import { loadEnv } from "../src/dotenv.js";

describe("dotenv test suite", () => {
  test("should load env file", async () => {
    const mockImplementation = async (path) => {
      return "PORT=3000\n";
    };
    const mockedReadFile = mock.method(fs, "readFile", mockImplementation);

    const env = await loadEnv(".env");

    assert.strictEqual(env.PORT, "3000");
    assert.strictEqual(mockedReadFile.mock.calls.length, 1);
  });
});

テストファイルでは、node:testからmockメソッドをインポートし、fs.readFileのモック実装を作成します。このモック実装は、渡されたファイルパスにかかわらず、文字列"PORT=3000\n"を返します。

次にloadEnv関数を呼び出し、assertモジュールを使って次の2点を確認します。

  1. 返されたオブジェクトにPORTプロパティがあり、値が"3000"であること。

  2. fs.readFileメソッドが1回だけ呼び出されたこと。

Node.js標準のモック機能を使えば、loadEnv関数をファイルシステムから効果的に切り離し、単独でテストできます。Node.js 20のモック機能は、タイマーのモックにも対応しています。

モックとは何ですか?

ソフトウェアテストにおけるモックとは、特定のモジュールの実際の機能を人工的なものに置き換えるプロセスです。主な目的は、テスト対象のコードを外部依存から切り離し、依存先の機能ではなく、対象のユニットの機能だけを検証できるようにすることです。またモックを使うと、依存先でエラーが発生するなど、実際の環境では一貫して再現するのが難しいさまざまな状況をシミュレートできます。

3. Node.js標準のテストカバレッジ

テストカバレッジとは何ですか?

テストカバレッジは、ソフトウェアテストで使用される指標です。アプリケーションのソースコードがどの程度テストされているかを開発者が把握するのに役立ちます。テストされていないコードベースの領域が明らかになるため、ソフトウェアの潜在的な弱点を特定できます。

テストカバレッジが重要なのはなぜでしょうか。バグを減らし、デグレードを防いでソフトウェアの品質を保てるからです。また、テストの有効性を把握し、より堅牢で信頼性が高く、安全なアプリケーションを作るための指針にもなります。

Node.js標準のテストカバレッジを活用する

バージョン20から、Node.jsランタイムにテストカバレッジの標準機能が含まれています。ただし、Node.js標準のテストカバレッジは現在、実験的機能に指定されています。利用は可能ですが、今後のリリースで変更される可能性があります。

Node.js標準のテストカバレッジを使うには、--experimental-test-coverageコマンドラインフラグを指定します。プロジェクトのテストを実行するtest:coverageエントリを、package.jsonのscriptsフィールドに追加する例を紹介します。

{
  "scripts": {
    "test": "node --test ./tests",
    "test:coverage": "node --experimental-test-coverage --test ./tests"
  }
}

上の例では、test:coverageスクリプトが--experimental-test-coverageフラグを使い、テストの実行中にカバレッジデータを生成します。

npm run test:coverageを実行すると、次のような出力が表示されます。

ℹ tests 7
ℹ suites 4
ℹ pass 5
ℹ fail 0
ℹ cancelled 0
ℹ skipped 1
ℹ todo 1
ℹ duration_ms 84.018917
ℹ start of coverage report
ℹ ---------------------------------------------------------------------
ℹ file                 | line % | branch % | funcs % | uncovered lines
ℹ ---------------------------------------------------------------------
ℹ src/dotenv.js        | 100.00 |   100.00 |  100.00 | 
ℹ src/math.js          | 100.00 |   100.00 |  100.00 | 
ℹ tests/dotenv.test.js | 100.00 |   100.00 |  100.00 | 
ℹ tests/math.test.js   |  94.64 |   100.00 |   91.67 | 24-26
ℹ ---------------------------------------------------------------------
ℹ all files            |  96.74 |   100.00 |   94.44 |
ℹ ---------------------------------------------------------------------
ℹ end of coverage report

このレポートには、テストでカバーされたステートメント、分岐、関数、行の割合が表示されます。

Node.js標準のテストカバレッジは、Node.jsアプリケーションの品質向上に役立つ強力なツールです。現在は実験的機能とされていますが、テストカバレッジに関する有益な情報を得て、テストの取り組みを導くことができます。この機能を理解して活用すれば、堅牢で信頼性が高く、安全なコードを実現できます。

4. Node.jsウォッチモード

Node.jsウォッチモードは、Node.jsファイルの変更をリアルタイムで監視し、スクリプトを自動で再実行できる、開発者にとって便利な機能です。

Node.js標準のウォッチ機能を詳しく見る前に、以前のNode.jsバージョンでこのニーズを満たしていた人気ツール、nodemonについて触れておきましょう。Nodemonは、ファイルディレクトリで変更を検出するとNode.jsアプリケーションを再起動するコマンドラインインターフェース(CLI)ツールです。

npm install -g nodemon
nodemon

この機能は開発中に特に役立ちます。ファイルを変更するたびに手動で再起動する必要がなくなり、時間を節約して生産性を高められます。

nodemon npmパッケージのパッケージの健全性ステータスを表示するSnyk Advisor。

Node.js自体の進化により、同じ結果を実現する機能が標準で使えるようになりました。そのため、nodemonのようなサードパーティの依存関係をプロジェクトに追加する必要がありません。

チュートリアルに入る前に、Node.js標準のウォッチモードはまだ実験的機能であり、変更される可能性がある点に注意してください。この機能に対応したバージョンのNode.jsを使用していることを必ず確認しましょう。

Node.js 20標準のウォッチ機能を使う

Node.js 20では、--watchコマンドラインフラグを使った標準のファイル監視機能が導入されました。使い方は簡単で、複雑なファイル監視のニーズに合わせてglobパターンにも対応できます。

--watchコマンドを使うには、次のようにコマンドラインでNode.jsスクリプトに追加します。

node --watch app.js

globパターンを使う場合は、--watchフラグに特定のパターンを指定して、複数のファイルやディレクトリを監視できます。特定のパターンに一致するファイル群を監視したい場合に便利です。

node --watch 'lib/**/*.js' app.js

--watchフラグは--testと組み合わせて、テストファイルが変更されるたびにテストを再実行することもできます。

node --watch --test '**/*.test.js'

この組み合わせを使えば、変更のたびにテストが自動実行されるため、テスト駆動開発(TDD)のプロセスを大幅に効率化できます。

Node.js 20時点では、ウォッチモード機能はまだ実験的機能に指定されています。機能自体は問題なく動作しますが、実験的ではないほかの機能ほど安定していない、または最適化されていない可能性があります。

実際に--watchフラグを使用すると、予期しない動作やバグに遭遇することがあります。

5. Node.js Corepack

Node.js Corepackは、ぜひ試してみたい興味深い機能です。Node.js 16で導入され、現在も実験的機能として扱われています。どのような機能なのか、JavaScriptプロジェクトでどう活用できるのかを見ていきましょう。

Corepackとは?

Corepackは、Node.jsプロジェクトと、そのプロジェクトで使用するパッケージマネージャーをつなぐ、実行時依存関係のないプロジェクトです。インストールするとcorepackというプログラムが利用可能になり、パッケージマネージャーをグローバルにインストールする手間なく、プロジェクトで適切なものを使えるようになります。

Corepackを使う理由

JavaScript開発者は、複数のプロジェクトを扱うことが多く、それぞれが異なるパッケージマネージャーを使う場合もあります。あるプロジェクトではpnpmで依存関係を管理し、別のプロジェクトではyarnを使う、といった具合です。そのため、パッケージマネージャーのバージョンを切り替える必要も出てきます。

こうした状況は、競合や一貫性の欠如につながることがあります。Corepackを使えば、各プロジェクトで好みのパッケージマネージャーを指定し、シームレスに利用できます。

さらにCorepackは、プロジェクトをグローバル環境から切り離して管理します。そのため、グローバルパッケージが更新または削除されても、プロジェクトを引き続き実行できます。プロジェクトの一貫性と信頼性が高まります。

Corepackのインストールと使い方

Corepackのインストールはとても簡単です。Node.js 16以降に同梱されているため、そのバージョン以降のNode.jsをインストールまたはアップグレードするだけです。

インストールしたら、次のようにpackage.jsonファイルでプロジェクトのパッケージマネージャーを指定できます。

{
  "packageManager": "yarn@2.4.1"
}

その後、プロジェクトで次のようにCorepackを使用できます。

corepack enable

プロジェクトのディレクトリでyarnと入力すると、Yarnがインストールされていない場合でも、Corepackが適切なバージョンを自動的に検出してインストールします。

これにより、システムにグローバルインストールされているYarnのバージョンに関係なく、Yarn 2.4.1を使ってプロジェクトの依存関係がインストールされます。

Yarnをグローバルにインストールする場合や、特定のバージョンを使う場合は、次のコマンドを実行します。

corepack install --global yarn@stable

Corepack:まだ実験的な機能

Node.js 16で導入されたCorepackは、現在も実験的機能として扱われています。正常に動作することが期待されていますが、開発が続いているため、今後、動作の一部が変更される可能性があります。

とはいえ、Corepackはインストールも使い方も簡単で、プロジェクトの信頼性を高める効果があります。ぜひ試して、開発ワークフローに取り入れたい機能です。

6. Node.jsの.envローダー

アプリケーションの設定は非常に重要です。Node.js開発者なら、API認証情報、サーバーのポート番号、データベース設定などの管理が必要になることはよくご存じでしょう。

開発者には、ソースコードを変更せずに環境ごとに異なる設定を適用する方法が必要です。Node.jsアプリケーションでよく使われる方法の1つが、.envファイルに保存した環境変数を利用することです。

dotenv npmパッケージ

Node.jsに.envファイルの読み込み機能が標準搭載されるまでは、主にdotenv npmパッケージが使われていました。dotenvパッケージは.envファイルから環境変数を読み込み、process.envに設定します。これにより、アプリケーション全体から環境変数を利用できます。

dotenvパッケージの一般的な使い方は次のとおりです。

require('dotenv').config();

console.log(process.env.MY_VARIABLE);

これは便利でしたが、プロジェクトに依存関係を追加する必要がありました。標準の.envローダーが導入されたことで、外部パッケージを使わずに環境変数を直接読み込めるようになりました。

Node.jsで.envファイルを読み込む標準機能の導入

Node.js 20以降では、.envファイルから環境変数を読み込む機能がランタイムに組み込まれています。この機能は現在も開発中ですが、すでに開発者にとって大きな変化をもたらしています。

.envファイルを読み込むには、Node.jsアプリケーションの起動時に--env-file CLIフラグを使用します。このフラグで、読み込む.envファイルのパスを指定します。

node --env-file=./.env index.js

指定した.envファイルの環境変数がprocess.envに読み込まれます。これまでと同じように、アプリケーション内で変数を利用できます。

複数の.envファイルを読み込む

Node.jsの.envローダーは、複数の.envファイルの読み込みにも対応しています。開発、テスト、本番など、環境ごとに異なる環境変数を使う場合に便利です。

複数の--env-fileフラグを指定すると、複数のファイルを読み込めます。ファイルは指定した順番で読み込まれ、後のファイルにある変数は、前のファイルにある変数を上書きします。

例を見てみましょう。

node --env-file=./.env.default --env-file=./.env.development index.js

この例では、./.env.defaultにデフォルトの変数を、./.env.developmentに開発環境用の変数を記述しています。./.env.developmentと./.env.defaultの両方に存在する変数は、./.env.defaultの値が上書きされます。

Node.jsで.envファイルを読み込む標準機能は、Node.js開発者にとって大きな改善です。設定管理がシンプルになり、追加パッケージも不要になります。Node.jsアプリケーションで--env-file CLIフラグを使い、その便利さを実際に体験してみましょう。

7. Node.jsのimport.metaによる__dirnameと__fileのサポート

Node.jsのCommonJSモジュール規約に慣れている方なら、現在のファイルのディレクトリ名やファイルパスを取得するためにfilenameや__dirnameを使ってきたでしょう。しかし最近まで、ESMではこれらを簡単に利用できず、__dirnameを取得するために次のようなコードを書く必要がありました。

import url from 'url'
import path from 'path'
const dirname = path.dirname(url.fileURLToPath(import.meta.url))

また、Matteo Collinaのファンなら、Matteoのnpmパッケージdesmを使う方法を見つけたかもしれません。

ESMプロジェクトでimport.metaオブジェクトのデータを通じて__dirnameと__filenameを提供するnpmパッケージ「desm」を紹介するMatteo Collina。

Node.jsは、ファイルやパスをより効率的に扱えるよう、進化を続けています。Node.js開発者にとって大きな改善となる変更が、Node.js v20.11.0およびNode.js v21.2.0で導入され、import.meta.dirnameとimport.meta.filenameが標準でサポートされました。

Node.jsのimport.meta.filenameとimport.meta.dirnameを使う

import.meta.filenameとimport.meta.dirnameの導入により、作業がずっと簡単になりました。新機能を使って設定ファイルを読み込む例を見てみましょう。

JavaScriptファイルと同じディレクトリに、読み込みたいYAML設定ファイルがあるとします。次のように記述できます。

import fs from 'fs';

const { dirname: __dirname, filename: __filename } = import.meta;
const projectSetup = fs.readFileSync(`${__dirname}/setup.yml`, "utf8");

console.log(projectSetup);

この例ではimport.meta.dirnameで現在のファイルのディレクトリ名を取得し、CommonJSのコード規約に合わせて__dirname変数に代入しています。

8. Node.js標準のタイマープロミス

ChromeのV8 JavaScriptエンジンを基盤とする人気のJavaScriptランタイム、Node.jsは、継続的なアップデートと新機能によって、開発者の作業をより簡単にすることを目指してきました。

Node.jsではv15からタイマーをプロミス構文で標準利用できるようになりましたが、正直なところ、私は定期的には使っていませんでした。

JavaScriptのsetTimeout()とsetInterval()タイマー:簡単なおさらい

標準のタイマープロミスについて説明する前に、JavaScriptのsetTimeout()とsetInterval()タイマーを簡単におさらいしましょう。

setTimeout() APIは、タイマーが切れると関数または指定したコードを一度だけ実行するJavaScript関数です。

setTimeout(function(){ 
    console.log("Hello World!"); 
}, 3000);

上記のコードでは、3秒(3000ミリ秒)後に「Hello World!」がコンソールに出力されます。

一方、setInterval()は、呼び出しのたびに間隔を置いて、指定した関数を繰り返し実行します。

setInterval(function(){ 
    console.log("Hello again!"); 
}, 2000);

上記のコードでは、2秒(2000ミリ秒)ごとに「Hello again!」がコンソールに出力されます。

従来の方法:setTimeout()をプロミスでラップする

以前は、setTimeout()を非同期で使うため、開発者がプロミスでラップすることがよくありました。これは、setTimeout()をasync/awaitと組み合わせて使えるようにするためです。

その方法の例を紹介します。

function sleep(ms) {
  return new Promise(resolve => setTimeout(resolve, ms));
}

async function demo() {
  console.log('Taking a break...');
  await sleep(2000);
  console.log('Two seconds later...');
}

demo();

このコードは「Taking a break...」と出力し、2秒待ってから「Two seconds later...」と出力します。

動作はしますが、コードが不必要に複雑になります。

Node.js標準のタイマープロミス:よりシンプルな方法

Node.js標準のタイマープロミスを使えば、setTimeout()をプロミスでラップする必要はありません。代わりにsetTimeout()をそのままasync/awaitと組み合わせて使えます。コードがすっきりして読みやすくなり、メンテナンスも簡単になります。Node.js標準のタイマープロミスの使い方を見てみましょう。

const {
  setTimeout,
} = require('node:timers/promises');

setTimeout(2000, 'Two seconds later...').then((res) => {
  console.log(res);  
});

console.log('Taking a break...');

上記のコードでは、setTimeout()をnode:timers/promisesからインポートし、そのままasync/awaitと組み合わせて使っています。「Taking a break...」と出力し、2秒待ってから「Two seconds later...」と出力します。

これにより非同期プログラミングが大幅にシンプルになり、コードの読み書きやメンテナンスがしやすくなります。

9. Node.jsの権限モデル

現在Node.js TSCに所属するRafael Gonzagaが、Node.jsの権限モジュールの開発を再始動しました。このモジュールはDenoと同様に、プロセス単位で設定できるリソース制約を提供します。

サプライチェーンのセキュリティ懸念、悪意のあるnpmパッケージ、その他のセキュリティリスクが広がるなか、セキュリティとコンプライアンスのために、Node.jsアプリケーションがアクセスできるリソースを管理・制御する重要性が高まっています。

こうした背景からNode.jsでは、アプリケーションのリソース権限を管理する「権限モジュール」という実験的機能が導入されました。この機能は--experimental-permissionコマンドラインフラグで有効にします。

Node.jsのリソース権限モデル

Node.jsの権限モデルは、ファイルシステム、ネットワーク、環境変数、ワーカースレッドなど、さまざまなリソースへのアクセスを管理するための抽象化を提供します。アプリケーションの特定の部分がアクセスできるリソースを制限したい場合に、特に役立つ機能です。

権限モデルで設定できる一般的なリソース制約は次のとおりです。

  • --allow-fs-read=*と--allow-fs-write=*を使ったファイルシステムの読み書き。ディレクトリや特定のファイルパスを指定でき、フラグを繰り返して複数のリソースを指定することもできます

  • --allow-child-processを使った子プロセスの起動

  • --allow-workerを使ったワーカースレッドの起動

Node.jsの権限モデルでは、process.permission.has(resource, value)を使うランタイムAPIも提供されており、特定のアクセス権を照会できます。

たとえば.envファイルの読み込みなど、許可されていないリソースにアクセスしようとすると、ERR_ACCESS_DENIEDエラーが表示されます。

> start:protected
> node --env-file=.env --experimental-permission server.js

node:internal/modules/cjs/loader:197
  const result = internalModuleStat(filename);
                 ^

Error: Access to this API has been restricted
    at stat (node:internal/modules/cjs/loader:197:18)
    at Module._findPath (node:internal/modules/cjs/loader:682:16)
    at resolveMainPath (node:internal/modules/run_main:28:23)
    at Function.executeUserEntryPoint [as runMain] (node:internal/modules/run_main:135:24)
    at node:internal/main/run_main_module:28:49 {
  code: 'ERR_ACCESS_DENIED',
  permission: 'FileSystemRead',
  resource: '/Users/lirantal/repos/modern-nodejs-runtime-features-2024/server.js'
}

Node.js v21.6.1

Node.jsの権限モデルの例

ファイルのアップロードを処理するNode.jsアプリケーションを考えてみましょう。アップロードされたファイルの保存先として指定したディレクトリにのみ、この部分のアプリケーションがアクセスできるよう制限したいとします。

Node.jsアプリケーションの起動時に--experimental-permissionフラグを指定して、実験的な権限機能を有効にします。

node --experimental-permission ./app.js

また、信頼できる2つのファイル.envとsetup.ymlの読み込みだけは許可したいため、上記を次のように変更します。

node --experimental-permission --allow-fs-write=/tmp/uploads --allow-fs-read=.env --allow-fs-read=setup.yml ./app.js

このようにすると、アプリケーションが指定したアップロード先以外で書き込みのためにファイルベースのシステムリソースにアクセスしようとした場合、エラーが発生して処理が停止します。

次のコード例では、try/catchでリソースへのアクセスを囲む方法と、例外エラーを発生させずにアクセス権を確認する別の方法としてNode.jsの権限ランタイムAPIを使う方法を紹介します。

 const { dirname: __dirname, filename: __filename } = import.meta;
// @TODO to avoid the Node.js resource permission issue you should update
// the path to be `setup.yml` in the current directory and not `../setup.yml`.
// the outside path for setup.yml was only changed in the source code to
// show you how Node.js resource permission module will halt if trying to access
// something outside the current directory.
const filePath = `${__dirname}/../setup.yml`;
try {
  const projectSetup = fs.readFileSync(filePath, "utf8");
  // @TODO do something with projectSetup if you want to
} catch (error) {
  console.error(error.code);
}
// @TODO or consider using the permissions runtime API check:
if (!process.permission.has("read", filePath)) {
  console.error("no permissions to read file at", filePath);
}

Node.jsの権限機能はまだ実験的であり、変更される可能性があることに注意してください。

権限や本番環境に適したセキュリティの規約について、Node.jsアプリケーションを安全に構築する方法をさらに知りたい方は、Snykの以下のブログ記事をご覧ください。

これらの記事では、Node.jsのWebアプリケーション向けに安全なコンテナイメージを構築する方法を包括的に解説しています。これは、安全なNode.jsアプリケーションを開発するうえで重要です。

10. Node.jsポリシーモジュール

Node.jsポリシーモジュールは、悪意のあるコードがNode.jsアプリケーションに読み込まれて実行されるのを防ぐためのセキュリティ機能です。読み込まれたコードの出所を追跡する機能はありませんが、潜在的な脅威に対する有効な防御策となります。

ポリシーモジュールでは、--experimental-policy CLIフラグを使用して、ポリシーベースのコード読み込みを有効にします。このフラグには、ポリシーマニフェストファイル(JSON形式)を引数として指定します。たとえば、--experimental-policy=policy.jsonのように指定します。

ポリシーマニフェストファイルには、モジュールの読み込み時にNode.jsが従うポリシーが記述されています。これにより、アプリケーションに読み込まれるコードの内容を確実に制御できます。

Node.jsポリシーモジュールの実装方法:ステップバイステップガイド

簡単な例を使って、Node.jsポリシーモジュールの使い方を見ていきましょう。

1. ポリシーファイルを作成します。 このファイルは、アプリでモジュールを読み込む際のポリシーを指定したJSONファイルです。ここでは、policy.jsonという名前にします。 

たとえば、次のようになります。

    {
      "resources": {
        "./moduleA.js": {
          "integrity": "sha384-xxxxx"
        },
        "./moduleB.js": {
          "integrity": "sha384-yyyyy"
        }
      }
    }

このポリシーファイルでは、moduleA.jsとmoduleB.jsを読み込むために、それぞれ特定の整合性値が必要であると指定しています。

ただし、直接依存関係と間接依存関係すべてを対象にポリシーファイルを生成するのは簡単ではありません。数年前、Bradley Meckがnode-policy npmパッケージを作成しました。このパッケージには、ポリシーファイルの生成を自動化するCLIが用意されています。

2. 次の --experimental-policy フラグを指定して、Node.jsアプリケーションを実行します。

  node --experimental-policy=policy.json app.js

このコマンドは、app.js内のモジュールを読み込む際に、Node.jsがpolicy.jsonで指定されたポリシーに従うよう指示します。

3. ポリシーファイルの改ざんを防ぐには、 --policy-integrity フラグを使って、ポリシーファイル自体の整合性値を指定します。

    node --experimental-policy=policy.json --policy-integrity="sha384-zzzzz" app.js

このコマンドにより、ディスク上のファイルが変更されても、ポリシーファイルの整合性が保たれます。

Node.jsの整合性ポリシーに関する注意点 

Node.jsランタイムには、ポリシーファイルを生成または管理する機能が組み込まれていません。また、本番環境と開発環境で異なるポリシーを管理したり、動的なモジュールインポートに対応したりする際に、問題が生じる可能性があります。

もうひとつの注意点は、すでに悪意のあるnpmパッケージが存在している場合、その状態のままではモジュール整合性ポリシーファイルを生成しても手遅れだということです。

この機能については、今後のアップデートを注視しながら、少しずつ段階的に導入することをお勧めします。

Node.jsポリシーモジュールの詳細については、Node.jsへの実験的な整合性ポリシーの導入の記事をご覧ください。Node.jsのポリシー整合性を利用するための手順が、より詳しく解説されています。

まとめ

2024年に使い始めたい最新のNode.jsランタイム機能を見てきました。これらの機能は、開発プロセスの効率化、アプリケーションのパフォーマンス向上、セキュリティの強化を目的としています。単なる流行ではなく、Node.js開発のあり方を大きく変える可能性を秘めています。

SnykでNode.jsのセキュリティを強化

Node.jsの機能を活用することで、開発プロセスやアプリケーションのパフォーマンスを大幅に向上できますが、潜在的なセキュリティ脅威への警戒も欠かせません。そんなとき、Snykが役立ちます。Node.jsの依存関係にある既知の脆弱性を発見・修正し、安全な開発エコシステムを維持するのに役立つ強力なツールです。

Snykの機能をぜひご活用ください。こちらから無料で登録して、より安全なNode.js開発を始めましょう。

カテゴリー: