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

【NestJS】ヘルスチェック

やりたいこと

NestJS x TypeORM の環境で、DBまで一気通貫したヘルスチェック用URLを作りたい。
terminusというNodeJS用のパッケージで、NestJS用のものがあるので、それを使います。

環境

インストール

yarn add @nestjs/terminus @godaddy/terminus --no-optional

// or

npm install --save @nestjs/terminus @godaddy/terminus --no-optional

参照: https://github.com/nestjs/terminus#installation

実装

  • ほとんど、↓のページに書いている通りです。

  • src/health/health.module.ts に置いていますが、場所は任意です。

src/health/health.module.ts
import { TYPE_ORM_CONFIG } from '../config/app.config'

import { Module } from '@nestjs/common'
import {
  TerminusModule,
  TerminusModuleOptions,
  TypeOrmHealthIndicator,
  HealthIndicatorResult,
} from '@nestjs/terminus'
import { TypeOrmModule } from '@nestjs/typeorm'

const getTerminusOptions = (
  db: TypeOrmHealthIndicator
): TerminusModuleOptions => ({
  endpoints: [
    {
      url: '/health',
      healthIndicators: [
        // Set the timeout for a response to 300ms
        async (): Promise<HealthIndicatorResult> =>
          db.pingCheck('database', { timeout: 300 }),
      ],
    },
  ],
})

@Module({
  imports: [
    TypeOrmModule.forRoot(TYPE_ORM_CONFIG),
    TerminusModule.forRootAsync({
      inject: [TypeOrmHealthIndicator],
      useFactory: (db) => getTerminusOptions(db),
    }),
  ],
})
export class HealthModule {}
  • src/app.module.tsimportsに追加します。
src/app.module.ts
import { HealthModule } from './health/health.module'  // この行と、、、

   imports: [
     TypeOrmModule.forRoot(TYPE_ORM_CONFIG),
     HealthModule,                                     // この行を追加 

前提

↑の前提として、TYPE_ORM_CONFIGconfig/app.config)に、DBの定義情報が設定されている必要があります。以下、例です。

src/config/app.config.ts
import { TypeOrmModuleOptions } from '@nestjs/typeorm'

export const TYPE_ORM_CONFIG: TypeOrmModuleOptions = {
  type: 'mysql',
  charset: 'utf8mb4_bin',
  host: process.env.DB_HOST || 'localhost',
  port: +(process.env.DB_PORT || 3306),
  username: process.env.DB_USERNAME || 'root',
  password: process.env.DB_PASSWORD,
  database: process.env.DB_DATABASE || 'your_database_name_comes_here',
  entities: [__dirname + '/../**/*.entity.{js,ts}'],
}

動作確認ログ

$ curl -X GET "http://localhost:3000/health"
{"status":"ok","info":{"database":{"status":"up"}},"details":{"database":{"status":"up"}}}% 
// DBに対して SELECT 1 が実行されていることが確認できます。

$ yarn start:dev
:
{"level":30,"time":1581485016754,"pid":62278,"hostname":"xl.local","msg":"Server listening at http://0.0.0.0:3000","v":1}
query: SELECT 1
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Node.js + Express サーバから、Docker(+Docker-Compose) + Redis サーバーにデータを送る・削除する

Node.js サーバーから、Docker サーバにデータを送る

  • Ubuntuサーバー環境は2つ使ってます
    • マシンA
      • Node.js + Express + express-session + connect-redis + Redis
    • マシンB
      • Docker + Docker-Compose + Redis

マシンA から マシンB を参照してデータを表示したかったので、こんな構成になってます。
色々サイト見てたけど、実際にやってみないとコレわかんねーわって思った。

マシンAの下準備

色々インストールする

Ubuntu と Node.js は既に導入済を想定

必要なものをインストールする

$ mkdir exp_redis_test
$ cd exp_redis_test
$ npm install --save express \
                     express-session \
                     connect-redis \
                     redis

マシンBの下準備

前回作ってたので、それを使う

