20191205のNode.jsに関する記事は17件です。

コマンドラインからスマートLEDランプ(TP-Link KL110/KL130)をつける

TP-Link社のWifiスマートLEDランプ KL110/KL130 を使って、同一ネットワーク内にあるmacbookからランプをコマンドライン制御します。

color-temp

このランプの良いところは、電灯のON/OFFだけでなく色温度や輝度を調節できる点です。
例えば

  • 暖色系の色で輝度を落として落ち着いた雰囲気で集中作業する
  • 手元でモノを探すときに輝度を上げる
  • 物理本を読むときに色温度を変える(Kindleの背景の白とセピアくらい紙の色味が変わる)

といったことが手元でできるのが良いです。

特に色温度を変えるとだいぶ色味が変わります。
↓実際にランプの色温度最低値と最高値で見比べた写真:2500K<--->9000K
color-temp

こういった色味、明るさ、薄暗さをそのときの気分でコマンドラインで調整できるようにします。

試してみた環境

macbook : macOS Mojave
Node.js : v12.12.0

準備として、最初にスマホアプリでKL110/KL130のWifi設定を済ませておきます。
そして、同一ネットワーク内にmacbookを用意して
macbook ---- Wifiルータ ---- KL110/KL130
という構成にしておきます。
ちなみにKL110/KL130は2.4GHz Wifiにしか対応していないようです。(5GHz未対応)

tplink-lightbulb をインストール

tplink-lightbulbをnpmでグローバルインストールしておきます。
すると tplight コマンドが使えるようになります。

$ npm i -g tplink-lightbulb
$ tplight
Usage: tplight <COMMAND>

コマンド:
  tplight scan                              Scan for lightbulbs
  tplight on <ip>                           Turn on lightbulb
  tplight off <ip>                          Turn off lightbulb
  tplight temp <ip> <color>                 Set the color-temperature of the
                                            lightbulb (for those that support
                                            it)
  tplight hex <ip> <color>                  Set color of lightbulb using hex
                                            color (for those that support it)
  tplight hsb <ip> <hue> <saturation>       Set color of lightbulb using HSB
  <brightness>                              color (for those that support it)
  tplight cloud <ip>                        Get cloud info
  tplight raw <ip> <json>                   Send a raw JSON command
  tplight details <ip>                      Get details about the device

オプション:
  -h, --help  ヘルプを表示                                                [真偽]
  --version   バージョンを表示                                            [真偽]

例:
  tplight scan -h     Get more detailed help with `scan` command
  tplight on -h       Get more detailed help with `on` command
  tplight off -h      Get more detailed help with `off` command
  tplight temp -h     Get more detailed help with `temp` command
  tplight hex -h      Get more detailed help with `hex` command
  tplight hsb -h      Get more detailed help with `hsb` command
  tplight cloud -h    Get more detailed help with `cloud` command
  tplight raw -h      Get more detailed help with `raw` command
  tplight details -h  Get more detailed help with `details` command

You need a command.

シェルスクリプトを書く

tplightコマンドを使ったシェルスクリプトを用意します。
スクリプト内のライトのIPアドレスは環境に合わせて書き換えてください。
~/bin/light などパスを通しているディレクトリにスクリプトを置きます。

#!/bin/bash
LIGHT_IP=192.168.1.14

ON_OFF=1
COLOR_TEMP=2500
BRIGHTNESS=100

if [ "${1}" = "on" ]; then
  ON_OFF=1
elif [ "${1}" = "off" ]; then
  ON_OFF=0
elif [ "${1}" = "hot" ]; then
  ON_OFF=1
  COLOR_TEMP=2500
elif [ "${1}" = "cold" ]; then
  ON_OFF=1
  COLOR_TEMP=9000
fi

if [ -n "${2}" ]; then
  BRIGHTNESS=${2}
fi

tplight raw ${LIGHT_IP} \
"{
  \"smartlife.iot.smartbulb.lightingservice\": {
    \"transition_light_state\": {
      \"on_off\": ${ON_OFF},
      \"color_temp\": ${COLOR_TEMP},
      \"brightness\": ${BRIGHTNESS},
      \"hue\": 0,
      \"saturation\": 0
    }
  }
}" > /dev/null

実行する

ライトを点ける

$ light on

ライトを消す

$ light off

暖色系のライトを点ける

$ light hot

寒色系のライトを点ける

$ light cold

暖色系のライトで輝度を20にする(max 100)

$ light hot 20

カスタマイズする

$ tplight details [IP address] コマンドで現在のライトの状態・設定値をみることができます。
設定値のlight_state あたりを眺めて $ tplight raw [json] でjsonを投げてやれば輝度や色温度を変えることができます。
on_offは1でon、0でoff、brightnessは輝度、color_tempは色温度、hue, saturationはカラー対応(KL130)の場合に色味の設定ができます。

$ tplight details 192.168.1.14
{
  "sw_ver": "1.8.11 Build 191113 Rel.105336",
  "hw_ver": "1.0",
  "model": "KL130(JP)",
  "description": "Smart Wi-Fi LED Bulb with Color Changing",
  "alias": "アプリで設定した名前がここに設定される",
  "mic_type": "IOT.SMARTBULB",
  "dev_state": "normal",
  "mic_mac": "xxxxxxxxxxxx",
  "deviceId": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
  "oemId": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
  "hwId": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
  "is_factory": false,
  "disco_ver": "1.0",
  "ctrl_protocols": {
    "name": "Linkie",
    "version": "1.0"
  },
  "light_state": {
    "on_off": 1,
    "mode": "normal",
    "hue": 0,
    "saturation": 0,
    "color_temp": 2500,
    "brightness": 100
  },
  "is_dimmable": 1,
  "is_color": 1,
  "is_variable_color_temp": 1,
  "preferred_state": [
    {
      "index": 0,
      "hue": 0,
      "saturation": 0,
      "color_temp": 2700,
      "brightness": 50
    },
    {
      "index": 1,
      "hue": 0,
      "saturation": 75,
      "color_temp": 0,
      "brightness": 100
    },
    {
      "index": 2,
      "hue": 120,
      "saturation": 75,
      "color_temp": 0,
      "brightness": 100
    },
    {
      "index": 3,
      "hue": 240,
      "saturation": 75,
      "color_temp": 0,
      "brightness": 100
    }
  ],
  "rssi": -69,
  "active_mode": "none",
  "heapsize": 284368,
  "tid": "tty.xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
  "err_code": 0,
  "lamp_beam_angle": 150,
  "min_voltage": 110,
  "max_voltage": 120,
  "wattage": 10,
  "incandescent_equivalent": 60,
  "max_lumens": 800,
  "color_rendering_index": 80
}

リンク

KL110 : https://www.tp-link.com/jp/home-networking/smart-bulb/kl110/
KL130(マルチカラー対応) : https://www.tp-link.com/jp/home-networking/smart-bulb/kl130/

tplink-lightbulb : https://www.npmjs.com/package/tplink-lightbulb

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

スポーツ解説アプリ SpoLive における分析基盤の構築

本記事は、NTTコミュニケーションズ Advent Calendar 2019 8日目の記事です。
昨日は、 @y-i さんの 社内ISUCONで優勝した時にやったこと でした。

これは何

  • スポーツ解説アプリ SpoLive における分析基盤構築についてのノウハウ共有
  • サーバーレスで、要件に応じて柔軟に分析できる基盤を構築した話

SpoLive について

SpoLive は、「スポーツファンとアスリートの距離を、デジタルの力で縮める」ことをビジョンに掲げた新サービスです。例えば試合中に、「なんで今のプレーが許されてるんだっけ?」「この選手どんな人だっけ?」といった気になることをすぐに解決できるのはもちろんのこと、より手軽に、より深く豊かな選手の情報に触れることができるようなアプリを目指して日々アップデートを重ねています。

本記事では、SpoLiveにおける分析基盤を構築したノウハウを述べます。構築の前提条件として下記がありました。

  • データをビジネスサイドのメンバー(SQL を書いて分析できるメンバーがいる)が柔軟に分析できる基盤を用意したい
  • 開発・運用コストはできるだけ抑えたい

分析基盤

SpoLive は、Firebaseexpo で開発をしています。
基盤構築の方針として下記の方針が考えられますが、後述の理由で2 を採用しました。

  • 1. Firebase Analytics + BigQuery
  • 2. Google Analytics + BigQuery
  • 3. 別の分析ツール(Amplitude・Mix Panel 等)

理由: Firebase を利用していたら、 Firebase Analytics を利用することが主流だと思いますが、 expo において対応できるライブラリは(自分の知る限り)なく、別の分析ツールではなくまずは世の中の知見も多くシンプルな構成から入ろうと考えたためです。

Bigqueryは、データ分析にあたってはよく用いられているサービスで、SQLで複数データを統合して集計することも容易ですし、例えば、Googleデータポータルを利用して可視化すること などができ、柔軟に分析ができます。

技術要素

前述の方針に基づき、分析基盤を構築するにあたり、下記の要素に分割できます。

アプリ =(1)=> Google Analytics =(2)=|
                     |
            Firebase  =(3)==> BigQyery

上記のうち、(2)(3)のGoogle Analytics/FirebaseからBigqueryへ転送するタスクは、Node.jsで実装し、CloudFunctions の 定期ジョブ としてデプロイします。

理由として、BigQuery のNode.js SDKが世の中に知見が溜まっていそうな点、expo(ReactNative)と技術要素をjsで揃えることによる、メンバーのメンテしやすさの点がありました。また、後述する通り、CloudFunctionsの定期ジョブは、コードだけでスケジューリング可能で、コンソールで手動で設定・・といった必要がなく、変更管理のしやすさを感じています。

(1)アプリ から Google Analytics

ライブラリ expo-analytics を利用して、アプリから GoogleNAalytics へアプリの利用状況に関するデータを収集します。

// analitics.js

import { Analytics as GoogleAnalytics, ScreenHit, Event } from "expo-analytics";

class Analytics {
  constructor(code = null) {
    this.ga = null;
    this.code = code;
  }

  init = () => {
      this.ga = new GoogleAnalytics(this.code);
  };

  EventHit = (category = null, action = null, label = null, value = 0) => {
    if (category && action) {
      const params = [category, action];
      if (label) {
        params[2] = label;

        if (value >= 0) {
          params[3] = value;
        }
      }
      this.ga.event(new Event(...params));
    }
  };
}

export default new Analytics(gaId);
// someview.js

import Analytics from "app/src/libs/analytics";

 Analytics.EventHit(
      "categoryName",
      "actionName",
      "labelName",
    );

転送タスクのデプロイ・自動起動

前述のとおり、(2)(3)のGoogle Analytics/FirebaseからBigqueryへ転送するタスクは、Node.jsで実装し、CloudFunctions の 定期ジョブ としてデプロイします。

# ディレクトリ構成
functions/src/index.ts
           |    
           |--- modules/
                 |    
                 |--- transferBigquery.ts
Node.js

import * as functions from "firebase-functions";
import { transferBigquery } from "./modules/transferBigquery";


// bigqueryへの定期エクスポート
module.exports.transferBigquery = functions
  .region("asia-northeast1")
  .runWith({
    timeoutSeconds: 9 * 60, // max: 9min
    memory: "1GB"
  })
  .pubsub.schedule("0 4 * * *")
  .timeZone("Asia/Tokyo")
  .onRun(transferBigquery);

(2)Google Analytics から BigQuery

GAからのデータ取得には、GoogleAPIs の Node.js向け SDKを利用します。
(3)とあわせて、一つのFunctionsとして実装します。

API利用時の認証情報は、Functionsの環境変数として保存しますが、ローカルでの開発時も functionsディレクトリで、 firebase functions:config:get > .runtimeconfig.json としておくと、ローカルでも環境変数をjsonファイルの値から利用できます。

Functionsのメイン処理は下記のイメージです

Node.js
// transferBigquery.ts

import firebase from "./firebase-admin";
import moment from "moment";

import { Analytics } from "../libs/googleAnalytics";
import { BigQuery } from "@google-cloud/bigquery";
import { Storage } from "@google-cloud/storage";

// main関数で、class初期化・メソッド実行
export const transferBigquery = async () => {
  const projectId = process.env.GCLOUD_PROJECT;
  if (!projectId) {
    console.error("projectId is invalid", projectId);
    return;
  }

  // firestoreの環境変数としてGAの認証情報を保存
  const { client_email, private_key, view_id } = functions.config().googleanalytics
  const ga = new Analytics(client_email, private_key, view_id);
  const tb = new TransferBigquery(projectId) // 後述

  // GAのデータは過去1日分を入れ替え
  const start: Moment = moment().add(-1, "days")
  const end: Moment = moment().add(-1, "days")
  await tb._delete_eventdata_from_bigquery(start,end)
  await tb._export_eventdata_to_storage(start, end) // ここが本節で説明したい部分
  await tb._save_eventdata_to_bigquery()

  //firebaseの試合情報は、全件更新
  await tb._export_gamedata_to_storage(); // 後述
  await tb._save_gamedata_to_bigquery();  // 後述
};


export class TransferBigquery {

  private projectId: string
  private suffix: string

  constructor(projectId: string) {
    this.projectId = projectId
  }

  async _export_eventdata_to_storage(start: Moment, end: Moment) {
    const eventData = await this.analytics._eventToJson(start, end) // GAからイベントデータを取得
    return await this._exportJSON(eventData.join("\n"), `events.json`); //CloudStorageへ保存(後述)
  }
  // ...省略...

}



GAのイベントデータを取得する処理は下記のとおり

Node.js
// libs/googleAnalytics.ts

import { google } from "googleapis";
import { Moment } from "moment";

export const Analytics = class {

  private jwtClient: any;
  private analytics: any;
  private viewId: string;

  constructor(clientEmail: string, privateKey: string, viewId: string) {
    this.jwtClient = new google.auth.JWT(
      clientEmail,
      undefined,
      privateKey,
      ["https://www.googleapis.com/auth/analytics.readonly"],
      undefined,
    );
    this.analytics = google.analytics("v3");
    this.viewId = viewId;
  }

  // event以外の情報を取得したい場合は、別のパラメータを指定する
  // - Dimensions & Metrics Explorer: https://ga-dev-tools.appspot.com/dimensions-metrics-explorer/
  _event_param(date: Moment) {
    const params: any = {
      "start-date": date.format("YYYY-MM-DD"),
      "end-date": date.format("YYYY-MM-DD"),
      metrics: "ga:uniqueEvents",
      dimensions:
        "ga:eventCategory,ga:eventAction,ga:eventLabel",
      sort: "ga:eventCategory"
    };
    return params
  }

  // 指定期間のデータを取得しJSONを生成
  async _eventToJson(start: Moment, end: Moment) {
    const eventJson = []
    while (start.diff(end) <= 0) {
      const eventData = await this._fetch_report_data(this._event_param(start));
      if (eventData == undefined) {
        start.add(1, "days");
        continue
      }
      console.log(`add gaEvent: ${start.format("YYYY-MM-DD")}`)
      for (const e of eventData) {
        eventJson.push(JSON.stringify({
          date: start.format("YYYY-MM-DD"),
          eventCategory: e[0],
          eventAction: e[1],
          eventLabel: e[2],
          uniqueEvents: e[3],
        }))
      }
      start.add(1, "days");
    }
    return eventJson
  }

  // GAからデータ取得
  async _fetch_report_data(params: any): Promise<any[]> {
    params.auth = this.jwtClient;
    params.ids = `ga:${this.viewId}`;
    return new Promise((resolve, reject) => {
      this.jwtClient.authorize((err: any, tokens: any) => {
        if (err) {
          reject(err);
        }
        this.analytics.data.ga.get(params, (err: Error, resp: any) => {
          if (err) {
            reject(err);
          }
          resolve(resp.data.rows);
        })
      });
    });
  }
};

