20190330のNode.jsに関する記事は7件です。

NodeでES modulesからCommon JSを使う、たったひとつ?の全く冴えないやりかた(2019年版)

先日ようやく拙作のnpmパッケージ(1)(2)(3)をnodeの --experimental-modules フラグ付きでの直接実行(babelやwebpackによる前処理無し)に対応させたので、その方法をご紹介いたします。

まずは復習

ES Modules (以下、ESM)では、Common JS (以下、CJS)と異なり、他のファイル上の関数や値にアクセスするためには、import / export構文を用います。

CJS

foo.js(CJS)
// 複数の関数・値を名前付きでエクスポートする場合
exports.bar = function(x) {
    return x * x;
}

// 一つの関数・値をエクスポートする場合
// module.exports = function(x) {
//     return x * x;
// }
index.js(CJS)
// 複数の関数・値が名前付きでエクスポートされている場合
const foo = require('./foo');
console.log(foo.bar(3)); // 9

// 一つの関数・値がエクスポートされている場合
// const bar = require('foo');
// console.log(bar(3)); // 9

ESM

foo.mjs(ESM)
// 複数の関数・値を名前付きでエクスポートする場合
export function bar(x) {
    return x * x;
}

// 一つの関数・値をエクスポートする場合 (デフォルトエクスポート)
// export default function bar(x) {
//     return x * x;
// }
index.mjs(ESM)
// 複数の関数・値を名前付きでエクスポートする場合
import * as foo from './foo';
console.log(foo.bar(3)); // 9

// 一つの関数・値がエクスポートされている場合 (デフォルトインポート=デフォルトエクスポートのインポート)
// import bar from 'foo';
// console.log(bar(3)); // 9

ESMのインポートにおける種類

// namespace import
import * as foo from 'foo';

// named import
import { bar, baz } from 'foo';

// default import
import bar from 'foo';

で、問題は何?

ESMからCJSをインポートする際の動作が歴史的な経緯によって統一されておらず、複数のターゲット(Web app(Webpack)/Node app(Webpack)/APIとしてnpm package(そのまま)/...)を考慮すると破綻をきたします。

Node(--experimental-modules .mjs)の場合

Nodeは、ESMからCJSをインポートする方法をデフォルトインポートのみとしています。
といっても、現時点の挙動(node 11)では、インポート時にエラーになるのではなく、CJSの中身がdefaultに包まれます。
つまり、デフォルトインポートしていないと、いざ使用する際に undefined is not a function などというエラーに遭遇します。

注: ESMでは、デフォルトエクスポートは default という名前でエクスポートされるお約束です。

foo.js(CJS)
exports.bar = function(x) {
    return x * x;
}
index.mjs(ESM)
import * as foo from './foo';
console.dir(foo);
出力
[Module] { default: { bar: [Function] } }

Babelの場合 (babel-core=7.4.0)

Nodeと同様です。CJSの中身がdefaultに包まれます。

トランスパイル前
import * as foo from './foo';
console.dir(foo);
トランスパイル後
"use strict";
var foo = _interopRequireDefault(require("./foo"));
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
console.dir(foo);
出力
{ default: { foo: [Function] } }

Webpack 4.x(.js .ts)の場合

CJSの中身がそのまま渡されます。
デフォルトインポートされたものを使おうとすると、default is not a function等のエラーを見ることになるでしょう。

index.ts_トランスパイル前
import * as foo from './foo';
console.dir(foo);
トランスパイル後
...
"use strict";
__webpack_require__.r(__webpack_exports__);
/* harmony import */ var _foo__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__("./foo.js");
/* harmony import */ var _foo__WEBPACK_IMPORTED_MODULE_0___default =
        /*#__PURE__*/__webpack_require__.n(_foo__WEBPACK_IMPORTED_MODULE_0__);
console.dir(_foo_WEBPACK_IMPORTED_MODULE_0__);
...
出力
{ foo: [Function] }

Webpack 4.x(.mjs)の場合

実は、Webpack 4.xは、Node(--experimental-modules)と同じモジュール解決をサポートしています。
方法は、webpack.config.js.mjs を受け入れるようにし、入力ファイルに.mjsを渡すか、(インポートしようとしている名前).mjsが存在するパスをimportします。

webpack.config.js
...
resolve: {
    extensions: ['.tsx', '.ts', '.jsx', '.mjs', '.cjs', '.js']
},
...
index.mjs_トランスパイル前
import * as foo from './foo';
console.dir(foo);
パッケージング中にエラー
ERROR in ./modules/index.mjs 3:12-19
Can't import the namespace object from non EcmaScript module (only default export is available)
@ multi ./modules/index.mjs

デフォルトインポート以外(namespace import, named import)が存在するとエラーになります。

TypeScriptの場合

幸いなことに、TypeScriptのトランスパイラーは、インポート先が何であるかに関わらず、固定的な変換をするだけです。

module (コンパイラオプション) namespace import named import default import
CommonJS const foo = require('foo') const foo = require('foo')
const bar = foo.bar
const foo = require('foo').default
ES2015 そのまま出力 そのまま出力 そのまま出力

どうすればよいのか?

彼方立てれば此方が立たぬ、といった状況ですが
見苦しいコードを受け入れることで回避できます。

defaultに包まれていれば剥がすことで、どちらの場合も動作します。

index.ts(TypeScriptの場合)
import * as foo_ from './foo';
const foo: typeof foo_ = (foo_ as any).default || foo_;
console.dir(foo);
index.js
import * as foo_ from './foo';
const foo = foo_.default || foo_;
console.dir(foo);
トランスパイル後
"use strict";
const foo_ = require("./foo");
const foo = foo_.default || foo_;
console.dir(foo);

追記 ( __filename / __dirname について)

ESMでは、 __filename __dirname といったシンボルも定義されません。

代替手段として、import.meta.urlからファイルURLを取得し、パスに変換します。

const thisFileName = url.fileURLToPath(import.meta.url);
const thisDirName = path.dirname(thisFileName);

