fbpx

DockerでNode.jsアプリをイイ感じに保つ4つの方法 #docker

この記事は1年以上前に投稿されました。情報が古い可能性がありますので、ご注意ください。

本記事はDockerキャプテンのBret Fisher氏によるゲスト投稿です。彼はDevOpsシステム管理者を長年務め、Docker Mastery for Node.jsを含む人気のDocker Masteryコース、毎週のYouTube Live、Dockerを採用している企業へのコンサルティングなど、コンテナスキルについて講演しています。Bret氏がNode.jsとDockerに関するデモや質疑応答を行う8月28日のオンラインミートアップに参加してみてください。


誰でもお気に入りの言語やフレームワークがあるはずです、私にとってはNode.jsが一番です。私は初期からミッションクリティカルなアプリでNode.jsをDocker化して動かしていました。 私の使命は、このフレームワークやnpm、Yarn、Dockerでのnodemonのような機能を最大限に活用する方法について、皆さんを教育することです。

DockerでNode.jsを使う方法について大量の情報がありますが、 そのほとんどは時代遅れになっています。 ここではNode.js 10以上とDocker 18.09以上向けの設定を最適化する方法をご案内します。DockerCon 2019の私のセッションでこれらのトピックなどをカバーしているので、YouTubeも見てみてください

では、Node.jsコンテナをイイ感じにするための4つの手順を始めましょう! 必要に応じて"Too Long; Didn't Read"(TL;DR, 要約)を含めています。

現在のベースディストリビューションの固定

TL;DR: もしNode.jsアプリをコンテナに移行しようとしているなら、現在本番環境で使っているホストOSと同じベースイメージを使いましょう。私のお気に入りのベースイメージは、node:alpineよりも公式のnode:slimです。node:alpineはよいものですが、通常利用するには多くの作業が必要で、制限もあります。

Node.jsアプリをDocker化しようとする人が最初に問題とする点の1つは、 "Node.jsのDockerfileは、どのベースイメージをFROMとしたらよいですか?" です。

これには議論の余地がある複数の要素を含んでいます。しかし、IoTや組み込みデバイスなど常にサイズを気にするのでない限り、「イメージサイズ」を最高優先度に置いてはいけません。 近年、slimイメージは150MBまでサイズが小さくなっており、かつさまざまな状況で最もよく動作するようになっています。 Alpineはとても小さなコンテナディストリビューションで、最小のnodeイメージはたった75MBです。しかし、パッケージマネージャをaptからapkに取り替えたり、 特別な状況 に対応したり、 セキュリティスキャンの制限 を回避したりする努力をするくらいなら、ほとんどの場合node:alpineをお勧めしません。

コンテナ技術を採用するときは、他の事と同じように、変えなければいけないことを減らしたいでしょう。たいへん多くの新しい技術や手順がコンテナと一緒にやってきます。 開発チームと運用チームが最も利用するベースイメージを選択することは、多くの予期せぬ恩恵があります。 よって、それが理に適っているときは固定しましょう。この考え方はCentOSやUbuntuなどのカスタムイメージを作るときでさえも通用します。

Nodeモジュールの取り扱い

TL;DR: 正しいローカル開発のためのいくつかのルールに従う限り、コンテナ内でnode_modulesを再配置する必要はありません。2つ目の方法は、Dockerfile内でnode_modulesディレクトリを上のほうに配置し、コンテナを正しく設定し、最も柔軟な方法を提供することです。ただし、これはすべてのnpmフレームワークで動作するとは限りません。

私達はいまや、アプリ内で実行するコードのすべてを書くわけではない、という世界に慣れています。つまり、アプリフレームワークの依存性を取り扱うことを意味します。 よくある疑問の1つは、アプリのサブディレクトリにあるコードの依存性を、コンテナ内でどのように扱うか、ということです。 開発用のローカルのバインドマウントは、アプリにさまざまな影響を与えます。それらの依存関係はホストOS上で動作するように意図されているもので、コンテナ内ではありません。

Node.jsにおけるこの問題の本質は、node_modulesはホストOS用にコンパイルされたバイナリを含みうるということです。もしホストOSがコンテナOSと異なっていたら、アプリを実行しようとして開発用ホストからnode_modulesをバインドマウントしたらエラーになるかもしれません。 あなたが純粋なLinux開発者で、Linux x64用にLinux x64で開発しているなら、このバインドマウント問題が常に影響するとは限らないことに注意しましょう。

Node.jsでの2つの方法をご紹介します。それぞれ利点と制限事項があります:

解決策A: シンプルにしておこう

node_modulesを動かさないでください。コンテナ内のアプリのデフォルトのサブディレクトリ内に置いたままにしておきましょう。これは開発中にコンテナ内で使うnode_modulesを、ホスト上に作ることを禁止しなければならないということを意味します。

これは純粋にDocker開発を行うときの私の好みの方法です。 ローカル開発におけるいくつかのルールに従うだけで、とてもうまくいきます:

  1. コンテナを通してのみ開発しましょう。それはなぜなのでしょう。基本的に、ホスト上のnode_modulesと、コンテナ内のnode_modulesをまぜこぜにしたくないでしょう。macOSとWindowsでは、Docker DesktopがOSの垣根を越えてコードをバインドマウントします。これがホストOS用にnpmでインストールしたコンテナOS内では実行できないバイナリの問題を引き起こしてしまいます。
    2.すべてのnpmコマンドをdocker-compose経由で実行しましょう。これは、プロジェクト用の最初のnpm installはdocker-compose run <サービス名> npm installであるべきだということを意味します。

解決策B: コンテナ側のモジュールを動かして、ホスト側のモジュールを隠す

Dockerfile内でのnode_modulesのファイルパスを再配置することで、コンテナの内外でNode.jsの開発が可能となります。ホストネイティブ開発とDockerベースの開発の両者を切り替えても依存関係は壊れません。

Node.jsは複数のOSやアーキテクチャで動作するように設計されているので、常にコンテナ内で開発する必要はありません。 もしNode.jsアプリを直接ホスト上で開発・動作するときがあったり、それをローカルコンテナで起動するときがあったりする柔軟性が必要なら、解決策Bを取るとよいでしょう。
この場合、ホスト上のnode_modulesはホストのOS用にビルドしている必要があり、コンテナ内のLinux用に別のnode_modulesが必要です。

この解決策のルールは次の通りです:

  1. node_modulesをコンテナイメージ内のディレクトリに移動しましょう。Node.jsは常に、node_modulesをサブディレクトリとして探します。ただし、もしも見つからなければ、見つかるまでディレクトリパスを探していきます。Dockerfile内でこれを行う例はこちらです
  2. ホストのnode_modulesサブディレクトリをコンテナ内に見せることを防ぐには、"空のバインドマウント"と呼んでいる回避策を使いましょう。これによってホストのnode_modulesをコンテナ内から使えないようにします。Compose YAMLは、このようになるでしょう
  3. これはほとんどのNode.jsコードで動作します。ただし、ある大きなフレームワークやプロジェクトは、node_modulesがサブディレクトリであることを仮定したハードコーディングを行っているようですので、この解決策を使用できないことがあります。

これらの解決策はどちらも、常にnode_modulesを.dockerignoreファイルに追加することを忘れないようにしましょう(.gitignoreと同じ書式です)。そうすることで、間違ってホスト側のnode_modulesでイメージを構築してしまわないようにします。常にイメージ構築 内で npm installを実行するようにしましょう。

nodeユーザを使い、最小権限で実行

すべての公式Node.jsイメージは、上流イメージでnodeというLinuxユーザを追加しています。 このユーザはデフォルトでは使われていません。つまり、Node.jsアプリはデフォルトではコンテナ内でrootとして動作するということを意味します。 権限はコンテナ内に隔離されているので、これは悪いことではありません。しかし、Node.jsをrootで実行する必要がないプロジェクトすべてで有効にしておくべきです。Dockerfileに次の1行を追加するだけです: USER node

これを使うためのルールは次の通りです:

  1. Dockerfile内での位置が重要です。apt/yum/apkコマンドの後、かつ 通常は npm installコマンドの前にUSERを追加します。
  2. これはすべてのコマンドに効果があるわけではありません。例えばCOPYのような、コピーするファイルの所有者を制御する書式を持っているものがあります。
  3. 必要ならばUSER rootとすることで通常は元に戻すことができます。これが必要となる、より複雑なDockerfileでは、例えばこのマルチステージビルドの例のように、オプションステージ中にテストとセキュリティスキャンの実行を含んでいます。
  4. コンテナ内はデフォルトの非rootユーザで実行するようになっているので、開発中のパーミッションは複雑になりがちです。npm installのようなものを実行するときに対応しなければいけないことは、一時的にrootでコマンドを実行したいとDockerに伝えることです: docker-compose run -u root npm install

本番環境でプロセスマネージャを使わない