(3)Firebase から BigQuery

Bigqurey の Node.js向け SDK google-cloud/bigquery を利用します。
Bigqureyへのデータインポートには、CloudStorageにあるファイルから一括インポートする方法と、 一度に 1 レコードずつ BigQuery にデータをストリーミング処理でインポートする方法がありますが、公式ドキュメントにもありますがコスト的には ストリーミング処理は必要がなければ避けるべきです。

上記の理由から、データを改行区切り JSON にしてCloudStorageへ出力し、Bigqueryへインポートさせます。

Node.js
// transferBigquery.ts

  // // 前述のメイン処理抜粋
  // // firebaseの試合情報は、全件更新
  // await tb._export_gamedata_to_storage();
  // await tb._save_gamedata_to_bigquery(); 

export class TransferBigquery {

  private projectId: string
  private analytics: any
  private location: string

  constructor(projectId: string, analytics: any) {
    this.projectId = projectId
    this.analytics = analytics
    this.location = 'US' //Bigquery>datasetのlocationを変更した場合はここも変更する
  }

  async _export_gamedata_to_storage(){
    const gameData = await firebase
     .firestore()
     .collection(`games`)
     .get()
     .then(querySnapshot => {
       const returnData: string[] = [];
       querySnapshot.forEach(doc => {
         const {
           team_homeName = null,
           team_awayName = null
         } = doc.data();

         // BigQueryがサポートしている改行区切りJSONを出力するために、ここで整形
         returnData.push(
           JSON.stringify({
             gameId: doc.id,
             team_homeName,
             team_awayName
           })
         );
       });
       return returnData;
    });
    return await this._exportJSON(gameData.join("\n"), "games.json");
  };

  // Cloud Storageへの保存
  async _exportJSON(jsonText: string, filename: string) {
    const storage = new Storage();
    const bucket = storage.bucket(`${this.projectId}.some_storage.com`);
    bucket.file(`somedir/${filename}`).save(jsonText, err => {
      if (!err) {
        bucket.file(`somedir/${filename}`).setMetadata(
          {
            metadata: {
              contentType: "application/json"
            }
          },
          (err: any, apiResponse: any) => {
            if (err) {
              console.log("err", err);
            } else {
              console.log(`finish saving ${JSON.stringify(apiResponse)}`);
            }
          }
        );
      } else {
        console.log("fail saving", err);
      }
    });
  };

  // Clound Storage からBigqueryへインポート
  async _save_gamedata_to_bigquery() {
    const schema: any = {
      fields: [
        { name: "gameId", type: "STRING", mode: "NULLABLE" },
        { name: "team_homeName", type: "STRING", mode: "NULLABLE" },
        { name: "team_awayName", type: "STRING", mode: "NULLABLE" }
      ]
    };
    return await this._saveBigquery({
      bqSchema: schema,
      tabeleName: "games",
      filename: `games.json`,
      isAppendMode: false,
    });
  };

  _loadStorageFile(filename: string) {
    const storage = new Storage();
    const bucket = storage.bucket(`${this.projectId}.some_storage.com`);
    return bucket.file(`somedir/${filename}`);
  };

  async  _saveBigquery(params: BigqueryParam) {
    const { bqSchema, tabeleName, filename, isAppendMode } = params
    const datasetId = "some_dataset";
    if (!this.projectId) {
      console.error("projectId is invalid", this.projectId);
      return;
    }
    const bigquery = new BigQuery({ projectId: this.projectId });
    const table = bigquery.dataset(datasetId).table(tabeleName);

    // https://cloud.google.com/bigquery/docs/reference/auditlogs/rest/Shared.Types/WriteDisposition
    const importMode: string = isAppendMode ? "WRITE_APPEND" : "WRITE_TRUNCATE";
    const metadata: any = {
      sourceFormat: "NEWLINE_DELIMITED_JSON",
      schema: bqSchema,
      // Set the write disposition to overwrite existing table data.
      writeDisposition: importMode,
    };

    table.load(this._loadStorageFile(filename), metadata, (err, apiResponse) => {
      if (err) {
        console.log("err", err);
      } else {
        console.log(`finish saving: ${JSON.stringify(apiResponse)}`);
      }
      return;
    });
  };

}

最後に

本記事では、Firebase / Cloud Functins / BigQuery などのクラウド基盤上に、サーバーレスでユーザー分析基盤を構築するノウハウについて述べました。

スポーツ解説アプリSpoLiveでは、これからも利用者の方により楽しくスポーツを楽しんでいただけるアプリを目指して日々改善中です。今現在、ラグビーやサッカーの試合がお楽しみいただける他、今後も対応スポーツを拡大していく予定です。ぜひご利用ください。

明日は、 @tetrapod117 さんの記事です。

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

【Node.js】zxcvbnを使ってパスワード強度をチェックする

「パスワード強度チェックするようなライブラリって何かあるのかな?」
と興味本位で調べてみたらzxcvbnというものが見つかったので、ご紹介。

zxcvbnとは

Dropbox社製のパスワード強度チェッカーです。
Node.js以外にも色々な言語に対応したライブラリが作られています。

dropbox/zxcvbn: Low-Budget Password Strength Estimation

準備

$ npm i zxcvbn

基本的な使い方

とりあえずhogehogeという文字列に対してパスワード強度をチェックしてみましょう。

const zxcvbn = require('zxcvbn');

const result = zxcvbn('hogehoge');
console.log(result);

基本的な使い方はとても簡単ですね。
第2引数に入力を渡して更に細かい設定をすることも可能なようですが、今回は遊んでみたかっただけなので割愛させてください。

出力内容を見てみましょう。

{
  password: 'hogehoge',
  guesses: 20003,
  guesses_log10: 4.301095134950942,
  sequence: [
    {
      pattern: 'repeat',
      i: 0,
      j: 7,
      token: 'hogehoge',
      base_token: 'hoge',
      base_guesses: 10001,
      base_matches: [Array],
      repeat_count: 2,
      guesses: 20002,
      guesses_log10: 4.301073422940843
    }
  ],
  calc_time: 3, # zxcvbnが計算するのにかかった時間(ミリ秒) あんまり気にしなくていい
  crack_times_seconds: { # パスワードが特定されるまでにかかる時間(秒)
    # 4種類の攻撃パターンごとの想定時間
    # 基本的には`offline_fast_hashing_1e10_per_second`の値だけ見ておけばいいかも
    online_throttling_100_per_hour: 720108,
    online_no_throttling_10_per_second: 2000.3,
    offline_slow_hashing_1e4_per_second: 2.0003,
    offline_fast_hashing_1e10_per_second: 0.0000020003
  },
  crack_times_display: { # パスワードが特定されるまでにかかる時間(わかりやすい形式)
    online_throttling_100_per_hour: '8 days',
    online_no_throttling_10_per_second: '33 minutes',
    offline_slow_hashing_1e4_per_second: '2 seconds',
    offline_fast_hashing_1e10_per_second: 'less than a second'
  },
  score: 1, # 0 ~ 4でパスワード強度を評価する
  feedback: {
    warning: 'Repeats like "abcabcabc" are only slightly harder to guess than "abc"',
    suggestions: [
      'Add another word or two. Uncommon words are better.',
      'Avoid repeated words and characters'
    ]
  }
}

crack_times_secondsの4種類のパターンについては、公式ドキュメントには次のように記述してありました。

  • online_throttling_100_per_hour

online attack on a service that ratelimits password auth attempts.

  • online_no_throttling_10_per_second

online attack on a service that doesn't ratelimit,
or where an attacker has outsmarted ratelimiting.

  • offline_slow_hashing_1e4_per_second

offline attack. assumes multiple attackers,
proper user-unique salting, and a slow hash function
w/ moderate work factor, such as bcrypt, scrypt, PBKDF2.

  • offline_fast_hashing_1e10_per_second

offline attack with user-unique salting but a fast hash
function like SHA-1, SHA-256 or MD5. A wide range of
reasonable numbers anywhere from one billion - one trillion
guesses per second, depending on number of cores and machines.
ballparking at 10B/sec.

うーん、セキュリティって難しい!

参考

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

はじめてのGitHubActionsMarketplace公開 - PRにマイルストーンをつけるActionをつくってみた

Actions使ってなにかするworkflowをつくるんじゃなくてAction自体をつくる話。

なにか簡単にGitHubActionsをつくってみたいと思い、GitHub公式のactions/labelerを参考にPRにラベルじゃなくてマイルストーンをつけるActionをつくろうと考えた。
Marketplaceで検索した結果、labelの方はちょいちょいあったがmilestoneの方はそんなにはまだなかった。マイルストーンの作成や更新がしたいのではなくてPRに既存のマイルストーンを結びつけたいってなると1,2件ぐらいしか無さそう。それらもこちらのやりたいこととは違った。
最終的につくったのはiyu/actions-milestone

GitHub公式のテンプレートからリポジトリを作成する

GitHubのテンプレート機能は実は使ったことがなかったが、用意してくれているのだから使わない理由はない。javascript版(actions/javascript-action)とtypescript版(actions/typescript-action)が用意されている。あとはテンプレートと呼べるのか謎のミニマムなコンテナ版(actions/container-action)もあった。今回は無難にtypescriptを選択する。

image.png
[Use this template]って書いてある緑色のボタンからリポジトリを作成する。

image.png
こんな画面が出てくるがあとはいつものリポジトリ作成と同じ手順。

image.png
作成し終わるとforkしたリポジトリみたいにリポジトリ名の下にテンプレートに使ったリポジトリが表示されていた。ただコミットとかは引き継がれずすべてコピペしてInitial commitだけしたような感じの状態だった。別の世界線というより別世界だからテンプレートの今後のコミットをcherry-pickするのは難しいかもしれない。そんな心配する必要もないが。

テンプレートでできたリポジトリからconfig情報を書き換える

すぐにプログラミングに取り掛かりたいところだけどまずはconfigだけを書き換える。完全に好みの問題だがプログラムの修正も全部同じコミットにすると次回テンプレートから作成したときに何が必要になるのかわかりにくいなと思ったので。
やることは、

  • README.mdの書き換え
    • Actionの開発方法とかが書いてあるので全部消して書き換えでいい
  • package.json, package-lock.jsonの書き換え
    • リポジトリ名だったり説明文だったりurlだったり書き換える
    • package-lock.jsonの方はpackage.json書き換えた後にnpm installでもしとけば自動で書き換わるので手動で書き換える必要はない
  • action.ymlの書き換え
    • action名だったり説明文だったり
    • この段階である程度input,output決まってるなら書き換えてもいいような、まだなような?

こちら書き換えたときのコミットなので参考に
https://github.com/iyu/actions-milestone/commit/4101ca509ee0fc10f84542abd65e416a3feca566

プログラムを書く

ここからやっとプログラミング

ESLintの設定

汚いコードをあとから修正するのは面倒なので最初に入れる。
tslintは2019年内に開発終了するらしいのでtypescriptのlintにはESLintを使う。

$ npm install --save-dev eslint

初期セットアップ (対話型のCLIでなんか色々聞かれるけど詳細は割愛)
image.png

ものすごくただの好みの問題だが自分はeslint-config-airbnb-baseを長らく使っている。

$ npm install --save-dev eslint-config-airbnb-base

このテンプレートではtestにjestを使っているみたいなのでjestのプラグインも入れる。

$ npm install --save-dev eslint-plugin-jest

最終的な.eslintrc.ymlはこんな感じ。好みで変えていく。

env:
  es6: true
  node: true
  jest/globals: true
extends:
  - airbnb-base
globals:
  Atomics: readonly
  SharedArrayBuffer: readonly
parser: '@typescript-eslint/parser'
parserOptions:
  ecmaVersion: 2018
  sourceType: module
plugins:
  - '@typescript-eslint'
  - jest
rules:
  no-console: off
  no-unused-vars: off
  '@typescript-eslint/no-unused-vars': error
settings:
  import/resolver:
    node:
      extensions:
        - .ts

実行時にtypescriptの拡張子を教えてあげる必要がある。早く省略できるようにして欲しい。

$ eslint --ext .ts,.js .

今回作るもの

大まかに区分するとこんな感じ。

  1. inputデータを取得
  2. PRの情報を取得
  3. configファイルの中身を取得
  4. ブランチ名からマイルストーンを割り出す
  5. PRにマイルストーンをつける

inputデータを取得する

action.ymlに欲しいinputデータを記述しておく。

name: 'Pull Request Milestone'
description: 'Add milestone to PRs'
author: 'iyu'
inputs:
  repo-token:
    description: 'The GITHUB_TOKEN secret'
  configuration-path:
    description: 'The path for the milestone configurations'
    default: '.github/milestone.yml'
outputs:
  milestone:
    description: 'The Added Milestone'
runs:
  using: 'node12'
  main: 'lib/main.js'

上記の場合だとrepo-tokenconfiguration-path
これをAction利用者が入力してくれるはずなのでそれをtypescript側で取得する。
こういう基本的なものはすべて公式のライブラリが取得方法を用意してくれているので簡単に取得できる。

import * as core from '@actions/core';

const token = core.getInput('repo-token', { required: true });
const configPath = core.getInput('configuration-path', { required: true });

requiredつけておけば値が存在しなかった時のエラー処理もいい感じにやってくれるのでエラー文考えなくても良い。便利。
(action.ymlでdefault付いてるくせにrequiredにしているのはただの嘘つき)

PRの情報を取得する

どのイベントから発火されたのかも公式のライブラリで簡単に取得できる。

import * as github from '@actions/github';

const pullRequest = github.context.payload.pull_request;
if (!pullRequest) {
  console.log('Could not get pull_request from context, exiting');
  return;
}

ブランチ名を利用してマイルストーンをつけるActionなのでPR以外のイベントの場合は上記のようになんかメッセージ表示して終了させる。

後々にPRのBaseブランチやHeadブランチの名前や現在のマイルストーンを利用したいのだが、このライブラリのtypesがしっかり全部書いてないので必要なパラメータは自分で拡張する必要がある。かなしい。

import * as github from '@actions/github';
import { WebhookPayload } from '@actions/github/lib/interfaces';

interface PullRequestWebhookPayload extends WebhookPayload {
  pull_request?: {
    [key: string]: any;
    number: number;
    html_url?: string;
    body?: string;

    milestone?: string;
    base: {
      ref: string;
    },
    head: {
      ref: string;
    },
  },
}
const pullRequest = (github.context.payload as PullRequestWebhookPayload).pull_request;

configファイルの中身を取得

inputで指定されたconfigファイルパスを取得する。action利用者がcheckoutをしているとも限らないのでファイルはAPI経由で取得する。このへんはactions/labelerを参考にしている。

import * as github from '@actions/github';

(async () => {
  // さっきのやつ
  // const token = core.getInput('repo-token', { required: true });
  // const configPath = core.getInput('configuration-path', { required: true });

  const client = new github.GitHub(token);

  const response = await client.repos.getContents({
    owner: github.context.repo.owner,
    repo: github.context.repo.repo,
    path: configPath,
    ref: github.context.sha,
  });
  const text = Buffer.from((response.data as { content: string }).content, 'base64').toString();
})();

(APIクライアントはPromise対応しているのでasync関数で囲ってる)
これでAPI経由でファイル取得可能だ。レスポンスの中のファイル内容についてはbase64文字列なのでtext変換している。

configファイルはこんな形式のyamlファイルを想定している。

base-branch:
  - "(master)"
  - "releases\\/(v\\d+)"