マシンBのRedis起動しておく

docker-compose up -d --build

マシンAで index.js を作る

node で実行するファイルを作成する

var と const と let が混じってるけど気にしない。

index.js
var express = require('express');
var app = express();
var session = require('express-session');
var RedisStore = require('connect-redis')(session);

const REDIS_HOST_NAME = '192.168.123.223';
const REDIS_PORT_NO = 6379;
var redis = require('redis');
let redisClient = redis.createClient(REDIS_PORT_NO, REDIS_HOST_NAME);

app.use(session({
    secret: 'secret',
    resave: false,
    saveUninitialized: true,
    store: new RedisStore({ client: redisClient }), // Redisの設定
    cookie: {
      path: '/',
      maxAge: 5 * 1000,
    }
}));

app.get('/', (req, res) => {
    // セッションデータが無ければランダム値を取得
    var begginer = req.session.value || Math.random();
    // valueという名前でセッションデータを生成
    req.session.value = begginer;
    res.send('Begginer value: ' + begginer);
});

app.get('/session-delete', function(req, res) {
  delete req.session.value;
  res.send('session variable deleted');
});

app.listen(3000, () => {
    console.log('Example app listening on port 3000!');
});

作ったら保存して起動する

$ node index.js
Example app listening on port 3000!

ブラウザで http://192.168.123.223:3000/ を起動すると、
数字は同じにはならないが、↓こんな感じになる
image.png

この状態のままで、マシンBを確認する

$ docker exec -it mysqltest_redis_1 bash
root@09a7f47d4784:/data# redis-cli
127.0.0.1:6379> keys *
1) "sess:VKmVo2FPTXAlspqLyLHgENmEdHhzK_pS"

データが入力されてるのが確認できる。中身を見ると、

$ docker exec -it mysqltest_redis_1 bash
root@09a7f47d4784:/data# redis-cli
127.0.0.1:6379> keys *
1) "sess:VKmVo2FPTXAlspqLyLHgENmEdHhzK_pS"
127.0.0.1:6379> get sess:VKmVo2FPTXAlspqLyLHgENmEdHhzK_pS
"{\"cookie\":{\"originalMaxAge\":null,\"expires\":null,\"httpOnly\":true,\"path\":\"/\"},\"value\":0.016689389693930634}"
127.0.0.1:6379>

こんな感じに、Valueが入力されているのが確認できる。
index.js を再確認したら、session-delete が出来るようにしてあるので、
http://192.168.123.223:3000/session-delete/ にアクセスしてみる。
↓こうなるので、またマシンBを確認する。
image.png

$ docker exec -it mysqltest_redis_1 bash
root@09a7f47d4784:/data# redis-cli
127.0.0.1:6379> keys *
1) "sess:VKmVo2FPTXAlspqLyLHgENmEdHhzK_pS"
127.0.0.1:6379> get sess:VKmVo2FPTXAlspqLyLHgENmEdHhzK_pS
"{\"cookie\":{\"originalMaxAge\":null,\"expires\":null,\"httpOnly\":true,\"path\":\"/\"}}"
127.0.0.1:6379>

Valueが削除されているのが確認できる。

参考

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

expo-cliというかnpmでconfigure errorが出たときの対応

いつも使ってるexpo-cliを利用しようとしたら、下記のようなエラーに遭遇した。

