のらねこの気まま暮らし

技術系だけど、Qiita向きではないポエムとかを書くめったに更新されないヤツ

nodejsで利用する構造化ロギングライブラリ、logoneを作成した

概要

www.npmjs.com

主にexpressなどのサーバー上で動作することを期待した、構造化ロギングライブラリを作成した。 この記事は、その辺の概要を覚書程度に記すものである。

背景

AWSのCloud WatchGCPのCloud Loggingといった環境の中で、Nodejsサーバーからログを書き出す方法論の一つとして構造化ロギングがある。

Nodejsが標準で搭載する console.log は柔軟にログを書き出せるが、前述する環境だと単なるテキストと扱ったり、複雑なデータ構造のデバッグに向かない。 そのため、JSON.stringify%jを用いてデータなどをJSONに変換したり、severityをその中に埋め込んだりと様々な工夫がある。

一方で、console.logは直ちにstdoutやstderrに書き出してしまうため、一つのリクエストの中であっても同時にリクエストが走った場合、ログの関係性が追いづらくなってしまう。

こんな感じで入り乱れるよね

request1 aaa
request2 aaa
request1 bbb
request1 ccc
request2 bbb

そこで、コンテクスト(例えば、リクエスト単位など)毎にログを集約・構造化することでログの関係性・前後関係を追いやすくすることが求められる。

こんな感じでまとめるイメージ。

{ context: request1, lines: [aaa, bbb, ccc] }
{ context: request2, lines: [aaa, bbb] }

今回、お仕事の中で紹介してもらった logone-go を参考にNodejs向きに実装した。 大本がGoであり、Nodejsの事情や利用シーンの想定から大幅に手を加えているが、大変参考になったのでここで作成者の hixi-hyi に感謝を示したい。

アーキテクチャ

今回の用途ではexpressから利用することを想定している。 −−が、その一方で、お仕事ではRemixを利用していたり、クライアントサイドから利用される可能性も考慮したかった。

なので、選択的にどこにログを出力するか、どこから利用できるかという観点と構造化ログを作成する核心の機能を別のパッケージで提供したかった。

multi-package architecture

現在提供しているライブラリは以下の3つある

www.npmjs.com

www.npmjs.com

www.npmjs.com

@logone/core

ログを集約し、構造化するコア。 adapterを注入することで、出力先を選択できる

@logone/adapter-node

nodejs上で動作するアダプタ。 stdout, stderrにログを書き出す。

他の環境やファイル出力など、このadapterを実装・交換することで可能になる。

※同期性や複数出力先などは考慮が足りてない... 今後の課題

@logone/express

express上でlogoneを開始・出力するためのライブラリ。 リクエストを受け付けたらログの収集を開始し、レスポンスが発行されたらログを出力する。

現在はadapterを内包しているが外から注入できたほうがいいかもなーと思っている。

monorepo

これらのパッケージの運用・リリースを行うのにmonorepoの構成を採用している。 そのため、開発リポジトリは以下の一つのみ。

github.com

monorepoの運用については以下の記事を書いた際に検証し、得た知見をそのまま利用している。

mizuki-r.hatenablog.com

Usage

以下のような実装を考える。

import { createLogone } from '@logone/core'
import { createAdapter } from '@logone/adapter-node'

const logone = createLogone(createAdapter())
const main = () => {
  const { logger, finish } = logone.start('example')

  logger.info('hello world')

  finish()
}

これを実行すると、このようなログを1行にまとめて出力する。

{
    "type": "example",
    "config": {
        "elapsedUnit": "1ms"
    },
    "context": {},
    "runtime": {
        "severity": "INFO",
        "startTime": "2024-05-01T01:02:03.456Z",
        "endTime": "2024-05-01T01:02:04.456Z",
        "elapsed": 1000,
        "lines": [
            {
                "severity": "INFO",
                "message": "hello world",
                "payload": [],
                "fileLine": 8,
                "fileName": "/path/to/example.ts",
                "time": "2024-05-01T01:02:03.456Z"
            }
        ]
    }
}

finish関数が実行されるまでログはlogoneに収集され、出力されない。 finish関数が実行されて初めてログはadapterを経由して出力される。

ここから、logoneにおける要素と想定ケースを説明していく。

context

ログの集約単位を示す情報をcontextには設定できる。 例えば、リクエストなんかでは以下のように使うことができる。

const { logger, finish } = logone.start('request', {
  url: req.url,
  method: req.method,
  host: req.headers.host,
  userAgent: req.headers['user-agent'],
})

この設定によって、そのログがどのリクエスト(などのコンテクスト)で実行されたかの関係性が自明になる。

runtime elapsed

ログの開始から終了までの経過時間を1ms単位で出力する。 configで渡すInterfaceにはしているが今のところ1ms固定なので注意。

runtime severity

Severityは以下のものを用意している。 - DEBUG - INFO - WRNING - ERROR - CRITICAL

各Lineで出力されたSeverityのなかでプライオリティが一番高いものをruntimeのSeverityとして指定している。 プライオリティは上のリストで下に行くほど高い。

今後の展望としてSeverityの脚切りができるようにはしたい。

file information

今回工夫したものとしては、このファイル情報の出力である。 なぜならば、JavaScriptは関数の呼び出し元という情報を原則保持しないためである。(実行環境によって差異がある、Nodeはサポートしていない)

ただ唯一の例外がErrorオブジェクトによるStack Traceである。 今回はErrorオブジェクトを発行して、呼び出した関数のStackTraceからファイル情報を抜き出している。

  private getCallerPosition(): [string | null, number | null, number | null] {
    const stack = new Error().stack
    const rows = stack ? stack.split(/\n/) : []
    const current = rows[4]
    if (!current) {
      return [null, null, null]
    }
    const [file, lines, chars] = current.replace(/\s*at\s+/, '').split(/:/)
    return [
      file ?? null,
      lines ? Number(lines) : null,
      chars ? Number(chars) : null
    ]
  }

github.com

雑に引っこ抜いているので、フォーマット崩れはあるかも... とはいえ、これによって(生成後のJSファイルではあるが...)実行場所を把握しやすくなっている。

まとめ

−−というわけで、logone-goの移植、@logone/coreを作成しました! adapterを追加すればあちこちで使えるんじゃないかな〜というのと、多分NodeJSだけで動くようには作ってないと思うのと、動作についてはzero-dependencyなので非常にライトに使えると思っています。

NodeJS界隈の構造化ログだと、child-processなどで集約していたり、かなり工夫して頑張っているライブラリも多いですが、logoneは汎用性に重きを置いている(それが本当に必要なのかはわからない)ため、非常にライトな実装と動作になっています。 気軽に試してみたい方はぜひ触ってみてください。

turborepoとchangesetを使って、monorepoのライブラリ開発における構成を検討する

概要

monorepoなライブラリをnpmに公開するための構成を調査、検討する。

検証リポジトリ

以下にこの記事で検証したリポジトリを示す。

github.com

動作環境

macOS Ventura バージョン13.6.6 Node 21.4.0

前提となる構成

turborepo

turboを使ってmonorepoを構成する

turbo.build

changeset

turborepoによると@changesets/cli - npmを推奨している。

www.npmjs.com

verdaccio

localで動作するnpm registry。 実験でpublishしまくるので公式のregistryは流石に使いにくい。

verdaccio.org

Verdaccioの構築

docker imageを使うのがお手軽そうなので、取得して立ち上げる。

以下の点に注意。 * portの指定忘れると開けない * volumeの指定しないと再起動でデータが消える

$ docker pull verdaccio/verdaccio:nightly-master 
$ docker run -it -p4873:4873 --rm verdaccio/verdaccio:nightly-master
(node:8) [DEP0040] DeprecationWarning: The `punycode` module is deprecated. Please use a userland alternative instead.
(Use `node --trace-deprecation ...` to show where the warning was created)
 info -=- local storage path /verdaccio/storage/data/.verdaccio-db.json
 info -=- no private database found, recreating new one on /verdaccio/storage/data/.verdaccio-db.json
 info --- using htpasswd file: /verdaccio/storage/htpasswd
 info --- http address http://0.0.0.0:4873/
 info --- version: 7.0.0-next-7.13
 info --- server started