head-branch:
  - "feature\\/(v\\d+)\\/.+"

yamlファイルの文字列持っててもtypescriptで扱いにくいので今回はこんな感じのパーサーを用意して変換した。

import * as yaml from 'js-yaml';

interface ConfigObject {
  baseBranchList: RegExp[];
  headBranchList: RegExp[];
}

const parse = (text: string) => {
  const config: {
    'base-branch'?: string[],
    'head-branch'?: string[],
  } = yaml.safeLoad(text) || {};

  const result: ConfigObject = {
    baseBranchList: [],
    headBranchList: [],
  };
  result.baseBranchList = (config['base-branch'] || []).map((item) => new RegExp(item));
  result.headBranchList = (config['head-branch'] || []).map((item) => new RegExp(item));

  return result;
};

ブランチ名からマイルストーンを割り出す

このへんはもうこのAction固有の処理なのでそんなに詳しく書きません。baseブランチ、headブランチはWebhookのpayloadの中に入っているのでそれを使う。

const pullRequest = (github.context.payload as PullRequestWebhookPayload).pull_request;
const {
  milestone,
  number: prNumber,
  base: { ref: baseBranch },
  head: { ref: headBranch },
} = pullRequest;

先程のconfigで取ってきた正規表現とmatchするものを見つけてくる。milestoneは1つしかつけることはないので1件ヒットしたら即返却で良い。

export const match = (
  baseBranch: string,
  headBranch: string,
  configObject: ConfigObject,
): string|undefined => {
  let hit: string|undefined;
  configObject.baseBranchList.some((regexp) => {
    const m = baseBranch.match(regexp);
    if (m && m[1]) {
      ([, hit] = m);
    }
    return !!hit;
  });
  if (hit) {
    return hit;
  }

  configObject.headBranchList.some((regexp) => {
    const m = headBranch.match(regexp);
    if (m && m[1]) {
      ([, hit] = m);
    }
    return !!hit;
  });
  return hit;
};

PRにマイルストーンをつける

実はちょっとハマった。30分ぐらい悩んだ。
actions/labelerを参考にしていたので、最後にaddLabelのmilestone版のgithubのAPIを叩けば終わると思っていたがSDKにそんな関数は見当たらない。最初はpayloadが不完全だったこともあってSDKが対応していないだけだろうと思っていたのだがAPIリファレンスにもaddMilestoneだったりsetMilestoneなるものは見当たらなかった。悩んだ。
結論、milestoneはissueUpdate(PRもissueの一種なので同一API)やissueCreate時につけてやるものらしい。labelだけaddLabelなんてAPIがあるから騙された。issueのtitleを変える感覚でmilestoneも変更するみたい。

さらにいうとlabelと違ってmilestoneはmilestone用のID(下記コードのnumberの部分)を使ってissueを更新する。先程の項目で割り出したのはmilestoneのタイトルなのでそれと一致するmilestoneのIDを既存のmilestone一覧から探してこなくてはならない。

const addMilestone = 'v1.0';
const milestones = await client.issues.listMilestonesForRepo({
  owner: github.context.repo.owner,
  repo: github.context.repo.repo,
});
const { number: milestoneNumber } = milestones.data.find(({ title }) => title === addMilestone) || {};

既存のmilestoneが見つからなかった場合はmilestoneCreateとかで作成しても良かったわけだが、今回はメッセージ出すだけにした。やるならオプションで作成も一緒にするかみたいなフラグを追加しようかな。

if (milestoneNumber) {
  await client.issues.update({
    owner: github.context.repo.owner,
    repo: github.context.repo.repo,
    issue_number: prNumber,
    milestone: milestoneNumber,
  });
  core.setOutput('milestone', addMilestone);
} else {
  console.log(`Milestone not found, Please create it first "${addMilestone}".`);
}

今回は紐付けたmilestoneをoutputとして出力するactionなのでcore.setOutput('milestone', addMilestone);という一文も忘れずに。

Marketplaceに公開する

公式のドキュメントに画像つきで丁寧に書いてあるのでそこまで説明いらないとは思う。
https://help.github.com/ja/actions/automating-your-workflow-with-github-actions/publishing-actions-in-github-marketplace

公開する成果物にはnode_modulesも含めビルド済みのjsファイルがないといけないので、masterブランチではgitignoreにnode_modulesが入っていてそれとは別にnode_modulesを除外しないreleaseブランチを作ろう、とテンプレートでは書かれている。
https://github.com/actions/typescript-action#publish-to-a-distribution-branch

他の手段としては@zeit/nccを使ってそもそもnode_modulesも含めすべて1ファイルにしてしまうという方法もある。
https://help.github.com/en/actions/automating-your-workflow-with-github-actions/creating-a-javascript-action

今回は前者の方法でやろうと思うが毎回そんな作業をするのも面倒なのでnpm preversionにビルド作業とnode_modules,ビルド成果物の追加を仕込んでおくことにした。package.jsonscriptsの部分を書き換える

"scripts": {
 "preversion": "npm ci && npm run build && npm ci --production && git add node_modules lib -f"
}

該当する作業コミットはこれ。
https://github.com/iyu/actions-milestone/commit/a95202bc14122a66a01dc5aad55a6ebfed5279bd

これは何なのかというと、npm versionというpackage.jsonのversion部分をアップデートしつつgit-tagをつけてくれるコマンドの前にhookされるスクリプト。npmにライブラリを公開している人ならおなじみだと思う。
詳しくはここ https://docs.npmjs.com/cli/version

$ npm version major

> actions-milestone@1.0.0 preversion iyu/actions-milestone
> npm ci && npm run build && npm ci --production && git add node_modules lib -f

npm WARN prepare removing existing node_modules/ before installation
added 651 packages in 7.098s

> actions-milestone@0.0.0 build git/iyu/actions-milestone
> tsc

npm WARN prepare removing existing node_modules/ before installation
added 50 packages in 2.174s
v1.0.0

やってることはシンプルで、

  1. npm ci node_modulesを消してからinstall (clean installの略だった気がする)
  2. npm run build scriptsのbuildを実行 (typescriptのビルドが書かれてる)
  3. npm ci --production ビルドのときしか必要でないpackageを消すために実行してる
  4. git add node_modules lib -f .gitignoreにかかれているnode_modulesとビルド成果物をforce add

このあとに本来のnpm versionが動く。 (package.jsonのversionの更新とgit-commit, git-tag)

githubにpushしたらリリース物として[Draft a new release]のボタンを押してPublish this Action to the GitHub Marketplaceにチェックする。
image.png
カテゴリとかiconとか必要なものを埋めていく。iconは別になくてもいいっぽい。

これで無事完成!検索すればちゃんとMarketplaceに出てくるようになった!
image.png

さいごに

今回は一発目だったので簡単なActionにしたがもっとちゃんと実用性のあるActionを作っていきたい。ただ仕事で使うやつとなるとたぶんMarketplaceには上げないと思うので今回はまぁ楽しめた。
近々、チーム開発でGitHubActionsを使うにあたって苦労した点を記事にして書きたいと思う。

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

堅牢な node.js プロジェクトのアーキテクチャとは?

こちらの記事は、Sam Quinn 氏により2019年 4月に公開された『 Bulletproof node.js project architecture 』の和訳です。
本記事は原著者から許可を得た上で記事を公開しています。

GitHub repositoryでの実装例: 2019/04/21 アップデート

始めに

Express.jsは、node.js のREST APIを作成するための優れたフレームワークですが、node.jsプロジェクトの設計方法についての手がかりを与えてくれるものではありません。

ばからしく聞こえるかもしれませんが、この問題は確かに存在するのです。

node.jsプロジェクト構造の正しい設計により、コードの重複を回避でき、安定性を向上させます。また、正しく設計されていれば、サービスをスケールさせるときに役に立つかもしれません。

この記事は、貧弱な構造のnode.jsプロジェクト、望ましくないパターン、そしてコードリファクタリングと構造の改善に無数の時間を費やし対応してきた、長年の経験に基づく広範囲な探求です。

本記事に合わせnode.jsプロジェクトのアーキテクチャを見直すために助けが必要な場合は、santiago@softwareontheroad.comにご連絡ください。

目次

  • フォルダ構造
  • 3層アーキテクチャ
  • サービスレイヤー
  • Pub/Subレイヤー
  • Dependency Injection (DI) --※日本語で「依存の注入」
  • ユニットテスト
  • Cron ジョブと定期的なタスク
  • 構成情報及びシークレット
  • ローダー 例(GitHub repojitory)

フォルダ構造

以下はこれから話を進めていくnode.jsプロジェクトの構造です。

構築するすべてのnode.js REST APIサービスで、これをを使用します。では、それぞれのコンポーネントが何をするのか詳しく見ていきましょう。

  │   app.js          # App entry point
  └───api             # Express route controllers for all the endpoints of the app
  └───config          # Environment variables and configuration related stuff
  └───jobs            # Jobs definitions for agenda.js
  └───loaders         # Split the startup process into modules
  └───models          # Database models
  └───services        # All the business logic is here
  └───subscribers     # Event handlers for async task
  └───types           # Type declaration files (d.ts) for Typescript

単なるJavascript ファイルの並び替えをする方法ではありません..

3層アーキテクチャ

下記のアイデアは、「関心の分離」の原則に基づき、ビジネスロジックをnode.js APIルーティングから分離させるものです。

これはあなたがいつか、CLIツールでビジネスロジックを使用したい、定期的なタスク処理では十分でない、と思うようになったときのためです。

そしてnode.jsサーバーからそれ自体へのAPI呼び出しは、良いアイディアではありません...

コントローラーにビジネスロジックを入れてはダメです!!

express.jsコントローラーを使用してアプリケーションのビジネスロジックを保存したくなるかもしれませんが、これはすぐにスパゲッティコードになります。ユニットテストを書く必要があるときには、リクエストまたはレスポンスexpress.jsオブジェクトの複雑なモックを扱うことになります。

いつ応答を送信するべきかを区別するのは複雑です。 バックグランドで処理が続行され、その後 応答がクライアントに送信されたとしましょう。

以下は望ましくない例です。

route.post('/', async (req, res, next) => {

    // This should be a middleware or should be handled by a library like Joi.
    const userDTO = req.body;
    const isUserValid = validators.user(userDTO)
    if(!isUserValid) {
      return res.status(400).end();
    }

    // Lot of business logic here...
    const userRecord = await UserModel.create(userDTO);
    delete userRecord.password;
    delete userRecord.salt;
    const companyRecord = await CompanyModel.create(userRecord);
    const companyDashboard = await CompanyDashboard.create(userRecord, companyRecord);

    ...whatever...


    // And here is the 'optimization' that mess up everything.
    // The response is sent to client...
    res.json({ user: userRecord, company: companyRecord });

    // But code execution continues :(
    const salaryRecord = await SalaryModel.create(userRecord, companyRecord);
    eventTracker.track('user_signup',userRecord,companyRecord,salaryRecord);
    intercom.createUser(userRecord);
    gaAnalytics.event('user_signup',userRecord);
    await EmailService.startSignupSequence(userRecord)
  });

ビジネスロジックをサービスレイヤーで扱っている

このレイヤーは、ビジネスロジックが存在すべき場所です。

それは、node.jsに適用されるSOLID原則に従って、明確な目的(情報)を持つクラスのコレクションです。

このレイヤーには「SQLクエリ」のいかなるフォームも存在するべきではありません。データアクセス層を使用してください。

  • express.jsルーターからソースコードを遠ざける
  • リクエストまたはレスポンスオブジェクトをサービスレイヤーに渡さない
  • ステータスコードやヘッダーなど、HTTPトランスポートレイヤーに関連するものをサービスレイヤーから返さない

route.post('/', 
    validators.userSignup, // this middleware take care of validation
    async (req, res, next) => {
      // The actual responsability of the route layer.
      const userDTO = req.body;

      // Call to service layer.
      // Abstraction on how to access the data layer and the business logic.
      const { user, company } = await UserService.Signup(userDTO);

      // Return a response to client.
      return res.json({ user, company });
    });

サービスが裏でどのように機能するかを以下に示します。

  import UserModel from '../models/user';
  import CompanyModel from '../models/company';

  export default class UserService {

    async Signup(user) {
      const userRecord = await UserModel.create(user);
      const companyRecord = await CompanyModel.create(userRecord); // needs userRecord to have the database id 
      const salaryRecord = await SalaryModel.create(userRecord, companyRecord); // depends on user and company to be created

      ...whatever

      await EmailService.startSignupSequence(userRecord)

      ...do more stuff

      return { user: userRecord, company: companyRecord };
    }
  }

Pub/Sub レイヤーも利用する

pub / subパターンは,、ここで提案されている従来の3層アーキテクチャを超えていますが、非常に便利です。

すぐにユーザーを作成できるシンプルなnode.js APIエンドポイントは、分析サービスであったり、あるいは電子メールシーケンスの開始などのサードパーティサービスを呼び出そうとするかもしれません。

遅かれ早かれ、そのシンプルな「作成」の操作はいくつかのことを実行し、1,000行にも及ぶコードがすべて1つの関数中で実行されることになるでしょう。

それは単一責任の原則に反しています。

したがって最初から責任を分離しておくほうが良く、それによってコードの保守性を維持できます。

  import UserModel from '../models/user';
  import CompanyModel from '../models/company';
  import SalaryModel from '../models/salary';

  export default class UserService() {

    async Signup(user) {
      const userRecord = await UserModel.create(user);
      const companyRecord = await CompanyModel.create(user);
      const salaryRecord = await SalaryModel.create(user, salary);

      eventTracker.track(
        'user_signup',
        userRecord,
        companyRecord,
        salaryRecord
      );

      intercom.createUser(
        userRecord
      );

      gaAnalytics.event(
        'user_signup',
        userRecord
      );

      await EmailService.startSignupSequence(userRecord)

      ...more stuff

      return { user: userRecord, company: companyRecord };
    }

  }

依存サービスへの呼び出し命令は、最良の方法ではありません。

ここでより良いアプローチは、イベントを発行することです。(例.「ユーザーはこのメールでサインアップしました」)

これで完了です。リスナーの仕事は、リスナーの責任としています。

  import UserModel from '../models/user';
  import CompanyModel from '../models/company';
  import SalaryModel from '../models/salary';

  export default class UserService() {

    async Signup(user) {
      const userRecord = await this.userModel.create(user);
      const companyRecord = await this.companyModel.create(user);
      this.eventEmitter.emit('user_signup', { user: userRecord, company: companyRecord })
      return userRecord
    }

  }

イベントハンドラー/リスナーを複数のファイルに分割できています。

 eventEmitter.on('user_signup', ({ user, company }) => {

    eventTracker.track(
      'user_signup',
      user,
      company,
    );

    intercom.createUser(
      user
    );

    gaAnalytics.event(
      'user_signup',
      user
    );
  })
 eventEmitter.on('user_signup', async ({ user, company }) => {
    const salaryRecord = await SalaryModel.create(user, company);
  })
 eventEmitter.on('user_signup', async ({ user, company }) => {
    await EmailService.startSignupSequence(user)
  })

awaitステートメントをtry-catchブロックにラップする、もしくは単に失敗処理として” unhandledPromise “プロセスとして処理することもできます。

依存性の注入 (D.I.)

依存性の注入(D.I.)、または制御の反転(IoC)は、クラスまたは関数の依存関係をコンストラクターに「注入」または渡すことで、コードの編成に役立つ一般的なパターンです。

このようにすることで、例えばサービスの単体テストを作成するときや、サービスが別のコンテキストで使用されるとき、「互換性のある依存関係」を注入する柔軟性が得られます。