TL;DR: ローカル開発を除き、nodeバイナリ起動コマンドをラッピングしてはいけません。npmやnodemonなどを使ってはいけません。DockerfileのCMDは ["node", "file-to-start.js"] のようにしましょう。そうするとコンテナの管理・置き換えが簡単になります。

nodemonやその他の"ファイル監視ツール"は開発中には必要ですが、Node.jsアプリにDockerを採用する最も大きな利点の1つは、pm2、nodemon、forever、systemdなどサーバ用に使っていたツールの作業をDockerが肩代わりしてくれることです。

Docker、Swarm、Kubernetesはヘルスチェックを実行したり、もしコンテナに障害が起きたら再起動・再作成を行ったりする役目を担っています。また、オーケストレータはアプリのレプリカ数をスケールするpm2やforeverが行っていたような役目も担っています。 なお、Node.jsはほとんどの場合はシングルスレッドであることを忘れないでください。単一サーバ上であっても複数CPUの利益を得るために、おそらく複数のコンテナレプリカを起動したくなるでしょう。

私の例のレポジトリでは、Dockerfile内で直接nodeバイナリを使う方法をご覧いただけます。そしてローカル開発では、docker build --target <ステージ名>で異なるイメージステージを構築するか、compose YAMLでCMDを上書きするかしています。

Dockerfile内でnodeバイナリを直接起動

TL;DR Dockerfile内でnpmを使ってアプリを起動することも推奨しません。説明しましょう。

nodeバイナリを直接呼び出すことを推奨します。大きくは、Node.jsアプリでこれを取り扱う方法についての混乱や誤解がインターネット上で見られる"PID 1問題"のためです。 インターネット上での混乱を解消するため、常にDockerとNode.jsの間に"init"ツールを配置する必要はありませんし、アプリをゆるやかに停止する方法を考えるためにより多くの時間を使うべきです

Node.jsはOSからのSIGINTやSIGTERMのようなシグナルを受け付け、転送します。これはアプリの適切なシャットダウンのために重要です。Node.jsは、これらのシグナルをどのように扱うかの決定をアプリに任せます。つまり、シグナルを扱うコードを書かなかったりモジュールを使わなかったりすると、アプリはゆるやかにシャットダウンしないことになってしまいます。これらのシグナルを無視すると、Dockerではデフォルト10秒、Kubernetesでは30秒のタイムアウトの後に強制終了されます。 本番環境でHTTPアプリを起動したら、アプリをアップデートしたいときに単に接続を遮断してしまわないようにするために、このことについてもっとたくさんの注意を払うことになるでしょう。

例えばnpmなどのNode.jsを起動する他のアプリを使っていると、このシグナル機能を壊してしまいます。 npmはこれらのシグナルをアプリに渡さないので、DockerfileのENTRYPOINTとCMDからは取り除いておく方がよいでしょう。コンテナ内で実行するバイナリを1つ少なくできるという利点もあります。他にも、実際の起動コマンドを調べるためにpackage.jsonを見なくても、コンテナを起動したときにアプリが実行する そのもの がDockerfileに書いてあるのですぐわかるという利点があります。

このためのdocker run --initのような起動オプションやDockerfileでtiniを使うことについての知識は、アプリのコードを変更できないときのよい代替手段です。そうだとしても、ゆるやかなシャットダウンのために正しくシグナルを取り扱うコードを書くことがよりよい解決策です。例えば私の定型コードや、stoppableのようなモジュールを探すとよいでしょう。

これですべてですか?

いいえ。これらはほぼすべてのNode.jsチームが取り扱う関心事で、これに加えてもっと多くの他の考慮点があります。マルチステージビルド、HTTPプロキシ、npm installのパフォーマンス、ヘルスチェック、CVEスキャン、コンテナログ、イメージ構築中のテスト、マイクロサービスdocker-compose設定などのトピックは、私のNode.jsの顧客や生徒のよくある共通の疑問です。

これらのトピックについてより多くの情報に興味があれば、私のDockerCon 2019セッションのビデオをご覧いただくか、https://www.bretfisher.com/nodeにてNode.jsでのDockerについての8時間のビデオを見てください。

ご覧いただきありがとうございます。私のTwitterアカウントはこちらです。毎週DevOpsとDockerのニュースレターを発行しています。週刊YouTubeビデオとライブを購読してください。その他のDockerリソースやコースもご覧ください。

Dockerしましょう!

もっと学びたいですか? 8月28日のオンラインミートアップ に参加してみてください。


原文: Top 4 Tactics To Keep Node.js Rockin' in Docker

New call-to-action
新規CTA