私たちの Twitter をフォローしましょう! @nodepractices
他の言語で読む: CN, BR, RU, PL, JA, EU (ES, FR, HE, KR and TR in progress!)
ステアリングコミッティーとコラボレーターによって運営されています
-
🎉 Node.js ベストプラクティスは 50k スターに到達しました: このプロジェクトをこのようなものにしてくれた全てのコントリビューターに感謝申し上げます! 私たちは、増え続ける Node.js のベストプラクティスリストをさらに拡大していくために、これから先たくさんの計画を画策しています。
-
🎧 ポッドキャスト: 私たちのチームの Yoni Goldberg が、前回の JS Party Podcast(とってもクールなポッドキャストです!)のエピソードに出演して、Node.js ベストプラクティスについて話をしました。🎧 ここから聞きましょう。
-
🐳 Node.js + Docker ベストプラクティス: Docker を用いたより良いコーディングテクニック 15 項目を含んだ、Docker と Node.js のセクションを新たに公開しました。
-
🎤 OdessaJS でのトーク: 今週、OdessaJS conference という素晴らしい舞台で、Node.js のテストについて話をします。
1. 実際、あなたは何十もの Node.js の最高の記事を読んでいます - このリポジトリは、Node.js のベストプラクティスに関するトップランクのコンテンツや、コラボレーターによって書かれたコンテンツをまとめたものです。
2. 最大の集大成であり、毎週のように増え続けています - 現在、80 以上のベストプラクティスやスタイルガイド、アーキテクチャのヒントが記載されています。この「生きた本」がアップデートされた状態を保つために、新しいイシューやプルリクエストは毎日のように作成されています。私たちは、コードの修正や翻訳作業、素晴らしい新たなアイデアの提案に至るまで、あなたの貢献を心待ちにしています。詳しくはライティングガイドラインをご覧ください。
3. ほとんどのベストプラクティスには追加情報があります - ほとんどの項目に 🔗 さらに読む というリンクがあります。このリンクは、コード例や厳選されたブログからの引用、その他多くの情報など、プラクティスを発展させる内容を含んでいます。
- プロジェクト構成のプラクティス (5)
- エラーハンドリングのプラクティス (11)
- コードスタイルのプラクティス (12)
- テストと総合的な品質のプラクティス (13)
- 本番環境移行のプラクティス (19)
- セキュリティのプラクティス (25)
- パフォーマンスのプラクティス (2) (進行中️ ✍️)
- Docker のプラクティス (15)
TL;DR: 大規模アプリケーションの最悪の落とし穴は、何百もの依存関係を持つ巨大なコードベースを維持することです。- そのようなモノリスは、新しい機能を取り入れようとする開発者の速度を低下させます。その代わりに、コードをコンポーネントに分割し、それぞれが独自のフォルダや専用のコードベースを取得し、各ユニットが小さくシンプルに保たれていることを確認してください。正しいプロジェクト構造の例を見るには、以下の「さらに読む」を参照してください。
さもないと: 新しい機能をコーディングする開発者が、自分の変更の影響を理解するのに苦労したり、他の依存するコンポーネントを壊すことを恐れたりすると、デプロイが遅くなり、リスクが高くなります。また、すべてのビジネスユニットが分離されていない場合、スケールアウトするのは難しいと考えられています。
TL;DR: 各コンポーネントは、ウェブ、ロジック、データアクセスコードのための専用オブジェクトである「レイヤー」を含むべきです。これにより、懸念点がきれいに分離されるだけでなく、システムのモックやテストが大幅に楽になります。これは非常に一般的なパターンですが、API 開発者は Web レイヤーオブジェクト (例: Express req, res) をビジネスロジックとデータレイヤーに渡すことでレイヤーを混ぜる傾向があります - これにより、アプリケーションが特定の Web フレームワークに依存してしまい、特定の Web フレームワークからしかアクセスできなくなってしまいます。
さもないと: Web オブジェクトと他のレイヤーが混在するアプリには、テストコードや CRON ジョブ、メッセージキューからのトリガーなどからアクセスすることはできません。
TL;DR: 大規模なコードベースで構成される大規模なアプリでは、logger や暗号化などの横断的に関心のあるユーティリティは、独自のコードでラップし、プライベートな npm パッケージとして公開する必要があります。これにより、複数のコードベースやプロジェクト間で共有することができます。
さもないと: デプロイと依存関係の車輪の作成をしなければいけなくなります
TL;DR: Express のアプリ全体を単一の巨大なファイルで定義するという厄介な習慣を回避します。- 「Express」の定義を、API 宣言( app.js )とネットワーク関連( WWW )の少なくとも 2 つのファイルに分離してください。より良い構造にするためには、API 宣言をコンポーネント内に配置してください。
さもないと: API は HTTP 呼び出しのみでテストにアクセスできるようになります(カバレッジレポートを生成するのがより遅く、はるかに困難になります)。何百行ものコードを一つのファイルで管理するのは、おそらく大きな喜びではないでしょう。
🔗 さらに読む: Express の「アプリ」と「サーバー」を分離する
TL;DR: 完璧で欠陥のない設定を行うには、次のようなことが必要です。(a) キーはファイルまたは環境変数から読み込むことができる (b) 秘密情報はコミットされたコードの外側に保持されている (c) 設定が階層化されており、見つけやすくなっている rc や nconf、 config、convict など、これらのボックスのほとんどを満たすのに役立つパッケージがいくつかあります。
さもないと: 設定要件のどれかを満たさないと、開発チームや DevOps チーム、おそらく両方ともの頭を悩ませてしまいます。
TL;DR: コールバックスタイルで非同期エラーを処理することは、おそらく地獄への最短経路でしょう(Pyramid of doom として知られています)。あなたができるコードへの最高の贈り物は、信頼できる promise ライブラリや async-await を使うことです。これらは、try-catch のような、よりコンパクトで親しみやすいコードシンタックスを可能にします。
さもなければ: Node.js のコールバックスタイル、つまり function(err, response) を利用することは、正常な処理を行うコードとエラーハンドリングの混同、過剰なネスト構造、そして厄介なコーディングパターンが原因となって、メンテナンス性の低いコードにつながります。
TL;DR: 多くがエラーとして文字列やカスタム型を投げます - これはエラー処理ロジックとモジュール間の相互運用性を複雑にします。promise を reject したのか、例外を投げたのか、エラーを排出したのかに関わらず、組み込みのエラーオブジェクト(またはそれを拡張したオブジェクト)だけ使うことは一貫性を向上させ、情報の欠落を防ぎます。
さもないと: ある要素を呼び出したとき、どの型のエラーが返ってくるか不確かである - といった状況は、適切なエラー処理をより難しいものにします。さらに悪いことに、エラーを表現するためにカスタム型を使うことは、スタックトレースのような重大なエラー情報を失うことに繋がるかもしれません。
🔗 さらに読む: 組み込みのエラーオブジェクトのみを使用する
TL;DR: 操作上のエラー(例: API が無効な入力を受け取る)は、エラーの影響が十分に理解され、そして丁寧に処理される既知のエラーのことを指します。一方で、プログラマーのエラー(例: 未定義の変数を参照しようとする)は、アプリケーションをすぐさま再起動させる、未知のコードエラーのことを指します。
さもないと: エラーが発生したときに毎回アプリケーションを再起動しているかもしれませんが、さほど重要でない、予測可能な、操作上のエラーを原因としてなぜ ~5000 人規模のオンラインユーザーをダウンさせるのでしょうか? その逆もまた理想的ではありません ー 未知のエラー(プログラマーのエラー)が発生したときにアプリケーションをそのまま起動し続けることは、予想外の振る舞いに繋がるかもしれません。この2つを区別することで、機転の利いた振る舞いをさせ、与えられたコンテキストに基づいた適切なアプローチを適用させることができます。
🔗 さらに読む: 操作上のエラーとプログラマーのエラーを区別する
TL;DR: 管理者へのメールやロギングのようなエラー処理ロジックは、エラーが発生したときに全てのエンドポイント(Express ミドルウェア、cron ジョブ、ユニットテストなど)が呼び出す、エラー処理専用の一元化されたオブジェクトにカプセル化されているべきです。
さもないと: エラーを一箇所で処理しないと、コードの重複や、不適切に処理されたエラーの発生に繋がる可能性があります。
🔗 さらに読む: エラー処理を一元化し、ミドウェア内で処理をしない
TL;DR: API の呼び出し元に、どのようなエラーが返ってくるかを示しておくことで、クラッシュすることなく丁寧にエラー処理を行うことができます。RESTful API の場合、通常 Swagger のようなドキュメントフレームワークを使用します。GraphQL を使用している場合は、スキーマやコメントも利用できます。
さもないと: API クライアントがクラッシュして再起動するのは、不明なエラーを受け取ったからかもしれません。注意: API の呼び出し元はあなた自身かもしれません(マイクロサービス構成では非常によくあることです)
🔗 さらに読む: Swagger または GraphQL を利用して API のエラーをドキュメント化する
TL;DR: 未知のエラーが発生した場合(プログラマーのエラー、ベストプラクティス 2.3 参照)、アプリケーションの健全性に不確実さがあります。一般的に、Forever や PM2 のようなプロセス管理ツールを利用してプロセスを慎重に再起動することが推奨されています。
さもないと: 不明な例外が発生した場合、一部のオブジェクトが不完全な状態(例えば、グローバルに使用されているイベントエミッタが内部的なエラーによりイベントを発火しなくなっている、など)になっている可能性があり、後に来るリクエストが失敗したり、予期せぬ挙動をしたりするかもしれません。
🔗 さらに読む: 見ず知らずの事象が起きたら潔くプロセスを終了する
TL;DR: Pino や Log4js のような成熟したロギングツールは、エラーの発見と理解を加速します。ですから、console.log のことは忘れましょう。
さもないと: console.log によるログに目を通したり、クエリツールやまともなログビューア無しで扱いにくいテキストファイルを手動で確認したりすると、遅くまで仕事をする羽目になるかもしれません。
🔗 さらに読む: エラーの可視性を高めるために成熟したロガーを使用する
TL;DR: プロ仕様の自動化された QA であろうと単純な手動の開発者によるテストであろうと、コードが正常系のシナリオを満たすだけでなく、正しくエラーを処理して返すことを保証してください。Mocha や Chai のようなテストフレームワークは、これを簡単に処理することができます(「さらに読む」の例を参照)
さもないと: 自動であっても手動であっても、テストがなければ、コードが正しいエラーを返すと信用することはできません。意味のあるエラーがなければ、エラー処理はできません。
🔗 さらに読む: お気に入りのテストフレームワークを使用してエラーフローをテストする
TL;DR: モニタリング・パフォーマンス計測を行う製品(APM として知られています)は、コードベースや API をプロアクティブに計測し、見落としていたエラーやクラッシュ、処理の遅い部分を自動的にハイライトすることができます。
さもないと: API のパフォーマンスとダウンタイムの計測に多大な労力を費やしているかもしれませんが、現実のシナリオにおいてどの部分のコードが最も遅いのか、そしてそれらがどのように UX に影響を及ぼしているのか、あなたが気づくことは恐らくないでしょう。
🔗 さらに読む: APM 製品を利用してエラーとダウンタイムを発見する
TL;DR: promise の中で投げられた全ての例外は、開発者が明示的に処理を行うことを忘れていない限り、飲み込まれて破棄されます。たとえコードが process.uncaughtException
をサブスクライブしていたとしてもです!process.unhandledRejection
イベントに登録することで、この問題を乗り越えることができます。
さもないと: あなたのエラーは飲み込まれて、何のトレースも残しません。心配することは、何も残りません。
🔗 さらに読む: 未処理の reject された promise を捕捉する
TL;DR: API の入力をアサートすることで、後から追跡するのが非常に難しい厄介なバグを避けることができます。ajv や Joi のような非常にクールなヘルパーライブラリを利用しない限り、バリデーションコードを書くことは一般的に退屈な作業です。
さもないと: 考えてみて下さい ー 関数は数値の引数「Discount」を受け取ることを期待していますが、呼び出し元が値を渡すのを忘れてしまいました。その後、コードが Discount!=0 (許容されたディスカウントの量が 0 よりも大きいことを想定) であるということをチェックし、そのチェックをクリアした場合にユーザーがディスカウントを受けられるようにしました。オーマイガー、なんて厄介なバグなんでしょう。わかりますか?(訳注:「さらに読む」に具体的なコード例が載っています)
🔗 さらに読む: 専用のライブラリを利用して引数の検証を高速に行う
TL;DR: ESLint は、コードエラーの可能性をチェックし、コードスタイルを修正するためのデファクトスタンダードで、細かい間隔の問題を特定するだけでなく、開発者が分類せずにエラーを投げるような深刻なコードアンチパターンを検出することもできます。ESLint はコードスタイルを自動的に修正することができますが、prettier や beautify のような他のツールは、フィックスの書式設定をより強力にし、ESLint と連携して動作します。
さもないと: 開発者は退屈な間隔や線幅の問題に集中し、プロジェクトのコードスタイルを考えすぎて時間を無駄にしてしまうかもしれません。
🔗 さらに読む: ESLint と Prettier を使う
TL;DR: vanilla JavaScript をカバーする ESLint の標準ルールに加えて、eslint-plugin-node や eslint-plugin-mocha、eslint-plugin-node-security のような Node.js に特化したプラグインを追加します。
さもないと: 多くの欠陥のある Node.js のコードパターンは、レーダーを逃れてしまうかもしれません。例えば、開発者は攻撃者が任意の JS スクリプトを実行できるように、パスとして与えられた変数を持つファイルを require(variableAsPath) しているかもしれません。Node.js の linters は、そのようなパターンを早期に検出して知らせてくれます。
TL;DR: コードブロックの冒頭の中括弧は、冒頭の文と同じ行でなければなりません。
// Do
function someFunction() {
// code block
}
// Avoid
function someFunction()
{
// code block
}
さもないと: このベストプラクティスから逸脱すると、以下の StackOverflow スレッドにあるように、予期せぬ結果を招く可能性があります。:
🔗 さらに読む: "なぜ中括弧の配置によって結果が変わるのか?" (StackOverflow)
ステートメントを区切るためにセミコロンを使うか使わないかに関わらず、不適切な改行や自動セミコロン挿入のよくある落とし穴を知っておくことで、通常の構文エラーをなくすことができます。
TL;DR: ESLint を使用して、分離の懸念について認識する。 Prettier や Standardjs は、これらの問題を自動的に解決することができます。
さもないと: 前のセクションで見たように、JavaScript のインタプリタは、セミコロンがない場合は自動的に文の最後にセミコロンを追加したり、ステートメントが本来あるべき場所で終わっていないとみなしたりすることで、望まない結果になってしまう可能性があります。代入を使用し、即時に呼び出された関数式の使用を避けることで、予期せぬエラーのほとんどを防ぐことができます。
// する
function doThing() {
// ...
}
doThing()
// する
const items = [1, 2, 3]
items.forEach(console.log)
// 避ける — 例外を投げる
const m = new Map()
const a = [1,2,3]
[...m.values()].forEach(console.log)
> [...m.values()].forEach(console.log)
> ^^^
> SyntaxError: Unexpected token ...
// 避ける — 例外を投げる
const count = 2 // 2() を実行しようとしますが、2 は関数ではありません
(function doSomething() {
// 凄いことをする
}())
// 直ちに呼び出された関数の前、const 定義の後にセミコロンを置く、匿名関数の戻り値を変数に保存する、あるいは IIFE を完全に回避する
🔗 さらに読む: "準 ESLint ルール" 🔗 さらに読む: "予期せぬ複数行を許さない ESLint のルール"
TL;DR: クロージャやコールバックを含むすべての関数に名前を付けます。匿名関数は避けてください。これは特に node アプリをプロファイリングするときに便利です。すべての関数に名前を付けることで、メモリスナップショットをチェックする際に何を見ているのかを簡単に理解することができます。
さもないと: コアダンプ(メモリスナップショット)を使用した本番環境の問題のデバッグは、匿名関数からのメモリ消費が大きいことに気づくと、困難になるかもしれません。
TL;DR: 定数、変数、関数の命名をするときは lowerCamelCase を使用し、クラスの命名をするときは**UpperCamelCase** (頭文字も大文字) を使用してください。これは、プレーンな変数/関数とインスタンス化を必要とするクラスを簡単に区別するのに役立ちます。記述的な名前を使用しますが、短くしてください。
さもないと: JavaScript は、最初にインスタンスを作成せずにコンストラクタ(「クラス」)を直接呼び出すことができる世界で唯一の言語です。その結果、クラスと関数構造体は UpperCamelCase から始まることで区別されます。
// クラスには、UpperCamelCase を使用します
class SomeClassExample {}
// const 名には const キーワードと lowerCamelCase を使用します
const config = {
key: "value",
};
// 変数や関数名には lowerCamelCase を使用します
let someVariableExample = "value";
function doSomething() {}
TL;DR: const
を使うということは、一度代入された変数は再代入できないということを意味します。const
を優先することで、同じ変数を異なる用途に使いたくなることを防ぎ、コードをより明確にすることができます。変数を再割り当てする必要がある場合、例えば for ループの中などでは、let
を使って宣言します。もう一つの重要な点は、let
を使って宣言された変数は、それが定義されたブロックスコープ内でのみ利用可能であるということです。var
はブロックスコープではなく関数スコープであり、ES6 では使うべきではない ので、const
と let
がある以上必要ありません。
さもないと: 頻繁に変化する変数に従うと、デバッグが非常に面倒になります。
🔗 さらに読む: JavaScript ES6+: var、let、それとも const ?
TL;DR: 各ファイルの先頭かつ、全ての関数の前かつ外でモジュールを require します。このシンプルなベストプラクティスは、ファイルの依存関係を簡単かつ迅速にトップに表示するのに役立つだけでなく、いくつかの潜在的な問題を回避することができます。
さもないと: Require は Node.js によって同期的に実行されます。関数内から呼び出された場合、他のリクエストがより重要なタイミングで処理されるのをブロックすることがあります。また、require されたモジュールやそれ自身の依存関係がエラーを出してサーバをクラッシュさせてしまった場合は、できるだけ早く見つけた方が良いでしょう。関数の中からモジュールが require されている場合は早く見つけることができないかもしれません。
TL;DR: モジュール/ライブラリをフォルダ内で開発する場合は、モジュールの内部を公開する index.js ファイルを配置し、すべての使用者がそれを通過するようにします。これはモジュールへの「インタフェース」として機能し、約束事を破ることなく将来の変更を容易にします。
さもないと: ファイルの内部構造や署名を変更すると、クライアントとのインタフェースが壊れてしまう可能性があります。
// する
module.exports.SMSProvider = require("./SMSProvider");
module.exports.SMSNumberResolver = require("./SMSNumberResolver");
// 避ける
module.exports.SMSProvider = require("./SMSProvider/SMSProvider.js");
module.exports.SMSNumberResolver = require("./SMSNumberResolver/SMSNumberResolver.js");
TL;DR: 弱い抽象的な等号演算子 ==
よりも厳密な等号演算子 ===
を優先してください。==
は 2 つの変数を共通の型に変換した後に比較します。===
には型変換はなく、両方の変数が同じ型で等しくなければいけません。
さもないと: ==
演算子で比較すると、同じでない値でも真を返すかもしれません。
"" == "0"; // false
0 == ""; // true
0 == "0"; // true
false == "false"; // false
false == "0"; // true
false == undefined; // false
false == null; // false
null == undefined; // true
" \t\r\n " == 0; // true
===
を使用した場合、上のすべてのステートメントは false を返します。
TL;DR: Node 8 LTS は Async-await を完全にサポートするようになりました。これは、コールバックやプロミスに取って代わる非同期コードの新しい扱い方です。Async-await はノンブロッキングであり、非同期コードを同期的に見せてくれます。コードに与えることができる最高の贈り物は、try-catch のようなよりコンパクトで親しみやすいコード構文を提供する async-await を使うことです。
Otherwise: コールバックスタイルで非同期エラーを処理するのは、おそらく地獄への最速の方法です。- このスタイルでは、エラーのチェックをすべて強制し、厄介なコードの入れ子を処理し、コードの流れについての推論を困難にします。
TL;DR: プロミスやコールバックを受け入れる古い API を扱う場合は、async-await を使用して関数パラメータを避けることをお勧めしますが、arrow 関数はコード構造をよりコンパクトにし、ルート関数の語彙的なコンテキストを保持します。(すなわち this
)
さもないと: コードが長いと( ES5 の関数では)バグが発生しやすく、読むのが面倒になります。
TL;DR: 多くのプロジェクトでは、短いタイムスケジュールが原因で自動化テストを行っていないか、または「テストプロジェクト」がコントロール不能となり廃れてしまうことがしばしばあります。そのため、優先度を決めて、書くのが最も容易であり、ユニットテストより多くのカバレッジを提供してくれる API テストから始めましょう(Postman のようなツールを利用して、コード無しで API テストを手作りすることもできます)。その後、リソースや時間に余裕が出てきたら、ユニットテストや DB テスト、パフォーマンステストといった発展的なタイプのテストを実施してください。
さもないと: ユニットテストを書くことに長時間費やしても、システムカバレッジが 20% しかないことに気づくかもしれません。
TL;DR: テストを要件レベルを表現することで、コード内部をよく知らない QA エンジニアや開発者に対しても説明的であるようにしてください。テスト名には、何がテストされていて(テスト対象のユニット)、どのような状況で、どのような結果が期待されているのかを記述してください。
さもないと: "Add product" という名付けられたテストが通らず、デプロイが失敗しました。これは、実際に何がうまく動作しなかったのかを示しているでしょうか?
TL;DR: 上手に分けられた 3 つのセクションを利用してテストを構成してください: Arrange、Act、そして Assert (AAA) です。まず最初の部分でテストのセットアップを行い、次にテスト対象のユニットの実行、そして最後にアサーションフェーズに入ります。この構造に従うことで、コードを読む人がテストプランを理解するために頭脳の CPU を費やさないことが保証されます。
さもないと: メインコードを理解するのに長時間費やすだけでなく、今までシンプルな部分であったはずのもの(テスト)が、脳のリソースを奪います。
TL;DR: Linter を使用して、コードの基本的な質をチェックし、アンチパターンを早期に検出してください。テスト前に Linter を実行し、コミット前の Git-hook として追加しておけば、レビューや問題を修正するのに必要な時間を最小限に抑えることができます。セクション 3 の「コードスタイルのプラクティス」も参考にしてください。
さもないと: アンチパターンや脆弱性を含む可能性のあるコードを本番環境に渡してしまうかもしれません。
TL;DR: テスト同士が結合してしまうことを防ぎ、テストフローの理解を容易にするために、各テストは独自の DB データ行のセットを用意し、それらを利用して実行されるべきです。テストがいくつかの DB データをプルしたり、その存在を仮定する必要がある場合には、明示的にデータを追加し、他のレコードに変更を加えないようにしなければなりません。
さもないと: テストが失敗したことによってデプロイが中止されるというシナリオを考えてみましょう。チームは貴重な時間を調査に費やし、結果として悲しい結論にたどり着きます: システムは機能していますが、テスト同士が干渉しあって、ビルドを壊しているのです。
🔗 さらに読む: グローバルなテストフィクスチャとシードを避け、テストごとにデータを追加する
TL;DR: Express のような最も評判の良い依存関係にも、既知の脆弱性があります。これは、ビルド毎に CI において実行できる 🔗 npm audit や 🔗 snyk.io といったコミュニティや商用のツールを利用することで、簡単に検査することができます。
さもないと: 専用のツールを使用せずに、コードを安全に保つには、新しい脅威についての情報を、常に追う必要があります。これは非常に面倒です。
TL;DR: 異なるテストは、異なるシナリオ下において実行しなければなりません: I/O の無いクイックスモークテストは、開発者がファイルを保存したりコミットした際に実施し、完全なエンドツーエンドテストは新しいプルリクエストが出されたときに実施する、などです。これは、テストの手綱を掴んで望み通りのテストセットを実行できるように、 #cold #api #sanity といったようにキーワードでテストをタグ付けすることで実現できます。例えば、Mocha を利用して sanity テストグループを実施する方法は次の通りです: mocha --grep 'sanity'
さもないと: 小さな変更をするたびに多くの DB クエリを実施するテストを含む全てのテストを実行することは、非常に遅く、そして開発者がテストを実行しなくなります。
TL;DR: Istanbul/NYC のようなコードカバレッジツールは 3 つの理由から素晴らしいといえます: 無料で提供されている(このレポートの恩恵を受けるために努力は必要ありません)、テストカバレッジの低下を特定するのに役立つ、そして最後に、テストのミスマッチを強調してくれることです。色付けされたコードカバレッジレポートを見ることで、例えば、キャッチ句のようなテストが全く実施されていない領域(つまり、テストがハッピーパスのみテストしていて、エラー時にどのように振る舞うかをテストしていない、ということです)に気づくかもしれません。カバレッジが特定に閾値を下回ったらビルドが失敗するように設定しましょう。
さもないと: コードの大部分がテストでカバーされていないことを教えてくれる、自動化されたメトリックが存在しないことになります。
TL;DR: お気に入りのツール(例えば、「npm outdated」や「npm-check-updates」など)を使って、インストールされたパッケージが古くなっていることを検出し、このチェックを CI パイプラインの中に組み込み、深刻な場合にはビルドを失敗させてください。深刻な場合とは例えば、インストールされているパッケージが 5 回のパッチコミット分遅れている場合(例えば、ローカルバージョンが 1.3.1 でリポジトリバージョンが 1.3.8 である、など)や、作者によって非推奨とタグ付けされている場合などがあります。ビルドをキルして、このバージョンのデプロイを禁止してください。
さもないと: 作者によって明示的に危険であるとタグ付けされたパッケージを、本番環境で実行することになります。
TL;DR: ライブデータを含むエンドツーエンド(e2e)テストは、DB のような複数の重たいサービスに依存するため、CI プロセスにおける最も弱い接続部となっていました。できる限り本番環境に近い環境を使用してください(注意: コンテンツが不足しています。「さもないと」から判断するに、docker-compose について言及されているはずです)
さもないと: docker-compose を使用しない場合、チームは開発者のマシンを含む各テスト環境のためのテスト DB を管理し、環境によって結果に差異が出ないようにそれらすべての DB が同期された状態を保たなくてはなりません。
TL;DR: 静的解析ツールを利用することは、客観的な視点をもたらし、コードの品質向上や保守性の維持に役立ちます。静的解析ツールを CI に追加することで、コードの臭いを発見した際にビルドを失敗させることができます。シンプルな linting に勝るポイントとしては、複数ファイルを含むコンテキストで品質を検査できること(例:重複の検出)、高度な分析を実施できること(例:コードの複雑さ)、そしてコードの問題の履歴や進行状況を追跡できることです。使用できるツールの例としては、Sonarqube (2,600+ stars) と Code Climate (1,500+ stars) の 2 つがあります。
さもないと: コードの品質が低いと、ピカピカの新しいライブラリや最新の機能では修正できない類のバグやパフォーマンスが常に問題となります。
TL;DR: 継続的インテグレーションプラットフォーム (CI/CD) は全ての品質に関わるツール(テストや lint など)をホストするので、プラグインのエコシステムが充実しているはずです。Jenkins は最大のコミュニティを持ち、非常に強力なプラットフォームであるため、多くのプロジェクトでデフォルトとして使われていましたが、複雑なセットアップと多大な学習コストが難点でした。最近では、CircleCI のような SaaS ツールを使用することで、CI ソリューションをセットアップすることが非常に簡単になってきました。こういったツールは、インフラ全体の管理にコストをかけることなく柔軟な CI パイプラインを構築することを可能にします。最終的には、堅牢性とスピードの間のトレードオフとなります ー 慎重にどちらを取るか選んでください。
さもないと: ニッチなベンダーを選択すると、高度なカスタマイズが必要になった際に困るかもしれません。一方で、Jenkins を選択するとインフラのセットアップに貴重な時間を費やすことになる可能性があります。
TL;DR: ミドルウェアが多くのリクエストにまたがる巨大なロジックを保持している場合は、ウェブフレームワーク全体を起動することなく、分離してテストする価値があります。これは、{req, res, next} オブジェクトをスタブ化してスパイすることで容易に達成することができます。
さもないと: Express ミドルウェアにおけるバグ === ほぼ全てのリクエストにおけるバグ
TL;DR: モニタリングとは、顧客よりも先に問題を発見するゲームです。– 明らかに、これは類を見ないほど重要なこととして割り当てられるべきです。市場には多くのオファーが溢れていますので、まずはあなたが守らなければならない基本的な指標を定義することから始めてみてください(私の提案はこの中にあります)。その後、追加の手の込んだ機能を確認し、すべてのボックスにチェックを入れるソリューションを選択します。ソリューションの概要については、以下の「要点」をクリックしてください。
さもないと: 失敗 === 失望したお客さん。シンプルです。
TL;DR: ログは、デバッグ ステートメント用のゴミ倉庫にも、アプリのストーリーを伝える美しいダッシュボードのイネーブラーにもなり得ます。1 日目からロギングプラットフォームを計画しましょう:ログをどのように収集、保存、分析するかで、必要な情報(エラー率、サービスやサーバーを介したトランザクション全体の追跡など)を実際に抽出できるようにします。
さもないと: 推論するのが難しいブラックボックスになってしまい、追加情報を追加するためにすべてのロギングステートメントを書き直します。
TL;DR: Node は gzip や SSL の終了などの CPU 負荷の高いタスクを行うのが非常に苦手です。代わりに nginx, HAproxy, クラウドベンダーのサービスのような「本物の」ミドルウェアサービスを使うべきです。
さもないと: 貧弱なシングルスレッドは、アプリケーションコアを処理する代わりにインフラタスクを行うことに忙しくなり、パフォーマンスはそれに応じて低下します。
🔗 さらに読む: 可能な限りのこと全て( gzip、SSL など)をリバースプロキシに委譲する
TL;DR: コードはすべての環境で同一でなければなりませんが、驚くべきことに npm はデフォルトで環境間で依存関係をドリフトさせることができます。– 様々な環境でパッケージをインストールすると、パッケージの最新のパッチバージョンを取得しようとします。これを克服するには、各パッケージの正確な (最新版ではない) バージョンを保存するように各環境に指示する npm 設定ファイル .npmrc を使用します。あるいは、より細かい制御を行うには npm shrinkwrap
を使用してください。*更新: NPM5 では、依存関係はデフォルトでロックされています。新しいパッケージマネージャー、Yarn もデフォルトでカバーしてくれました。
さもないと: QA はコードを徹底的にテストし、本番環境では異なる挙動をするバージョンを承認します。さらに悪いことに、同じクラスタ内の異なるサーバが異なるコードを実行する可能性があります。
TL;DR: プロセスが進み、失敗した時点で再起動しなければなりません。単純なシナリオでは、PM2 のようなプロセス管理ツールで十分かもしれませんが、今日の「 docker 化」された世界では、クラスタ管理ツールも考慮する必要があります。
さもないと: 明確な戦略を持たずに何十ものインスタンスを実行し、あまりにも多くのツール(クラスタ管理、docker、PM2)を一緒に使いすぎると、DevOps のカオスにつながる可能性があります。
🔗 さらに読む: 適切なツールを使用してプロセスの稼働時間を守る
TL;DR: 基本的な形として、Node アプリは他のすべてがアイドル状態のままで、単一の CPU コア上で動作します。 Node プロセスを複製し、すべての CPU を利用するのはあなたの義務です。 – 中小規模のアプリでは、Node クラスタや PM2 を使用することができます。大規模なアプリケーションでは、Docker クラスタ( K8S や ECS など)や Linux の init システム( systemd など)をベースにしたデプロイスクリプトを使用してプロセスを複製することを検討してください。
さもないと: あなたのアプリは、利用可能なリソースの 25%、もしくはそれ以下しか使用していない可能性が高いです(!)。一般的なサーバは 4 つ以上の CPU コアを持っていますが、 Node.js のナイーブなデプロイでは 1 つしか利用していないことに注意してください( AWS beanstalk のような PaaS サービスを利用している場合でも!)。
TL;DR: メモリ使用量や REPL などのシステム関連情報をセキュアな API で公開します。標準ツールや歴戦のツールに頼ることを強くお勧めしますが、貴重な情報や操作はコードを使った方が簡単にできるものもあります。
さもないと: 多くの「診断デプロイ」を実行していることがわかります。– 診断目的のための情報を抽出するためだけにコードを本番環境に出荷するなど
TL;DR: アプリケーションモニタリングおよびパフォーマンス製品( APM)は、コードベースと API を積極的に測定することで、従来のモニタリングを超えて、サービスや階層間のユーザーエクスペリエンス全体を自動的に測定することができます。例えば、一部の APM 製品では、エンドユーザー側でロードが遅すぎるトランザクションを強調表示しながら、根本的な原因を示唆することができます。
さもないと: API のパフォーマンスやダウンタイムの測定に多大な労力を費やすことになるかもしれません。実世界のシナリオで最も遅いコード部分はどれか、それが UX にどのように影響するのか、おそらくあなたは意識することはないでしょう。
🔗 さらに読む: APM 製品を使用してエラーやダウンタイムを発見する
TL;DR: ゴールを意識してコードを作成し、1 日目から制作計画を立てます。ちょっと漠然としているように聞こえるので、生産保守と密接に関係する開発のヒントをいくつかまとめてみました(下の Gist をクリックしてください)。
さもないと: 世界チャンピオンの IT/DevOps の男でも、下手に書かれたシステムを救うことはできません。
TL;DR: Node.js はメモリとの関係で物議を醸しています: v8 エンジンはメモリ使用量にソフトな制限(1.4 GB )があり、Node のコードにはメモリリークの経路が知られています。– そのため、Node のプロセスメモリを監視することは必須です。小さなアプリでは、シェルコマンドを使って定期的にメモリを測定することができますが、中規模以上のアプリでは、メモリ監視を堅牢な監視システムに組み込むことを検討してください。
さもないと: あなたのプロセスメモリは、Walmart で起こったように、1 日に 10MB もリークするかもしれません。
TL;DR: 専用のミドルウェア (nginx, S3, CDN) を使用してフロントエンドのコンテンツを提供します。なぜなら、シングルスレッドモデルのため、多くの静的ファイルを扱う場合、Node のパフォーマンスは非常に痛手を受けるからです。
さもないと: あなたの Node のシングルスレッドは、何百もの html/images/angular/react ファイルのストリーミングに忙殺され、本来の目的のために生まれたタスクにすべてのリソースを確保することができません。– 動的コンテンツの提供
🔗 さらに読む: フロントエンドの資産を Node から取り出す
TL;DR: 任意のタイプのデータ(ユーザーセッション、キャッシュ、アップロードされたファイルなど)を外部データストア内に保存します。定期的にサーバを「停止する」ことを検討するか、ステートレスな動作を明示的に行う「サーバレス」プラットフォーム(AWS Lambda など)を使用することを検討してください。
Otherwise: 特定のサーバーで障害が発生すると、障害のあるマシンを停止する代わりに、アプリケーションのダウンタイムが発生します。さらに、特定のサーバーに依存しているため、スケーリングアウトの弾力性はより困難になります。
🔗 さらに読む: ステートレスのままで、ほぼ毎日サーバーを停止させる
TL;DR: Express のような最も評判の良い依存関係にも、(時折)システムを危険にさらす既知の脆弱性があります。脆弱性を常にチェックして警告するコミュニティや商用ツール(ローカルまたは GitHub)を使えば簡単に手なずけることができ、いくつかはすぐにパッチを当てることもできます。
さもないと: 専用のツールを使用せずに脆弱性からコードをクリーンに保つには、新しい脅威についてのオンライン出版物を常にフォローする必要があります。とても面倒です。
TL;DR: transaction-id: {任意の値} で、単一のリクエスト内の各ログエントリに同じ識別子を割り当てます。そうすることで、ログのエラーを検査する際に、前後に何が起こったかを簡単に結論付けることができます。残念ながら、非同期の性質上、これを Node で実現するのは容易ではありません。内部のコード例を参照してください。
さもないと: – 前に何が起こったのか – というコンテキストなしでプロダクションのエラーログを見ると、問題の原因を究明するのが非常に難しくなり、時間がかかります。
🔗 さらに読む: 各ログ文に 'TransactionId' を割り当てる
TL;DR: 環境変数 NODE_ENV を「production」または「development」に設定して、本番環境での最適化を有効にするかどうかのフラグを立てます。– 多くの npm パッケージが現在の環境を判断し、本番用にコードを最適化します。
さもないと: この単純なプロパティを省略すると、パフォーマンスが大きく低下する可能性があります。例えば、サーバサイドのレンダリングに Express を使用する場合、NODE_ENV
を省略すると 3 倍も遅くなります。
🔗 さらに読む: NODE_ENV=production を設定する
TL;DR: 調査によると、多くのデプロイを行うチームほど、深刻なプロダクションの問題が発生する確率が低くなることがわかっています。リスクの高い手動ステップやサービスのダウンタイムを必要としない高速で自動化されたデプロイは、デプロイプロセスを大幅に改善します。Docker と CI ツールを組み合わせることが、合理化されたデプロイのための業界標準となったため、これを使って達成する必要があるでしょう。
さもないと: 長時間のデプロイ -> プロダクションのダウンタイムと人為的なミス -> デプロイに自信のないチーム -> デプロイ数と機能の減少
TL;DR: 重要なバグフィックス、セキュリティアップデート、パフォーマンスの改善を受けるために、Node.js の LTS バージョンを使用していることを確認してください。
さもないと: 新たに発見されたバグや脆弱性は、本番環境で運用中のアプリケーションを悪用するために使用される可能性があり、アプリケーションは様々なモジュールでサポートされておらず、保守が困難になる可能性があります。
🔗 さらに読む: Node.js の LTS リリースを使用する
TL;DR: ログの送信先は、アプリケーションコード内で開発者によってハードコーディングされるべきではなく、代わりに、アプリケーションが実行される実行環境によって定義されるべきです。開発者はロガーユーティリティを使って stdout
にログを書き、実行環境 (コンテナやサーバなど) に stdout
のストリームを適切な宛先 (Splunk, Graylog, ElasticSearch など) にパイプさせるべきです。
さもないと: アプリケーションがログのルーティングをハンドリングする === スケールアップが難しい、ログの損失、懸念事項の分離が悪い
TL;DR: 本番環境のコードが、テストしたパッケージの正確なバージョンを使用していることを確認する必要があります。 npm ci
を実行して、package.json と package-lock.json に一致する依存関係のクリーンインストールを厳密に行います。このコマンドは、継続的インテグレーションパイプラインのような自動化された環境で使用することをお勧めします。
さもないと: QA はコードを徹底的にテストし、本番環境では異なる動作をするバージョンを承認します。さらに悪いことに、同じプロダクションクラスタ内の異なるサーバが異なるコードを実行する可能性があります。
TL;DR: eslint-plugin-security のようなセキュリティ関連の linter プラグインを利用して、セキュリティの脆弱性や問題をできる限り早く、できればコーディング段階で捕まえてください。これにより、eval の使用や子プロセスの呼び出し、(ユーザー入力などの)文字列リテラルを持つモジュールのインポートなど、セキュリティ上の弱点をキャッチするのに役立ちます。下記の「さらに読む」をクリックして、セキュリティ linter によって捕捉されるコード例を参照してください。
さもなければ: 開発時には単純なセキュリティ上の弱点だったかもしれないものが、本番環境では大きな問題となります。同様に、プロジェクトが一貫したコードセキュリティプラクティスに従わない場合もあり、脆弱性が入り込んだり、リモートリポジトリに機密情報がコミットされたりする可能性があります。
TL;DR: DOS 攻撃はとても非常に有名で、比較的簡単に実行することができます。クラウドのロードバランサーやファイアウォール、nginx、rate-limiter-flexible パッケージなどの外部サービス、または(小規模で重要度の低いアプリケーションの場合は)レートリミットミドルウェア(express-rate-limit など)などを使用して、レートリミットを実装してください。
さもないと: アプリケーションは攻撃を受ける可能性があり、結果としてユーザーに不十分なサービスを提供したり、サービス停止をしなければならない状況に陥ります。
TL;DR: 設定ファイルやソースコードに平文でシークレットを格納してはいけません。代わりに、Vault 製品や Kubernetes/Docker シークレット、環境変数のようなシークレット管理システムを利用してください。仕方なく、ソースコントロールにシークレットを格納する場合は、暗号化して管理(キーのローリング、有効期限の設定、監査など)をする必要があります。誤ってシークレットをコミットしないように、pre-commit/push hooks を利用してください。
さもないと: ソースコントロールは、たとえプライベートリポジトリであっても誤ってパブリックになる可能性があり、その時点で全てのシークレットが公開されてしまいます。外部サービスに与えられたソースコントロールへのアクセス権限は、関連するシステム(データベース、API、その他サービスなど)へのアクセス権限をうっかり与えてしまうことがあります。
TL;DR: SQL/NoSQL インジェクションやその他の悪意ある攻撃を防ぐために、データをエスケープしたり、名前付きやインデックス付きのパラメータ化されたクエリをサポートしていたり、ユーザー入力が期待する型となっているか検証してくれる O/R マッパ/ODM やデータベースライブラリを活用してください。JavaScript のテンプレート文字列や文字列の結合を使用して値をクエリに挿入してはいけません。これはアプリケーションに広範囲の脆弱性を与えます。全ての評判の良い Node.js データアクセスライブラリ(Sequelize, Knex, mongoose など)はインジェクション攻撃に対してあらかじめ対策されています。
さもないと: 未検証またはサニタイズされていないユーザー入力は、MongoDB のような NoSQL データベースで作業している際にオペレーターインジェクションを招きますし、適切なサニタイズシステムまたは O/R マッパ を利用しないことは容易に SQL インジェクション攻撃を招き、多大な脆弱性を生みます。
🔗 さらに読む: ORM/ODM ライブラリを使用してクエリインジェクション脆弱性を防ぐ
TL;DR: Node.js とは直接関係のないセキュリティに関するアドバイス集です ー Node における実装は他の言語とあまり違いはありません。さらに読むをクリックして、読み進めてください。
TL;DR: クロスサイトスクリプティング(XSS)やクリックジャッキング、その他の悪意のある攻撃などの一般的な攻撃を攻撃者が行うことを防ぐために、アプリケーションは安全なヘッダを使用するべきです。これらは helmet のようなモジュールを使って簡単に設定することができます。
さもないと: 攻撃者がアプリケーションユーザーに対して直接攻撃を行い、甚大なセキュリティ脆弱性につながる可能性があります。
🔗 さらに読む: アプリケーションでセキュアなヘッダーを利用する
TL;DR: npm のエコシステムでは、プロジェクトにおいて多くの依存関係があることが一般的です。新たな脆弱性が発見された場合には、依存関係を常にチェックしておくべきです。npm audit や snyk のようなツールを利用して、脆弱性のある依存関係を追跡、監視し、パッチを適用しましょう。これらのツールを CI セットアップと統合することで、本番環境にデプロイされる前に脆弱性のある依存関係を発見することができるでしょう。
さもないと: 攻撃者がウェブフレームワークを特定して、全ての既知の脆弱性を突いてくる可能性があります。
TL;DR: パスワードやシークレット(API キー)は、JavaScript の実装においてパフォーマンス面・セキュリティ面で優れた選択肢である bcrypt
のようなセキュアなハッシュ+ソルト関数を利用して保存するべきです。
さもないと: セキュアな関数を使わずに永続化されたパスワードやシークレット情報は、ブルートフォース攻撃や辞書攻撃に弱く、結果として情報漏えいに繋がります。
TL;DR: ブラウザに送信された信頼されていないデータは、ただ表示される代わりに実行される可能性があり、これは一般的にクロスサイトスクリプティング(XSS)攻撃と呼ばれています。これを軽減するには、データを、実行されるべきではない純粋なコンテンツとして明示的にマークする専用のライブラリを使用します(エンコーディング、エスケープなど)。
さもないと: 攻撃者は悪意のある JavaScript のコードを DB に保存し、それをそのまま脆弱なクライアントに送信する可能性があります。
TL;DR: 受信したリクエストの body ペイロードを検証し、期待する条件を満たすことを確認し、期待通りでない場合はすぐに失敗するようにしてください。各ルート内での面倒な検証コードの実装を避けるために、jsonschema や joi のような、軽量の JSON ベースの検証ライブラリを利用するかもしれません。
さもないと: あなたの寛大で寛容なアプローチは攻撃対象を大幅に拡大させ、攻撃者がアプリケーションをクラッシュさせるための組み合わせを見つけるまで、多くの入力を試してみるように促すことに繋がります。
TL;DR: JSON Web Token を(例えば Passport.js などを用いて)利用する場合、デフォルトでは、発行されたトークンからのアクセスを無効にする仕組みはありません。悪意のあるユーザーのアクティビティを発見したとしても、そのユーザーが有効なトークンを持っている限り、システムへのアクセスを止めることはできません。各リクエストで検証される、信頼されていないトークンのブラックリストを実装することで、この問題を緩和することができます。
さもないと: 期限切れや、誤って配置されたトークンは、アプリケーションにアクセスしたり、トークンの所有者になりすますために、サードパーティによって悪意を持って利用される可能性があります。
🔗 さらに読む: JSON Web Token のブラックリスト
TL;DR: シンプルで強力なテクニックは、次の 2 つのメトリクスを用いて認証の試行回数を制限することです:
- 同じユーザー固有の ID/名前、そして IP アドレスからの連続失敗回数
- ある IP アドレスからの長い期間の失敗回数。例えば、1 日で 100 回失敗した IP アドレスをブロックする
さもないと: 攻撃者が、アプリケーションの特権アカウントへのアクセス権を得るために、無制限の自動化されたパスワード試行を行うことができます。
TL;DR: Node.js が無制限の権限を持った root ユーザとして実行されるという一般的なシナリオがあります。例えば、Docker コンテナにおけるデフォルトの挙動です。root ではないユーザを作成して Docker イメージに組み込む(例は以下にあります)か、"-u username" フラグを利用してコンテナを起動することで、非 root ユーザでプロセスを実行することが推奨されます。
さもないと: サーバ上でスクリプトを実行することに成功した攻撃者が、ローカルマシンにおける無制限の権限を獲得してしまいます(例:iptable を変更して、攻撃者のサーバに再ルーティングする)。
🔗 さらに読む: 非 root ユーザとして Node.js を実行する
TL;DR: Body ペイロードが大きければ大きいほど、シングルスレッドでの処理が重くなります。これは、攻撃者にとっては、膨大なリクエストを送信(DoS/DDoS 攻撃)せずともサーバーをダウンさせることができる機会となります。エッジ(例:firewall、ELB)で受信するリクエストのボデサイズを制限する、もしくは express body parser を用いて小さいサイズのペイロードのみを受け付けることで、緩和してください。
さもないと: アプリケーションは大きなリクエストを処理しなければなくなり、他の重要な仕事を完遂させることができず、パフォーマンスへの影響や DDoS 攻撃に対する脆弱性につながります。
TL;DR: eval
は、ランタイムにおいてカスタム JavaScript コードの実行を許可しているため、有害です。これは単にパフォーマンス的な懸念だけではなく、ユーザーの入力を元にした悪意のある JavaScript コードのために、重大なセキュリティ的な懸念でもあります。その他の避けるべき言語仕様は、new Function
コンストラクタです。setTimeout
と setInterval
も動的な JavaScript コードに渡されるべきではありません。
さもないと: 悪意のある JavaScript コードが eval
やその他のリアルタイムに評価する JavaScript の関数に渡されるテキストへたどり着き、そのページにおける JavaScript の完全な権限を獲得してしまいます。この脆弱性はしばしば XSS 攻撃として顕在化します。
🔗 さらに読む: JavaScript の eval 構文を避ける
TL;DR: 正規表現(RegEx)は便利ですが、JavaScript アプリケーション全体、特に Node.js プラットフォームに対して真の脅威となります。テキストのユーザー入力をマッチさせることは、処理に大量の CPU サイクルを必要とするかもしれません。RegEx の処理は、10 Word を検証する単一のリクエストが 6 秒間イベントループ全体をブロックし、CPU に 🔥 を点けるほどには非効率であるかもしれません。そのため、独自の RegExp パターンを記述する代わりに validator.js のようなサードパーティ検証パッケージを利用するか、脆弱な正規表現パターンを検出するために safe-regex を利用するようにしましょう。
さもないと: 下手な正規表現の記述は、イベントループを完全にブロックしてしまう正規表現 DoS 攻撃の影響を受ける可能性があります。例えば、人気のある moment
パッケージでは、2017 年 11 月に悪意のある RegEx の使用による脆弱性が発見されています。
TL;DR: ユーザー入力が起因となって問題が生じる恐れがあるため、パラメータとして与えられたパスを用いて他のファイルを require/import しないようにしてください。この原則は、ユーザー入力に基づいた動的な変数を用いた、一般的なファイルアクセス(fs.readFile()
など)やその他のセンシティブなリソースアクセスにも拡張することができます。Eslint-plugin-security linter はそのようなパターンを検知して、早期に警告を出すことができます。
さもないと: 悪意のあるユーザー入力は、既存のシステムファイルに、前にファイルシステムにアップロードされたファイルのような改変されたファイルを要求したり、既存のシステムファイルにアクセスするために利用されるパラメータを操作する可能性があります。
TL;DR: ランタイムにおいて与えられた外部のコード(プラグインなど)を実行するようなタスクを行うとき、独立していて、メインコードをプラグインから保護する任意の「サンドボックス」実行環境を使用してください。これは、専用のプロセス(cluster.fork()
など)やサーバーレス環境、またはサンドボックスとして動作する専用の npm パッケージを使用することで実現できます。
さもないと: プラグインは、無限ループやメモリーオーバーロード、センシティブなプロセスの環境変数へのアクセスなど、あらゆる手段を通じて攻撃可能となります。
🔗 さらに読む: サンドボックス内で安全でないコードを実行する
TL;DR: 可能なら子プロセスの仕様を避け、それでも使用する必要がある場合には、入力を検証しサニタイズすることで、シェルインジェクション攻撃を軽減してください。属性の集合を持つ単一コマンドのみを実行し、シェルパラメータ拡張を許可しない child_process.execFile
の使用を優先してください。
さもないと: 子プロセスを愚直に利用することは、サニタイズされていないシステムコマンドに渡される悪意のあるユーザー入力が原因となって、結果としてリモートからのコマンド実行、またはシェルインジェクション攻撃を受けることにつながります。
TL;DR: 統合されている express のエラーハンドラはデフォルトでエラーの詳細を隠します。しかし、(多くの人がベストプラクティスだと考えている)カスタムエラーオブジェクトを使用して独自のエラー処理ロジックを実装する可能性は大いにあります。その場合、クライアントに、機密なアプリケーション詳細を含む恐れのあるエラーオブジェクト全体を返さないようにしてください。
さもないと: 攻撃者によって悪用される可能性のある、サーバファイルのパス、使用中のサードパーティモジュール、アプリケーションのその他内部ワークフローなど、機密性の高いアプリケーションの詳細情報が、スタックトレース内に残された情報から漏洩する可能性があります。
TL;DR: 開発におけるどのステップも、MFA(多要素認証)で保護されるべきです。npm/Yarn は開発者のパスワードに触れることができる攻撃者にとって絶好の機会となります。開発者の認証情報を利用して、プロジェクトやサービスにおいて広くインストールされている攻撃者は悪意のあるコードを注入することができます。もしパブリックに公開されている場合は、ウェブ全体にまで及ぶかもしれません。npm において 2 要素認証を設定することで、攻撃者がパッケージコードを改ざんする可能性はほぼゼロになります。
TL;DR: それぞれの Web フレームワークや技術には、既知の弱点があります - 攻撃者に対してどの Web フレームワークを利用しているかを伝えることは、攻撃者にとって大きな助けになることです。セッションミドルウェアのデフォルトの設定を利用することは、X-Powered-By
ヘッダー同様に、アプリケーションをモジュールやフレームワーク固有のハイジャック攻撃にさらす可能性があります。技術スタック(例:Node.js、express など)を識別したり、明らかにするものは極力隠すようにしてください。
さもないと: Cookie は安全でないコネクションを通じて送信される恐れがあり、攻撃者はセッション識別子を利用して背後にある Web アプリケーションフレームワークや、モジュール固有の脆弱性を特定する可能性があります。
🔗 さらに読む: クッキー(Cookie)とセッションの安全性
TL;DR: エラーが処理されなかった場合、Node プロセスはクラッシュします。多くのベストプラクティスは、たとえエラーがキャッチされ処理されたとしても、exit することを推奨しています。例えば、Express は、非同期エラーが発生すると、ルートをキャッチ節でラップしない限りクラッシュします。これは、どんな入力がプロセスをクラッシュさせるのかを認識し、繰り返し同様のリクエストを送信する攻撃者にとって絶好の攻撃スポットを提供します。この問題に対する即席の対策はありませんが、いくつかのテクニックはペインを軽減することができます: 処理されていないエラーによってプロセスがクラッシュした場合は常に重大な警告を出す、入力を検証して無効なユーザー入力によるプロセスのクラッシュを回避する、すべてのルートをキャッチで囲み、(グローバルに発生した場合とは対象的に)リクエスト内でエラーが発生した際にクラッシュしないように考慮する、などです。
さもないと: これはあくまで経験に基づいた推測ですが、多くの Node.js アプリケーションを見たときに、すべての POST リクエストにおいて空の JSON ボディを渡そうとすると、一部のアプリケーションはクラッシュしてしまいます。その時点で、同じリクエストを繰り返し送信することによって簡単にアプリケーションを停止させることができます。
TL;DR: ユーザー入力を検証しないリダイレクトは、攻撃者がフィッシング詐欺をしたり、ユーザーの認証情報を盗んだり、その他の悪質なアクションを実行することを可能にします。
さもないと: もし攻撃者が、外部のユーザーから与えられた入力を検証していないことを発見した場合、特別に作成されたリンクをフォーラムやソーシャルメディア、その他のパブリックな場所に投稿してユーザーにクリックさせることで、この脆弱性を悪用する恐れがあります。
TL;DR: 誤ってシークレットをパブリック npm レジストリに公開してしまうリスクを回避するように、注意を払ってください。.npmignore
ファイルを利用して、特定のファイルやフォルダをブラックリスト化したり、package.json
内の files
配列をホワイトリストとして利用することができます。
さもないと: プロジェクトの API キーやパスワード、その他のシークレットが公開され、その情報を目にしたすべての人に悪用されることで、結果として金銭的な損失、なりすまし、その他リスクに繋がってしまいます。
Our contributors are working on this section. Would you like to join?
TL;DR: CPU 集約的なタスクは、主にシングルスレッドのイベントループをブロックし、専用のスレッド、プロセス、またはコンテキストに基づいて別の技術にそれらをオフロードするため、避けてください。
さもないと: イベントループがブロックされると、Node.js は他のリクエストを処理することができなくなり、同時接続ユーザーの遅延を引き起こします。3000 人のユーザーがレスポンスを待っていて、コンテンツを提供する準備ができていたとしても、1 つのリクエストがサーバからの結果のディスパッチをブロックしていしまいます*
TL;DR: ネイティブメソッドよりも lodash
や underscore
のようなユーティリティライブラリを使う方が、不要な依存関係やパフォーマンスの低下につながるため、よりペナルティが大きいことがよくあります。
新しい ES 標準と一緒に新しい V8 エンジンが導入されたことで、ネイティブメソッドが改善され、ユーティリティライブラリよりも約 50% のパフォーマンスが向上したことを覚えておいてください。
さもないと: すでに利用可能なものを単純に使用できたり、いくつかのファイルと引き換えに、数行で処理することができるような、パフォーマンスの低いプロジェクトを維持しなければならないでしょう。
🔗 さらに読む: ユーザーランドなユーティリティよりもネイティブを使用する
🏅 Bret Fisher氏からは、次のような実践を多く学ぶことができました。感謝します。
TL;DR: マルチステージビルドを使用して、必要な本番環境のアーティファクトだけをコピーします。多くのビルド時の依存関係やファイルは、アプリケーションの実行には必要ありません。マルチステージビルドでは、これらのリソースをビルド中に使用することができ、ランタイム環境には必要なものだけが含まれています。マルチステージビルドは、過剰な負荷とセキュリティの脅威を取り除く簡単な方法です。
さもないと: イメージが大きくなるとビルドとリリースに時間がかかり、ビルド専用ツールには脆弱性が含まれている可能性があり、ビルド段階でのみ使用されるシークレット情報が漏洩する可能性があります。
FROM node:14.4.0 AS build
COPY . .
RUN npm ci && npm run build
FROM node:slim-14.4.0
USER node
EXPOSE 8080
COPY --from=build /home/node/app/dist /home/node/app/package.json /home/node/app/package-lock.json ./
RUN npm ci --production
CMD [ "node", "dist/app.js" ]
TL;DR: アプリの起動には CMD ['node','server.js']
を使用し、OS のシグナルをコードに渡さない npm スクリプトの使用は避けてください。これにより、子プロセス、シグナル処理、グレースフルシャットダウン、ゾンビプロセスの問題を防ぐことができます。
さもないと: シグナルが渡されない場合、あなたのコードはシャットダウンについて通知されることはありません。これがなければ、適切に閉じる機会を失い、現在のリクエストやデータを失う可能性があります。
さらに読む: node コマンドを使用して 、npm の起動を避けた Bootstrap コンテナ
TL;DR: Docker ランタイムオーケストレーター(Kubernetes など)を使用する場合は、中間プロセスマネージャやプロセスを複製するカスタムコード(PM2、Cluster モジュールなど)を使用せずに、Node.js プロセスを直接呼び出します。ランタイムプラットフォームは、配置の決定を行うためのデータ量と可視性が最も高く、必要とされるプロセスの数、それらをどのように分散させ、クラッシュが発生した場合にどうすればよいかを最もよく知っています。
さもないと: リソース不足でクラッシュし続けるコンテナは、プロセスマネージャによって無期限に再起動されてしまいます。Kubernetes がそれを認識していれば、別の余裕のあるインスタンスに移すことができます。
🔗 さらに読む: プロセスを再起動と複製を Docker オーケストレーターに任せる
TL;DR: 一般的な秘密ファイルや開発成果物をフィルタリングする .dockerignore
ファイルを含めてください。そうすることで、秘密がイメージに漏れるのを防ぐことができるかもしれません。ボーナスとして、ビルド時間が大幅に短縮されます。また、すべてのファイルを再帰的にコピーするのではなく、何を Docker にコピーするかを明示的に選択するようにしてください。
さもないと: .env
, .aws
, .npmrc
のような共通の個人秘密ファイルは、イメージにアクセスできる人全員に共有されます (例: Docker リポジトリ)。
TL;DR: ビルドやテストのライフサイクルの中で Dev-Dependencies が必要になることもありますが、最終的には本番に出荷されるイメージは、開発の依存関係を最小限に抑え、クリーンなものにしなければなりません。このようにすることで、必要なコードのみが出荷され、潜在的な攻撃の量 (すなわち攻撃の表面) が最小限に抑えられることが保証されます。マルチステージビルドを使用する場合 (専用の箇条書きを参照)、最初にすべての依存関係をインストールし、最後に npm ci --production
を実行することでこれを実現できます。
さもないと: 悪名高い npm のセキュリティ侵害の多くは開発パッケージ内で発見されました (例: eslint-scope
🔗 さらに読む: 開発依存性の除去
TL;DR: プロセス SIGTERM イベントを処理し、既存のすべての接続とリソースをクリーンアップします。これは進行中のリクエストに応答している間に行う必要があります。Docker 化されたランタイムでは、コンテナのシャットダウンは珍しいイベントではなく、むしろルーチン作業の一部として頻繁に発生します。これを実現するためには、いくつかの可動部分をオーケストレーションするための思慮深いコードが必要です: ロードバランサ、キープアライブ接続、HTTP サーバ、その他のリソースです。
さもないと: 即座に kill してしまうことは、何千人もの失望したユーザーに対応しないことを意味します。
TL;DR: Docker と JavaScript のランタイムフラグの両方を使用して、常にメモリ制限を設定してください。Docker の制限は、コンテナ配置の思慮深い判断をするために必要であり、--v8 のフラグ max-old-space は、GC を時間通りにキックオフし、メモリ使用率の低下を防ぐために必要です。実質的には、v8 の古いスペースメモリをコンテナの制限値より少しだけ小さく設定します。
さもないと: docker の定義は、思慮深いスケーリングの決定を行い、他の市民を飢えさせないようにするために必要です。v8 の制限を定義しないと、コンテナリソースを十分に利用できません。 - 明示的な指示がないと、ホストリソースの ~50-60% を利用するときにクラッシュします。
🔗 さらに読む: Docker のみを使用してメモリ制限を設定する
TL;DR: 正しく行えば、ほぼ瞬時にキャッシュから Docker イメージ全体をリビルドすることができます。あまり更新されない処理は Dockerfile の上の方に記述し、更新が多い処理(アプリケーションのコードなど)は下の方に記述するべきです。
さもないと: Docker のビルドが非常に長くなり、小さな変更をした場合でも多くのリソースを消費することになります。
TL;DR: 明示的なイメージダイジェスト、またはバージョンラベルを指定し、latest
を参照しないようにしてください。開発者はしばしば、latest
タグを指定することでリポジトリ内の最新のイメージが提供されると思い込みがちですが、そうではありません。ダイジェスト(digest)を利用することで、サービスのすべてのインスタンスが全く同じコードを実行していることが保証されます。
さらに、イメージタグを参照することは、決定論的インストールにおいてイメージタグを頼りにすることができないために、ベースイメージが変更される可能性があることを意味します。代わりに、決定論的なインストールが想定される場合には、SHA256 ダイジェストを使用して正確なイメージを参照することができます。
さもないと: 破壊的変更を含むベースイメージの新しいバージョンが本番環境にデプロイされ、意図しないアプリケーションの挙動を引き起こす可能性があります。
🔗 さらに読む: イメージタグを理解して「latest」タグを注意して使う
TL;DR: 大きなイメージは、脆弱性にさらされる可能性を高め、リソースの消費量を増加させます。Slim や Alpine Linux のような、スリムな Docker イメージを使うことで、この問題を軽減することができます。
さもないと: イメージのビルド、プッシュ、プルに時間を要し、未知の攻撃の因子が悪意のあるアクターによって使用され、より多くのリソースが消費されます。
TL;DR: Docker のビルド環境からシークレットが漏洩することを避けてください。Docker イメージは一般的に、 CI や本番環境ほどサニタイズされていないレジストリといった複数の環境で共有されます。典型例としては、通常 dockerfile に引数として渡される npm トークンがあります。このトークンは必要となったタイミング以降も残り続け、攻撃者がプライベート npm レジストリにアクセスすることを無期限に許可することになります。これは、シークレットを .npmrc
のようなファイルにコピーしてマルチステージビルドを用いてそれを削除する(ビルド履歴も削除するべきであることに注意してください)か、トレースを残さない Docker build-kit のシークレット機能を利用することで回避することができます。
さもないと: CI と docker レジストリへのアクセス権限を持っている人は誰でも、おまけとして貴重な組織の情報にアクセスできてしまいます。
🔗 さらに読む: ビルド時のシークレットをクリーンアウトする
TL;DR: コード依存関係の脆弱性をチェックすることに加えて、プロダクションで利用される最終的なイメージもスキャンするようにしてください。Docker イメージスキャナはコード依存関係だけでなく、OS のバイナリもチェックします。この E2E セキュリティスキャンはより多くの領域をカバーし、ビルド中に悪者が悪い因子を注入していないことを確認します。そのため、デプロイ前の最後のステップとしてこれを実行することをおすすめします。CI/CD プラグインも提供している、無料または商用のスキャナがいくつか存在します。
さもないと: コードは脆弱性から完全に脆弱性から解放されているかもしれませんが、アプリケーションで一般的に使用されている OS レベルのバイナリ(例:OpenSSL、TarBall)の脆弱性が原因となって、ハッキングされる可能性が依然としてあります。
🔗 さらに読む: プロダクションの前にイメージ全体をスキャンする
TL;DR: コンテナに依存関係をインストールした後は、ローカルのキャッシュを削除してください。今後のインストールを高速化することを目的として依存関係を複製しても、意味はありません - Docker イメージは不変です。一行のコードで、数十 MB(通常は画像サイズの 10~50%)を削ることができます。
さもないと: 使用されないファイルが原因で、サイズが 3 割増のイメージがプロダクションにデプロイされることになります。
🔗 さらに読む: NODE_MODULE キャッシュをクリーンアップする
TL;DR: Node.js とは直接関係の無い、Docker に関するアドバイス集です - Node における実装は他の言語とあまり変わりません。「さらに読む」から読み進めてください。
TL;DR: Dockerfile を linting することは、ベストプラクティスとは異なってしまっている Dockerfile の問題点を特定するための重要なステップです。Docker 専用の linter を使って潜在的な欠落をチェックすることで、パフォーマンスとセキュリティの改善可能箇所を容易に特定することができ、無駄な時間を削り、またプロダクションコードにおけるセキュリティの問題から解放してくれます。
さもないと: Dockerfile の作者が誤って root を本番ユーザーにしてしまい、不明なソースリポジトリからの Docker イメージを使用してしまう、といったことが起こり得ます。これは、シンプルな litner を利用することで回避することができます。
このガイドを維持し最新に保つために、私たちはコミュニティの助けを借りながらガイドラインとベストプラクティスを常に更新、改良しています。このプロジェクトに貢献したい場合は、私たちのマイルストーンをチェックしたり、ワーキンググループに参加することができます。
すべての翻訳はコミュニティによって支えられています。完成済みの翻訳、進行中の翻訳、または新たな翻訳のいずれにおいても、サポートを受けられたら幸いです!
- Brazilian Portuguese - Courtesy of Marcelo Melo
- Chinese - Courtesy of Matt Jin
- Russian - Courtesy of Alex Ivanov
- Polish - Courtesy of Michal Biesiada
- Japanese - Courtesy of Yuki Ota, Yuta Azumi
- Basque - Courtesy of Ane Diaz de Tuesta & Joxefe Diaz de Tuesta
- French (Discussion)
- Hebrew (Discussion)
- Korean - Courtesy of Sangbeom Han (Discussion)
- Spanish (Discussion)
- Turkish (Discussion)
ステアリングコミッティーのメンバーをご紹介します。このプロジェクトのガイダンスと将来の方向性を提供するために協力してくださっている方々です。さらに、コミッティーの各メンバーは、GitHub プロジェクトで管理されているプロジェクトをリードしています。
アメリカ、ヨーロッパ、イスラエルにおいて、大規模な Node.js アプリケーション開発をサポートするコンサルタント。上記の多くのベストプラクティスは当初 goldbergyoni.com で公開されました。Yoni への連絡は @goldbergyoni または me@goldbergyoni.com までお願いします。
💻 フルスタック web エンジニア、Node.js & GraphQL の熱狂的なファン
ニュージーランドを拠点とするフルスタックデベロッパー & サイトリライアビリティエンジニア。Web アプリケーションセキュリティや、グローバルスケールで稼働する Node.js アプリケーションの構築に関心があります。
Ops や自動化に関心のあるフルスタックデベロッパー。
JavaScript とそのエコシステム(React、Node.js、TypeScript、GraphQL、MongoDB など、システムのあらゆるレイヤーで JS/JSON が関係するものであるなら何でも)の専門家。世界で最も認知されているブランドのために Web プラットフォームを利用してプロダクトを構築しています。Node.js ファウンデーションの個人メンバー。
すべてのコラボレーターの方々に感謝いたします! 🙏
私たちのコラボレーターは、新たなベストプラクティスの提案やイシューの優先順位付け、プルリクエストのレビューなどその他多くのことを通じて、定期的にこのリポジトリに貢献してくださっているメンバーの方々です。多くの人々がより良い Node.js アプリケーションを構築できるように導く私たちをサポートすることにもし興味があるのであれば、貢献ガイドラインをお読み下さい 🎉
Ido Richter (Founder) | Keith Holliday |
Refael Ackermann |
もしオープンソースに貢献したいと思ったことがあるのなら、いまがチャンスです! 詳細は貢献ドキュメントを参照してください。
このリポジトリに貢献してくれた素晴らしい方々に感謝いたします。