D.I. なしのコード

  import UserModel from '../models/user';
  import CompanyModel from '../models/company';
  import SalaryModel from '../models/salary';  
  class UserService {
    constructor(){}
    Sigup(){
      // Caling UserMode, CompanyModel, etc
      ...
    }
  }

手動でD.I. を実装したコード

 export default class UserService {
    constructor(userModel, companyModel, salaryModel){
      this.userModel = userModel;
      this.companyModel = companyModel;
      this.salaryModel = salaryModel;
    }
    getMyUser(userId){
      // models available throug 'this'
      const user = this.userModel.findById(userId);
      return user;
    }
  }

これでカスタマイズされた依存関係を注入できます。

  import UserService from '../services/user';
  import UserModel from '../models/user';
  import CompanyModel from '../models/company';
  const salaryModelMock = {
    calculateNetSalary(){
      return 42;
    }
  }
  const userServiceInstance = new UserService(userModel, companyModel, salaryModelMock);
  const user = await userServiceInstance.getMyUser('12346');

サービスが持つことのできる依存関係の量は無限で、新しく追加する際にいちいちインスタンス化をリファクタリングするのは、退屈でエラーが発生しやすいタスクです。

そういうわけでDI フレームワークが作成されました。

これにより、クラスで依存関係を宣言し、そのクラスのインスタンスが必要な場合には、 'Service Locator'を呼び出すだけでよくなります。

typedi “を用いてnode.jsにDIをもたらすnpmライブラリの例を見てみましょう。

“ typedi “の使用方法の詳細については公式ドキュメントをご覧ください。

注意: typescript での例

 import { Service } from 'typedi';
  @Service()
  export default class UserService {
    constructor(
      private userModel,
      private companyModel, 
      private salaryModel
    ){}

    getMyUser(userId){
      const user = this.userModel.findById(userId);
      return user;
    }
  }

ここでtypediはUserServiceが必要とする依存関係を解決します。

services/user.js
  import { Container } from 'typedi';
  import UserService from '../services/user';
  const userServiceInstance = Container.get(UserService);
  const user = await userServiceInstance.getMyUser('12346');

サービスロケーター呼び出しの乱用はアンチパターンです

Node.jsのExpress.jsでDIを使用する
express.jsでDIを使用する
これがnode.jsプロジェクトアーキテクチャのパズルの最後のピースです。

ルーティングレイヤー

 route.post('/', 
    async (req, res, next) => {
      const userDTO = req.body;

      const userServiceInstance = Container.get(UserService) // Service locator

      const { user, company } = userServiceInstance.Signup(userDTO);

      return res.json({ user, company });
    });

Awesome! 素晴らしいプロジェクトになりました!

とても整理されていて、「今すぐ何かをコーディングしたい!」という気持ちになりますね。

サンプルのレポジトリにアクセスする

単体テストの例

DI とこれらの設計パターンを使用することにより、単体テストは非常にシンプルになります。

リクエスト / レスポンス オブジェクトのモックや “ require … “ などの呼び出しを行う必要はありません。

例:サインアップユーザーメソッドの単体テスト

tests/unit/services/user.js
 import UserService from '../../../src/services/user';

  describe('User service unit tests', () => {
    describe('Signup', () => {
      test('Should create user record and emit user_signup event', async () => {
        const eventEmitterService = {
          emit: jest.fn(),
        };

        const userModel = {
          create: (user) => {
            return {
              ...user,
              _id: 'mock-user-id'
            }
          },
        };

        const companyModel = {
          create: (user) => {
            return {
              owner: user._id,
              companyTaxId: '12345',
            }
          },
        };

        const userInput= {
          fullname: 'User Unit Test',
          email: 'test@example.com',
        };

        const userService = new UserService(userModel, companyModel, eventEmitterService);
        const userRecord = await userService.SignUp(teamId.toHexString(), userInput);

        expect(userRecord).toBeDefined();
        expect(userRecord._id).toBeDefined();
        expect(eventEmitterService.emit).toBeCalled();
      });
    })
  })

Cronジョブと定期的なタスク

ここまででビジネスロジックがサービスレイヤーにカプセル化されたので、Cronジョブから使用するのが簡単になりました。

node.js のsetTimeoutや、その他の原始的なコード実行を遅らせる方法に頼るのではなく、ジョブやデータベース内での処理を永続化するフレームワークを使用するべきです。

こうすることで、失敗したジョブの制御や、成功した人のフィードバックを得ることができます。
node.js.
別の記事で、これらのグッドプラクティスについて既に書いていますので、こちらのガイドを確認してください。

構成情報及びシークレット

node.jsにおいて研鑽された概念である「Twelve-Factor App」に従えば、 APIキーとデータベース文字列の対応情報を保存するもっとも良い方法は、dotenvを使用することです。

決してコミットしてはいけない .envファイルを配置すると(ただし、リポジトリにデフォルト値で存在する必要があります)、 npm パッケージのdotenv
.envファイルをロードし、変数を node.js のprocess.envオブジェクトに挿入します。

これでも十分かもしれませんが、もうワンステップ加えたいと思います。

npmパッケージの dotenv が 参照するディレクトリ(今回の例では /config)配下に" index.js "ファイルを配置し、.envファイルを読み込むことで 、変数を格納するオブジェクトを使用できます。これで構造とコードの自動補完を保持できます。

config/index.js
 const dotenv = require('dotenv');
  // config() will read your .env file, parse the contents, assign it to process.env.
  dotenv.config();

  export default {
    port: process.env.PORT,
    databaseURL: process.env.DATABASE_URI,
    paypal: {
      publicKey: process.env.PAYPAL_PUBLIC_KEY,
      secretKey: process.env.PAYPAL_SECRET_KEY,
    },
    paypal: {
      publicKey: process.env.PAYPAL_PUBLIC_KEY,
      secretKey: process.env.PAYPAL_SECRET_KEY,
    },
    mailchimp: {
      apiKey: process.env.MAILCHIMP_API_KEY,
      sender: process.env.MAILCHIMP_SENDER,
    }
  }

こうすることでprocess.env.MY_RANDOM_VARによってコード記述の氾濫を回避でき、自動補完によって環境変数の命名方法を知る必要がなくなります。

サンプルのレポジトリにアクセスする

ローダー

このパターンはW3Techマイクロフレームワークから取得しましたが、そのパッケージには依存していません。

このアイデアでは、node.jsサービスの起動プロセスをテスト可能なモジュールに分割することが可能です。

古典的なexpress.jsアプリの立ち上げ手順を見てみましょう

  const mongoose = require('mongoose');
  const express = require('express');
  const bodyParser = require('body-parser');
  const session = require('express-session');
  const cors = require('cors');
  const errorhandler = require('errorhandler');
  const app = express();

  app.get('/status', (req, res) => { res.status(200).end(); });
  app.head('/status', (req, res) => { res.status(200).end(); });
  app.use(cors());
  app.use(require('morgan')('dev'));
  app.use(bodyParser.urlencoded({ extended: false }));
  app.use(bodyParser.json(setupForStripeWebhooks));
  app.use(require('method-override')());
  app.use(express.static(__dirname + '/public'));
  app.use(session({ secret: process.env.SECRET, cookie: { maxAge: 60000 }, resave: false, saveUninitialized: false }));
  mongoose.connect(process.env.DATABASE_URL, { useNewUrlParser: true });

  require('./config/passport');
  require('./models/user');
  require('./models/company');
  app.use(require('./routes'));
  app.use((req, res, next) => {
    var err = new Error('Not Found');
    err.status = 404;
    next(err);
  });
  app.use((err, req, res) => {
    res.status(err.status || 500);
    res.json({'errors': {
      message: err.message,
      error: {}
    }});
  });


  ... more stuff 

  ... maybe start up Redis

  ... maybe add more middlewares

  async function startServer() {    
    app.listen(process.env.PORT, err => {
      if (err) {
        console.log(err);
        return;
      }
      console.log(`Your server is ready !`);
    });
  }

  // Run the async function to start our server
  startServer();

ご覧のとおり、アプリケーションのこの部分は非常に煩雑化しています。

これに関して効果的な対処法は以下です。

  const loaders = require('./loaders');
  const express = require('express');

  async function startServer() {

    const app = express();

    await loaders.init({ expressApp: app });

    app.listen(process.env.PORT, err => {
      if (err) {
        console.log(err);
        return;
      }
      console.log(`Your server is ready !`);
    });
  }

  startServer();

ここでローダーは、簡潔な目的を持つ単なる小さなファイルです

loaders/index.js
  import expressLoader from './express';
  import mongooseLoader from './mongoose';

  export default async ({ expressApp }) => {
    const mongoConnection = await mongooseLoader();
    console.log('MongoDB Intialized');
    await expressLoader({ app: expressApp });
    console.log('Express Intialized');

    // ... more loaders can be here

    // ... Initialize agenda
    // ... or Redis, or whatever you want
  }

express ローダー

loaders/express.js
  import * as express from 'express';
  import * as bodyParser from 'body-parser';
  import * as cors from 'cors';

  export default async ({ app }: { app: express.Application }) => {

    app.get('/status', (req, res) => { res.status(200).end(); });
    app.head('/status', (req, res) => { res.status(200).end(); });
    app.enable('trust proxy');

    app.use(cors());
    app.use(require('morgan')('dev'));
    app.use(bodyParser.urlencoded({ extended: false }));

    // ...More middlewares

    // Return the express app
    return app;
  })

mongo ローダー

loaders/mongoose.js
  import * as mongoose from 'mongoose'
  export default async (): Promise<any> => {
    const connection = await mongoose.connect(process.env.DATABASE_URL, { useNewUrlParser: true });
    return connection.connection.db;
  }

ローダーの完全な例はこちらをご覧ください

最後に..

ここまでで、私達は実績のあるnode.jsプロジェクトストラクチャについて深く理解できました。要約すると下記のような内容でしたね。

  • 3層アーキテクチャを使用する
  • ビジネスロジックをexpress.jsコントローラーに入れない
  • PubSubパターンを使用してバックグラウンドタスクのイベントを発行する
  • 負担を減らすためDI を実装する
  • パスワード、シークレット、APIキーなどを漏らさないために構成マネージャーを使用する
  • node.jsサーバー構成を、個別にロードできる小さな- モジュールに分割する

リポジトリの例はこちらからご覧ください。

ちょっと待って!まだ続きがあります。

この記事を楽しんでいただけたら、他の有益な情報も見逃すことがないように、私のメーリングリストを購読することをお勧めします。

何かを売りつけるようなことはしません。約束します!

今後の投稿もお見逃しなく!きっと気に入ってくれると思います :)

この記事のような、すごい記事がたくさんあるので、是非私のブログに来てください。

翻訳協力

Original Author: Sam Quinn
Thank you for letting us share your knowledge!

この記事は以下の方々のご協力により公開する事が出来ました。
改めて感謝致します。
選定担当: @aoharu
翻訳担当: @upaldus
監査担当: @aoharu
公開担当: @posaune0423

私たちと一緒に記事を作りませんか?

私たちは、海外の良質な記事を複数の優秀なエンジニアの方の協力を経て、日本語に翻訳し記事を公開しています。
活動に共感していただける方、良質な記事を多くの方に広めることに興味のある方は、ぜひご連絡ください。
Mailでタイトルを「参加希望」としたうえでメッセージをいただく、もしくはTwitterでメッセージをいただければ、選考のちお手伝いして頂ける部分についてご紹介させていただく事が可能です。
※ 頂いたメッセージには必ずご返信させて頂きます。

ご意見・ご感想をお待ちしております

今回の記事は、いかがだったでしょうか?
・こうしたら良かった、もっとこうして欲しい、こうした方が良いのではないか
・こういったところが良かった
などなど、率直なご意見を募集しております。
いただいたお声は、今後の記事の質向上に役立たせていただきますので、お気軽にコメント欄にてご投稿ください。Twitterでもご意見を受け付けております。
みなさまのメッセージをお待ちしております。

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

React で eject せずに Scoped SASS (.scss) を使う

概要

  • scoped sass (ファイル内限定で適用されるスタイル) を使いたいでござる
  • でもnpm run ejectはしたくないでござる
  • cra-sass を導入するとかんたんにできるでござる

参考文献

実行環境

  • create-react-app で作った react project
    • 既存プロジェクトなのでversionわからん すまん
  • TypeScript

サンプルコード (変更前)

node-sass を入れてふつーにscssを使うとこうなる。

Sample.tsx

Sample.tsx
import * as React from "react";
import "./Sample.scss";

export const Sample: React.FC = () => {
  return (
    <div className="outer">
      OUTER
      <div className="inner">INNER</div>
      <ul>
        {["red", "blue", "green"].map((each, index) => (
          <li className={each} key={index}>
            {each.toUpperCase()}
          </li>
        ))}
      </ul>
    </div>
  );
};

export default Sample;

Sample.scss

Sample.scss
.outer {
  &,
  * {
    display: flex;
    flex-direction: column;
    padding: 8px;
    border-left: 1px solid gray;
  }

  font-size: 1.2rem;
  .inner {
    font-weight: bold;
  }
  ul {
    li {
      &.red {
        color: red;
      }
      &.green {
        color: green;
      }
      &.blue {
        color: blue;
      }
    }
  }
}

ビルド結果(html)

<div class="outer">
  OUTER
  <div class="inner">INNER</div>
  <ul>
    <li class="red">RED</li>
    <li class="blue">BLUE</li>
    <li class="green">GREEN</li>
  </ul>
</div>

実行結果

この実装の問題点

Sample.scss に記述したスタイルのscopeはグローバルである。
すなわち、Sample.tsx と同時にロードされるコンポーネントに、
同じclassName(例えば.outer)が割りあたっていると、互いに影響を受け合いバグの原因となる

解決策

閉じたscopeを扱うことのできるsass loaderを導入する

導入手順

cra-sass を導入

npm install --save-dev cra-sass

cra-sass を実行

$(npm bin)/cra-sass

するとなんかいっぱいインストールしてプロジェクトが魔改造される

package.json
@ devDependencies
+    "cra-sass": "0.0.5",

@ dependencies
+    "node-sass-chokidar": "^1.4.0",
+    "npm-add-script": "^1.1.0",
+    "npm-run-all": "^4.1.5",

@ scripts
-    "start": "react-scripts start",
-    "build": "react-scripts --max-old-space-size=2048 build",
+    "start": "npm-run-all -p watch-css start-js",
+    "build": "npm run build-css && react-scripts build",
     "test": "react-scripts test",
-    "eject": "react-scripts eject"
+    "eject": "react-scripts eject",
+    "build-css": "node-sass-chokidar src/ -o src/",
+    "watch-css": "npm run build-css && node-sass-chokidar src/ -o src/ --watch --recursive",
+    "start-js": "react-scripts start"

--max-old-space-size=2048 とか無くなってぶっ壊れてんじゃん!
ってことで無駄にぶっこわされたとこは直しておく

package.json
-    "build": "react-scripts --max-old-space-size=2048 build",
+    "build": "npm run build-css && react-scripts --max-old-space-size=2048 build",

サンプルコード(リファクタ後)

Sample.scss

Sample.module.scss に改名する

Sample.tsx

  • scssのimport
  • classNameの割当てのしかた

だけを変更

Sample.tsx
import * as React from "react";
import styles from "./Sample.module.scss";

export const Sample: React.FC = () => {
  return (
    <div className={styles.outer}>
      OUTER
      <div className={styles.inner}>INNER</div>
      <ul>
        {["red", "blue", "green"].map((each, index) => (
          <li className={styles[each]} key={index}>
            {each.toUpperCase()}
          </li>
        ))}
      </ul>
    </div>
  );
};

export default Sample;