ただ、こちらは webpack が対応しておらず
Support for the experimental syntax 'importMeta' isn't currently enabled のようにトランスパイル時にエラーとなります。
@open-wc/webpack-import-meta-loader という webpackプラグインを導入することでトランスパイル時エラーは回避できますが、
実行時に下記のエラーが発生します。

url: `${window.location.protocol}//${window.location.host}/C:\path\to\source\file\index.mjs`
ReferenceError: window is not defined

ブラウザでのみ動作するようです。
動作させるには、次のコードに変更します。

const isWebpack = typeof __webpack_require__ === 'function';
let thisFileName = '';
let thisDirName = '';
if (isWebpack) {
    thisFileName = __filename;
    thisDirName = __dirname;
} else {
    thisFileName = url.fileURLToPath(import.meta.url);
    thisDirName = path.dirname(thisFileName);
}
webpack.config.js
...
module: {
    rules: [{
        test: /\.m?jsx?$/,
        use: [
            'babel-loader',
            require.resolve('@open-wc/webpack-import-meta-loader'),
        ],
        exclude: /node_modules[\/\\]/
    }, {
...

追記 (parcelについて)

今回、webpackと併せてparcelでの動作も比較しました。
parcelでは、webpackのtypeof __webpack_require__ === 'function'のように、パックされたコードかどうかを判定する手段が無いこと、import.meta.urlのエラーを回避する手段を見つけられなかったことで、私のnpmパッケージでのparcel対応ができませんでした。

注:私のnpmパッケージでは、CSSの require() について、パックされているかどうかで動作を変えています。
ESMのファイル内のため、Nodeからの実行では require() が存在しないため、呼び出しをCJS経由でバイパスしていますが、
パックされている場合は require() が存在するため、直接 require() を呼び出します。
※ 文字列リテラルで渡す require('path/to/stylesheet.css') が存在しないとバンドルできないため行っています。

ソース

上述のnpmパッケージを node --experimental-modules と webpack 両方で動作させる全コードです。
https://github.com/shellyln/menneu-api-usage-on-esm

参考資料

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

問.以下のAPIにAjaxでリクエストを送り、レスポンスを表示しなさい(Promiseと再帰)

問題

以下のAPIにAjaxでGETリクエストを送り、レスポンスデータの "message" フィールドの値をコンソールに表示してください。

https://sheltered-island-72837.herokuapp.com/

なお、上記APIはそこそこの確率でエラーレスポンス(ステータスコード500)を返してきます。
もし、1回のリクエストで正常なレスポンス(ステータスコード200)が得られなかった場合は、再度APIへの問い合わせを行ってください。
その際、各リクエストは3秒以上の間を設けるようにし、また最大リトライ回数は2回とします。
リクエストにはaxiosなどのライブラリを使用して構いません。

実装例

const axios = require('axios')
const url = 'https://sheltered-island-72837.herokuapp.com/'

const fetch = (executeCount = 1) => {
  return axios
    .get(url)
    .then(({data}) => data.message)
    .catch(({response}) => {
      if (executeCount < 3) {
        return new Promise(resolve => {
          setTimeout(() => {
            resolve(fetch(++executeCount))
          }, 3000)
        })
      }
      return response.data.message
    })
}

fetch().then(res => console.log(res))

解説

APIへリクエストを送る

まずは単純に、APIに1回だけ問い合わせてコンソールに表示してみます。

Ajaxをする際、axiosを利用することが多いと思います。
もちろん、Fetch API を使ってもいいでしょう。
今回は axios でいきます。

const axios = require('axios')
const url = 'https://sheltered-island-72837.herokuapp.com/'

axios
  .get(url)
  .then(({data}) => console.log(data.message))
  .catch(({response}) => console.log(response.data.message))

{data} という書き方は、分割代入 (Destructuring assignment) といいます。
オブジェクトや配列の一部だけを取り出して、別の変数に代入してくれます。

リトライ機能

メソッド化

まず、先ほどの「APIへリクエストを送る」処理を再利用できるようにメソッドにします。
合わせて、thenやcatch内で直接console.logを呼んでいたのを、値を返すように修正します。

const axios = require('axios')
const url = 'https://sheltered-island-72837.herokuapp.com/'

const fetch = () => {
  return axios
    .get(url)
    .then(({data}) => data.message)
    .catch(({response}) => response.data.message)
}

fetch().then(res => console.log(res))

thenやcatchのなかで値を返すとその値はPromiseでラップされる、ということはとても大事なので覚えておきましょう。
Promise.prototype.then()

「値を返すように修正って、return書いてなくない...?」と思った方は、アロー関数の構文を復習しましょう。
アロー関数

再帰

実現したいことは、「エラーになったら再度リクエストを送る」です。
「エラーになったら」なので、catchの中にその処理を書いていくことになります。
そして、「リクエストを送る」処理は、まさにfetchメソッド自身がそのための処理なのですから、自分を呼び出してしまえばいいわけです。

まず、fetchメソッドが実行回数を取るように引数を追加します。
そしてエラーになった時、実行された回数が上限(初回 + リトライ2回 = 3回)に達していなければ実行回数を+1して自分自身を呼び出します。
そうでなければ普通にレスポンスからメッセージを取り出して return します。

再帰を使うときはその終了条件をしっかり書いてあげないと、いつまでたっても自分を呼び出し続けて終わらなくなりますので十分意識してください。

なお、処理の流れを確認するため、一時的にconsole.logを追加しています。

const axios = require('axios')
const url = 'https://sheltered-island-72837.herokuapp.com/'

const fetch = (executeCount = 1) => {
  console.log(`${executeCount}回目の試行です`)
  return axios
    .get(url)
    .then(({data}) => data.message)
    .catch(({response}) => {
      if (executeCount < 3) {
        console.log('失敗したので再度リクエストします')
        return fetch(++executeCount)
      }
      return response.data.message
    })
}

fetch().then(res => console.log(res))

間隔を開けてリクエストする

最後に、リトライ時、リクエストごとに間隔を開ける処理を実装しましょう。
リトライ時の挙動の調整ですから、再帰でfetchを呼び出している部分を修正します。

〜秒処理を待つ、といえば setTimeout ですね。

console.log('a')

setTimeout(() => {
  console.log('b')
}, 1)

console.log('c')

//出力順
// a
// c
// b

では、setTimeout内で実行される関数から、返り値を受け取りたいときはどうすれば良いでしょう。

const res = setTimeout(() => {
  return 'b'
}, 1)

これで res = 'b' となるでしょうか? 違いますね。
setTimeoutは、指定した時間(第2引数)待って、第1引数に設定した処理を行う、というタイマーを設定する関数です。
setTimeoutの返り値は、そのとき設定されたタイマーのidです。

正解は、setTimeout全体をPromiseでラップして、返したい値でresolveすることです。

const res = new Promise(resolve => {
  setTimeout(() => {
    resolve('b')
  }, 1)
})

res.then(data => console.log(data))
// b が出力される

したがって、修正は以下のようになります。
3秒待つので、setTimeout(() => {}, 3000)、setTimeout自体をPromiseでラップして、返したい値(fetch(++executeCount)) でresolveしています。

const axios = require('axios')
const url = 'https://sheltered-island-72837.herokuapp.com/'

const fetch = (executeCount = 1) => {
  return axios
    .get(url)
    .then(({data}) => data.message)
    .catch(({response}) => {
      if (executeCount < 3) {
        return new Promise(resolve => {
          setTimeout(() => {
            resolve(fetch(++executeCount))
          }, 3000)
        })
      }
      return response.data.message
    })
}

fetch().then(res => console.log(res))

実際に外部のAPIを叩く時でも、エラーが返ってくるということはAPIに何かしら不具合・過負荷が起きている可能性がありますから、優しいリクエストを送ってあげることは大事です。

よくわからなかったという人へ

解説と言いつつ全く詳細を説明していませんので、初心者の方にはちんぷんかんぷんだと思います。
(当たり前のように require とか使ってるし、Promiseって何なのかも説明していないし...)
でも、Javascriptを書いていく上で理解しておいた方がいいことばかりなので、勉強しましょう!
特にPromiseは大事です。フロントエンドでもサーバーサイドでも、Javascriptにおいて非同期処理の扱いはとっっても大事です。

https://sheltered-island-72837.herokuapp.com/

上記APIはしばらく稼働させておきますので、練習にお使いください。
(節度のあるリクエストをお願いします!)

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

タブレットとWeb両立プロジェクト開発(二)Cordovaをインストールします

Cordovaの特徴

Cordova(コルドバ)とは、オープンソースのハイブリッドアプリ開発フレームワークです。複数のプラットフォームで動作するモバイルアプリをワンソースで開発できます。
Cordovaは、Web標準技術(HTML5+CSS+JavaScript)を使って、iPhone/Androidなどの複数のモバイルOSに対応したクロスプラットフォームなアプリを開発することができるフレームワークです。
複数のOSに対してそれぞれネイティブアプリを開発する作業と比較すると、1つのソースコードで複数OSに対応できるため、大幅に開発コストを削減できます。Webアプリと同様の技術で開発するため学習コストも削減できます。

Cordova CLIのインストール

Cordova CLIとは、Cordovaのコマンドラインインターフェイスです。
Cordova CLIは、すぐに使用できる形式でのnpm(Node.jsのパッケージ管理ツール)パッケージとして配布されるので、npmでCordovaのCLIをインストールします。

npm install -g cordova
#以下の結果が表示
added 438 packages from 349 contributors in 20.705s
╭───────────────────────────────────────────────────────────────╮
│       New minor version of npm available! 6.7.0 → 6.9.0       │
│   Changelog: https://github.com/npm/cli/releases/tag/v6.9.0   │
│               Run npm install -g npm to update!               │
╰───────────────────────────────────────────────────────────────╯

インストール完了後、バージョンを確認しておきます。

cordova -v
9.0.0

Cordovaプロジェクトの作成

Cordova CLIを使って、プロジェクトの作成を行います。今回は“Hello Cordova!”を出力するCordovaプロジェクトを作ります。
ちなみに、このプロジェクトはiOSまたはAndroidを全部対象とします。

#プロジェクト作成コマンドの構造: cordova create ディレクトリ名 識別子 アプリ名
cordova create Hello com.demo.hello HelloCordova

Helloというディレクトリが作成されたので、次の処理が行われる前にHelloに移動必要です。

Cordovaプラットフォームを追加

作成したプロジェクトが対応するプラットフォーム用のファイルをCLIで追加します。
iosを追加すれば、コマンドは、cordova platform add iosとします。
androidを追加すれば、コマンドは、cordova platform add androidとします。
今回は、iosなので、以下を実行します。

cd Hello
cordova platform add ios
#以下が表示
iOS project created with cordova-ios@5.0.0
Discovered plugin "cordova-plugin-whitelist" in config.xml. Adding it to the project
Installing "cordova-plugin-whitelist" for ios
Adding cordova-plugin-whitelist to package.json

追加したらプラットフォームの確認をしておきましょう。

cordova platform ls
#以下が表示
Installed platforms:
  ios 5.0.0
Available platforms: 
  android ^8.0.0
  browser ^6.0.0
  electron ^1.0.0
  osx ^5.0.0
  windows ^7.0.0

?削除コマンドは次の通りです。
cordova platform rm ios
あとはビルドやデバッグをVSCodeの拡張機能を使って実行できるようにします。


✨✨✨次の文章へ:タブレットとWeb両立プロジェクト開発(三)VSCodeでJavaScriptプロジェクトを作ります

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

ESLint v5.16.0

v5.15.0 | 次 (2019/04/13 JST)

ESLint 5.16.0 がリリースされました。
小さな機能追加とバグ修正が行われました。

特に問題がなければ、これが ESLint 5.x 系の最後のリリースになります。次回は ESLint 6.0.0 プレリリースの予定です。

質問やバグ報告等ありましたら、お気軽にこちらまでお寄せください。

? 日本語 Issue 管理リポジトリ
? 日本語サポート チャット
? 本家リポジトリ
? 本家サポート チャット

? 本体への機能追加

RFC 10: カスタム フォーマッターからルールの定義データにアクセスできるようになりました。

従来はフォーマッターはリント結果のみにアクセスできました。しかし、各ルールのメタデータ (例えば各ルールのドキュメントへの URL) を使いたいという要望が強くあったため、これにアクセスできるようになりました。

詳細はドキュメントを参照ください。

RFC 10: フォーマッター json-with-metadata が追加されました。

リント結果を、各ルールのメタデータを含む JSON 形式で出力するフォーマッターです。

? 新しいルール

特になし。

? オプションが追加されたルール

特になし。

✒️ eslint --fix をサポートしたルール

特になし。

⚠️ 非推奨になったルール

特になし。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Stackdriver Debugger で簡単に本番環境をデバッグする

はじめに

本番環境にデプロイしたらバグが出て、デバッグしたいけど本番環境のソースコードにログを仕込んで再起動を繰り返さなきゃみたいなこと、経験したことないでしょうか。
本番環境にブレークポイントや変数を見るためにログを仕込む、そんなことを簡単に行えるのが Stackdriver Debugger です。
今回は Node.js を使って、Google Compute Engine(GCE)にデプロイして Stackdriver Debugger がどんな風に使えるか試してみます。
本記事の執筆時点(2019/03)では、Node.js の Stackdriver Debugger エージェントがベータ版です。
変更になる可能性があることに注意してください。

環境

  • macOS High Sierra Version 10.13.6
  • Node.js v10.15.3
  • npm 6.9.0
  • Google Cloud SDK 240.0.0
  • express 4.16.4
  • @google-cloud/debug-agent 3.1.0

Stackdriver Debugger とは

Stackdriver Debugger は、実行中のアプリケーションに対して現時点のアプリケーションの状態を確認できるサービスです。
アプリケーションの処理速度は低下せず、ユーザーに影響しないと言われています。
用途としては、本番でしか再現しないコードを調査したい場合などに大きな威力を発揮します。

Google Cloud SDK をインストールする

以下のドキュメントを参考に Google Cloud SDK をインストールし、プロジェクトの設定をしてください。
Google Cloud SDK documentation

サンプルアプリケーションの作成

Node.js が事前にインストールされている前提で話を進めます。
インストールされていない場合は、事前に Node.js のインストールを実施してください。

まずは、作成するサンプルアプリケーションを Git で管理します。
今回は、GCP で完結させるために Cloud Source Repositories を利用します。
バージョン管理システムとしては、Cloud Source Repositories の他に GitHub や Bitbucket、GitLab などに対応しています。
それではリポジトリを作成してローカルに clone します。
サンプルとして stackdriver-debugger-sample という名前で作成しています。

$ gcloud source repos create stackdriver-debugger-sample
$ gcloud source repos clone stackdriver-debugger-sample

まずは express を利用した Node.js のアプリケーションを作成します。
プロジェクトを作成して express をインストールします。

$ cd stackdriver-debugger-sample
$ npm init -y
$ npm install --save express

以下のような 0 から 10 までの数値をランダム生成して、5 より大きい、または小さいを判断する index.js を作成します。

index.js
const express = require('express');

const PORT = 80;
const HOST = '0.0.0.0';

const app = express();

const getRandomNumber = (min, max) =>
  Math.floor(Math.random() * (max - min + 1)) + min;

app.get('/', (req, res) => {
  const num = getRandomNumber(0, 10);

  if (num > 5) {
    res.send('5 より大きい');
  } else {
    res.send('5 より小さい');
  }
});

app.listen(PORT, HOST);
console.log(`Running on http://${HOST}:${PORT}`);

サンプルアプリケーションの確認をしましょう。
index.js を起動します。

$ sudo node index.js

別の shell でリクエストすると以下のようなレスポンスであることを確認します。

$ curl http://127.0.0.1
5 より小さい

サンプルアプリケーションを Git で管理していきましょう。
不要なファイルをコミットしないように、.gitignore ファイルを作成します。
gitignore.io というサイトから Node.js 用の .gitignore ファイルの設定を利用します。

$ curl https://www.gitignore.io/api/node > .gitignore

最初のコミットをしましょう。

$ git add .
$ git commit -m 'initial commit'

次に Node.js 用の Stackdriver Debugger のパッケージをインストールします。

$ npm install --save @google-cloud/debug-agent

インストールしたパッケージを利用するために、アプリケーションのエントリポイントである index.js の先頭で Stackdriver Debugger のエージェントを有効にします。

index.js
+require('@google-cloud/debug-agent').start({
+  allowExpressions: true,
+  serviceContext: {
+    service: process.env.SERVICE,
+    version: process.env.VERSION,
+  }
+});
+
 const express = require('express');

 const PORT = 80;
 const HOST = '0.0.0.0';

allowExpressions は、後述するログポイントでメッセージ形式を指定するに当たって必要なため有効にします。
service には、アプリケーションの名前、version には、アプリケーションのバージョンを設定します。
これらの値をデプロイするたびに変えられるように環境変数から設定できるようにしておきます。

再度コミットしてプッシュします。

$ git commit -am 'Add Stackdriver Debugger agent'
$ git push origin master

GCE へデプロイ

それでは GCE のインスタンスにソースコードを配置しデバッグを行うため、GCE のインスタンスを作成します。
スコープに Cloud Source Repositories の読み取り権限と Stackdriver Debugger の権限を付与しておきます。
サンプルとして example-instance という名前で作成しています。
作成すると以下のような情報が表示されます。

$ gcloud compute instances create example-instance \
    --zone asia-northeast1-a \
    --machine-type g1-small \
    --image-family ubuntu-1804-lts \
    --image-project ubuntu-os-cloud \
    --scopes https://www.googleapis.com/auth/source.read_only,https://www.googleapis.com/auth/cloud_debugger \
    --tags http-server
Created [https://www.googleapis.com/compute/v1/projects/stackdriver-debugger-235505/zones/asia-northeast1-a/instances/example-instance].
NAME              ZONE               MACHINE_TYPE  PREEMPTIBLE  INTERNAL_IP  EXTERNAL_IP   STATUS
example-instance  asia-northeast1-a  g1-small                   10.146.0.7   34.85.116.83  RUNNING

作成した GCE のインスタンスに SSH 接続します。

$ gcloud compute ssh example-instance

Node.js のアプリケーションなので、Node.js と npm をインストールします。

$ sudo apt update && sudo apt install -y nodejs npm

Cloud Source Repositories のリポジトリから作成したアプリケーションをインスタンス内に clone して、npm module のインストールを行います。

$ gcloud source repos clone stackdriver-debugger-sample
$ cd stackdriver-debugger-sample
$ npm install

ソースコードが自動で選択されるようにソースコンテキスト情報を含んだファイル source-context.json を生成します。
アプリケーションと一緒にバージョン管理する必要はありませんが、このファイルを含めた状態でデプロイする必要があります。
今回はサンプルのためインスタンス内で生成します。

$ gcloud debug source gen-repo-info-file

サンプルアプリケーションを起動します。
起動時に、環境変数から設定できるようにした Stackdriver Debugger エージェントの変数を指定します。

$ sudo SERVICE=sample VERSION=1.0.0 node index.js

試しに別の shell でリクエストすると正常にレスポンスされることが分かります。

$ curl 34.85.116.83
5 より小さい

デバッガを設定する

Google Cloud Console の左メニューから「デバッグ」メニューを開きます。
すると以下のようにデバッグ画面が開きます。

Stackdriver Debugger の画面

デバッグ画面が表示されたところで、スナップショットの表示から 22 行目をクリックしてブレークポイントを設定します。
この時点で変数 num にどんな値が入っているか確認できることを期待します。
再びリクエストを行うとブレークポイントの時点の変数やコールスタックが確認できます。
そして変数 num は 4 であることが分かりました。

ブレークポイントを設定して確認

ログポイントを設定する

ログポイントはデプロイしたアプリケーションのままログ出力を設定できる機能です。
これによって、本番で確認したいがために本番へログインしてソースコードを書き換え再起動するという手間が無くなります。

先ほどの画面からログポイントの画面に切り替えます。

ログポイントの画面

再び 22 行目をクリックすると、ログポイントの設定が表示されます。
ログポイントにはログが出力される条件とログのメッセージ形式が指定できます。
true の部分が出力条件、var = {var} の部分がメーセージ形式を指定する項目です。
またログレベルも指定でき、「情報」・「警告」・「エラー」の 3 種類から選択できます。

ログポイントの設定

今回は、メッセージ形式に num = {num} を指定し追加をクリックしましょう。
追加をすると 22 行目に num = {num} が追加されたことが分かります。

ログポイントの追加

追加後、サンプルアプリケーションに何度かリクエストすると、サンプルアプリケーションを起動した標準出力にログポイントのログが出力されていることが分かります。
GCE の場合は、ログの出力先がアプリケーションの標準出力になります。
ログビューア上で見る場合は、別途ロギングエージェントなどを利用した転送が必要です。
GAE や GKE であれば、標準で Stackdriver Logging に対応しているため Stackdriver Logging のリクエストログから確認できます。

$ sudo SERVICE=sample VERSION=1.0.0 node index.js
Running on http://0.0.0.0:80
LOGPOINT: num = 8
LOGPOINT: num = 7
LOGPOINT: num = 10
LOGPOINT: num = 8
LOGPOINT: num = 4

次にログポイントの条件を指定してみましょう。
先ほど作成したログポイントを編集して、条件に num > 5 と入力し適用をクリックすると編集完了です。

ログポイントに条件を追加

再びサンプルアプリケーションに何度かリクエストすると、 num > 5 の条件の場合にのみログ出力されることが分かります。

$ sudo SERVICE=sample VERSION=1.0.0 node index.js
Running on http://0.0.0.0:80
LOGPOINT: num = 10
LOGPOINT: num = 9
LOGPOINT: num = 8
LOGPOINT: num = 8
LOGPOINT: num = 6

お掃除

以下のコマンドを実行し、y でリポジトリを削除します。

$ gcloud source repos delete stackdriver-debugger-sample

以下のコマンドを実行し、y でインスタンスを削除します。

$ gcloud compute instances delete example-instance

さいごに

ここまで Stackdriver Debugger を実際に使ってアプリケーションのデバッグを試しました。
Stackdriver Debugger を使えば簡単かつ便利に本番環境でもデバッグできることがお分かりいただけたのではないでしょうか。
是非有効にして、アプリケーションの調査に役立てましょう。

参考

【GCP 入門編・第 24 回】 Stackdriver Debugger で本番環境のデバッグを行おう! | 株式会社トップゲート
Stackdriver Debugger documentation | Stackdriver Debugger

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

KubernetesとNode.jsでマイクロサービスを作成する 5/8 Dockerを使ったサービス構築

第5章 Dockerを使ったサービス構築

第4章までで、Twitterライクなマイクロサービスを構成する、3つのサービスを作成してきました。
次はいよいよ、これらのサービスをマイクロサービスとして構築するのですが、いきなりKubernetesを利用する前にワンクッションはさみます。

本章では、各サービスのDockerイメージを作成し、以下のような構成で、ローカルのDocker環境でTwitterライクなサービスを起動します。

microservice-sample-docker.png

完成版のリポジトリはこちらにあるので、実装がうまくいかない場合は比較してみてください。
reireias/microservice-sample-integration

チュートリアル全体

構成

microservice-tutorial01.png

dockerとdocker-compose

まずはローカルにdockerdocker-composeがインストールされていることを確認しましょう。

docker version
Client:
 Version:           18.06.1-ce
 API version:       1.38
 Go version:        go1.10.4
 Git commit:        e68fc7a
 Built:             Fri Jan 25 14:33:54 2019
 OS/Arch:           linux/amd64
 Experimental:      false

Server:
 Engine:
  Version:          18.06.1-ce
  API version:      1.38 (minimum version 1.12)
  Go version:       go1.10.4
  Git commit:       e68fc7a
  Built:            Thu Jan 24 10:56:33 2019
  OS/Arch:          linux/amd64
  Experimental:     false
docker-compose version
docker-compose version 1.23.2, build 1110ad0
docker-py version: 3.7.0
CPython version: 3.7.2
OpenSSL version: OpenSSL 1.0.2g  1 Mar 2016

microservice-sample-integrationリポジトリの作成

ここからは、各サービスのリポジトリ内のコードを利用して、DockerイメージやKubernetesの設定を作成していくことになります。
そのためには、各サービスのコードが参照できる構成が望ましいです。

今回は下記のディレクトリ構成をとるような、microservice-sample-integrationリポジトリを作成します。

microservice-sample-integration
└── services
    ├── microservice-sample-tweet
    ├── microservice-sample-user
    └── microservice-sample-web

実際の開発現場では、このリポジトリの管理をSREチームが主導して行ったり、各サービスの開発者が協力して行ったりします。

microservice-sample-integrationという名前でリポジトリを作成し、cloneしておきましょう。

つづいて、このリポジトリ内で行う各種操作をMakefileに記述し、makeコマンドから実行できるようにします。
作成するコマンドは下記の通りです。

  • clone: 各サービスをservicesディレクトリ以下にcloneする
  • pull: 各サービスをgit pullする
  • build: 各サービスのDockerイメージのビルドを行う
  • up: 全サービスをdocker-composeを使って、ローカルのDocker環境で立ち上げる
  • down: 全サービスをdocker-composeを使って削除する
  • seed: UserサービスとTweetサービスでscripts/initialize.jsを実行する

Makefileは以下のようになります。

Makefile
WEB_REPOSITORY := https://github.com/reireias/microservice-sample-web
USER_REPOSITORY := https://github.com/reireias/microservice-sample-user
TWEET_RESPOSITORY := https://github.com/reireias/microservice-sample-tweet

clone:
    git clone $(WEB_REPOSITORY) ./services/microservice-sample-web
    git clone $(USER_REPOSITORY) ./services/microservice-sample-user
    git clone $(TWEET_RESPOSITORY) ./services/microservice-sample-tweet

pull:
    cd ./services/microservice-sample-web && git pull
    cd ./services/microservice-sample-user && git pull
    cd ./services/microservice-sample-tweet && git pull

build:
    docker-compose build

up:
    docker-compose up -d

down:
    docker-compose down

seed:
    docker-compose exec user node /app/scripts/initialize.js
    docker-compose exec tweet node /app/scripts/initialize.js

Makefileが作成できたら、make pullを実行し、各サービスをservicesディレクトリ配下へcloneします。

make clone

Dockerイメージの作成

つづいて、各サービスのDockerイメージを作成するための、Dockerファイルを実装していきます。

このファイルから生成されるDockerイメージは本番環境でも使用されます。
実際の開発現場では各サービスが責任を持って作成する場合もありますし、SREチームなど、Dockerのノウハウに長けたチームがサービス横断で管理するケースもあるでしょう。

services/microservice-sample-tweet/Dockerfile
FROM node:11.10-alpine

ENV NODE_ENV=production

WORKDIR /app
RUN apk add --no-cache curl && \
    curl -sfL https://install.goreleaser.com/github.com/tj/node-prune.sh | sh
ADD https://github.com/ufoscout/docker-compose-wait/releases/download/2.5.0/wait /wait
RUN chmod +x /wait

COPY package.json .
RUN yarn install && ./bin/node-prune
COPY . .
EXPOSE 3000
CMD /wait && yarn start
services/microservice-sample-user/Dockerfile
FROM node:11.10-alpine

ENV NODE_ENV=production

WORKDIR /app
RUN apk add --no-cache curl && \
    curl -sfL https://install.goreleaser.com/github.com/tj/node-prune.sh | sh
ADD https://github.com/ufoscout/docker-compose-wait/releases/download/2.5.0/wait /wait
RUN chmod +x /wait

COPY package.json .
RUN yarn install && ./bin/node-prune
COPY . .
EXPOSE 3000
CMD /wait && yarn start
services/microservice-sample-web/Dockerfile
FROM node:11.10-alpine as builder

ENV NODE_ENV=production
ARG GITHUB_CLIENT_ID
ARG GITHUB_CLIENT_SECRET
WORKDIR /app

RUN apk add --no-cache curl && \
    curl -sfL https://install.goreleaser.com/github.com/tj/node-prune.sh | sh

COPY package.json .
COPY yarn.lock .
RUN yarn install && ./bin/node-prune
COPY . .
RUN yarn build

FROM node:11.10-alpine

WORKDIR /app
ADD package.json ./
ADD nuxt.config.js ./

COPY --from=builder ./app/server ./server
COPY --from=builder ./app/node_modules ./node_modules
COPY --from=builder ./app/.nuxt ./.nuxt

EXPOSE 3000
CMD yarn start

Webサービスに関してはマルチステージビルドを利用して最終的なイメージのサイズを軽くするように工夫しています。

実装後、各Dockerfilecommitしておきましょう。

docker-compose.ymlの作成

上記で用意したDockerfileを、dockerコマンドを利用してビルド、コンテナの起動等やってもよいのですが、環境変数等、引数が多くなってしまうため、今回はdocker-composeを利用して一元管理します。

docker-compose.ymlに起動したいコンテナの設定を書いていくことで、一括で立ち上げたり、削除することが可能です。

今回は、Webサービス、Userサービス、Tweetサービスと、Userサービス用DB、Tweetサービス用DBの合計5つのコンテナを定義します。

docker-compose.yml
---
version: '3'
services:
  web:
    build:
      context: ./services/microservice-sample-web
      dockerfile: Dockerfile
    image: microservice_web:1.0
    ports:
      - 3000:3000
    environment:
      NUXT_HOST: 0.0.0.0
      USER_SERVICE: http://user:3000
      TWEET_SERVICE: http://tweet:3000
      GITHUB_CLIENT_ID: ${GITHUB_CLIENT_ID}
      GITHUB_CLIENT_SECRET: ${GITHUB_CLIENT_SECRET}
  user:
    build:
      context: ./services/microservice-sample-user
      dockerfile: Dockerfile
    image: microservice_user:1.0
    environment:
      MONGODB_URL: mongodb://user-db:27017/user
      MONGODB_ADMIN_NAME: root
      MONGODB_ADMIN_PASS: example
      WAIT_HOSTS: user-db:27017
    depends_on:
      - user-db
  user-db:
    image: mongo
    volumes:
      - user-db:/data/db
    environment:
      MONGO_INITDB_ROOT_USERNAME: root
      MONGO_INITDB_ROOT_PASSWORD: example
  tweet:
    build:
      context: ./services/microservice-sample-tweet
      dockerfile: Dockerfile
    image: microservice_tweet:1.0
    environment:
      MONGODB_URL: mongodb://tweet-db:27017/user
      MONGODB_ADMIN_NAME: root
      MONGODB_ADMIN_PASS: example
      WAIT_HOSTS: tweet-db:27017
      USER_SERVICE: http://user:3000
    depends_on:
      - tweet-db
  tweet-db:
    image: mongo
    volumes:
      - tweet-db:/data/db
    environment:
      MONGO_INITDB_ROOT_USERNAME: root
      MONGO_INITDB_ROOT_PASSWORD: example
volumes:
  user-db:
  tweet-db:

GitHubのOAuthのキー等はGit管理する対象ではないので、.envファイルに記述します。
docker-composeの機能により、カレントディレクトリの.envファイルが読み込まれ、docker-compose.yml中で利用することができるようになります。
値はDocker環境の場合エンドポイントがlocalhost:3000と、前章までと同じなため、前章で使用していたのと同じGitHubのOAuth設定が利用可能です。

.env
GITHUB_CLIENT_ID=xxxxxxxxxxxxxxxxxxx
GITHUB_CLIENT_SECRET=xxxxxxxxxxxxxxxxxxxxxxxxxxxxx

services/.gitkeepファイルを作成しておきます。

touch services/.gitkeep

.gitignoreファイルを以下のように設定します。

.gitignore
services/*
!services/.gitkeep
.env

サービスを起動してみる

それでは、サービスを起動してみます。

# Dockerイメージをビルド
make build
# サービスの立ち上げ
make up
# ダミーデータの投入
make seed

http://localhost:3000へアクセスしてみましょう。
問題なく動作していれば本章の実装は完了になります。

第5章まとめ

本章では、各サービスのDockerfileを作成し、docker-composeを利用してマイクロサービスをローカルのDocker環境上に構築しました。

次の章ではいよいよKubernetes上でマイクロサービスを動作させてみます。

次章: 第6章 Kubernetes with minikube

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

KubernetesとNode.jsでマイクロサービスを作成する 5/6 Dockerを使ったサービス構築

第5章 Dockerを使ったサービス構築

第4章までで、Twitterライクなマイクロサービスを構成する、3つのサービスを作成してきました。
次はいよいよ、これらのサービスをマイクロサービスとして構築するのですが、いきなりKubernetesを利用する前にワンクッションはさみます。

本章では、各サービスのDockerイメージを作成し、以下のような構成で、ローカルのDocker環境でTwitterライクなサービスを起動します。

microservice-sample-docker.png

完成版のリポジトリはこちらにあるので、実装がうまくいかない場合は比較してみてください。
reireias/microservice-sample-integration

チュートリアル全体

構成

microservice-tutorial01.png

dockerとdocker-compose

まずはローカルにdockerdocker-composeがインストールされていることを確認しましょう。

docker version
Client:
 Version:           18.06.1-ce
 API version:       1.38
 Go version:        go1.10.4
 Git commit:        e68fc7a
 Built:             Fri Jan 25 14:33:54 2019
 OS/Arch:           linux/amd64
 Experimental:      false

Server:
 Engine:
  Version:          18.06.1-ce
  API version:      1.38 (minimum version 1.12)
  Go version:       go1.10.4
  Git commit:       e68fc7a
  Built:            Thu Jan 24 10:56:33 2019
  OS/Arch:          linux/amd64
  Experimental:     false
docker-compose version
docker-compose version 1.23.2, build 1110ad0
docker-py version: 3.7.0
CPython version: 3.7.2
OpenSSL version: OpenSSL 1.0.2g  1 Mar 2016

microservice-sample-integrationリポジトリの作成

ここからは、各サービスのリポジトリ内のコードを利用して、DockerイメージやKubernetesの設定を作成していくことになります。
そのためには、各サービスのコードが参照できる構成が望ましいです。

今回は下記のディレクトリ構成をとるような、microservice-sample-integrationリポジトリを作成します。

microservice-sample-integration
└── services
    ├── microservice-sample-tweet
    ├── microservice-sample-user
    └── microservice-sample-web

実際の開発現場では、このリポジトリの管理をSREチームが主導して行ったり、各サービスの開発者が協力して行ったりします。

microservice-sample-integrationという名前でリポジトリを作成し、cloneしておきましょう。

つづいて、このリポジトリ内で行う各種操作をMakefileに記述し、makeコマンドから実行できるようにします。
作成するコマンドは下記の通りです。

  • clone: 各サービスをservicesディレクトリ以下にcloneする
  • pull: 各サービスをgit pullする
  • build: 各サービスのDockerイメージのビルドを行う
  • up: 全サービスをdocker-composeを使って、ローカルのDocker環境で立ち上げる
  • down: 全サービスをdocker-composeを使って削除する
  • seed: UserサービスとTweetサービスでscripts/initialize.jsを実行する

Makefileは以下のようになります。

Makefile
WEB_REPOSITORY := https://github.com/reireias/microservice-sample-web
USER_REPOSITORY := https://github.com/reireias/microservice-sample-user
TWEET_RESPOSITORY := https://github.com/reireias/microservice-sample-tweet

clone:
    git clone $(WEB_REPOSITORY) ./services/microservice-sample-web
    git clone $(USER_REPOSITORY) ./services/microservice-sample-user
    git clone $(TWEET_RESPOSITORY) ./services/microservice-sample-tweet

pull:
    cd ./services/microservice-sample-web && git pull
    cd ./services/microservice-sample-user && git pull
    cd ./services/microservice-sample-tweet && git pull

build:
    docker-compose build

up:
    docker-compose up -d

down:
    docker-compose down

seed:
    docker-compose exec user node /app/scripts/initialize.js
    docker-compose exec tweet node /app/scripts/initialize.js

Makefileが作成できたら、make pullを実行し、各サービスをservicesディレクトリ配下へcloneします。

make clone

Dockerイメージの作成

つづいて、各サービスのDockerイメージを作成するための、Dockerファイルを実装していきます。

このファイルから生成されるDockerイメージは本番環境でも使用されます。
実際の開発現場では各サービスが責任を持って作成する場合もありますし、SREチームなど、Dockerのノウハウに長けたチームがサービス横断で管理するケースもあるでしょう。

services/microservice-sample-tweet/Dockerfile
FROM node:11.10-alpine

ENV NODE_ENV=production

WORKDIR /app
RUN apk add --no-cache curl && \
    curl -sfL https://install.goreleaser.com/github.com/tj/node-prune.sh | sh
ADD https://github.com/ufoscout/docker-compose-wait/releases/download/2.5.0/wait /wait
RUN chmod +x /wait

COPY package.json .
RUN yarn install && ./bin/node-prune
COPY . .
EXPOSE 3000
CMD /wait && yarn start
services/microservice-sample-user/Dockerfile
FROM node:11.10-alpine

ENV NODE_ENV=production

WORKDIR /app
RUN apk add --no-cache curl && \
    curl -sfL https://install.goreleaser.com/github.com/tj/node-prune.sh | sh
ADD https://github.com/ufoscout/docker-compose-wait/releases/download/2.5.0/wait /wait
RUN chmod +x /wait

COPY package.json .
RUN yarn install && ./bin/node-prune
COPY . .
EXPOSE 3000
CMD /wait && yarn start
services/microservice-sample-web/Dockerfile
FROM node:11.10-alpine as builder

ENV NODE_ENV=production
ARG GITHUB_CLIENT_ID
ARG GITHUB_CLIENT_SECRET
WORKDIR /app

RUN apk add --no-cache curl && \
    curl -sfL https://install.goreleaser.com/github.com/tj/node-prune.sh | sh

COPY package.json .
COPY yarn.lock .
RUN yarn install && ./bin/node-prune
COPY . .
RUN yarn build

FROM node:11.10-alpine

WORKDIR /app
ADD package.json ./
ADD nuxt.config.js ./

COPY --from=builder ./app/server ./server
COPY --from=builder ./app/node_modules ./node_modules
COPY --from=builder ./app/.nuxt ./.nuxt

EXPOSE 3000
CMD yarn start

Webサービスに関してはマルチステージビルドを利用して最終的なイメージのサイズを軽くするように工夫しています。

実装後、各Dockerfilecommitしておきましょう。

docker-compose.ymlの作成

上記で用意したDockerfileを、dockerコマンドを利用してビルド、コンテナの起動等やってもよいのですが、環境変数等、引数が多くなってしまうため、今回はdocker-composeを利用して一元管理します。

docker-compose.ymlに起動したいコンテナの設定を書いていくことで、一括で立ち上げたり、削除することが可能です。

今回は、Webサービス、Userサービス、Tweetサービスと、Userサービス用DB、Tweetサービス用DBの合計5つのコンテナを定義します。

docker-compose.yml
---
version: '3'
services:
  web:
    build:
      context: ./services/microservice-sample-web
      dockerfile: Dockerfile
    image: microservice_web:1.0
    ports:
      - 3000:3000
    environment:
      NUXT_HOST: 0.0.0.0
      USER_SERVICE: http://user:3000
      TWEET_SERVICE: http://tweet:3000
      GITHUB_CLIENT_ID: ${GITHUB_CLIENT_ID}
      GITHUB_CLIENT_SECRET: ${GITHUB_CLIENT_SECRET}
  user:
    build:
      context: ./services/microservice-sample-user
      dockerfile: Dockerfile
    image: microservice_user:1.0
    environment:
      MONGODB_URL: mongodb://user-db:27017/user
      MONGODB_ADMIN_NAME: root
      MONGODB_ADMIN_PASS: example
      WAIT_HOSTS: user-db:27017
    depends_on:
      - user-db
  user-db:
    image: mongo
    volumes:
      - user-db:/data/db
    environment:
      MONGO_INITDB_ROOT_USERNAME: root
      MONGO_INITDB_ROOT_PASSWORD: example
  tweet:
    build:
      context: ./services/microservice-sample-tweet
      dockerfile: Dockerfile
    image: microservice_tweet:1.0
    environment:
      MONGODB_URL: mongodb://tweet-db:27017/user
      MONGODB_ADMIN_NAME: root
      MONGODB_ADMIN_PASS: example
      WAIT_HOSTS: tweet-db:27017
      USER_SERVICE: http://user:3000
    depends_on:
      - tweet-db
  tweet-db:
    image: mongo
    volumes:
      - tweet-db:/data/db
    environment:
      MONGO_INITDB_ROOT_USERNAME: root
      MONGO_INITDB_ROOT_PASSWORD: example
volumes:
  user-db:
  tweet-db:

GitHubのOAuthのキー等はGit管理する対象ではないので、.envファイルに記述します。
docker-composeの機能により、カレントディレクトリの.envファイルが読み込まれ、docker-compose.yml中で利用することができるようになります。
値はDocker環境の場合エンドポイントがlocalhost:3000と、前章までと同じなため、前章で使用していたのと同じGitHubのOAuth設定が利用可能です。

.env
GITHUB_CLIENT_ID=xxxxxxxxxxxxxxxxxxx
GITHUB_CLIENT_SECRET=xxxxxxxxxxxxxxxxxxxxxxxxxxxxx

services/.gitkeepファイルを作成しておきます。

touch services/.gitkeep

.gitignoreファイルを以下のように設定します。

.gitignore
services/*
!services/.gitkeep
.env

サービスを起動してみる

それでは、サービスを起動してみます。

# Dockerイメージをビルド
make build
# サービスの立ち上げ
make up
# ダミーデータの投入
make seed

http://localhost:3000へアクセスしてみましょう。
問題なく動作していれば本章の実装は完了になります。

第5章まとめ

本章では、各サービスのDockerfileを作成し、docker-composeを利用してマイクロサービスをローカルのDocker環境上に構築しました。

次の章ではいよいよKubernetes上でマイクロサービスを動作させてみます。

次章: 第6章 Kubernetes with minikube

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む