指定したportをブラウザで開く。

open http://0.0.0.0:4873/

ひとまず初期画面が表示される。

Verdaccioの初期画面

検証用のリポジトリを作成

こちらに作成しました。作成の意図としては - nodejs で動作するpackageをイメージ - turborepoで構成 - 複数のpackageを構成

github.com

changesetの導入

packageにインストール

$ npm -w packages/core i -D @changesets/cli 
$ npm -w packages/express i -D @changesets/cli 

coreとexpressのpackage.jsonchangesetスクリプトを追加

diff --git a/packages/core/package.json b/packages/core/package.json
index f8facde..c7876fd 100644
--- a/packages/core/package.json
+++ b/packages/core/package.json
@@ -7,12 +7,14 @@
   "scripts": {
     "build": "tsup src/*",
     "lint:eslint": "eslint --max-warnings 0 --ext .ts,.tsx .",
-    "lint:prettier": "prettier './src/**/*.{ts,tsx,js,json,css}' --check"
+    "lint:prettier": "prettier './src/**/*.{ts,tsx,js,json,css}' --check",
+    "changeset": "changeset"
diff --git a/packages/express/package.json b/packages/express/package.json
index f8facde..c7876fd 100644
--- a/packages/express/package.json
+++ b/packages/express/package.json
@@ -7,12 +7,14 @@
   "scripts": {
     "build": "tsup src/*",
     "lint:eslint": "eslint --max-warnings 0 --ext .ts,.tsx .",
-    "lint:prettier": "prettier './src/**/*.{ts,tsx,js,json,css}' --check"
+    "lint:prettier": "prettier './src/**/*.{ts,tsx,js,json,css}' --check",
+    "changeset": "changeset"

changesetの初期化

$ npm -w packages/core run changeset init
$ npm -w packages/express run changeset init

publishのaccessをpublicに変更

diff --git a/packages/core/.changeset/config.json b/packages/core/.changeset/config.json
index 91b6a95..fce1c26 100644
--- a/packages/core/.changeset/config.json
+++ b/packages/core/.changeset/config.json
@@ -4,7 +4,7 @@
   "commit": false,
   "fixed": [],
   "linked": [],
-  "access": "restricted",
+  "access": "public",
   "baseBranch": "main",
   "updateInternalDependencies": "patch",
   "ignore": []
diff --git a/packages/core/package.json b/packages/core/package.json
index 5f59f4d..1987880 100644
--- a/packages/core/package.json
+++ b/packages/core/package.json
@@ -14,5 +14,8 @@
     "@changesets/cli": "^2.27.1",
     "@example-monorepo/dev-config": "*",
     "tsup": "^8.0.2"
+  },
+  "publishConfig": {
+    "access": "public"
   }
 }
diff --git a/packages/express/.changeset/config.json b/packages/express/.changeset/config.json
index 91b6a95..fce1c26 100644
--- a/packages/express/.changeset/config.json
+++ b/packages/express/.changeset/config.json
@@ -4,7 +4,7 @@
   "commit": false,
   "fixed": [],
   "linked": [],
-  "access": "restricted",
+  "access": "public",
   "baseBranch": "main",
   "updateInternalDependencies": "patch",
   "ignore": []
diff --git a/packages/express/package.json b/packages/express/package.json
index c7876fd..586fd64 100644
--- a/packages/express/package.json
+++ b/packages/express/package.json
@@ -19,5 +19,8 @@
     "@types/express": "^4.17.21",
     "express": "^4.19.2",
     "tsup": "^8.0.2"
+  },
+  "publishConfig": {
+    "access": "public"
   }
 }

changesetの設定を行う

$ npm -w packages/core run changeset                                            [branch:main](untracked)[~/project/example-monorepo]

> @example-monorepo/core@0.0.0 changeset
> changeset

🦋  Which packages would you like to include? · @example-monorepo/core, @example-monorepo/express
🦋  Which packages should have a major bump? · No items were selected
🦋  Which packages should have a minor bump? · No items were selected
🦋  The following packages will be patch bumped:
🦋  @example-monorepo/core@0.0.0
🦋  @example-monorepo/express@0.0.0
🦋  Please enter a summary for this change (this will be in the changelogs).
🦋    (submit empty line to open external editor)
🦋  Summary · the first update
🦋  
🦋  === Summary of changesets ===
🦋  patch:  @example-monorepo/core, @example-monorepo/express
🦋  
🦋  Note: All dependents of these packages that will be incompatible with
🦋  the new version will be patch bumped when this changeset is applied.
🦋  
🦋  Is this your desired changeset? (Y/n) · true
🦋  Changeset added! - you can now commit it
🦋  
🦋  If you want to modify or expand on the changeset summary, you can find it here

bump up

version, publishへのエイリアスを作成する。

diff --git a/package.json b/package.json
index 106fd31..3f6c163 100644
--- a/package.json
+++ b/package.json
@@ -5,6 +5,8 @@
     "build": "turbo run build",
     "lint:eslint": "turbo run lint:eslint",
     "lint:prettier": "turbo run lint:prettier",
+    "version": "turbo run version",
+    "publish": "turbo run publish",
     "format": "prettier --write \"**/*.{ts,tsx,md}\""
   },
   "devDependencies": {
diff --git a/packages/core/package.json b/packages/core/package.json
index 1987880..74c6f4a 100644
--- a/packages/core/package.json
+++ b/packages/core/package.json
@@ -8,7 +8,9 @@
     "build": "tsup src/*",
     "lint:eslint": "eslint --max-warnings 0 --ext .ts,.tsx .",
     "lint:prettier": "prettier './src/**/*.{ts,tsx,js,json,css}' --check",
-    "changeset": "changeset"
+    "changeset": "changeset",
+    "version": "changeset version",
+    "publish": "changeset publish"
   },
   "devDependencies": {
     "@changesets/cli": "^2.27.1",
diff --git a/packages/express/package.json b/packages/express/package.json
index 586fd64..531b6b9 100644
--- a/packages/express/package.json
+++ b/packages/express/package.json
@@ -8,7 +8,9 @@
     "build": "tsup src/*",
     "lint:eslint": "eslint --max-warnings 0 --ext .ts,.tsx .",
     "lint:prettier": "prettier './src/**/*.{ts,tsx,js,json,css}' --check",
-    "changeset": "changeset"
+    "changeset": "changeset",
+    "version": "changeset version",
+    "publish": "changeset publish"
   },
   "dependencies": {
     "@example-monorepo/core": "*"
diff --git a/turbo.json b/turbo.json
index 8402ed6..c3df47b 100644
--- a/turbo.json
+++ b/turbo.json
@@ -18,6 +18,15 @@
     "dev": {
       "cache": false,
       "persistent": true
+    },
+    "version": {
+      "cache": false,
+      "persistent": true
+    },
+    "publish": {
+      "cache": false,
+      "persistent": true
     }
+
   }
 }

versionを上げる

$ npm run version
commit 62802584c2bbc5be7fec65d73d33d2efc12b01c3
Author: mizuki_r <ry.mizuki@gmail.com>
Date:   Mon Apr 29 15:40:07 2024 +0900

    bump 0.0.1