ビルド結果

<div class="Sample_outer__144wv">
  OUTER
  <div class="Sample_inner__EiBfI">INNER</div>
  <ul>
    <li class="Sample_red__1ktYQ">RED</li>
    <li class="Sample_blue__32ZOZ">BLUE</li>
    <li class="Sample_green__2OrZU">GREEN</li>
  </ul>
</div>
css部分の抜粋
.Sample_outer__144wv {
  font-size: 1.2rem; }
  .Sample_outer__144wv,
  .Sample_outer__144wv * {
    display: flex;
    flex-direction: column;
    padding: 8px;
    border-left: 1px solid gray; }
  .Sample_outer__144wv .Sample_inner__EiBfI {
    font-weight: bold; }
  .Sample_outer__144wv ul li.Sample_red__1ktYQ {
    color: red; }
  .Sample_outer__144wv ul li.Sample_green__2OrZU {
    color: green; }
  .Sample_outer__144wv ul li.Sample_blue__32ZOZ {
    color: blue; }

その他やったこと

scriptsが壊されてないかチェックしよう

start, build, test が、 cra-sass によって破壊されている恐れがある
特にdefaultから変更している場合注意しよう

.cssが.scssと同階層に出力されるようになってうっおとしい

  • node-sass-chokidar のしわざくさい
  • でもoutputしなくするオプションとかなさげ
  • めんどいから、別階層に吐かせて、ignoreすることにした
package.json
-    "build-css": "node-sass-chokidar src/ -o src/",
+    "build-css": "node-sass-chokidar src/ -o built-css/",
-    "watch-css": "npm run build-css && node-sass-chokidar src/ -o src/ --watch --recursive",
+    "watch-css": "npm run build-css && node-sass-chokidar src/ -o built-css/ --watch --recursive",
.gitignore
+/built-css

node-sass をすでに使っていた場合、不要になる

npm r node-sass

おしまい

これで快適な React x Scoped SASS 生活が始まる

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

Firebaseをなるべく安い料金で頑張りたい人へ

この記事はFirebaseアドベントカレンダーの5日目の記事です。

どうも!ハムカツおじさんという名前でtwitterやってます(@hmktsu)?
自分でだったり弊社でだったりなどFirebaseを使ってサービスを作っています。

ちなみに先日Firebase Meetup #15 Cloud Functions Dayにて下記のスライドを発表させていただいたので、興味ある人はご覧になってください!
Firebaseオンリー + React Nativeでアプリを作ると果たして簡単になるのか?

はじめに

色んなツールを作るのにFirebaseを使うととても便利だなぁという感想を持っています。
料金に関しては体感としてですが常時サーバを立ち上げてるのとは違いお安くなります。
それでももっと安く済ませたいとかそういったことはあるのでしょうか。

本番環境でそれなりにお金がかかるのはしょうがないんだけども、テスト環境ではなるべくお金かけたくないしなぁ〜ということもあるのではないでしょうか。

ということでここらへんを気をつければなるべく料金を抑えれるんじゃないかなということを今回は紹介させていただきます。

対象とするFirebaseのサービス

  • Firestore
  • Functions
  • Hosting

ざっくり気をつけたポイント

Firestore

  • リージョンをどうするか
  • ドキュメントの読み出し/書き込み/削除
  • ページングの方法
  • セキュリティールールについて

Functions

  • メモリ(CPU)の割り当て
  • 料金プラン

Hosting

  • どういったファイルを置いているのか

こういう風に工夫するとよいかも

Firestore

  • USじゃないといけない理由がないならば他のリージョンを使う
    • USはマルチリージョンなのでちょっと他よりもお高め(地域別料金)
  • このデータが入っているか?という確認をしたいときに、コレクションからwhereするのではなく、複数のドキュメントを配列に格納したドキュメントを用意して、その中に入ってるかどうか比較する
  • 読み出しをする際にオフセットを使わないでカーソルを使う
  • ルールの評価はリクエストごとに1回のみ課金されるので、複数ドキュメントを呼び出すときはえいやと思い切って呼び出してみる(Cloud Firestore セキュリティ ルール)

Functions

  • メモリ量でGB秒やCPU秒の料金が変わるので、さっと終わる処理ならばメモリを小さくする(コンピューティング時間)
  • Blazeプランを使うと料金プランページに表示されているSparkプラン以上に無料で使える範囲が広くなる

Hosting

  • Storageと比べると転送量や保存量が割高なので、Storageに画像とかをちゃんとおく

まとめ

正直なところFirebaseはAWSなどで組むのと違ってあまりお金はかかりません。
それにスケーラブルなので落ちないという利点もあります。

ただその代わりこの記事みたいに落ちないからこそ、使い方などを間違えてしまうと異常な料金になってしまうこともあります。
地味によくある話としてはStorageを使わずにHostingのみでWebサイトを作り、バズってしまったがために中々な請求金額がきてしまうとか。

ちなみに最終手段ですが、1日の使用料を実は制限することができます。
また1ヶ月の予算を設定して使用料に応じてアラートを出すこともできます。
1日あたりの費用制限を設定する
1か月の予算を設定する

これらを使うこともある意味ありなんじゃないかなと思います。

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

ホットリロードができるBrainfuck環境

はじめに

この記事はアドベントカレンダーがマズくなったときのために書き溜めておいたものです。
これが公開されているということは、そういうことなのでしょう。

Brainfuckの開発を行うにあたって、ホットリロード機能が欲しいと思ったことはないですか?
むしろ、思い通りに動くほうが珍しい難解言語なので必須機能とも言えるでしょう。
そこで、自分でホットリロードができるBrainfuck環境を構築することにしました。

Brainfuck

Brainfuckとは

コンパイラがなるべく小さくなるように設計された簡潔な言語です1
以下に簡単なプログラムの例を示します。

src/hello.bf
>+++++++++[<++++++++>-]<.>+++++++[<++++>-]<+.+++++++..+++.[-]>++++++++[<++
++>-]<.>+++++++++++[<+++++>-]<.>++++++++[<+++>-]<.+++.------.--------.[-]>
++++++++[<++++>-]<+.[-]++++++++++.
出力
Hello World!
src/abc.bf
++++++++++++++++++++++++++++++++++++++++
+++++++++++++++++++++++++.+.+.>++++++++++.
出力
ABC

導入

今回はBrainfuckのコンパイル環境としてjsbrainfuck2を使います。

$ yarn add jsbrainfuck

jsbrainfuck.interpret()の引数にBrainfuckの文字列を渡すと、コードを実行することができます。
適当に実行用のスクリプトを作成しました。

run.js
const { readFileSync } = require("fs");
const { interpret } = require("jsbrainfuck");
// 引数から対象ファイル名を取得して各ファイルを実行する
process.argv.slice(2).forEach((name) => {
  const source = readFileSync(name, "utf-8");
  console.log(`Executing ${name} =>`);
  const { print } = interpret(source); // Brainfuckコードを実行
  print(); // 実行結果を表示
});

以下のように複数のファイルを実行することもできます。

$ node run.js src/hello.bf src/abc.bf
Executing src/hello.bf =>
Hello World!
Executing src/abc.bf =>
ABC

jsbrainfuck.compile()の引数にBrainfuckの文字列を渡すとJSコードに変換された文字列が返ってきます。
適当にコンパイル用のスクリプトを作成しました。

compile.js
const { readFileSync, writeFileSync } = require("fs");
const { compile } = require("jsbrainfuck");

const name = process.argv[2]; // 入力ファイル名を引数から取得
const source = readFileSync(name, "utf-8");
const { code } = compile(source); // BrainfuckからJSコード生成
const out = process.argv[3]; // 出力ファイル名を引数から取得
writeFileSync(out, code);
$ node compile.js src/hello.bf hello.js
$ node hello.js
Hello World!

ホットリロード

ホットリロードとは

ファイルを変更したとき自動でコンパイルや実行を行うことを言います。
変更するたびに手動でコンパイルして実行する手間を省けます。

導入

Nodemon3というソース監視ツールを導入します。

$ yarn add nodemon

package.jsonに以下のような設定を追加して、./srcの中の.bfファイルを監視して、変更があったらnode ./run.js ./src/*.bfを実行するようにします。

package.json
{
  "nodemonConfig": {
    "watch": [
      "./src"
    ],
    "ext": "bf",
    "exec": "node ./run.js ./src/*.bf"
  }
}

Nodemonを起動すると監視が始まって、変更があるとリロードされていることがわかります。

$ yarn run nodemon
[nodemon] 2.0.1
[nodemon] to restart at any time, enter `rs`
[nodemon] watching dir(s): src/**/*
[nodemon] watching extensions: bf
[nodemon] starting `node ./run.js ./src/*.bf`
Executing ./src/abc.bf =>
ABC
Executing ./src/hello.bf =>
Hello World!
[nodemon] clean exit - waiting for changes before restart
[nodemon] restarting due to changes...
[nodemon] starting `node ./run.js ./src/*.bf`
Executing ./src/abc.bf =>
DEF
Executing ./src/hello.bf =>
Hello World!
[nodemon] clean exit - waiting for changes before restart

最後に

仕上げにpackage.jsonに以下のようなスクリプトを追加すると、yarn startでホットリロードができるBrainfuck環境が動くようになります。

package.json
{
  "scripts": {
    "start": "nodemon",
    "build": "node compile.js"
  }
}

これでBrainfuckのコーディングを快適に嗜むことができます。

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

node.jsを触るために簡単なチャットシステムを作る(メッセージ送信編)

Node.js を触ってみたいと思ったので、備忘録も兼ねて以下に記します。
よりよい方法やバグ等ございましたら、アドバイスいただけると光栄です。

今回は「メッセージ送信編」ということで、クライアントからサーバーへのメッセージの送信の処理を作成します。

※前回 node.jsを触るために簡単なチャットシステムを作る(サーバー接続編) という表題で、サーバー接続の処理を作成していますので、まだな方はこちらを参照ください。

クライアントからサーバーへメッセージを送信する

メッセージ入力フォームを作成

/public/index.html に、以下のメッセージ入力フォームを追加します。

index.html
<form action="">
    <input type="text" id="input_message" autocomplete="off" />
    <button type="submit">Send</button>
</form>

index.html 全体としては、以下のようになります。

index.html
<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8">
    <title>mychat</title>
</head>
<body>
    <h1>node.js を触ってみた</h1>
    <form action="">
        <input type="text" id="input_message" autocomplete="off" />
        <button type="submit">Send</button>
    </form>
    <script src="/socket.io/socket.io.js"></script>
    <script src="https://ajax.googleapis.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
    <script src="client.js"></script>
</body>
</html>

クライアント側の処理

/public/client.js の末尾に、下記のようなSendボタンを押したときの処理を追加します。

client.js
// 「Send」ボタンを押したときの処理
$('form').submit(() => {
    const $inp = $('#input_message');
    const text = $inp.val();

    console.log('#input_message :', text);

    if (text) {
        // サーバーに、イベント名 'new message' で入力テキストを送信
        socket.emit('new message', text);
        // テキストボックスを空に
        $inp.val('');
    }
    // フォーム送信はしない
    return false;
});

/public/client.js 全体としては、以下のようになります。

client.js
'use strict';

// クライアントからサーバーへの接続要求
const socket = io.connect();

// 接続時の処理
socket.on('connect', () => {
    console.log('connect');
});

// 「Send」ボタンを押したときの処理
$('form').submit(() => {
    const $inp = $('#input_message');
    const text = $inp.val();

    console.log('#input_message :', text);

    if (text) {
        // サーバーに、イベント名 'new message' で入力テキストを送信
        socket.emit('new message', text);
        // テキストボックスを空に
        $inp.val('');
    }
    // フォーム送信はしない
    return false;
});

サーバー側の処理

server.js の「接続時の処理」の中に、下記処理を追加します。

server.js
// 新しいメッセージ受信時の処理
socket.on('new message', (strMessage) => {
    console.log('new message', strMessage);
});

server.js 全体としては、以下のようになります。

server.js
'use strict';

// モジュール
const http     = require('http');
const express  = require('express');
const socketIO = require('socket.io');
const moment   = require('moment');

// オブジェクト
const app    = express();
const server = http.Server(app);
const io     = socketIO(server);

// 定数
const PORT = process.env.PORT || 3000;

// グローバル変数
let iCountUser = 0; // ユーザー数

// 接続時の処理
io.on('connection', (socket) => {
    console.log('connection');

    // 切断時の処理
    socket.on('disconnect', () => {
        console.log('disconnect');
    });

    // 新しいメッセージ受信時の処理
    socket.on('new message', (strMessage) => {
        console.log('new message', strMessage);
    });
});

// 公開フォルダの指定
app.use(express.static(__dirname + '/public'));

// サーバーの起動
server.listen(PORT, () => {
    console.log('server starts on port: %d', PORT);
});

動作を確認する

サーバーを立ち上げた状態で、
http://localhost:3000 にアクセスします。

「aaa」と「あああ」というメッセージをフォームに入力し、「Send」ボタンを押します。
Google Chrome のデベロッパーツールの Console に、connectに続いて、
#input_message : aaa
#input_message : あああ
と表示されます。

下記のようにサーバー側で connection に続いて、
new message aaa
new message あああ
と表示されれば完了です。

node server.js
server starts on port: 3000
connection
new message aaa
new message あああ

サーバーからクライアントへメッセージを拡散する

サーバー側の処理

server.js の「接続時の処理」の中に、下記処理を追加します。

server.js
// 送信元含む全員に送信
io.emit('spread message', strMessage);

server.js 全体としては、以下のようになります。

server.js
'use strict';

// モジュール
const http     = require('http');
const express  = require('express');
const socketIO = require('socket.io');
const moment   = require('moment');

// オブジェクト
const app    = express();
const server = http.Server(app);
const io     = socketIO(server);

// 定数
const PORT = process.env.PORT || 3000;

// グローバル変数
let iCountUser = 0; // ユーザー数

// 接続時の処理
io.on('connection', (socket) => {
    console.log('connection');

    // 切断時の処理
    socket.on('disconnect', () => {
        console.log('disconnect');
    });

    // 新しいメッセージ受信時の処理
    socket.on('new message', (strMessage) => {
        console.log('new message', strMessage);

        // 送信元含む全員に送信
        io.emit('spread message', strMessage);
    });
});

// 公開フォルダの指定
app.use(express.static(__dirname + '/public'));

// サーバーの起動
server.listen(PORT, () => {
    console.log('server starts on port: %d', PORT);
});

クライアント側の処理

/public/client.js の末尾に、下記のようなSendボタンを押したときの処理を追加します。

client.js
// サーバーからのメッセージ拡散に対する処理
socket.on('spread message', (strMessage) => {
    console.log('spread message :', strMessage);

    // 拡散されたメッセージをメッセージリストに追加
    const li_element = $('<li>').text(strMessage);
    $('#message_list').prepend(li_element);
});

/public/client.js 全体としては、以下のようになります。

client.js
'use strict';

// クライアントからサーバーへの接続要求
const socket = io.connect();

// 接続時の処理
socket.on('connect', () => {
    console.log('connect');
});

// 「Send」ボタンを押したときの処理
$('form').submit(() => {
    const $inp = $('#input_message');
    const text = $inp.val();

    console.log('#input_message :', text);

    if (text) {
        // サーバーに、イベント名 'new message' で入力テキストを送信
        socket.emit('new message', text);
        // テキストボックスを空に
        $inp.val('');
    }
    // フォーム送信はしない
    return false;
});

// サーバーからのメッセージ拡散に対する処理
socket.on('spread message', (strMessage) => {
    console.log('spread message :', strMessage);

    // 拡散されたメッセージをメッセージリストに追加
    $('#message_list').prepend($('<li>').text(strMessage));
});