gyp WARN EACCES current user ("nobody") does not have permission to access the dev dir "/Users/xxxxxx/Library/Caches/node-gyp/12.3.1"
gyp WARN EACCES attempting to reinstall using temporary dev dir "/Users/xxxxxx/.anyenv/envs/nodenv/versions/12.3.1/lib/node_modules/expo-cli/node_modules/chokidar/node_modules/fsevents/.node-gyp"
gyp WARN install got an error, rolling back install
gyp WARN install got an error, rolling back install
gyp ERR! configure error
gyp ERR! stack Error: EACCES: permission denied, mkdir '/Users/xxxxxx/.anyenv/envs/nodenv/versions/12.3.1/lib/node_modules/expo-cli/node_modules/chokidar/node_modules/fsevents/.node-gyp'
gyp ERR! System Darwin 19.3.0
gyp ERR! command "/Users/xxxxxx/.anyenv/envs/nodenv/versions/12.3.1/bin/node" "/Users/xxxxxx/.anyenv/envs/nodenv/versions/12.3.1/lib/node_modules/npm/node_modules/node-gyp/bin/node-gyp.js" "rebuild"
gyp ERR! cwd /Users/xxxxxx/.anyenv/envs/nodenv/versions/12.3.1/lib/node_modules/expo-cli/node_modules/chokidar/node_modules/fsevents
gyp ERR! node -v v12.3.1
gyp ERR! node-gyp -v v5.0.5
gyp ERR! not ok

結論から言えば、下記のように--unsafe-permオプションをつけて実行すればインストール自体はできた。

sudo npm install --unsafe-perm -g expo-cli

基本的にroot権限でnpm installすることは推奨されていないようで、場合により--unsafe-permオプションが必要なようです。

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

Node.js + Express + express-session でセッション変数を使ってみる

Express + express-session でセッション変数を試す

事前準備として、フォルダ作ったりパッケージインストールしたりする

mkdir exps_test
cd exps_test
npm install --save express \
                   express-session \
                   body-parser \
                   ejs

http で確認したいので、 index.ejs を作っておく

index.ejs は view として作るので、views フォルダを作っておく

views フォルダ内に index.ejs を作る

mkdir views
cd views
nano index.ejs

index.ejs の内容はコレ

index.ejs
<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8">
</head>
<body>
    <p><%=message %></p>
    <form action="/" method="post">
        <input type="text" name="message">
        <button type="submit" name="button">send</button>
    </form>
    <a href="/">LastUpdateData</a>
</body>
</html>

exps_test フォルダに戻って、nodeで起動するための index.js を作る

cd ../
nano index.js

index.js の内容はコレ

index.js
const express = require('express');
const ejs = require('ejs');
const bodyParser = require("body-parser");
const session = require("express-session");
const app = express();

app.set('ejs', ejs.renderFile);

app.use(bodyParser.urlencoded({ extended: false }));

app.use(session({
    secret: "secret",
    resave: false,
    saveUninitialized: true,
}));

app.get('/', (req, res) => {
    let message = 'no message';
    if (req.session.message != undefined) {
        message = "LastUpdateData:" + req.session.message;
    }
    res.render('index.ejs', {
        message: message
    });
});

app.post("/", (req, res) => {
    let message = req.body.message;
    req.session.message = message;
    res.render('index.ejs', {
        message: message
    });
});

app.listen(3000, () => {
    console.log('Example app listening on port 3000!');
});

node.js で index.js を起動する

$ node index.js
Example app listening on port 3000!

ブラウザでアクセスする

↓こんな感じになるはず
image.png

何かしらメッセージを入力してSendすると、no message の所が変わります。
LastUpdateData をクリックすると、最後に入力した内容が表示されます。
ブラウザを閉じるとデータが消えるので no message に戻るはず。
node を再起動してもデータが消えるので no message に戻ります。

session 設定しているとこに cookie の設定を足すと、データの有効期限が付けられたりします。
↓こんな感じにすると5秒後に LastUpdateData をクリックした場合、 no message に戻ってます。

app.use(session({
    secret: "secret",
    resave: false,
    saveUninitialized: true,
    cookie: { maxAge: 5 * 1000 }
}));

参考

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

Cloud Firestore まとめ

概要

  • サーバーレスなKeyValueのデータストア
  • Cloud functionsから利用できる
  • クライアント側のアプリからも直接アクセスできる
    • セキュリティ設定に注意が必要
  • データの更新について、リアルタイムに通知が受け取れる

簡単なサンプル

CloudFunctionsからデータを取得する

import * as admin from 'firebase-admin';

const userId = 'test_user'