diff --git a/packages/core/CHANGELOG.md b/packages/core/CHANGELOG.md
new file mode 100644
index 0000000..726ce34
--- /dev/null
+++ b/packages/core/CHANGELOG.md
@@ -0,0 +1,7 @@
+# @example-monorepo/core
+
+## 0.0.1
+
+### Patch Changes
+
+- the first update
diff --git a/packages/core/package.json b/packages/core/package.json
index 74c6f4a..8b1b54c 100644
--- a/packages/core/package.json
+++ b/packages/core/package.json
@@ -1,6 +1,6 @@
 {
   "name": "@example-monorepo/core",
-  "version": "0.0.0",
+  "version": "0.0.1",
   "private": true,
   "main": "build/index.js",
   "types": "src/index.ts",
diff --git a/packages/express/CHANGELOG.md b/packages/express/CHANGELOG.md
new file mode 100644
index 0000000..07d956c
--- /dev/null
+++ b/packages/express/CHANGELOG.md
@@ -0,0 +1,9 @@
+# @example-monorepo/express
+
+## 0.0.1
+
+### Patch Changes
+
+- the first update
+- Updated dependencies
+  - @example-monorepo/core@0.0.1
diff --git a/packages/express/package.json b/packages/express/package.json
index 531b6b9..1d38dc0 100644
--- a/packages/express/package.json
+++ b/packages/express/package.json
@@ -1,6 +1,6 @@
 {
   "name": "@example-monorepo/express",
-  "version": "0.0.0",
+  "version": "0.0.1",
   "private": true,
   "main": "build/index.js",
   "types": "src/index.ts",

verdaccioとの接続

verdaccioのURLをnpmrcに登録

diff --git a/.npmrc b/.npmrc
new file mode 100644
index 0000000..30fb789
--- /dev/null
+++ b/.npmrc
@@ -0,0 +1 @@
+registry=http://0.0.0.0:4873

adduserしてverdaccioにユーザを登録

$  npm adduser
npm notice Log in on http://0.0.0.0:4873/
Username: mizuki_r
Email: (this IS public) XXXXX@gmail.com
Logged in on http://0.0.0.0:4873/.

いざ、publish、とおもったのに....

$ npm run publish  

> example-monorepo@0.0.0 publish
> turbo run publish

• Packages in scope: @example-monorepo/core, @example-monorepo/dev-config, @example-monorepo/express
• Running publish in 3 packages
• Remote caching disabled
@example-monorepo/core:publish: cache bypass, force executing 6d170e85bb8ec0a1
@example-monorepo/express:publish: cache bypass, force executing 97213dc3ac891ae0
@example-monorepo/express:publish: 
@example-monorepo/express:publish: > @example-monorepo/express@0.0.1 publish
@example-monorepo/express:publish: > changeset publish
@example-monorepo/express:publish: 
@example-monorepo/core:publish: 
@example-monorepo/core:publish: > @example-monorepo/core@0.0.1 publish
@example-monorepo/core:publish: > changeset publish
@example-monorepo/core:publish: 
@example-monorepo/express:publish: 🦋  warn No unpublished projects to publish
@example-monorepo/core:publish: 🦋  warn No unpublished projects to publish

 Tasks:    2 successful, 2 total
Cached:    0 cached, 2 total
  Time:    701ms 

packageがprivate: trueだったからです。

diff --git a/packages/core/package.json b/packages/core/package.json
index 8b1b54c..cc96e00 100644
--- a/packages/core/package.json
+++ b/packages/core/package.json
@@ -1,7 +1,7 @@
 {
   "name": "@example-monorepo/core",
   "version": "0.0.1",
-  "private": true,
+  "private": false,
   "main": "build/index.js",
   "types": "src/index.ts",
   "scripts": {
diff --git a/packages/express/package.json b/packages/express/package.json
index 1d38dc0..df29381 100644
--- a/packages/express/package.json
+++ b/packages/express/package.json
@@ -1,7 +1,7 @@
 {
   "name": "@example-monorepo/express",
   "version": "0.0.1",
-  "private": true,
+  "private": false,
   "main": "build/index.js",
   "types": "src/index.ts",
   "scripts": {

今度こそpublish

$ npm run publish

> example-monorepo@0.0.0 publish
> turbo run publish

• Packages in scope: @example-monorepo/core, @example-monorepo/dev-config, @example-monorepo/express
• Running publish in 3 packages
• Remote caching disabled
@example-monorepo/express:publish: cache bypass, force executing 95f6040856482a97
@example-monorepo/core:publish: cache bypass, force executing 304ea4a437f8c0b8
@example-monorepo/express:publish: 
@example-monorepo/express:publish: > @example-monorepo/express@0.0.1 publish
@example-monorepo/express:publish: > changeset publish
@example-monorepo/express:publish: 
@example-monorepo/core:publish: 
@example-monorepo/core:publish: > @example-monorepo/core@0.0.1 publish
@example-monorepo/core:publish: > changeset publish
@example-monorepo/core:publish: 
@example-monorepo/express:publish: 🦋  info npm info @example-monorepo/core
@example-monorepo/core:publish: 🦋  info npm info @example-monorepo/core
@example-monorepo/core:publish: 🦋  info npm info @example-monorepo/express
@example-monorepo/express:publish: 🦋  info npm info @example-monorepo/express
@example-monorepo/core:publish: 🦋  warn Received 404 for npm info "@example-monorepo/core"
@example-monorepo/express:publish: 🦋  warn Received 404 for npm info "@example-monorepo/express"
@example-monorepo/express:publish: 🦋  warn Received 404 for npm info "@example-monorepo/core"
@example-monorepo/express:publish: 🦋  info @example-monorepo/core is being published because our local version (0.0.1) has not been published on npm
@example-monorepo/express:publish: 🦋  info @example-monorepo/express is being published because our local version (0.0.1) has not been published on npm
@example-monorepo/express:publish: 🦋  info Publishing "@example-monorepo/core" at "0.0.1"
@example-monorepo/express:publish: 🦋  info Publishing "@example-monorepo/express" at "0.0.1"
@example-monorepo/core:publish: 🦋  warn Received 404 for npm info "@example-monorepo/express"
@example-monorepo/core:publish: 🦋  info @example-monorepo/core is being published because our local version (0.0.1) has not been published on npm
@example-monorepo/core:publish: 🦋  info @example-monorepo/express is being published because our local version (0.0.1) has not been published on npm
@example-monorepo/core:publish: 🦋  info Publishing "@example-monorepo/core" at "0.0.1"
@example-monorepo/core:publish: 🦋  info Publishing "@example-monorepo/express" at "0.0.1"
@example-monorepo/core:publish: 🦋  error an error occurred while publishing @example-monorepo/express: E409 409 Conflict - PUT http://0.0.0.0:4873/@example-monorepo%2fcore - this package is already present 
@example-monorepo/express:publish: 🦋  error an error occurred while publishing @example-monorepo/express: E409 409 Conflict - PUT http://0.0.0.0:4873/@example-monorepo%2fexpress - this package is already present 
@example-monorepo/core:publish: 🦋  error npm notice Publishing to http://0.0.0.0:4873 with tag latest and public access
@example-monorepo/core:publish: 🦋  error npm ERR! code E409
@example-monorepo/core:publish: 🦋  error npm ERR! 409 Conflict - PUT http://0.0.0.0:4873/@example-monorepo%2fcore - this package is already present
@example-monorepo/core:publish: 🦋  error 
@example-monorepo/core:publish: 🦋  error 
@example-monorepo/express:publish: 🦋  error npm notice Publishing to http://0.0.0.0:4873 with tag latest and public access
@example-monorepo/express:publish: 🦋  error npm ERR! code E409
@example-monorepo/express:publish: 🦋  error npm ERR! 409 Conflict - PUT http://0.0.0.0:4873/@example-monorepo%2fexpress - this package is already present
@example-monorepo/express:publish: 🦋  error 
@example-monorepo/express:publish: 🦋  error 
@example-monorepo/express:publish: 🦋  success packages published successfully:
@example-monorepo/core:publish: 🦋  success packages published successfully:
@example-monorepo/express:publish: 🦋  @example-monorepo/core@0.0.1
@example-monorepo/express:publish: 🦋  Creating git tag...
@example-monorepo/core:publish: 🦋  @example-monorepo/core@0.0.1
@example-monorepo/core:publish: 🦋  Creating git tag...
@example-monorepo/express:publish: 🦋  New tag:  @example-monorepo/core@0.0.1
@example-monorepo/core:publish: 🦋  New tag:  @example-monorepo/core@0.0.1
@example-monorepo/core:publish: 🦋  error packages failed to publish:
@example-monorepo/core:publish: 🦋  @example-monorepo/express@0.0.1
@example-monorepo/express:publish: 🦋  error packages failed to publish:
@example-monorepo/express:publish: 🦋  @example-monorepo/express@0.0.1

あー、個別にpublishするとだめなのか。 一応publishできているけど、多分rootに入れれば動くっぽい

rootにchangesetを設定する

$ npm install -D @changesets/cli
$ npm run changeset init
$ npm run changeset
$ npm run changeset version
$ npm run changeset publish

> example-monorepo@0.0.0 publish
> changeset publish

🦋  info npm info @example-monorepo/core
🦋  info npm info @example-monorepo/express
🦋  info @example-monorepo/core is being published because our local version (0.0.2) has not been published on npm
🦋  info @example-monorepo/express is being published because our local version (0.0.2) has not been published on npm
🦋  info Publishing "@example-monorepo/core" at "0.0.2"
🦋  info Publishing "@example-monorepo/express" at "0.0.2"
🦋  success packages published successfully:
🦋  @example-monorepo/core@0.0.2
🦋  @example-monorepo/express@0.0.2
🦋  Creating git tags...
🦋  New tag:  @example-monorepo/core@0.0.2
🦋  New tag:  @example-monorepo/express@0.0.2

version upに成功した画面

まとめ

  • turborepo x changesetsで良さそう
  • changesetsはrootに置く
    • 必要なパッケージを選択式でpublishできる
    • 変更をstackしてまとめてbumpできる
    • publishも一括でやってくれる
  • パッケージ作成実験にverdaccioは便利

App Engineを試す vol4 ~サービス同士を連携させる~

概要

AppEngine勉強日記、4日目。

この記事では、defaultサービス上のNuxtからexpressを経てapisサービスのAPIにアクセスするところを検証します。

すごい、4日連続で続いたよ。アドベントカレンダーいけんじゃね?

背景

AppEngineが便利っていう話を聞いたので、AppEngineでどこまでできるのかを試す連載。 連載のゴールとしては、NuxtをBFFとしておいてAPIで処理を行うというマルチサービスの構成。

作るサービスの仕様

  1. 題名と本文を投稿できる
  2. 投稿すると、題名と本文を閲覧できるページに遷移する

以上。いわゆるnopasteと呼ばれるサービスです。slackの時代、もう知らないひともいるんじゃなかろうか。

あ、コードやテストについては、日付更新まで2hしかなかったのでとりあえずなるはやの全力で書いたので結構雑です。 そのうちリファクタする。

defaultサービスの変更

通信用にaxiosをとりあえず入れる。

> cd root
> npm i axios

ページの作成

ついでにlayoutも作るけど省略。

./root/pages/index.vue

投稿用の画面を作る。 titlebodyを入力して、postを押下すると/apis/postsPOSTリクエストを投げる。

<template>
  <form class="form">
    <div class="form-group">
      <label for="title" class="form-label">Title</label>
      <div>
        <input type="text" id="title" class="form-control" v-model="input.title">
      </div>
    </div>
    <div class="form-group">
      <label for="body" class="form-label">Body</label>
      <div>
        <textarea id="body" rows="10" v-model="input.body"></textarea>
      </div>
    </div>
    <div class="form-group">
      <button type="button" class="btn-primary" :disabled="!is_valid" @click="submit">Post</button>
    </div>
  </form>
</template>

<script>
import axios from 'axios'
export default {
  data() {
    return {
      input: { title: null, body: null }
    }
  },
  methods: {
    is_valid() {
      return this.body
    },
    async submit() {
      const { data } = await axios.post('/apis/posts', {
        title: this.input.title,
        body: this.input.body
      })
      this.$router.push(`/posts/${data.id}`)
    }
  }
}
</script>

pages/posts/_id.vue

閲覧画面の作成。 /apis/posts/:idから文書情報を取得して表示する。

<template>
  <div v-if="content">
    <h2>{{content.title || 'no title'}}</h2>
    <div>
      {{content.body}}
    </div>
  </div>
</template>

<script>
import axios from 'axios'
export default {
  data() {
    return { content: null }
  },
  mounted() {
    axios.get(`/apis/posts/${this.$route.params.id}`)
      .then(({ data }) => {
        this.content = data
      })
  }
}
</script>

expressサーバにAPIを追加

POSTリクエストを受け取れるようにbodyParserをセット。

GET /apis/posts/:idPOST /apis/posts のルーティングを作成。 apisサービスのAPIを立たけるようにする。

このとき、apisサービスのオリジンは環境変数から渡せるようにする。

diff --git a/root/app.js b/root/app.js
index 35a950f..c9120af 100644
--- a/root/app.js
+++ b/root/app.js
@@ -5,17 +5,34 @@ const express = require('express');
 const app = express();
 const { Nuxt, Builder } = require('nuxt')

+app.use(express.json())
+app.use(express.urlencoded({ extended: true }));
+
 // Nuxt.js をオプションとともにインスタンス化する
 const config = require('./nuxt.config')
 config.dev = process.env.NODE_ENV !== 'production'
 const nuxt = new Nuxt(config)

-app.get('/message', (req, res) => {
-  res
-    .status(200)
-    .send('Hello, world!')
-    .end();
-});
+const axios = require('axios')
+app.post('/apis/posts', (req, res) => {
+  const data = req.body
+  axios.post(`${process.env.APIS_ORIGIN}/posts`, {
+    title: data.title,
+    body: data.body
+  })
+    .then(({ data }) => {
+      res.json({ id: data.id })
+    })
+})
+app.get('/apis/posts/:id', (req, res) => {
+  axios.get(`${process.env.APIS_ORIGIN}/posts/${req.params.id}`)
+    .then(({ data }) => {
+      res.json(data)
+    })
+    .catch(({ response }) => {
+      res.status(response.status).json(response.body)
+    })
+})

 app.use(nuxt.render)

app.yamlにapisサービスのオリジンを設定

diff --git a/root/app.yaml b/root/app.yaml
index eed1e47..6c4cf07 100644
--- a/root/app.yaml
+++ b/root/app.yaml
@@ -1 +1,4 @@
 runtime: nodejs10
+
+env_variables:
+  APIS_ORIGIN: https://apis-dot-astral-web-260712.appspot.com

deploy

> npm run build
> gcloud app deploy

これだけではまだ見れないので、続けてapisサービスを作っていく

apisサービスの変更

GET /posts/:idにリクエストがきたらサービスから任意のIDのレコードを取得して返却。

POST /postsにリクエストが来たらサービスにtitlebodyを渡してidを返却。

import { Controller, Post, Body, Get, Param, NotFoundException } from '@nestjs/common';
import { PostsService } from './posts.service';

@Controller('posts')
export class PostsController {
  constructor(private readonly postService: PostsService) {
  }

  @Get(':id')
  async fetchById(@Param('id') id: string): Promise<{id: number, title: null|string, body: string}> {
    const row = await this.postService.fetchById(Number(id));
    if (!row) {
      throw new NotFoundException();
    }
    return row;
  }

  @Post('')
  async save(
    @Body('title') title: null|string,
    @Body('body') body: string,
  ): Promise<{ id: number }> {
    return await this.postService.save({ title, body });
  }
}

サービスを定義する。面倒なのでメモリ上にレコードを保持する。

import { Injectable } from '@nestjs/common';

@Injectable()
export class PostsService {
  private rows: Array<{ id: number, title: string|null, body: string }>;

  constructor() {
    this.rows = [];
  }

  async fetchById(id: number) {
    return this.rows.find((row) => row.id === id);
  }

  async save({ title, body }: { title: string|null, body: string }): Promise<{ id: number }> {
    const id = this.rows.length + 1;
    this.rows.push({ id, title, body });
    return { id };
  }
}

PostsModuleを定義して、ControllerとServiceを関連付ける。

import { Module } from '@nestjs/common';
import { PostsController } from './posts.controller';
import { PostsService } from './posts.service';

@Module({
  imports: [],
  controllers: [PostsController],
  providers: [PostsService],
})
export class PostsModule {}

最後にAppModuleにPostsModuleを追加して完了

diff --git a/apis/src/app.module.ts b/apis/src/app.module.ts
index 8662803..5c11c51 100644
--- a/apis/src/app.module.ts
+++ b/apis/src/app.module.ts
@@ -1,9 +1,10 @@
 import { Module } from '@nestjs/common';
 import { AppController } from './app.controller';
 import { AppService } from './app.service';
+import { PostsModule } from './posts/posts.module';

 @Module({
-  imports: [],
+  imports: [PostsModule],
   controllers: [AppController],
   providers: [AppService],
 })

deployする

> npm run build
> gcloud app deploy

動作を確認する

gcloud app browse -s defaultすると投稿画面が開く。 投稿フォーム実行画面

値を入力してPostすると表示ページに遷移する。 表示画面

できたー!

まとめ

なんとか4日目の対応を乗り切ったぜ...!

ここまで勧めてみたけど、いくつか気になる点がある。

  • メモリ上に乗せてるけどインスタンス増えたら任意のデータが取れないよね...?
  • apisサービスのURIに直接アクセスできちゃうよね...?

今後の課題にしていきます!

次回予告

というわけで、明日はデータストア周りさわってみたいなー!

App Engineを試す vol3 ~NestJSでサブシステムを動かす~

概要

AppEngine勉強日記、3日目。 この記事ではNestJSで立てたAPIサーバをAppEngineでサブシステムとして動かすとこまでを解説します。

背景

AppEngineが便利っていう話を聞いたので、AppEngineでどこまでできるのかを試す連載。 連載のゴールとしては、NuxtをBFFとしておいてAPIで処理を行うというマルチサービスの構成。

ディレクトリの整理

昨日作ったNuxtのサービスをディレクトリを掘って移動する。 defaultサービスになるのでわかり易い名前を...と思ったが、defaultでは流石にわかりにくい。 とりあえずroot命名して移動する

> git status
On branch master
Changes to be committed:
  (use "git reset HEAD <file>..." to unstage)

  renamed:    app.js -> root/app.js
    renamed:    app.yaml -> root/app.yaml
    renamed:    nuxt.config.js -> root/nuxt.config.js
    renamed:    nuxt/pages/index.vue -> root/nuxt/pages/index.vue
    renamed:    package-lock.json -> root/package-lock.json
    renamed:    package.json -> root/package.json

NestJSのセットアップ

APIサーバには最近気になっているNestJSを使う。

@nestjs/cliを入れる。

> npm i -g @nestjs/cli
> nodenv rehash

apisディレクトリを指定してプロジェクトをセットアップする。

> nest new apis
⚡  We will scaffold your app in a few seconds..

CREATE /apis/.prettierrc (51 bytes)
CREATE /apis/README.md (3370 bytes)
CREATE /apis/nest-cli.json (64 bytes)
CREATE /apis/package.json (1687 bytes)
CREATE /apis/tsconfig.build.json (97 bytes)
CREATE /apis/tsconfig.json (336 bytes)
CREATE /apis/tslint.json (426 bytes)
CREATE /apis/src/app.controller.spec.ts (617 bytes)
CREATE /apis/src/app.controller.ts (274 bytes)
CREATE /apis/src/app.module.ts (249 bytes)
CREATE /apis/src/app.service.ts (142 bytes)
CREATE /apis/src/main.ts (208 bytes)
CREATE /apis/test/app.e2e-spec.ts (630 bytes)
CREATE /apis/test/jest-e2e.json (183 bytes)

? Which package manager would you ❤️  to use? npm
✔ Installation in progress... ☕

🚀  Successfully created project apis
👉  Get started with the following commands:

$ cd apis
$ npm run start


                          Thanks for installing Nest 🙏
                 Please consider donating to our open collective
                        to help us maintain this package.


               🍷  Donate: https://opencollective.com/nest

こんな感じのディレクトリが作成される。

> ls -lah apis/
total 848
drwxr-xr-x   15 mizuki  staff   480B 12  3 23:12 .
drwxr-xr-x    9 mizuki  staff   288B 12  3 23:11 ..
drwxr-xr-x    9 mizuki  staff   288B 12  3 23:11 .git
-rw-r--r--    1 mizuki  staff   375B 12  3 23:11 .gitignore
-rw-r--r--    1 mizuki  staff    51B 12  3 23:11 .prettierrc
-rw-r--r--    1 mizuki  staff   3.3K 12  3 23:11 README.md
-rw-r--r--    1 mizuki  staff    64B 12  3 23:11 nest-cli.json
drwxr-xr-x  668 mizuki  staff    21K 12  3 23:12 node_modules
-rw-r--r--    1 mizuki  staff   389K 12  3 23:12 package-lock.json
-rw-r--r--    1 mizuki  staff   1.6K 12  3 23:11 package.json
drwxr-xr-x    7 mizuki  staff   224B 12  3 23:11 src
drwxr-xr-x    4 mizuki  staff   128B 12  3 23:11 test
-rw-r--r--    1 mizuki  staff    97B 12  3 23:11 tsconfig.build.json
-rw-r--r--    1 mizuki  staff   336B 12  3 23:11 tsconfig.json
-rw-r--r--    1 mizuki  staff   426B 12  3 23:11 tslint.json

とりあえずローカル環境でサーバを起動する。

> cd apis
> npm start
> apis@0.0.1 start /Users/mizuki/workspace/20191203-example-appengine/apis
> nest start

[Nest] 47954   - 2019-12-03 11:15:03 PM   [NestFactory] Starting Nest application...
[Nest] 47954   - 2019-12-03 11:15:03 PM   [InstanceLoader] AppModule dependencies initialized +18ms
[Nest] 47954   - 2019-12-03 11:15:03 PM   [RoutesResolver] AppController {/}: +5ms
[Nest] 47954   - 2019-12-03 11:15:03 PM   [RouterExplorer] Mapped {/, GET} route +2ms
[Nest] 47954   - 2019-12-03 11:15:03 PM   [NestApplication] Nest application successfully started +2ms
> curl -v localhost:3000
* Rebuilt URL to: localhost:3000/
*   Trying ::1...
* TCP_NODELAY set
* Connected to localhost (::1) port 3000 (#0)
> GET / HTTP/1.1
> Host: localhost:3000
> User-Agent: curl/7.54.0
> Accept: */*
>
< HTTP/1.1 200 OK
< X-Powered-By: Express
< Content-Type: text/html; charset=utf-8
< Content-Length: 12
< ETag: W/"c-Lve95gjOVATpfV8EL5X4nxwjKHE"
< Date: Tue, 03 Dec 2019 14:15:52 GMT
< Connection: keep-alive
<
* Connection #0 to host localhost left intact
Hello World!%

プロジェクトをデプロイする

例によってapp.yamlを作成する。 今回はdefaultのサービスではなく、サブシステムとなるapisserviceに指定する。

> echo '
service: apis
runtime: nodejs10
' > app.yaml

apisディレクトリでデプロイコマンドを実行する。 以下のようにターゲットが表示されるので、問題なければyで実行。 target urlhttps://apis-dot-astral-web-260712.appspot.comになっていることに注目。

> gcloud app deploy
Services to deploy:

descriptor:      [/Users/mizuki/workspace/20191203-example-appengine/apis/app.yaml]
source:          [/Users/mizuki/workspace/20191203-example-appengine/apis]
target project:  [astral-web-260712]
target service:  [apis]
target version:  [20191203t232045]
target url:      [https://apis-dot-astral-web-260712.appspot.com]


Do you want to continue (Y/n)?  y

Beginning deployment of service [apis]...
Created .gcloudignore file. See `gcloud topic gcloudignore` for details.
╔════════════════════════════════════════════════════════════╗
╠═ Uploading 29 files to Google Cloud Storage               ═╣
╚════════════════════════════════════════════════════════════╝
File upload done.
Updating service [apis]...done.
Setting traffic split for service [apis]...done.
Deployed service [apis] to [https://apis-dot-astral-web-260712.appspot.com]

You can stream logs from the command line by running:
  $ gcloud app logs tail -s apis

To view your application in the web browser run:
  $ gcloud app browse -s apis

さー、デプロイできたぞー、と思ったがーー

> curl https://apis-dot-astral-web-260712.appspot.com

<html><head>
<meta http-equiv="content-type" content="text/html;charset=utf-8">
<title>500 Server Error</title>
</head>
<body text=#000000 bgcolor=#ffffff>
<h1>Error: Server Error</h1>
<h2>The server encountered an error and could not complete your request.<p>Please try again in 30 seconds.</h2>
<h2></h2>
</body></html>

ーーなんかエラーになるな... 困ったときはとりあえずログを確認。-sでサービスを指定する。

> gcloud app logs tail -s apis
Waiting for new log entries...
2019-12-03 14:22:46 apis[20191203t232045]  "GET / HTTP/1.1" 500
2019-12-03 14:22:46 apis[20191203t232045]  sh: 1: exec: nest: not found
2019-12-03 14:22:48 apis[20191203t232045]  "GET /favicon.ico HTTP/1.1" 500
2019-12-03 14:22:49 apis[20191203t232045]  sh: 1: exec: nest: not found
2019-12-03 14:22:52 apis[20191203t232045]  "GET / HTTP/1.1" 500
2019-12-03 14:22:52 apis[20191203t232045]  sh: 1: exec: nest: not found
2019-12-03 14:22:52 apis[20191203t232045]  "GET /favicon.ico HTTP/1.1" 500
2019-12-03 14:22:53 apis[20191203t232045]  sh: 1: exec: nest: not found
2019-12-03 14:23:22 apis[20191203t232045]  "GET / HTTP/1.1" 500
2019-12-03 14:23:22 apis[20191203t232045]  sh: 1: exec: nest: not found

nest not foundらしい... @nestjs/clidevDependenciesに含まれているため、インストールされないようだ。

npm startのコマンドからnestの依存を外す。

diff --git a/package.json b/package.json
index 6f344f7..01bd10c 100644
--- a/package.json
+++ b/package.json
@@ -8,7 +8,7 @@
     "prebuild": "rimraf dist",
     "build": "nest build",
     "format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"",
-    "start": "nest start",
+    "start": "npm run start:prod",
     "start:dev": "nest start --watch",
     "start:debug": "nest start --debug --watch",
     "start:prod": "node dist/main",

さあさ、デプロイデプロイっ

> gcloud app deploy
Services to deploy:

descriptor:      [/Users/mizuki/workspace/20191203-example-appengine/apis/app.yaml]
source:          [/Users/mizuki/workspace/20191203-example-appengine/apis]
target project:  [astral-web-260712]
target service:  [apis]
target version:  [20191203t233112]
target url:      [https://apis-dot-astral-web-260712.appspot.com]


Do you want to continue (Y/n)?  y

Beginning deployment of service [apis]...
╔════════════════════════════════════════════════════════════╗
╠═ Uploading 1 file to Google Cloud Storage                 ═╣
╚════════════════════════════════════════════════════════════╝
File upload done.
Updating service [apis]...done.
Setting traffic split for service [apis]...done.
Deployed service [apis] to [https://apis-dot-astral-web-260712.appspot.com]

You can stream logs from the command line by running:
  $ gcloud app logs tail -s apis

To view your application in the web browser run:
  $ gcloud app browse -s apis

ログを起動しとく

> gcloud app logs tail -s apis
2019-12-03 14:33:24 apis[20191203t233112]
2019-12-03 14:33:24 apis[20191203t233112]  > apis@0.0.1 start:prod /srv
2019-12-03 14:33:24 apis[20191203t233112]  > node dist/main
2019-12-03 14:33:24 apis[20191203t233112]
2019-12-03 14:33:25 apis[20191203t233112]  [Nest] 28   - 12/03/2019, 2:33:25 PM   [NestFactory] Starting Nest application...
2019-12-03 14:33:25 apis[20191203t233112]  [Nest] 28   - 12/03/2019, 2:33:25 PM   [InstanceLoader] AppModule dependencies initialized +21ms
2019-12-03 14:33:25 apis[20191203t233112]  [Nest] 28   - 12/03/2019, 2:33:25 PM   [RoutesResolver] AppController {/}: +6ms
2019-12-03 14:33:25 apis[20191203t233112]  [Nest] 28   - 12/03/2019, 2:33:25 PM   [RouterExplorer] Mapped {/, GET} route +3ms
2019-12-03 14:33:25 apis[20191203t233112]  [Nest] 28   - 12/03/2019, 2:33:25 PM   [NestApplication] Nest application successfully started +3ms

満を持してcurlを叩くーー

> curl https://apis-dot-astral-web-260712.appspot.com

ーーが、レスポンスが帰ってこない!なんぞ!? 首をかしげながら考える私。 んー通信出来へんてこたぁサービス起動してへんのかな? やー、ゆうてもログでは起動しるやん?

... ... ...あっ 唐突に思いつく私。

diff --git a/src/main.ts b/src/main.ts
index 13cad38..7357152 100644
--- a/src/main.ts
+++ b/src/main.ts
@@ -3,6 +3,6 @@ import { AppModule } from './app.module';

 async function bootstrap() {
   const app = await NestFactory.create(AppModule);
-  await app.listen(3000);
+  await app.listen(Number(process.env.PORT) || 3000);
 }
 bootstrap();

あー

> npm run build
> gcloud app deploy

ああー

> gcloud app logs tail -s apis
2019-12-03 14:40:25 apis[20191203t233845]  > apis@0.0.1 start:prod /srv
2019-12-03 14:40:25 apis[20191203t233845]  > node dist/main
2019-12-03 14:40:25 apis[20191203t233845]
2019-12-03 14:40:27 apis[20191203t233845]  [Nest] 28   - 12/03/2019, 2:40:27 PM   [NestFactory] Starting Nest application...
2019-12-03 14:40:27 apis[20191203t233845]  [Nest] 28   - 12/03/2019, 2:40:27 PM   [InstanceLoader] AppModule dependencies initialized +23ms
2019-12-03 14:40:27 apis[20191203t233845]  [Nest] 28   - 12/03/2019, 2:40:27 PM   [RoutesResolver] AppController {/}: +7ms
2019-12-03 14:40:27 apis[20191203t233845]  [Nest] 28   - 12/03/2019, 2:40:27 PM   [RouterExplorer] Mapped {/, GET} route +25ms
2019-12-03 14:40:27 apis[20191203t233845]  [Nest] 28   - 12/03/2019, 2:40:27 PM   [NestApplication] Nest application successfully started +3ms

あ〜〜〜〜〜〜〜

> curl https://apis-dot-astral-web-260712.appspot.comHello World!%

まとめ

AppEngineかんたんでとてもわかり易いですね!!!!!

次回予告

サービス2つ作っただけじゃね? 明日は連携できるようにコード書きます!がんばります!

App Engineを試す vol2 ~NuxtをAppEngineで動かす~

vol2 NuxtをAppEngineで動かす

概要

この記事ではAppEngineのデフォルトサービスでNuxtを動かすまでの手順を解説します。

背景

AppEngineが便利っていう話を聞いたので、AppEngineでどこまでできるのかを試す連載。 連載のゴールとしては、NuxtをBFFとしておいてAPIで処理を行うというマルチサービスの構成。

ローカルマシンにnode v10.16をインストール

nodenvを使ってるけどお好きなのでどうぞ。

> nodenv install 10.16.0
> nodenv local 10.16.0

プロジェクトの初期化

> npm init

nuxtのセットアップ

Expressで連携させたいので一旦手動でnuxtを入れますよ。 create-nuxt-appを使いたい方は作成してからよしなに調整するのじゃ。

> npm install --save nuxt

ソースの作成

以下のファイルを用意する。

  • nuxt.config.js
  • nuxt/pages/index.vue
> cat nuxt.config.js
module.exports = {
  srcDir: "./nuxt"
}
> mkdir -p nuxt/pages
> cat nuxt/pages/index.vue
<template>
  <p>hello world!</p>
</template>

起動コマンドを指定して、動作確認

取り替えず開発用にビルドできるようにする

diff --git a/package.json b/package.json
index b493bb3..d3dc0a3 100644
--- a/package.json
+++ b/package.json
@@ -4,6 +4,7 @@
   "description": "",
   "main": "index.js",
   "scripts": {
+    "start": "nuxt dev",
     "test": "echo \"Error: no test specified\" && exit 1"
   },
   "author":  

起動する。

> npm start

動作を確認する。

curl http://localhost:3000/

AppEngineへのdeploy

一旦この段階でAppEngineにdeployしてみる。 app.yamlを最低限の構成で追加する。

> cat app.yaml
runtime: nodejs10

devのままだと動かないので、startに変更。

diff --git a/package.json b/package.json
index d3dc0a3..0c2ac81 100644
--- a/package.json
+++ b/package.json
@@ -4,7 +4,8 @@
   "description": "",
   "main": "index.js",
   "scripts": {
-    "start": "nuxt dev",
+    "start": "nuxt start",
+    "build": "nuxt build",
     "test": "echo \"Error: no test specified\" && exit 1"
   },
   "author": "",

ローカルでビルドしておく。

> npm run build

Let's deploy

> gcloud app deploy

deploy後、gcloud app browseでページを開いてみると、 Nuxtで実装したページがホスティングされたのがわかります。

Expressでハンドルする。

Nuxtが動いたので記事タイトル的にはOKなんですけど、最終的にはバックエンドでAPIと通信したいのです。 そのためにはBFFと連携するバックエンド部分の実装が必要なのです。

そこをお手軽にExpressで作っちゃいます。

Expressのエントリポイントを作成し、Nuxtをバンドルするようにします。 /messageはExpressで処理されているのをわかりやすく確認するために追加したルートです。

> cat app.js
'use strict';

const express = require('express');

const app = express();
const { Nuxt, Builder } = require('nuxt')

// Nuxt.js をオプションとともにインスタンス化する
const config = require('./nuxt.config')
config.dev = process.env.NODE_ENV !== 'production'
const nuxt = new Nuxt(config)

app.get('/message', (req, res) => {
  res
    .status(200)
    .send('Hello, world!')
    .end();
});

app.use(nuxt.render)

// Start the server
if (config.dev) {
  // hot-reload for development
  new Builder(nuxt).build().then(listen)
} else {
  // for production
  listen()
}

function listen() {
  const PORT = process.env.PORT || 8080;
  app.listen(PORT, () => {
    console.log(`App listening on port ${PORT}`);
    console.log('Press Ctrl+C to quit.');
  });
}

module.exports = app;

npm startでexpressのエントリポイントを実行するように変更します

diff --git a/package.json b/package.json
index 0c2ac81..a616d33 100644
--- a/package.json
+++ b/package.json
@@ -4,7 +4,7 @@
   "description": "",
   "main": "index.js",
   "scripts": {
-    "start": "nuxt start",
+    "start": "node app.js",
     "build": "nuxt build",
     "test": "echo \"Error: no test specified\" && exit 1"
   },

さあ、デプロイしましょう。

> npm run build
> gcloud app deploy

gcloud app browseで変わらずにNuxtのページが表示されていることが確認できます。 Expressに追加したルートが動いているかも確認しましょう。

> curl https://astral-web-260712.appspot.com/message
Hello, world!%

実行できていますね! これで今日の目標は達成ですっ

次回予告

なんとなくBFFとしてのサービスが動きそうなことが確認できたので、 明日はAPIサーバ用のサービスを作ってみます!

App Engineを試す vol1 - ExpressをAppEngineで動かす -

概要

この記事ではAppEngineのデフォルトサービスでサンプルのExpressを動作させることを目指します。

背景

AppEngineが便利っていう話を聞いたので、AppEngineでどこまでできるのかを試す連載。 連載のゴールとしては、NuxtをBFFとしておいてAPIで処理を行うというマルチサービスの構成。

AppEngineの準備

  1. GCPでプロジェクトの作成
  2. gcloudコマンドのインストール
  3. gcloud initでプロジェクトの指定

gcloud initでデプロイ対象のプロジェクトを選択できるので行っておく。 ※これを行わない場合はprojectオプションで都度project idを指定して実行することも可能。

> gcloud -v
Google Cloud SDK 272.0.0
beta 2019.05.17
bq 2.0.50
core 2019.11.16
gsutil 4.46`

初期プロジェクトの準備

以下からサンプルコードを取得する https://cloud.google.com/appengine/docs/standard/nodejs/quickstart?hl=ja#download_the_sample_code

> git clone https://github.com/GoogleCloudPlatform/nodejs-docs-samples
Cloning into 'nodejs-docs-samples'...
remote: Enumerating objects: 17475, done.
remote: Total 17475 (delta 0), reused 0 (delta 0), pack-reused 17475
Receiving objects: 100% (17475/17475), 15.33 MiB | 212.00 KiB/s, done.
Resolving deltas: 100% (11409/11409), done.

AppEngine用のディレクトリに移動する。

> cd nodejs-docs-samples/appengine/hello-world/standard

ディレクトリの構成はこんな感じ。

> ls
README.md    app.js       app.yaml     package.json test

app.yamlがAppEngineの挙動指定のファイル。 中身としては runtime: nodejs10 が指定されている。 スタンダード環境なので、動作がNodeJSのv10なんだよね。

> cat app.yaml
# Copyright 2017, Google, Inc.
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#    http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

# [START gae_quickstart_yaml]
runtime: nodejs10
# [END gae_quickstart_yaml]

次は実行する app.js の中身。 至って普通のExpressのアプリだが、const PORT = process.env.PORT || 8080でポートを環境変数から受け取れるようにしている。 AppEngineはPORTを指定して起動するので、環境変数から受け取れるようにしないとサービスの疎通ができないのだ。

> cat app.js
// Copyright 2017 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//      http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

'use strict';

// [START gae_node_request_example]
const express = require('express');

const app = express();

app.get('/', (req, res) => {
  res
    .status(200)
    .send('Hello, world!')
    .end();
});

// Start the server
const PORT = process.env.PORT || 8080;
app.listen(PORT, () => {
  console.log(`App listening on port ${PORT}`);
  console.log('Press Ctrl+C to quit.');
});
// [END gae_node_request_example]

module.exports = app;

最後にpackage.json. scripts.startnode app.jsが指定されており、起動時にapp.jsを実行するようになっている。

> cat package.json
{
  "name": "appengine-hello-world",
  "description": "Simple Hello World Node.js sample for Google App Engine Standard Environment.",
  "version": "0.0.1",
  "private": true,
  "license": "Apache-2.0",
  "author": "Google Inc.",
  "repository": {
    "type": "git",
    "url": "https://github.com/GoogleCloudPlatform/nodejs-docs-samples.git"
  },
  "engines": {
    "node": ">=8.0.0"
  },
  "scripts": {
    "start": "node app.js",
    "test": "mocha --exit test/*.test.js"
  },
  "dependencies": {
    "express": "^4.16.3"
  },
  "devDependencies": {
    "mocha": "^6.1.4",
    "supertest": "^4.0.2"
  }
}

こいつをデプロイしてみる。 deployの内容がCLI上で確認できる。

> gcloud app deploy
Services to deploy:

descriptor:      [/Users/mizuki/workspace/20191201-example-appengine/nodejs-docs-samples/appengine/hello-world/standard/app.yaml]
source:          [/Users/mizuki/workspace/20191201-example-appengine/nodejs-docs-samples/appengine/hello-world/standard]
target project:  [astral-web-260712]
target service:  [default]
target version:  [20191201t220456]
target url:      [https://astral-web-260712.appspot.com]


Do you want to continue (Y/n)?

プロジェクトやソースが適切なのを確認する。 問題ないので、yを押してEnter。

Do you want to continue (Y/n)?  y

Beginning deployment of service [default]...
Created .gcloudignore file. See `gcloud topic gcloudignore` for details.
╔════════════════════════════════════════════════════════════╗
╠═ Uploading 6 files to Google Cloud Storage                ═╣
╚════════════════════════════════════════════════════════════╝
File upload done.
Updating service [default]...⠧

deploy中... 暫く待つ。 暫く待つと、deploy結果が出てくる。

File upload done.
Updating service [default]...done.
Setting traffic split for service [default]...done.
Deployed service [default] to [https://astral-web-260712.appspot.com]

You can stream logs from the command line by running:
  $ gcloud app logs tail -s default

To view your application in the web browser run:
  $ gcloud app browse

gcloud app logs tail -s default を実行すると defaultサービスのログをtailしてくれる。 gcloud app browseを実行すると、ブラウザでサービスを開いてくれる。

面倒な人はcurlで確認する

> curl https://astral-web-260712.appspot.com
Hello, world!%

アプリが動作していることがわかった。

モバイルファクトリー在籍中に開発したOSSの振り返り

この記事はモバイルファクトリー Advent Calendar 2018の17日目の記事です。

どうも、@mizuki_rです。 2018年にモバイルファクトリーを退職して、今は弁護士ドットコムという企業でエンジニアをやっています。

モバイルファクトリーには2011年に新卒として入社して2018年まで8年近く務めました。 その中で、様々なプロジェクトや役回りを任されてきましたが、今日は在籍期間に制作したOSSを供養したいと思います。

代表作

Spica

github.com

Spica - the HTTP client for dealing with complex WEB API.

2013年に制作した、複雑なAPIクライアントを簡易化するOSSです。 なんでスピカって名前だったのかはもう覚えてない。

当時複数キャリアや様々な外部APIとの通信が必要なプロジェクトにおり、 APIごとに異なるフォーマットや命名規則をModel層に持ち込むことが嫌で実装しました。

APIのリクエストをORMのようにラップし、共通のI/Fでリクエスト、IteratorとRowクラスを返し、 APIの仕様で閉じた実装を隔離するためのモジュールです。

APIから受け取った情報をビジネスロジックで運用したいのに、 なぜ整形処理や変換処理をビジネスロジックでやらねばならんのかと。 外部APIが連携することでテスト不能になっていいのかという思いで作りました。

僕の中ではトップ3に入るくらい熱量のこもった作品です。もう作れない。

mizuki-r.hatenablog.com

検索したら当時YAPCでLTしたときの動画が出てきて震えた。

hariko

GitHub - rymizuki/node-hariko: Mock Server that implements the API Blueprint specification.

Mock Server that implements the API Blueprint specification.

2015年に制作した、API-Blueprintの仕様に基づくモックサーバーです。 張り子。ハリボテを用意するツールなので。命名したのは@nekobatoくんです。

当時の開発フローがフロントエンドで情報設計とプロトタイピングの後にAPI実装という流れだったので、 APIをモックサーバで代替する必要がありました。 しばらくJSONベースの簡易モックサーバを運用して、しかしドキュメンテーション不足を感じたので、 ドキュメントとモック、あわよくばテストデータをかね揃えるツールが欲しいとなり作りました。

当時、API-BlueprintとSwaggerが勢力を二分していた(はず)時期で、Markdownでかけるってことでこちらを採用。 実際開発中もヘビーに利用していました。

mizuki-r.hatenablog.com

syntagme

github.com

Syntagme is a flux's flamework.

2016年に制作した、fluxフレームワークです。 草案によると「記号化されたfluxを紬、拡張する」ことを目指していたようです。恥ずかしいなこれ。

もともとは、AngularJSにFluxを導入してビジネスロジックを抽象化し、ReactなりRiotなりに載せ替えようっていう媒のツールを目指していました。 いくつかこだわりがあって、「非同期処理を前提とする」「シンプルなReducer」「利用者はActionとReducerだけを考えればいい」「拡張可能」をいかに氏実現するかを考えました。

当時からすでにReduxは存在していましたが、Reduxの複雑性はサーバとフロントを兼任するエンジニアたちにはコストが高いと考え、 よりシンプルな頭で思考停止しても書ける学習コストの低いフレームワークを目指しました。

実際これにより、AngularJSとVueJSの共存が実現し、かつフロントエンドもだいぶ実装しやすくなったので、僕はこのフレームワークを生み出したことを誇りに思ってます。まあ、もう使うことはあるまいが...

speakerdeck.com

Perl

代表作って程いばれるわけではないけど一応書いたぞっていう奴ら。 当時はAmon2にハマっていて、いい感じに欲しいものをプラグイン化したくてかいたのが多いです。

Amon2-Web-Auth-Path

github.com

Set the trigger for each certification path.

2013年。URI単位で認証を切り替えられるヤツ。

mizuki-r.hatenablog.com

Amon2-Plugin-Web-Validator

github.com

Request validator keep it simple controller.

2013年。コントローラーでの煩雑なリクエストバリデーションを簡略化するやつ。 2016年くらいに後輩がこれを更に強力にしたようなヤツ書いてて、あー...ってなった思いで。

mizuki-r.hatenablog.com

Amon2-AccessControl

github.com

2013年。詳細が思い出せない。 多分Auth-Pathをより強力にしたようなやつ。

jQuery

はい、jQuery系ですよ。form周りの処理をいい感じにしたかった。

jquery-fillInForm

github.com

2013年。$form.fillInForm(data)ってしたらそれに紐づくinputを探して値を打ち込む。

jquery-formize

github.com

jquery-formize is form utilities for jquery.

2013年。真面目に書いたやつ。 formのcontrol要素に対してゲッターとセッターをはやしてオブジェクト的に扱えるようにしたやつ。

jquery-pageNavigator

github.com

2013年。たぶんpagerをいい感じにするやつ。

Heimdall

github.com

Heimdall is a form validation module for jquery or zepto.

2013年。温泉発火村での作品。 zeptoのことみんな覚えてる?たしか 仮面の人にzeptoでも使えるようにしない?って提案されて対応した気がする。

フォームバリデーションツールとして、Backbone、jQuery, Zeptoでの利用を想定していた。

mizuki-r.hatenablog.com

そもそもなんで「ヘイムダル」なのか。北欧神話の神が由来で、巨人の軍勢がビフレストを渡ってアースガルズへ侵攻するのを知らせる、という門番の役割を持ってる、ということから。

えもい(恥ずかしい

Backbone系

いぇーーーい、Backboneだぜーーーー!

backbone-pjax

github.com

2013年。BackboneでPjaxしたかった。 jquery.pjax.jsとbackboneを連携しようとして生み出されたヤツ。

mizuki-r.hatenablog.com

backbone-validator

github.com

2013年。後のHeimdall

AngularJS系

AngularJSは1.x系だよ。

angular-statename

github.com

AngularJS Directive for insert the current state name in the DOM.

2015年。ui-routerのstate名を任意のタグに差し込んでくれる。 ページによって背景とかスタイルを大きく変えたいときとかに使う。

angular-scroller

github.com

AngularJS Directive for shorthand of IScroll 5.

AngularJSでIScrollを使いたいときに、いい感じに隠蔽してくれるやつ。

angular-period

github.com

AngularJS Directive for switching the DOM in the period.

2015年。期間の前・中・後に合わせてDOMを出し分けするヤツ。 Dateにハマったとの、setTimeoutに指定できる時間に上限があることを知った。

mizuki-r.hatenablog.com

その他

とくに分類できないけど、せっかく書いたので供養。

action-tracker.js

github.com

The easy-to-use library for Google Analytics's tracking

2015年。GoogleAnalyticsをいい感じにwrapするヤツ。 いらなくね?って後で気づいた。むしろ書いたことを忘れててプロジェクトの中に入ってるのに気づいて「まじかよ...」ってなった。

grunt-xslate

github.com

Compile Text::Xslate template files from Grunt.

2015年。Perl製テンプレートエンジンをあろうことかGruntJSでコンパイルを試みたプロジェクト。 普通にプロジェクトで使ってた気がする。

hubot-scheduler

github.com

Extension of hubot to execute scheduled task.

2018年。指定時刻にhubotに発言させる拡張。 当時slackでpostとかsnippetとかを指定時刻に投稿できなくて必要だった。 確かcronで動いてたWebHookをhubotに移植するときに作ったんだっけな。

今は叛逆性MAのイベントリコメンドに使ってます。

electron-ffp

github.com

2015年。Electronのおためしで作ったプロジェクト。みてみてcoffee-scriptだよ。 ffpはFastFindPositionの略で、minifyされたJavaScriptから任意の位置をすばやく発見するためのツール。

errorログに流れてきたけどそれがどこのエラーなのかわからない!っていうときにつかった。

まとめ

8年に渡って実装してきた公開ライブラリを一気に紹介しました。 これ以外にも僕のリポジトリには作り途中で放棄したライブラリなどが残っており...

こうして振り返ってみると、思いの外向けにライブラリを発信していて(発信力はともかくとして)高いモチベを意地していたんだなと思います。 OSSで作る意義は、先日の吉祥寺pmで@fujiwaraさんが話していて、「これだ!!!」ってなったのでぜひ読んでみてください。

speakerdeck.com

こういったOSS活動を促進してくれる文化があり、またともに意見交換できる環境があるモバイルファクトリーに感謝を。 願わくば、この文化を今後も発展させていってほしいですね!