メッセージをビュー側に表示

/public/index.html のメッセージ入力フォームの下に、以下のメッセージリストを追加します。

index.html
<ul id="message_list"></ul>

index.html 全体としては、以下のようになります。

index.html
<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8">
    <title>mychat</title>
</head>
<body>
    <h1>node.js を触ってみた</h1>
    <form action="">
        <input type="text" id="input_message" autocomplete="off" />
        <button type="submit">Send</button>
    </form>
    <ul id="message_list"></ul>
    <script src="/socket.io/socket.io.js"></script>
    <script src="https://ajax.googleapis.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
    <script src="client.js"></script>
</body>
</html>

動作を確認する

メッセージの表示

サーバーを立ち上げた状態で、
http://localhost:3000 にアクセスします。

「aaa」と「あああ」というメッセージをフォームに入力し、「Send」ボタンを押します。

送信したメッセージが、リスト表示されます。

別ブラウザでもメッセージを表示

ブラウザを別に立ち上げ、
http://localhost:3000 にアクセスします。

「bbb」と「いいい」というメッセージをフォームに入力し、「Send」ボタンを押します。

送信したメッセージが、両方のブラウザに、即座に、リスト表示されれば完了です。

最後に

今回作成する機能としては以上となります。

ここで作成した機能をベースに、
以下のような機能や他にも自分で思いついた機能などを追加してみるのも良い学習になると思います。

  • メッセージに時刻を表示
  • メッセージに発信者名を表示
  • ユーザーの入室、退室を表示

ソースコードは以下に載せていますので、よろしければ参照ください。
https://github.com/genki-sano/express-socketio-chat

関連記事

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

Unityライセンスのalfファイルを自動でulfにしたい!

CIのActivateとかでライセンスを自動でActivateさせたい!

CIでUnityを扱う時はJenkinsとかであれば問題ないのですが、CircleCIやGitHub Actionsを使用するときにDockerでのライセンス認証では.ulfファイルというのが必要になってきます。

現在.ulfファイルをコマンドラインから生成することはできません。生成するにはブラウザ経由の一択です。
それを今回Puppeteerというnode.jsのツールを使って自動化してみました。

※今回の認証フローはPersonalEdition固定になります。

Puppetterとは、Webブラウザでの操作をソースコードから行えるものになります。
詳しくはこちら
Puppeteer

今回のリポジトリはこちら
MizoTake/unity-license-activate

実装

npm経由でPuppeteerを入れて以下のjsで実装しました。

今回はライセンスの認証が必要になるので https://license.unity3d.com/manual のページで操作を行います。
手元にあるalfファイルから最終的にulfファイルをダウンロードする操作になります。

叩くコマンドは以下になります
node activate.js $email $password $alf_file_path

以下が今回のScriptの全容ですが細かく分けてどうなっているのか下で記述します。

activate.js
const puppeteer = require('puppeteer')
const fs = require('fs')

;(async () => {
  const browser = await puppeteer.launch()
  const page = await browser.newPage()

  const downloadPath = process.cwd()
  const client = await page.target().createCDPSession()
  await client.send('Page.setDownloadBehavior', {
    behavior: 'allow',
    downloadPath: downloadPath
  })

  await page.goto('https://license.unity3d.com/manual')

  await page.waitForNavigation({
    timeout: 60000,
    waitUntil: 'domcontentloaded'
  })

  const email = `${process.argv[2]}`
  await page.type('input[type=email]', email)

  const password = `${process.argv[3]}`
  await page.type('input[type=password]', password)
  await page.click('input[name="commit"]')

  await page.waitForNavigation({
    timeout: 60000,
    waitUntil: 'domcontentloaded'
  })

  const input = await page.$('input[name="licenseFile"]')

  const alfPath = `${process.argv[4]}`
  await input.uploadFile(alfPath)

  await page.click('input[name="commit"]')

  await page.waitForNavigation({
    timeout: 60000,
    waitUntil: 'domcontentloaded'
  })

  const selectedTypePersonal = 'input[id="type_personal"][value="personal"]'
  await page.evaluate(
    s => document.querySelector(s).click(),
    selectedTypePersonal
  )

  const selectedPersonalCapacity =
    'input[id="option3"][name="personal_capacity"]'
  await page.evaluate(
    s => document.querySelector(s).click(),
    selectedPersonalCapacity
  )

  await page.click('input[class="btn mb10"]')

  await page.waitForNavigation()

  await page.click('input[name="commit"]')

  let _ = await (async () => {
    let ulf
    do {
      for (const file of fs.readdirSync(downloadPath)) {
        ulf |= file.endsWith('.ulf')
      }
      await sleep(1000)
    } while (!ulf)
  })()

  function sleep(milliSeconds) {
    return new Promise((resolve, reject) => {
      setTimeout(resolve, milliSeconds)
    })
  }

  await browser.close()
})()

画面ごとの処理

1.png

  // ライセンス認証を行うページに行く
  await page.goto('https://license.unity3d.com/manual')

  await page.waitForNavigation({
    timeout: 60000,
    waitUntil: 'domcontentloaded'
  })

  // ライセンス認証を行うページに飛ばしたがリダイレクトでUnityのログインページに飛んでいる

  //コマンド引数からメールアドレスとパスワードをとってくる
  const email = `${process.argv[2]}`
  await page.type('input[type=email]', email)

  const password = `${process.argv[3]}`
  await page.type('input[type=password]', password)

  // Sign inのボタンを押す
  await page.click('input[name="commit"]')

2.png

  const input = await page.$('input[name="licenseFile"]')

  // コマンドライン引数で指定したpathからfileを添付
  const alfPath = `${process.argv[4]}`
  await input.uploadFile(alfPath)

  // Nextボタンを押す
  await page.click('input[name="commit"]')

3.png

これでファイル添付ができてることがわかります。ファイル添付までできるのすげぇ…Puppeteer…

4.png

  // Personal Editionの選択
  const selectedTypePersonal = 'input[id="type_personal"][value="personal"]'
  await page.evaluate(
    s => document.querySelector(s).click(),
    selectedTypePersonal
  )

  // Personal Edition選択後に出てくるのOptionを選択
  const selectedPersonalCapacity =
    'input[id="option3"][name="personal_capacity"]'
  await page.evaluate(
    s => document.querySelector(s).click(),
    selectedPersonalCapacity
  )
  
 // Nextボタンを押す
  await page.click('input[class="btn mb10"]')

この画面の実行後はこうなっています。
5.png

6.png

  // Download license fileボタンを押す
  await page.click('input[name="commit"]')

  // ダウンロードが始まるので手元に.ulfファイルができるまで待つ
  let _ = await (async () => {
    let ulf
    do {
      for (const file of fs.readdirSync(downloadPath)) {
        ulf |= file.endsWith('.ulf')
      }
      await sleep(1000)
    } while (!ulf)
  })()

  function sleep(milliSeconds) {
    return new Promise((resolve, reject) => {
      setTimeout(resolve, milliSeconds)
    })
  }

以上のような流れになっています。

さいごに

Puppeteer便利!!!!

  await page.screenshot( {
    path: "./example.png"
  });

でScreenShotを撮りつつ実装してました。

ブラウザでしか行えない操作もこれがあればできるので色々と捗るんじゃないかなと思っています。

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

Unityの.alfファイルから自動で.ulfをダウンロードしたい!

CIのActivateとかでライセンスを自動でActivateさせたい!

CIでUnityを扱う時はJenkinsとかであれば問題ないのですが、CircleCIやGitHub Actionsを使用するときにDockerでのライセンス認証では.ulfファイルというのが必要になってきます。

現在.ulfファイルをコマンドラインから生成することはできません。生成するにはブラウザ経由の一択です。
それを今回Puppeteerというnode.jsのツールを使って自動化してみました。

※今回の認証フローはPersonalEdition固定になります。

Puppetterとは、Webブラウザでの操作をソースコードから行えるものになります。
詳しくはこちら
Puppeteer

今回のリポジトリはこちら
MizoTake/unity-license-activate

実装

npm経由でPuppeteerを入れて以下のjsで実装しました。

今回はライセンスの認証が必要になるので https://license.unity3d.com/manual のページで操作を行います。
手元にあるalfファイルから最終的にulfファイルをダウンロードする操作になります。

叩くコマンドは以下になります
node activate.js $email $password $alf_file_path

以下が今回のScriptの全容ですが細かく分けてどうなっているのか下で記述します。

activate.js
const puppeteer = require('puppeteer')
const fs = require('fs')

;(async () => {
  const browser = await puppeteer.launch()
  const page = await browser.newPage()

  const downloadPath = process.cwd()
  const client = await page.target().createCDPSession()
  await client.send('Page.setDownloadBehavior', {
    behavior: 'allow',
    downloadPath: downloadPath
  })

  await page.goto('https://license.unity3d.com/manual')

  await page.waitForNavigation({
    timeout: 60000,
    waitUntil: 'domcontentloaded'
  })

  const email = `${process.argv[2]}`
  await page.type('input[type=email]', email)

  const password = `${process.argv[3]}`
  await page.type('input[type=password]', password)
  await page.click('input[name="commit"]')

  await page.waitForNavigation({
    timeout: 60000,
    waitUntil: 'domcontentloaded'
  })

  const input = await page.$('input[name="licenseFile"]')

  const alfPath = `${process.argv[4]}`
  await input.uploadFile(alfPath)

  await page.click('input[name="commit"]')

  await page.waitForNavigation({
    timeout: 60000,
    waitUntil: 'domcontentloaded'
  })

  const selectedTypePersonal = 'input[id="type_personal"][value="personal"]'
  await page.evaluate(
    s => document.querySelector(s).click(),
    selectedTypePersonal
  )

  const selectedPersonalCapacity =
    'input[id="option3"][name="personal_capacity"]'
  await page.evaluate(
    s => document.querySelector(s).click(),
    selectedPersonalCapacity
  )

  await page.click('input[class="btn mb10"]')

  await page.waitForNavigation()

  await page.click('input[name="commit"]')

  let _ = await (async () => {
    let ulf
    do {
      for (const file of fs.readdirSync(downloadPath)) {
        ulf |= file.endsWith('.ulf')
      }
      await sleep(1000)
    } while (!ulf)
  })()

  function sleep(milliSeconds) {
    return new Promise((resolve, reject) => {
      setTimeout(resolve, milliSeconds)
    })
  }

  await browser.close()
})()

画面ごとの処理

1.png

  // ライセンス認証を行うページに行く
  await page.goto('https://license.unity3d.com/manual')

  await page.waitForNavigation({
    timeout: 60000,
    waitUntil: 'domcontentloaded'
  })

  // ライセンス認証を行うページに飛ばしたがリダイレクトでUnityのログインページに飛んでいる

  //コマンド引数からメールアドレスとパスワードをとってくる
  const email = `${process.argv[2]}`
  await page.type('input[type=email]', email)

  const password = `${process.argv[3]}`
  await page.type('input[type=password]', password)

  // Sign inのボタンを押す
  await page.click('input[name="commit"]')

2.png

  const input = await page.$('input[name="licenseFile"]')

  // コマンドライン引数で指定したpathからfileを添付
  const alfPath = `${process.argv[4]}`
  await input.uploadFile(alfPath)

  // Nextボタンを押す
  await page.click('input[name="commit"]')

3.png

これでファイル添付ができてることがわかります。ファイル添付までできるのすげぇ…Puppeteer…

4.png

  // Personal Editionの選択
  const selectedTypePersonal = 'input[id="type_personal"][value="personal"]'
  await page.evaluate(
    s => document.querySelector(s).click(),
    selectedTypePersonal
  )

  // Personal Edition選択後に出てくるのOptionを選択
  const selectedPersonalCapacity =
    'input[id="option3"][name="personal_capacity"]'
  await page.evaluate(
    s => document.querySelector(s).click(),
    selectedPersonalCapacity
  )
  
 // Nextボタンを押す
  await page.click('input[class="btn mb10"]')

この画面の実行後はこうなっています。
5.png

6.png

  // Download license fileボタンを押す
  await page.click('input[name="commit"]')

  // ダウンロードが始まるので手元に.ulfファイルができるまで待つ
  let _ = await (async () => {
    let ulf
    do {
      for (const file of fs.readdirSync(downloadPath)) {
        ulf |= file.endsWith('.ulf')
      }
      await sleep(1000)
    } while (!ulf)
  })()

  function sleep(milliSeconds) {
    return new Promise((resolve, reject) => {
      setTimeout(resolve, milliSeconds)
    })
  }

以上のような流れになっています。

さいごに

Puppeteer便利!!!!

  await page.screenshot( {
    path: "./example.png"
  });

でScreenShotを撮りつつ実装してました。

ブラウザでしか行えない操作もこれがあればできるので色々と捗るんじゃないかなと思っています。

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

Custom Vision Service を使用してインテリアの樹種分析

CustomVisionService を 簡単なサイトを作って使えるようにしたい

今週はMicrosoftAzureから画像認識を試したのでそちらの成果を簡単に使えるサイトを作ろうと
思いました。内容はせっかくなので本業にちなんでインテリアの画像から樹種を解析してくれるものを目指しました。
内装を考える時にテーマとなる樹種を一つ決めて組み合わせていくと空間にまとまりが出るので、
自分が良いなと思う家具や内装がどの樹種なのかを調べられると買う家具や似合う色が決まってきて
インテリアを構築する時の参考になると良いなと思いました。

環境

Node.js v10.16.3
Windows 10 pro
Visual Studio Code v1.39.1

概要

①MicrosoftAzureでアカウントを作成
②Microsoft Custom Vision Service を使用して画像解析のプロジェクトを作成、トレーニング
③作成したプロジェクトをAPIとして使用する
④APIを取得する為のコードを書く

参考資料

主に①~②の参考にいつもどおり先陣の知恵をお借りします。
【資料1】Microsoft Custom Vision Service を使用した鼓膜画像認識

③、④はこの資料を参考に。
【資料2】Node.jsでAzure Face APIを使ってみる

随時わからない所があるので補足でネットサーフィンしたログを
【資料3】[axios] axios の導入と簡単な使い方
【資料3】JavaScript テキストボックスの値を取得/設定する

①MicrosoftAzureでアカウントを作成

【資料1】を参考に、、、

②Microsoft Custom Vision Service を使用して画像解析のプロジェクトを作成

【Microsoft Custom Vision】の演習に沿って新たなプロジェクトを作ります。

image.png

プロジェクトを開いたら画像を追加から画像をアップロードします。
image.png

今回は樹種を分析したいので、色味の違う樹種を三種類、
ウォルナット、チーク、メイプルの画像を用意しました。
また、分析結果が何に対してなのかわかりやすいように、用意した画像の項目を家具と部屋にわけました。

image.png

≪家具タグ≫
image.png

≪部屋タグ≫
image.png

分析結果

トレーニングで検証してみると、
image.png

99.8%家具。ちゃんと見極めてます。
材質は72.1%チーク。
7.2%のメイプルは、、、床材ですかね。
写真の要素を読み取ってくれました。

③作成したプロジェクトをAPIとして使用する

image.png
性能タブから公開し、予測URLを発行する。
image.png

④APIを取得する為のコードを書く

フォルダ構成は前回と同じ。
image.png

フォルダを作成し、中に >node_modules:node.jsのデータが入っているフォルダ
            >public:htmlデータを入れるフォルダ
             >index.html:サイトを構成する静的ファイル
            >index.js:作成したpublicの静的ファイルをexpressで表示させるコード
            >gitignore:herokuで実装する時に不要なデータを送らないよう指定するファイル
            >package-lock.json:npm init -yで作成される
            >package.json:npm init -yで作成される
             インストールしたライブラリデータ等のパッケージが登録されている
            >Procfile:Herokuを起動するのに必要なファイル