const user = (await admin
  .firestore()
  .collection('users')
  .doc(userId)
  .get()).data();

console.info(JSON.stringify(user));
  • firebase-adminを使用すると、Firestoreのアクセス制限の設定を無視してデータを取得できる
    • CloudFunctionsから利用するときはfirebase-adminを利用して、別途CloudFunctions側でアクセス制限する
  • CloudFunctionsからではなくクライアント側から直接Firestoreにアクセスするような時は、Firestoreのアクセス制限機能を利用する

気にするべき特徴

  • 1秒に1回しか更新できない
  • インデックスがないと検索できない
    • whereに入れるためにはインデックスが必要
    • なければ自動で作るためのURLをログに出してくれるので、そんなに面倒ではない
  • !=で検索できない
    • 国籍が日本以外のデータを検索、みたいなことができない
      • 他の国籍全てについて==で検索して、全ての結果をマージするしかない
  • 範囲検索が一つしかできない
    • 年齢xx歳以上、体重xx以上、のように複数の範囲条件が指定できない
      • 別々に検索してマージするしかない
    • orderByが範囲検索と捉えられるため、実質は一個しか指定できない感じ
    • よくある「商品データベース」みたいなものに利用すると、検索できなくなって困ると思う
  • トランザクションがある
    • 既に他の誰かが編集中だったりすると失敗してリトライされる
  • 検索条件などに癖があるため、利用はきちんと検討してからにするべき

アクセス制限について

  • rulesファイルを書く
  • アクセス制限のことを考えてデータ構造を設計しなくてはならない

データの項目にユーザーIDを入れておいてアクセス制限する例

service cloud.firestore {
  match /databases/{database}/documents {
    match /users/{uid} {
      allow list: if request.auth != null && (request.auth.uid == resource.data.uid)
    }
  }
}

グループに所属するユーザーのみにアクセスを許可する例

  • グループに所属しているかどうかを調べるのにwhere的に検索したいが、そのようなことはできない
  • 仕方ないのでコレクションキーuidにして、existsで調べる
service cloud.firestore {
  match /databases/{database}/documents {
    match /groups/{gid} {
      allow read: if request.auth != null && exists(/databases/$(database)/documents/groups/$(gid)/members/$(request.auth.uid));
    }
  }
}

参考サイト

目次

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

Cloud Functions まとめ

概要

  • アプリケーションのサーバーサイドのプログラムを実行する
  • 主にAPIを作成するために利用される
  • 関数一つが一つのAPIになるイメージ
  • Authenticationサービスと連携することによってアクセス制限が可能

簡単なサンプル

  • 実行すると、CloudFunctionsのログ(管理コンソールから確認できる)にHello sfjwr !!と表示される

CloudFunctions側のAPI

import * as functions from 'firebase-functions';

export const hello = functions.https.onCall(
  (params, context) => {
    console.info(`Hello ${params.name} !!`);
  },
);

APIを呼び出すクライアント側

import firebase from 'firebase';

firebase.functions().httpsCallable('hello')({name: 'sfjwr'});

気にするべき特徴

  • Cloud Functions内へのデータの保存はできない

    • 何かを保存したければ、Cloud FirestoreCloud Storageやその他サービスへ渡すしかない
  • 実行時間に制限がある

    • 最大9分
    • そのためバッチ処理には不向き
  • ユーザー側からの呼び出し以外に、Firebaseの他サービスからの通知によって実行することもできる

    • ファイルがCloud Storageにアップロードされた時
      • 自動でサムネイルを作るとか
    • Authenticationでユーザーが作られた時
      • 自サービス用のユーザーデータをFirestoreに放り込むとか
    • 定期実行も可能
      • 1時間毎とか1日毎とか
  • 関数毎にメモリの割り当てを指定することができる

    • 環境変数なども指定できる
    • Firebaseのコンソールからではなく、Google Cloud Platformのコンソールから指定する
    • FirebaseといいつつGoogle Cloud PlatformCloud Functionsと同じなので、Google Cloud Platform側の画面で色々できる
      • StackDriverでログを制御する、とか
  • 結構遅い

    • レスポンスまでに1秒くらいは見ておいた方がいいかも?
    • 特に内部的にインスタンスが立ち上がる時が遅い
      • しばらくアクセスがなくて久しぶりにアクセスが会った時とか
      • 5秒くらいかかるイメージ
    • Firestoreからデータを取得する時にループを回して、中でawaitとかすると凄まじく遅くなる
      • ループではなく並列取得するべき