上記のファイルを作成し、Herokuまたは
http://localhost:8080/
で起動させ、確認しながら進める。

今回のコードを起動必要なライブラリは

npm init -y
npm i body-parser express
npm i axios                   

をそれぞれターミナルに入力し、インストール。

index.html

<!DOCTYPE html>
<html>

<head>
    <title>Step 01</title>
    <script src="https://unpkg.com/vue"></script>
</head>

<body>

    <p>画像URL  <span id="span1"></span></p>
    <form name="form1">  
        <input  
        type="text"  
        name="text1"
        value="red"
        size="100" 
        > 

        <!--
           <input>タグ内で 
           type="url" を指定するとURL入力欄の作成
            name="text1" フォーム部品の名前
          value="red"   送信される値を指定
            size="100"  表示される文字数
            maxlength="" 入力できる最大文字数の指定

            -->
    </form>

    <input type="button" value="解析" onclick="clickBtn1()">
    <input type="button" value="クリア" onclick="clickBtn2()">

    <script>

        'use strict';
        // JavaScript内でuse strict を宣言すると、コードがstrict(厳格)モードで実行されるようになる。 
        //strictモードでは、より的確なエラーチェックが行われ、 
        //これまでエラーにならなかったような曖昧な実装がエラー扱いになる


        function clickBtn1() { // clickBtn1をクリックされた時の値を取得
            const t1 = document.form1.text1.value; //form1のtext1のvalueに入力値をt1に代入 
            document.getElementById("span1").textContent = t1;
        }

        function clickBtn2() {
         document.getElementById("span1").textContent = "";
        }



    const axios = require('axios');// axiosを読み込む。require使う場合
    const subscriptionKey = ''; //キーを指定
    const uriBase ='https://url';
    const imageUrl = ti;

    // Request parameters.
    const params = {
    'returnFaceId': 'true',
    'returnFaceLandmarks': 'false',
    'returnFaceAttributes': 'age,gender,headPose,smile,facialHair,glasses,' +
    'emotion,hair,makeup,occlusion,accessories,blur,exposure,noise'
    };

    const config = {
    baseURL: uriBase,
    method: 'post',
    headers: {
    'Content-Type': ' application/json',
    'Prediction-Key': subscriptionKey
    },
    data: '{"url": ' + '"' + imageUrl + '"}',
    params: params,
    }

    axios.request(config)
    .then(res => {
    const jsonResponse = JSON.stringify(res.data, null, ' ');
    console.log('JSON Response\n');
    console.log(jsonResponse);
    })

    .catch(error => console.log(error.response.data));





    </script>
</body>

</html>

image.png

簡単なボタンが出来ました。
試しに画像のURL入力し解析をクリック。

image.png

ちゃんと の"span1"に反映されている為t1にはきちんと代入されているはず。

image.png

コードがうまく動かない。

ターミナルにヘロクのログを見るコードを打ち込んでみる。

heroku logs -t 
エラー
2019-12-03T13:11:14.837911+00:00 heroku[router]: at=info method=GET path="/" host=s191127-sample.herokuapp.com request_id=1d9cfa53-4255-4901-8d40-72a1df83f945 fwd="114.69.33.94" dyno=web.1 connect=1ms service=4ms status=304 bytes=237 protocol=https
2019-12-03T13:11:54.832593+00:00 heroku[router]: at=info method=GET path="/" host=s191127-sample.herokuapp.com request_id=886df22d-3db7-4f66-8b38-92045a51b489 fwd="114.69.33.94" dyno=web.1 connect=1ms service=2ms status=304 bytes=237 protocol=https
2019-12-03T13:12:00.185770+00:00 heroku[router]: at=info method=GET path="/" host=s191127-sample.herokuapp.com request_id=5fe36a9e-5f40-4bfb-8bdf-6084058293c6 fwd="114.69.33.94" dyno=web.1 connect=1ms service=3ms status=304 bytes=237 protocol=https
2019-12-03T13:12:33.770914+00:00 heroku[router]: at=info method=GET path="/" host=s191127-sample.herokuapp.com request_id=474ad7cc-d4b9-4d21-9b81-05d9e0d0cf81 fwd="114.69.33.94" dyno=web.1 connect=1ms service=2ms status=304 bytes=237 protocol=https
2019-12-03T13:17:58.000000+00:00 app[api]: Build started by user sayu5713@gmail.com
2019-12-03T13:18:10.733484+00:00 heroku[web.1]: Restarting
2019-12-03T13:18:10.737606+00:00 heroku[web.1]: State changed from up to starting
2019-12-03T13:18:10.486482+00:00 app[api]: Release v4 created by user sayu5713@gmail.com
2019-12-03T13:18:10.486482+00:00 app[api]: Deploy e73341ab by user sayu5713@gmail.com
2019-12-03T13:18:10.000000+00:00 app[api]: Build succeeded
2019-12-03T13:18:11.420251+00:00 heroku[web.1]: Stopping all processes with SIGTERM
2019-12-03T13:18:11.483511+00:00 heroku[web.1]: Process exited with status 143
2019-12-03T13:18:12.514200+00:00 heroku[web.1]: Starting process with command `node index.js`
2019-12-03T13:18:14.818512+00:00 app[web.1]: server start! (heroku)
2019-12-03T13:18:15.462076+00:00 heroku[web.1]: State changed from starting to up
2019-12-03T13:18:17.019143+00:00 heroku[router]: at=info method=GET path="/" host=s191127-sample.herokuapp.com request_id=94efca27-f6a1-48a6-8ca7-ee2305b16ed5 fwd="114.69.33.94" dyno=web.1 connect=1ms service=22ms status=200 
bytes=1087 protocol=https
2019-12-03T13:52:15.499404+00:00 heroku[web.1]: Idling
2019-12-03T13:52:15.508200+00:00 heroku[web.1]: State changed from up to down
2019-12-03T13:52:19.993362+00:00 heroku[web.1]: Stopping all processes with SIGTERM
2019-12-03T13:52:20.117700+00:00 heroku[web.1]: Process exited with status 143
2019-12-03T13:57:16.000000+00:00 app[api]: Build started by user sayu5713@gmail.com
2019-12-03T13:57:28.515154+00:00 heroku[web.1]: State changed from down to starting
2019-12-03T13:57:28.121717+00:00 app[api]: Deploy ba7a5e71 by user sayu5713@gmail.com
2019-12-03T13:57:28.121717+00:00 app[api]: Release v5 created by user sayu5713@gmail.com
2019-12-03T13:57:28.000000+00:00 app[api]: Build succeeded
2019-12-03T13:57:30.578721+00:00 heroku[web.1]: Starting process with command `node index.js`
2019-12-03T13:57:32.601844+00:00 app[web.1]: server start! (heroku)
2019-12-03T13:57:34.225781+00:00 heroku[web.1]: State changed from starting to up
2019-12-03T13:57:37.046079+00:00 heroku[router]: at=info method=GET path="/" host=s191127-sample.herokuapp.com request_id=4111dd64-a841-43a0-a582-e4e4bd3ac757 fwd="114.69.33.94" dyno=web.1 connect=1ms service=27ms status=200 
bytes=2243 protocol=https
2019-12-03T14:32:17.716968+00:00 heroku[web.1]: Idling
2019-12-03T14:32:17.721462+00:00 heroku[web.1]: State changed from up to down
2019-12-03T14:32:18.890822+00:00 heroku[web.1]: Stopping all processes with SIGTERM
2019-12-03T14:32:18.992477+00:00 heroku[web.1]: Process exited with status 143
^Cバッチ ジョブを終了しますか (Y/N)? Y

(´;ω;`)ウゥゥ
今回はここで断念です、、、
どうにも力不足…また出直します。。。
ありがとうございました!!

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

How to build a Twitter bot with NodeJs

Building a Twitter bot using their API is one of the fundamental applications of the Twitter API. To build a Twitter bot with Nodejs, you’ll need to take these steps below before proceeding:

Create a new account for the bot.
Apply for API access at developer.twitter.com
Ensure you have NodeJS and NPM installed on your machine.
We’ll be building a Twitter bot with Nodejs to track a specific hashtag then like and retweet every post containing that hashtag.

Getting up and running
Firstly you’ll need to initialize your node app by running npm init and filling the required parameters. Next, we install Twit, an NPM package that makes it easy to interact with the Twitter API.

$ npm install twit --save

Now, go to your Twitter developer dashboard to create a new app so you can obtain the consumer key, consumer secret, access token key and access token secret. After that, you need to set up these keys as environment variables to use in the app.

Building the bot

Now in the app’s entry file, initialize Twit with the secret keys from your Twitter app.

// index.js
const Twit = require('twit');
const T = new Twit({
consumer_key: process.env.APPLICATION_CONSUMER_KEY_HERE,
consumer_secret: process.env.APPLICATION_CONSUMER_SECRET_HERE,
access_token: process.env.ACCESS_TOKEN_HERE,
access_token_secret: process.env.ACCESS_TOKEN_SECRET_HERE
});

Listening for events

Twitter’s streaming API gives access to two streams, the user stream and the public stream, we’ll be using the public stream which is a stream of all public tweets, you can read more on them in the documentation.

We’re going to be tracking a keyword from the stream of public tweets, so the bot is going to track tweets that contain “#JavaScript” (not case sensitive).

Tracking keywords
// index.js
const Twit = require('twit');
const T = new Twit({
consumer_key: process.env.APPLICATION_CONSUMER_KEY_HERE,
consumer_secret: process.env.APPLICATION_CONSUMER_SECRET_HERE,
access_token: process.env.ACCESS_TOKEN_HERE,
access_token_secret: process.env.ACCESS_TOKEN_SECRET_HERE
});

// start stream and track tweets
const stream = T.stream('statuses/filter', {track: '#JavaScript'});
// event handler
stream.on('tweet', tweet => {
// perform some action here
});

Responding to events

Now that we’ve been able to track keywords, we can now perform some magic with tweets that contain such keywords in our event handler function.

The Twitter API allows interacting with the platform as you would normally, you can create new tweets, like, retweet, reply, follow, delete and more. We’re going to be using only two functionalities which is the like and retweet.

// index.js
const Twit = require('twit');
const T = new Twit({
consumer_key: APPLICATION_CONSUMER_KEY_HERE,
consumer_secret: APPLICATION_CONSUMER_SECRET_HERE,
access_token: ACCESS_TOKEN_HERE,
access_token_secret: ACCESS_TOKEN_SECRET_HERE
});

// start stream and track tweets
const stream = T.stream('statuses/filter', {track: '#JavaScript'});
// use this to log errors from requests
function responseCallback (err, data, response) {
console.log(err);
}
// event handler
stream.on('tweet', tweet => {
// retweet
T.post('statuses/retweet/:id', {id: tweet.id_str}, responseCallback);
// like
T.post('favorites/create', {id: tweet.id_str}, responseCallback);
})

Retweet

To retweet, we simply post to the statuses/retweet/:id also passing in an object which contains the id of the tweet, the third argument is a callback function that gets called after a response is sent, though optional, it is still a good idea to get notified when an error comes in.

Like

To like a tweet, we send a post request to the favourites/create endpoint, also passing in the object with the id and an optional callback function.

Deployment
Now the bot is ready to be deployed, I use Heroku to deploy node apps so I’ll give a brief walkthrough below.

Firstly, you need to download the Heroku CLI tool, here’s the documentation. The tool requires git in order to deploy, there are other ways but deployment from git seems easier, here’s the documentation.

There’s a feature in Heroku where your app goes to sleep after some time of inactivity, this may be seen as a bug to some persons, see the fix here.

You can read more on the Twitter documentation to build larger apps, It has every information you need to know about.

Here is the source code in case you might be interested.

Source - CodeSource.io

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

discord.jsでDiscord Botを作ってみた

概要

discord.jsでbotを作ったので、それを書き留めときます。
インストールは、Ubuntu mateを想定したものを紹介致します。
他のOSを使っている方に関しては、調べてください

Node.js&npmのインストール方法

コマンドラインを開き下のコマンドを実行します。

sudo apt install -y nodejs npm

discord.jsのインストール方法

こちらもコマンドラインを開き下のコマンドを実行します。

sudo npm i discord.js

実際に書いていく

まずは、discord.jsを読み込むコードを書きます。

index.js
 const discord = require("discord.js");
 const client = new discord.Client;

簡単な返事機能のコードを書きます。

index.js
//続き
client.on("message", message =>{
 if (message.author.bot || !message.guild) return
 if(message.content === "こんにちは"){
    message.reply('さん、こんにちはー');
 }
});
client.login("Botのトークン")

Botのトークンのところには、Discord Developer Portal
にある、自分のBotの右にあるBotへ行きtokenの下にあるcopyを押して、index.jsのBotのトークンのところへ貼り付けます。

実際にBotを起動してみる

Botを起動させるためにはコマンドラインで下のコマンドを実行します。

node index.js

ちゃんとBotがONLINEになっていれば成功です。
色々な種類のコードを書いていく予定ですので、よろしくお願いします。

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

Node.jsのasync/awaitとPromiseを超ざっくり

現在関わっているプロジェクトでNode.jsを使って開発しています。
その過程でハマったこと、今回はasync/await・Promiseについての記事になります。

といってもNode.jsを使ったことのない人もいると思うので簡単にNodeの非同期処理について紹介してから、ハマったポイントについて書きたいと思います。
※100番煎じなので非同期の書き方に関してはあんまり詳しく書きません。
※arrow関数使ってません。説明省くので。
※間違いあれば教えていただければと思います。

目次

  1. Node.jsの非同期処理について
  2. 非同期処理の書き方
    2.1 コールバック(callback)関数
    2.2 Promise   ←この記事はココまで!
    2.3 asycn/await
    3. Promiseとasycn/awaitが一緒だと思ったらハマった件
  3. 参考

Nodeを使ったことがあって1~2についてわかってるよって方は3まで飛ばしてください。

1.Node.jsの非同期処理について

Nodeは非同期を多用するという特徴がある。

非同期処理とは

よそに処理を依頼したときに、その場で完了を待たない

処理のことらしい。
ここでは詳しい説明は割愛するが、Nodeは単一のスレッドしか持たないため(シングルスレッドアーキテクチャ)、いちいち同期処理をしていては他の処理の実行を妨げてしまう。

例)同期処理の場合

example_sync.js
//受け取った数字を出力する(同期)
function printNumSync(num) {
    console.log(num)
}
printNumSync(1)
printNumSync(2)
printNumSync(3)

結果

console
1
2
3

同期処理の場合は呼び出した順番通りに処理が実行されます。
→前の処理の完了を待ってから次の処理に進む。

・非同期処理の場合
次に同期、非同期、同期処理の順に呼び出してみます。

example_async.js
//受け取った数字を出力する(同期)
function printNumSync(num) {
    console.log(num)
}
//受け取った数字を1秒後に出力する(非同期)
function printNumAsync(num) {
    setTimeout(function () {
        console.log(num)
    }, 1000);
}
printNumSync(1)
printNumAsync(2)
printNumSync(3)

結果

console
1
3
2

先に述べたように非同期処理(例の場合はsetTimeoutが非同期)は、完了を待たずに次の処理へ進むため1の出力後、2つ目の処理の完了前に3が表示されます。

2.非同期処理の書き方

非同期処理にもいくつか書き方があります。

2.1.コールバック関数

「コールバック関数」とはわかりやすく言えば 終わったらコレやっといてコレに当たる部分。全然わかりやすくないですね。はい。
非同期処理には処理の完了を待たないという性質がありました。では非同期処理が終わったら次に何をすればいいのか。そんな悩みにこたえるのがコールバック関数。
非同期処理にコールバック関数を渡す1ことで、非同期処理の完了後にコールバック関数が実行される。

実は上の非同期の例でも使われていますがsetTimeoutを見てみましょう。

setTimeout
//1秒後に「callbackの中です」と表示
    setTimeout(function () {
        console.log('callbackの中です')
    }, 1000);

//↓↓わかりやすくするとこうなる

//callbackFuncに関数を格納
var callbackFunc = function () {
    console.log('callbackの中です')
}
//「setTimeout」が終わったら「callbackFunc」をやっといて
//→ ≒「1秒待つ」が終わったら「callbackFunc」をやっといて
setTimeout(callbackFunc, 1000);

2.2.Promise

以下Node.jsデザインパターンより引用

プロミスは簡単に言えば、「非同期処理の結果を表現するオブジェクト」です。~中略~
プロミスは完了される(fulfilled、成功)か棄却される(rejected、失敗)のいずれかで、このいずれかが起きることは保証されます。完了されてから後で棄却されたり、複数の結果が起きることはありません。

ようは非同期処理が成功したか、失敗したかを扱うオブジェクト。
例)使い方

example_promise.js
var fs = require('fs').promises;//ライブラリ読み込み
//1.ファイル読み込み(Promiseを返却する)
fs.readFile('./work.txt')
.then(function (content) {//2.ファイル読み込みが完了したら実行される
    console.log(content.toString())
})
.catch(function (error){//2.ファイル読み込みが失敗したら実行される
    console.log(error)
});

時間になってしまったので続きは後日書きます。

4.参考

Node.js 非同期処理・超入門 -- Promiseとasync/await
Node.jsデザインパターン


  1. Node(というかECMAScript)では関数はオブジェクトとみなされるため、変数に入れることができます。 

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

Node.jsのasync/awaitとPromiseを超ざっくり_前編

現在関わっているプロジェクトでNode.jsを使って開発しています。
その過程でハマったこと、今回はasync/await・Promiseについての記事になります。

といってもNode.jsを使ったことのない人もいると思うので簡単にNodeの非同期処理について紹介してから、ハマったポイントについて書きたいと思います。
※すみません、未完成なのでタイトル詐欺になってしまっています。
※100番煎じなので非同期の書き方に関してはあんまり詳しく書きません。
※arrow関数使ってません。説明省くので。
※間違いあれば教えていただければと思います。

目次

  1. Node.jsの非同期処理について
  2. 非同期処理の書き方
    2.1 コールバック(callback)関数
    2.2 Promise   ←この記事はココまで!
    2.3 asycn/await
    3. Promiseとasycn/awaitが一緒だと思ったらハマった件
  3. 参考

Nodeを使ったことがあって1~2についてわかってるよって方は3まで飛ばしてください。

1.Node.jsの非同期処理について

Nodeは非同期を多用するという特徴がある。

非同期処理とは

よそに処理を依頼したときに、その場で完了を待たない

処理のことらしい。
ここでは詳しい説明は割愛するが、Nodeは単一のスレッドしか持たないため(シングルスレッドアーキテクチャ)、いちいち同期処理をしていては他の処理の実行を妨げてしまう。

例)同期処理の場合

example_sync.js
//受け取った数字を出力する(同期)
function printNumSync(num) {
    console.log(num)
}
printNumSync(1)
printNumSync(2)
printNumSync(3)

結果

console
1
2
3

同期処理の場合は呼び出した順番通りに処理が実行されます。
→前の処理の完了を待ってから次の処理に進む。

・非同期処理の場合
次に同期、非同期、同期処理の順に呼び出してみます。

example_async.js
//受け取った数字を出力する(同期)
function printNumSync(num) {
    console.log(num)
}
//受け取った数字を1秒後に出力する(非同期)
function printNumAsync(num) {
    setTimeout(function () {
        console.log(num)
    }, 1000);
}
printNumSync(1)
printNumAsync(2)
printNumSync(3)

結果

console
1
3
2

先に述べたように非同期処理(例の場合はsetTimeoutが非同期)は、完了を待たずに次の処理へ進むため1の出力後、2つ目の処理の完了前に3が表示されます。

2.非同期処理の書き方

非同期処理にもいくつか書き方があります。

2.1.コールバック関数

「コールバック関数」とはわかりやすく言えば 終わったらコレやっといてコレに当たる部分。全然わかりやすくないですね。はい。
非同期処理には処理の完了を待たないという性質がありました。では非同期処理が終わったら次に何をすればいいのか。そんな悩みにこたえるのがコールバック関数。
非同期処理にコールバック関数を渡す1ことで、非同期処理の完了後にコールバック関数が実行される。

実は上の非同期の例でも使われていますがsetTimeoutを見てみましょう。

setTimeout
//1秒後に「callbackの中です」と表示
    setTimeout(function () {
        console.log('callbackの中です')
    }, 1000);

//↓↓わかりやすくするとこうなる

//callbackFuncに関数を格納
var callbackFunc = function () {
    console.log('callbackの中です')
}
//「setTimeout」が終わったら「callbackFunc」をやっといて
//→ ≒「1秒待つ」が終わったら「callbackFunc」をやっといて
setTimeout(callbackFunc, 1000);

2.2.Promise

以下Node.jsデザインパターンより引用

プロミスは簡単に言えば、「非同期処理の結果を表現するオブジェクト」です。~中略~
プロミスは完了される(fulfilled、成功)か棄却される(rejected、失敗)のいずれかで、このいずれかが起きることは保証されます。完了されてから後で棄却されたり、複数の結果が起きることはありません。

ようは非同期処理が成功したか、失敗したかを扱うオブジェクト。
例)使い方

example_promise.js
var fs = require('fs').promises;//ライブラリ読み込み
//1.ファイル読み込み(Promiseを返却する)
fs.readFile('./work.txt')
.then(function (content) {//2.ファイル読み込みが完了したら実行される
    console.log(content.toString())
})
.catch(function (error){//2.ファイル読み込みが失敗したら実行される
    console.log(error)
});

時間になってしまったので続きは後日書きます。

4.参考

Node.js 非同期処理・超入門 -- Promiseとasync/await
Node.jsデザインパターン


  1. Node(というかECMAScript)では関数はオブジェクトとみなされるため、変数に入れることができます。 

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

【待望リリース!】もう Lambda×RDS は怖くない!LambdaでRDSプロキシを徹底的に検証してみた 〜全てがサーバレスになる〜

本日の reinvent でのリリースで衝撃のアップデートがたくさん出ましたね。EKS on Fargate や SageMaker の大幅アップデートも魅力的ですが Lambda の常識をくつがえす RDS のプロキシ機能が登場しました ?

Lambda から RDS に対するアクセスはコネクション数の上限に達してしまうという理由からアンチパターンとされてきました。そのため、RDS をデータストアに選択する場合は ECS や EC2 上にアプリケーションをホストする事が一般的でした。Lambda の接続先 DB に RDS を選べるということはほとんどのWebアプリケーションがサーバレスで実行できるようになるので夢が広がります。

本記事では RDS プロキシを使った Lambda の構成を作ってコネクション数の挙動について検証してみました。
https://aws.amazon.com/blogs/compute/using-amazon-rds-proxy-with-aws-lambda/
※本記事は上記のブログを参考にしています。一部文脈で引用している箇所があります。

image

RDS プロキシは、データベースへの接続プールを維持します。これにより Lambda から RDS データベースへの多数の接続を管理できます。
Lambda 関数は、データベースインスタンスの代わりに RDS プロキシと通信します。スケーリング起動した Lambda 関数によって作成された多くの同時接続を保持するために必要な接続プーリングを処理します。これにより、Lambda アプリケーションは関数呼び出しごとに新しい接続を作成するのではなく、既存の接続を再利用できます。

従来はアイドル接続のクリーンアップと接続プールの管理を処理するコードを用意していたのではないでしょうか。これが不要になります。劇的な進化です。関数コードは、より簡潔でシンプルで、保守が容易になります。

現在はまだプレビュー版ですがこの機能を徹底検証していきましょう。

せっかく検証するのですから従来 ECS などで一般的に使ってたフレームワークを例に上げてみましょう。今回は NestJS を Lambda にデプロイして RDS と接続してみます。

lamba-rds-proxy.png

NestJS を Lambda にデプロイする

Serverless Framework を使用して NestJS アプリケーションを AWS Lambda にデプロイします。
こちらのサンプルソースが参考になりました。ほぼそのまま引用させていただきます。

Lambda がデプロイできたことを確認しておきましょう。

$ sls deploy
Serverless: Packaging service...
Serverless: Excluding development dependencies...
Serverless: Creating Stack...
~~~~~~~~~~~~~~~~~~~~~~~~~~ 省略 ~~~~~~~~~~~~~~~~~~~~~~~~~~
endpoints:
  ANY - https://djpjh5aklf.execute-api.us-east-1.amazonaws.com/dev/
  ANY - https://djpjh5aklf.execute-api.us-east-1.amazonaws.com/dev/{proxy+}
functions:
  index: serverless-nestjs-dev-index
layers:
  None
Serverless: Run the "serverless" command to setup monitoring, troubleshooting and testing.

生成されたエンドポイントにアクセスします。

image.png

これで準備ができました。まずは Hello World! と文字列を返す NestJS アプリケーションを Lambda をデプロイできました。これから MySQL と接続できるアプリケーションを作っていきます。開発過程は省略しますが、以下のリポジトリに完成品をアップロードしておきます。

完成品:https://github.com/daisuke-awaji/serverless-nestjs

参考:Nest(TypeScript)で遊んでみる 〜DB 連携編〜

タスクの CRUD 操作ができるアプリケーションを用意しました。
image.png

Secret Manger に RDS への接続情報を登録

事前に作成しておいたこちらの RDS を使用します。
image.png

まずは Secret Manger コンソールで RDS への接続情報を登録するようです。
image.png
image.png

シークレットができたら ARN をメモしておきましょう。あとで使います。

IAM

次に、RDS プロキシがこのシークレットを読み取ることができる IAM ロールを作成します。RDS プロキシはこのシークレットを使用して、データベースへの接続プールを維持します。IAM コンソールに移動して、新しいロールを作成します。 前の手順で作成したシークレットに secretsmanager アクセス許可を提供するポリシーを追加します 。

IAM ポリシー

image.png

image.png

IAM ロール

image.png
image.png

rds-get-secret-role という名前で IAM ロールを作成しました。

RDS Proxy

さて、ここからが本題です。
RDS のコンソールを開くと Proxies の項目があります。Lambda の接続先をこのプロキシに向けることでコネクションプールをうまく使いまわしてくれるようです。

image.png

作成してみましょう。先ほど作成した IAM ロールや RDS を入力します。
image.png
image.png
image.png

作成まではしばらく時間がかかるようです。
image.png

Lambda の向き先を RDS から RDS Proxy に切り替える

RDS インスタンスに対して直接接続する代わりに、RDS プロキシに接続します。これを行うには、2 つのセキュリティオプションがあります。IAM 認証を使用するか、Secrets Manager に保存されているネイティブのデータベース認証情報を使用できます。IAM 認証は、機能コードに認証情報を埋め込む必要がないため、推奨されているようです。この記事では、Secrets Manager で以前に作成したデータベース資格情報を使用します。

DBに接続するアプリケーションの設定を変更してデプロイしましょう。

db.config.ts
import { TypeOrmModuleOptions } from "@nestjs/typeorm";
import { TaskEntity } from "./tasks/entities/task.entity";

export const dbConfig: TypeOrmModuleOptions = {
  type: "mysql",
  host: "rds-proxy.proxy-ch39q0fyjmuq.us-east-1.rds.amazonaws.com", // <-- DBの向き先をProxyに切り替える
  port: 3306,
  username: "user",
  password: "password",
  database: "test_db",
  entities: [TaskEntity],
  synchronize: false
};
$ npm run build && sls deploy

まだ Serverless では RDS Proxy をサポートしていないようでしたので Lambda のコンソールから設定してみます。セキュリティグループやサブネットなどは適宜各自の環境に合わせて作成してください。

image.png
image.png

RDS Proxy 経由でも無事に接続できました ?
※事前に DB にはテストデータを入れてあります
image.png

準備に使用した SQL

CREATE TABLE `tasks` (
  `id` int(36) unsigned NOT NULL AUTO_INCREMENT,
  `overview` varchar(256) DEFAULT NULL,
  `priority` int(11) DEFAULT NULL,
  `deadline` date DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=94000001 DEFAULT CHARSET=utf8mb4;

INSERT INTO `tasks` (`id`, `overview`, `priority`, `deadline`)
VALUES
    (1, '掃除', 0, '2020-11-11'),
    (2, '洗濯', 2, '2020-12-03'),
    (3, '買い物', 0, '2020-11-28');

負荷テストを実行してみる

コネクション数が Lambda のスケールに合わせて増え続けるような挙動を取らないか確認してみましょう。

今回は負荷のために Artillary を使用します。
yaml ファイルでシナリオを記述して実行する Nodejs 製の負荷テストツールです。

Artillary のインストール

$ npm install -g artillery

実行

yaml ファイルを記述しなくてもワンラインで実行できる手軽さも魅力的なツールで愛用しています。
以下のようなコマンドで簡単に実行できます。30 ユーザが 300 回リクエストを送るといった内容です。

$ artillery quick --count 300 -n 30 https://djpjh5aklf.execute-api.us-east-1.amazonaws.com/dev/tasks

実行された Lambda を確認します。Invocations が 9000 回を記録しています。

image.png

一方で RDS のコネクション数はなんと 43 になっていました。すごい。
ちなみに MySQL の現在のコネクション数は show status like 'Threads_connected' で確認できます。

負荷テスト開始前 最大リクエスト時
18 43

RDS Proxy を使わない場合はどうなるか

アプリケーションの向き先を RDS 本体に直接接続するように変更してみます。
この状態でもう一度負荷テストを行うとどうなるでしょうか。

import { TypeOrmModuleOptions } from '@nestjs/typeorm';
import { TaskEntity } from './tasks/entities/task.entity';

export const dbConfig: TypeOrmModuleOptions = {
  type: 'mysql',
  host: 'aurora.cluster-ch39q0fyjmuq.us-east-1.rds.amazonaws.com', // <-- RDS 本体に向ける
  port: 3306,
  username: 'user',
  password: 'password',
  database: 'test_db',
  entities: [TaskEntity],
  synchronize: false,
};

実行

コネクション数が 124 まで膨れ上がってしまいました。
やはりプロダクションロードで普通に Lambda+RDS の組み合わせはやってはいけないアンチパターンになりそうですね。RDS Proxy の威力を改めて感じることができました。

$ artillery quick --count 300 -n 10 https://djpjh5aklf.execute-api.us-east-1.amazonaws.com/dev/tasks
負荷テスト開始前 最大リクエスト時
18 124

まとめ

RDS プロキシを使用することで、データベースへの接続プールを保持することが確認できました。これで API やユーザリクエストを受けるようなワークロードでも Lambda から RDS への多数の接続を管理できます。とてつもなく強力なアップデートを体感できました。今後追加で RDS Proxy を使用する場合と使用しない場合とで、レスポンスタイムに違いが出てくるのかなど細かなところまで検証したいと思います。

クラウドはいよいよここまで成長してきました。
次は RDS がインスタンスを意識することなく水平にスケールするようになるのでしょうか。
その時は完全にサーバレスなクラウドが完成しますね。待ち遠しいです。

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