参考サイト

目次

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

Google Firebase まとめ

GoogleのクラウドサービスであるFirebaseについて、まとめていきます。

はじめに

中級者が気軽にパッと見てある程度の内容が把握できる、くらいのところを目標に記述していきます。初心者向けではないです。
なるべくわかりやすくするため、基本的に短文箇条書き形式にて記します。
サンプルコードが必要なところはJavaScriptにて記載します。

間違いの指摘などあれば、コメントお待ちしております。

Firebaseとは?

  • アプリケーションのサーバーサイドを楽に作れる仕組み
  • 基本的にサーバーレス
  • 例えば以下のような物がサーバーレスで利用できる
    • データベース(Firestore)
    • ファイルサーバー(Storage)
    • API(Cloud functions)
    • Webサーバー(Hosting)

サーバーレスとは?

  • その名の通り、サーバーという概念が無い

    • 実際には裏には存在しているが、意識しなくてよいようになっている
    • イメージ的には、ファイルやアプリケーションのプログラムをアップローダーからアップロードするだけで動くようになるよ、的な感じ
  • メリット

    • メンテナンスの手間が少ない
      • セキュリティパッチやディスクの故障など何も心配する必要がない
      • アクセスが急激に増えても自動スケールアウトで勝手にうまく処理してくれる
  • デメリット

    • 従量制なのでかかる費用がよくわからない
      • 基本的にはオンプレよりは安くなるはず?
    • あまり細かい設定はできない
      • 特殊な要件だと対応できないかも
      • パフォーマンスチューニングとか
        • 一台ごとの性能云々はあまり調整できないが、自動スケールアウトでお金で解決
        • レイテンシが問題になることはあるかも?

主要サービスの概要

Cloud Functions

  • アプリケーションを実行するためのもの
  • 主にAPIを作るために利用できる
  • 言語はJavaScriptPythonGo
  • 手元でコードを書いて、アップロードコマンドを実行することで、CloudFunctionsに配置できる
    • 指定のURLを開く等で実行できる

Cloud Firestore

  • KeyValueのデータベース
  • Cloud Functionsから簡単に利用できる
  • データの更新をリアルタイムに取得できる

Authentication

  • 自作サービスのユーザー登録機能を自分で作らなくてよくするためのもの
    • SMSやE-mailによる本人確認
    • SNSによる本人確認
  • Firebaseのその他の機能と連携できる
    • Authenticationのユーザー情報を利用してCloud Functionsの呼び出しを制限する
    • Authenticationのユーザー情報を利用してFirestoreCloud storageのデータへのアクセスを制限する

Cloud Storage

  • いわゆるファイルサーバー
    • サーバーレスなので勝手にスケールする
    • 同時接続数とか考えなくても良い
  • CloudFunctionsからアップロードされたファイルを保存したり等に利用できる

Hosting

  • いわゆるWebサーバー
    • サーバーレスなので勝手にスケールする
    • 同時接続数とか考えなくても良い
  • 静的なファイルをホスティングできる

Webでよくある開発パターン

  • CloudFunctionsでサーバーサイドのAPIを作る
    • CloudFunctionsからFirestoreへデータベースのデータを格納する
    • ファイルはCloudStorage
  • クライアントサイドをSPAで作る
    • 作ったSPAHostingでWebに配置
    • SPAからはCloudFunctionsAPIを叩きに行く

各サービス毎のまとめ

以下にまとめて行きます。

参考サイト

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