WESEEK Tech Blog

WESEEK のエンジニアブログです

SaaS運用での大障害の思い出と対策の共有

この記事は、2021/5/27 に行われた WESEEK Tech Conference の内容です。

弊社が開発するSaaS型社内wiki・ナレッジベースサービス GROWI.cloud で、過去に発生した障害をピックアップしてご紹介します。

思い出すのもツラいところがありますが、発生した障害の中でも比較的大きいものを紹介しておりますので、 当時感じたツラさを感じてください。(^^;

記事内容と見どころ

障害に関連するシステム情報をはじめ、原因と対策は技術・運営の観点を紹介してますので、次の点が役に立つかもしれません。

  • システム可用性の向上に対するヒント
  • 同じプロダクトへの対策
  • 似た構成への対策の参考
  • 障害時の運営方針決定の参考

目次

  • 記事内容と見どころ
  • 目次
  • 1章. 大障害は準備が整ったと思った時にやってくる (障害Lv 1)
    • どのようなサービスなのか?
    • どのような構成なのか?
    • どのように監視をしているのか?
    • ステータス確認方法は?
    • アラート発生時はどうするのか?
    • 運用の目標は?
    • 運用準備が整ったと思ったら発生した大障害
    • まずは状況確認
    • 暫定復旧方法
    • 大障害1のまとめと恒久対策
  • 2章. そのリソースが足りなくなるとは思わなかった (障害Lv 2)
    • システムリソース管理
    • GROWI app の増加に伴い増えるリソース
    • Kubernetes ノード数を自動で増やす仕組み
    • ノードのオートスケールができるようになったが、あるリソースの枯渇によって発生した大障害
    • まずは状況確認
    • 検討・暫定対応したこと
    • 大障害2のまとめと恒久対策
  • 3章. WEBアプリケーションの操作が失敗するのは内部が原因とは限らない (障害Lv 1)
    • GROWI.cloud という SaaS
    • オーダー処理により利用者の操作を自動でクラスタへ反映できる構成で、内部が原因ではなく失敗を起こした大障害
    • まずは状況確認
    • 検討・暫定対応したこと
    • 大障害3のまとめと恒久対策
  • 障害を振り返って思うこと
  • 予告
  • 著者プロフィール
  • 株式会社WESEEKについて
    • GROWI
    • GROWI.cloud
    • WESEEK Tech Conference
  • 一緒に働く仲間を募集しています
続きを読む

もう知らずにはいられないGitOpsを ArgoCDで学ぶ!

この記事は、 2021/5/13 に行われた WESEEK Tech Conference の内容です。

ArgoCDを使用したGitOpsについてお話ししました。実際のArgoCDの設定例などを紹介しながら、デモ形式でのGitOps構築までの道のりを紹介しました。

目次

  • 目次
  • GitOpsとは
  • GitOpsを導入していない場合の課題
  • ArgoCDでGitOpsをしてみた理由
    • 感情的動機
    • ちょっと真面目な理由
  • GitOps を実現できるツール
  • ArgoCD のアーキテクチャ
  • ArgoCD の主なCRD
    • Application
    • AppProject
  • SSO での RBAC
    • サポート方式
    • ArgoCD における Dex の役割
    • 設定例(helm values)
    • ロールの作成・割り当て
  • サポートしているマニフェスト定義ツール
    • 4つの標準ツール
    • サポートされていないマニフェスト定義ツールを使用したい場合
  • マニフェストの依存関係の考慮
    • なぜ依存関係を考慮する必要があるのか
    • ArgoCD の Sync にはいくつかのフェーズがあり依存関係の考慮ができる
    • Resource Hooks
    • Waves
  • まとめ
    • ArgoCD
    • 実務に取り入れるための課題
  • 著者プロフィール
  • 株式会社WESEEKについて
    • GROWI
    • GROWI.cloud
    • WESEEK Tech Conference
    • 一緒に働く仲間を募集しています
続きを読む

コスト7割減!Kubernetes本番サービス環境の運用ノウハウ

この記事は、 2021/4/22 に行われた WESEEK Tech Conference の内容です。

Google Compute Engine のプリエンプティブルインスタンスを利用したクラウド利用料を節約するお話をしました。 Google Cloud Platform 上で稼働している GROWI.cloud を実例に上げつつ、節約額、稼働率の運用実績についてもご紹介しました。

目次

開始早々タイトル訂正

f:id:skomma:20210427155111j:plain

イベント開催を告知した時点では

「コスト8割減!Kubernetes本番サービス環境の運用ノウハウ」

というタイトル名でしたが、本資料を作成し、改めて価格を比較してみたところ以下のタイトルが適切であることが判明したため、タイトルを訂正しました(笑)

「コスト一部8割7割減!Kubernetes本番サービス環境の運用ノウハウ」

GROWI.cloud とは?

GROWI.cloud の利用状況

2021/04/14 現在、以下のような利用状況です。

  • 総 GROWI 数: 1022
    • うち約 20 % が課金プラン利用の GROWI
  • GROWI が載っているノード数: 32
    • うちプリエンプティブルノード数は 27 台

利用技術

アプリ

f:id:skomma:20210427163157j:plain

インフラ

f:id:skomma:20210427163215j:plain

なぜ GKE を利用しているか

以下の 4 点から、GROWI.cloud では GKE を採用しました。

  1. 利用実績があった
    • GROWI.cloud 開始前の別プロジェクトで利用していた
    • そのプロジェクトでは、アプリ・ミドルを載せるだけのシンプルな構成だった
    • GROWI.cloud では、もっといろいろな機能にチャレンジしている
  2. master がマネージドである
    • 自前で master を構築・運用するのは大変
    • ただ、これが原因でなかなかトラブルが解消されなかった例も…
      • トラブルの詳細は、5/27 の本イベントで話される予定です!
  3. ノードの増減が簡単である
    • GUI/CLI で簡単に増減できる
    • OS、kubernetes node として動かすためのディスクイメージが用意されているため、そこを考える必要がなくなる
    • オートスケールもできる
  4. ノードプールという単位で、ノードの構成が変えられる
    • これにより、ノード/プリエンプティブルの有効無効の切り替え、オートスケールの有無の切り替えなど、ノードの設定を塊ごとに管理できる
    • 1 つの GKE クラスタに様々な設定のノードを混在させることが可能となる

コストを下げたい!

上述の通り、GKE はクラウド上で Kubernetes クラスタを稼働させるのに、いろいろな便利な機能が付加されています。
しかし、GROWI.cloud のように SaaS 型で利用者が増えるたびにノードを増やす必要がある場合、料金はうなぎ上りに増えていきます…

そこで、どうにか安く利用する方法がないか模索したところ、今回ご紹介するプリエンプティブルノーにたどり着きました。

プリエンプティブルノードとは?

cloud.google.com

  • Google Compute Engine のノードの 1 種類
  • インスタンスタイプで、通常のノードと比較して約 70 %割引で使える
  • ただし、最大 24 時間しか連続で稼働できない
    • 「最大」なので、24 時間以内の停止ももちろんありうる
    • AWS だと EC2 スポットインスタンスが同じ概念の位置する
    • が、利用者同士でインスタンスの売買ができるほど賢くはない

プリエンプティブルノードの使いどころ

  • 突然停止しても問題ないワークロードが載っているノード

これらのアプリを、プリエンプティブルノード上で稼働させると利用料を抑えることができます。

GROWI.cloud クラスタ内のノード構成

大きく以下の括りでノードを構成しています。

  1. growi.cloud のシステムコンポーネントが載るノード
    • growi.cloud アプリ
    • GROWI を作るための workflow engine(brigade.js)
    • 管理用アプリ
  2. GROWI が載るノード
  3. GROWI の DB をバックアップするスクリプトが動くノード
    1. のうち特に分離しておきたいコンポーネントが載るノード
    2. メモリを極端に必要とするミドルウェアなど

上記のうち、2./3. についてプリエンプティブルノードを利用しています。

ノードの振り分け方

Kubernetes 上にある以下の機能を利用して実現しています。

  1. Node Affinity
  2. Taint/Toleration

Node Affinity

  • Kubernetes が Pod をスケジュールする際に、スケジュールされる先のノードを指定する機能
  • Pod 上の設定として記載する
  • ノードはノード名で指定するのではなく、key/value のラベルで設定する
  • 具体的な YAML 例(app=growi-app というラベルが指定されているノードに必ずスケジュールする)
spec:
  affinity:
    nodeAffinity:
      requiredDuringSchedulingIgnoredDuringExecution:
        nodeSelectorTerms:
        - matchExpressions:
          - key: app
            operator: In
            values:
            - growi-app

※参考 kubernetes.io

Taint/Toleration

  • Taint
    • 許容(tolerate)できない Pod のスケジュール/実行を禁止する機能
    • Node 側に、key/value/effect を設定する
    • 具体的な YAML
taints:
- effect: NoExecute
  key: app
  value: growi-app
  • Toleration
    • Taint が設定されたノードへ Pod をスケジュールするために、許容する機能
    • Pod 側に設定する
    • 具体的な YAML
tolerations:
- key: app
  operator: Equals
  value: growi-app
  effect: NoExecute

※参考 kubernetes.io

GROWI 作成時のフロー

フリープラン利用者が GROWI を起動するときのフローを例に、実際の動きをご紹介します。

f:id:skomma:20210427163524j:plain

まず、GROWI.cloud UI 上でユーザが GROWI を作成すると、

f:id:skomma:20210427163530j:plain

GROWI.cloud アプリからワークフローエンジンである brigade.js へ REST で作成リクエストが飛びます。

f:id:skomma:20210427163539j:plain

brigade.js では helm で GROWI マニフェストのデプロイを試みます。

f:id:skomma:20210427163552j:plain

helm によりデプロイされたマニフェストを元に、Kubernetes(kube-scheduler) がスケジューリング先ノードを決定します。

f:id:skomma:20210427163606j:plain

まず、Node Affinity で指定されたノードのラベルを比較し、真ん中のノードがスケジュール先候補として選ばれます。
フリープランはプリエンプティブルノードに配置されるプランのため、cloud.google.com/gke-preemptible=true をラベルとして持っているノードのみが選択されるようになっています。

f:id:skomma:20210427163618j:plain

その後、Node の taints と Pod 内の tolerations を比較し、スケジュール先が決定されます。

f:id:skomma:20210427163625j:plain

晴れて、GROWI.cloud 利用者が GROWI を利用できるようになります!

f:id:skomma:20210427170121j:plain

同様のフローで、通常ノードに配置されるべきスタンダートプラン利用者の GROWI は、通常のノードにスケジュールされます。

プリエンプティブルノードの罠と現時点での対策

コストが下がってみんなハッピー、というわけではなく、それなりに運用でカバーしないといけない点が出てきました。

必ず 24 時間動くとは限らない

公式ドキュメントに記載されてますね、はい…

ライト・バリュープランについては、以下のように対策を行いました。

  • 1 度ノードの再起動がかかると、5~10分程度 GROWI がダウンする
    • フリープランは SLO を設定していないため、問題ない
  • ライト・バリュープランに関しては、プリエンプティブルノードを利用していて安価だが、SLO 付き
    • いつ落ちるかわからないノードだと SLO まで稼働率を上げられない…
    • -> GROWI Pod を冗長化して対応した
    • 冗長化した後、稼働率の向上に成功

常に在庫があるとは限らない

これまでの経験上、1年に1回程度は発生するようです。

在庫不足に陥ったときは、日本近辺のリージョン(オーストラリア、シンガポールなど)でもプリエンプティブルノードが起動しなかったことを確認しました。
ML などで大量にノードを奪ってく人がいるんでしょうか…

そのようなケースについて、以下の対策を行いました。

  • affinity 内の nodeSelectorTerms 内に、在庫が足りなかったときのための設定を追加しておく
    • nodeSelectorTerms は array で指定でき、OR 判定になる
  • 実際に、プリエンプティブルノードが異常に DOWN したら、人手で適合するラベル/taint をつけた緊急配備用のノードプールを作成する
    • 新たに作成したノードに GROWI Pod が載る
    • プリエンプティブルノードを試しに起動してみて、起動できそうだったら緊急配備用のノードを drain する

f:id:skomma:20210427163754j:plain

通常スケジュール用の nodeSelectorTerm に加え、緊急配備用の nodeSelectorTerm を予め設定しています。

時々腐ったノードが出てくることがある(再現性不明)

載っている GROWI Pod が長期間(30分以上) healthy にならない事象が出るノードが時々出てくることがあります。
この事象に対しては、まだ効果的な対策が打てていない現状です。

再現性不明のため、アラートで長期間 down している GROWI が出てきたら、アラート発生時にエンジニアが手動対応しています。
具体的には、問題のあるノードを drain して、都度載っている GROWI を別ノードに移動しています。

ここも将来的には、ノード上のリソース利用状況を見つつ、自動化していきたい分野です。

プリエンプティブルノードの実績値

費用比較

割引適用なし

  • GROWI プリエンプティブルノード数: 28(2021/04/21 現在)
  • 通常ノードの場合: USD 2,902.86 per 1 month
  • プリエンプティブルノードの場合: USD 870.86 per 1 month

通常ノードを利用している分を単純にプリエンプティブルノードに置き換えると、約 70% の削減が可能です。

3年継続割引利用時

  • 通常ノードの場合: USD 1,306.29 per 1 month
  • プリエンプティブルノードの場合: USD 870.86 per 1 month

3年継続割引利用の通常ノードをプリエンプティブルノードに置き換えると、約 33% の削減が可能です。

GROWI.cloud 全体

  • 通常ノード(3年確約利用適用)だけで構成した場合
    • USD 2,303.26 per 1 month
  • GROWI が載るノードに、プリエンプティブルノードを利用した場合
    • USD 1,867.84 per 1 month

GROWI.cloud クラスタで利用しているノード全体のうち、プリエンプティブルを許容する GROWI が載るノードを プリエンプティブルノードに置き換えると、約 20% の削減ができました。

稼働率

実際の稼働率

  • 対象期間: 2021/04/14 ~ 2021/04/21
    • GROWI.cloud 全体: 99.53%
    • 通常ノード上の GROWI: 99.97%

プリエンプティブルノードを利用した場合でも、Pod を冗長化すると稼働率を大幅に向上させることができました!

プリエンプティブルノードの実際の再起動回数

  • 対象期間: 2021/04/14 ~ 2021/04/21
    • プリエンプティブルノード数: 28
    • 平均再起動回数: 8.64 回 (7回~12回)
    • 稼働時間ワースト3: 00:06:47, 00:08:21, 00:12:56

平均を取ると、ほぼ 1 日 1~2 回再起動するノードがほとんどでした。
しかし、稼働時間のワーストを取ると、やはり稼働時間が保証されていないノードであることを示すように、時々 10 分前後で再起動されるノードもいました。

まとめ

プリエンプティブルノードの活用方法

  • プリエンプティブルノードとは?
    • 最大 24 時間稼働するノード
      • 24 時間以内に再起動することもありうる
    • 以下のような用途向け
  • GROWI.cloud では以下の用途で活用している
    • バックアップスクリプトを動かすため
    • フリー/ライト/バリュープランの GROWI を動かすため
      • ライト/バリュープランの GROWI については Pod を冗長化することで稼働率が向上しました

今後

  • GROWI の稼働率向上(案)
    • プリエンプティブルノードでもノード再起動前に意図的に Pod or ノードの再起動を自動的に実施し、稼働率を向上させる
    • プリエンプティブルノード再起動時にノード上に載っている GROWI の起動タイミングを自動的にずらす
      • GROWI は起動時に一番 CPU を使うため、同一ノードに乗る GROWI 数を減らすことで、起動時間短縮を狙う
  • プリエンプティブルノード再起動時の 503 画面でユーザに原因をわかりやすくする
    • 現状 GROWI.cloud 上の GROWI 詳細画面では出るが、GROWI アクセスだけではわからないので、ユーザに不親切な状況です…

著者プロフィール

今間 俊介

株式会社WESEEK / バックエンドエンジニア

ISP に 2 年弱勤務した後、2013 年 3 月に WESEEK へ join。
現在は、GROWI.cloud などプロジェクト問わず、Kubernetes を中心としたインフラ/アプリの設計・構築・運用に携わる。
最近の業務時間外は、子供の面倒見、家事手伝い。

株式会社WESEEKについて

株式会社WESEEKは、システム開発のプロフェッショナル集団です。

【現在の主な事業】

  1. 通信大手企業の業務フロー自動化プロジェクト
  2. ソーシャルゲームの受託開発
  3. 自社発オープンソースプロダクト「GROWI」「GROWI.cloud」の開発

GROWI

GROWIは、Markdown記法でページを記述できるオープンソースWikiシステムです。

【主な特徴】

  • テキストも図表もどんどん書ける、強力な編集機能
  • チーム拡大に迅速に対応できる管理者向け機能を提供
  • 充実した機能・サポートでエンタープライズにも対応

GROWI.cloud

GROWI.cloudOSSのGROWIを専門的知識がなくても簡単に運用・管理できる、法人・個人向けの商用サービスです。

大手SierISPから中小企業・大学などの教育機関まで幅広くご利用いただき、さらに個人や大学サークルでもご利用いただいています。

【導入事例記事】
インターネットマルチフィード株式会社様 growi.cloud

株式会社HIKKY(VR法人HIKKY)様 growi.cloud

WESEEK Tech Conference

WESEEK Tech Conferenceは、株式会社WESEEKが主催するエンジニア向けの勉強会です。
月に2回ほど、WESEEKに所属するエンジニアが様々なテーマで発表を行う予定です。

次回は、4/22(木) 19:00~20:00に開催予定です。 ArgoCDを使ったGitOpsの利用について、インターンでエンジニアをしている岡がお話します。

現在、connpassやTECH PLAYで参加受付中です。皆様のご参加をお待ちしております! weseek.connpass.com TECH PLAYはこちらから

一緒に働く仲間を募集しています

東京の高田馬場オフィス、大分にある別府サテライトオフィスにてエンジニアを募集しております。
中途採用だけではなく、インターンシップも積極的に受け入れています!

詳しい募集要項は、弊社HPの採用ページからご確認ください。

Raspberry Piで、コミュニケーションシンクロ率を上げる

この記事は、 2021/4/8 に行われた WESEEK Tech Conference の内容です。

Raspberry PiArduino を使ってデバイスを作成し、社内環境の改善をするお話をしました。今回のお話は、ハードウェアについてが多めです。時間の都合でソフトウェアの話まで広げられなかったので、それはまた後日記載したいと思います。

目次

  • 目次
  • WESEEK の 日常と課題
  • 理想のデバイス探し
  • バイス製作の工房探し
    • mokumoku 会とは
    • WESEEK の工房化
  • 賢いカメラ neighbo <ネイボ> 誕生
  • バイス製作の試行錯誤
    • 試行錯誤1: 試作・設計
      • 試行錯誤1: 試作・設計 の気づき
    • 試行錯誤2: チルトユニットの製作
      • 試行錯誤2: チルトユニットの製作 の気づき
    • その他の試行錯誤
    • neighbo の成長
  • 導入と効果
  • かかった費用
    • 気づき
  • まとめ
    • neighbo の展望
  • 著者プロフィール
  • 株式会社WESEEKについて
    • GROWI
    • GROWI.cloud
    • WESEEK Tech Conference
  • 一緒に働く仲間を募集しています
続きを読む

JavaScript/Node.jsエンジニア必見!MongoDB+Mongoose利用時のデータマイグレーション

こちらは 「JavaScript/node.jsエンジニア必見!MongoDB+Mongoose利用時のデータマイグレーション」からの転載です。


古くは「LAMP」に代表されるような技術スタック、JavaScript 界隈では「MEAN」スタックという用語もあるくらい、JavaScript(Node.js) との組み合わせでは MongoDB がよく使われるようです。

NoSQL ではスキーマレスのためデータマイグレーションを気にせず開発することも多いわけですが、例えば一度バージョン1をリリースして既にユーザーがついているシステムを更改するような場合、後方互換を保つためにもデータマイグレーション機構は重要です。

本記事では、Node.js 環境で MongoDB および Mongoose を利用しているようなシステムにデータマイグレーション機構を導入する Tips を紹介します。

データマイグレーション機構とは?

有名処で言えば、Ruby on RailsActiveRecord に搭載されている機能を想像するとよいでしょう。そこには以下のような特徴があります。

  • データベーススキーマを時系列に進化(またはロールバック)させることができる
    • Rails では rake タスクとして実行できる
  • migration ファイルを決まったスキームで実装できる
  • ActiveRecord のモデルを利用することができる

Node.js の世界でのデータマイグレーション事情

Ruby の世界では Rails, ActiveRecord というデファクトスタンダードが確立されていますが、Node.js の世界でのデファクトフレームワークは残念ながら Express であり、オールインワンのフレームワークでもフルスタックのフレームワークでもありません。

ORM/ODM にしても、ORM/ODM のデファクトは、RDBMS なら Sequelize、MongoDB なら Mongoose で固まりつつありますが、それぞれデータマイグレーション機構を備えているわけではなく、データマイグレーション機構が欲しい場合に「すぐ使える」状況ではないのが実情です。

そのため、ライブラリ選定から進める必要があります。

ライブラリ選定

npm でそれっぽいものを検索したところ、以下を見つけました。

npm パッケージ ライセンス npm weekly downloads
2018.10 時点
GitHub stars 直近1ヶ月でのコミット数
2018.10 時点
migrate-mongoose MIT 1405 86 0
mongoose-migrate (none) 506 63 0
migrate-mongo MIT 3515 97 25
db-migrate MIT 28399 1480 0
db-migrate-mongodb MIT 2604 17 2

weekly downloads と GitHub stars を見ると db-migrate がダントツなのですが、こちらは db-migrate-mongodb の親プロジェクトで、データベースの差異を吸収するような抽象化がなされています(子プロジェクトとして、MySQL 版や PostgreSQL 版が存在する)。

こと db-migrate-mongodb のみの数字で言えば、migrate-mongo が若干優勢といった感じです。今回は、直近コミット数で現在も開発が進んでいると見受けられる migrate-mongo を採用することにします。

Let's Try ~npm script の準備~

Rails の rake タスクのように使えるとなにかと便利なので、まず npm script を準備しましょう。

    "migrate": "npm run migrate:up",
    "migrate:create": "migrate-mongo create -f config/migrate.js -- ",
    "migrate:status": "migrate-mongo status -f config/migrate.js",
    "migrate:up": "migrate-mongo up -f config/migrate.js",
    "migrate:down": "migrate-mongo down -f config/migrate.js",

-f オプションで設定しているのはコンフィグファイルです。MongoDB への接続設定等が入ると思います。環境変数から接続先を取りたい場合は以下のようなファイルにするとよいでしょう。

/**
 * Configuration file for migrate-mongo
 * @see https://github.com/seppevs/migrate-mongo
 *
 * @author Yuki Takei <yuki@weseek.co.jp>
 */

function getMongoUri(env) {
  return env.MONGODB_URI ||
    env.MONGO_URI ||
    ((env.NODE_ENV === 'test') ? 'mongodb://localhost/myprj_test' : 'mongodb://localhost/myprj');
}

const mongoUri = getMongoUri(process.env);
const match = mongoUri.match(/^(.+)\/([^/]+)$/);
module.exports = {
  mongoUri,
  mongodb: {
    url: match[1],
    databaseName: match[2],
    options: {
      useNewUrlParser: true, // removes a deprecation warning when connecting
    },
  }
};

Let's Try ~初めての migration ファイル作成~

npm script を叩きましょう。

npm run migrate:create my-first-migration

Let's Try ~migration の実装1~

まずは Mongoose 対応の migration ファイルの書き方です。

my-first-migration.js

'use strict';

require('module-alias/register');
const logger = require('@alias/logger')('growi:migrate:my-first-migration');

const mongoose = require('mongoose');
const config = require('@root/config/migrate');

module.exports = {

  async up(db) {
    logger.info('Apply migration');
    mongoose.connect(config.mongoUri, config.mongodb.options);

    const User = require('@server/models/user');

    ...

    logger.info('Migration has successfully applied');
  },

  async down(db) {
    logger.info('Undo migration');
    mongoose.connect(config.mongoUri, config.mongodb.options);

    const User = require('@server/models/user');

    ...

    logger.info('Migration has successfully undoed');
  }

};

@alias@root@server 等は、module-alias パッケージで設定したエイリアスパスです。ご自身の環境に合わせて読み替えてください。

コード解説

up function が migration の適用のためのコード、 down function がロールバックのためのコードになります。それぞれ ES6 で記述可能な async function として定義しています。

User インスタンスは、 mongoose.model() で作成されるモデルインスタンスという想定です。

それぞれの function の2行目の mongoose.connect() を呼ばなければ Mongoose のモデルインスタンスのメソッドを呼んでも MongoDB にアクセスされませんので注意してください。

Let's Try ~migration の実装2~

次に、 MongoDB Driver を直に利用するような Example を紹介します。例えば既存の Mongoose モデルを廃棄するような場合です。

abolish-myrelations.js

'use strict';

require('module-alias/register');
const logger = require('@alias/logger')('growi:migrate:abolish-myrelations');

const mongoose = require('mongoose');
const config = require('@root/config/migrate');

/**
 * BEFORE
 *   - 'myrelations' collection exists (related to models/myrelations.js)
 *     - schema:
 *       {
 *         "_id" : ObjectId("5bc9de4d745e137e0424ed89"),
 *         "group" : ObjectId("5b028f13c1f7ba2e58d2fd21"),
 *         "user" : ObjectId("5b07e6e6929bad5d3cce9995"),
 *         "__v" : 0
 *       }
 * AFTER
 *   - 'myrelations' collection is dropped and models/myrelations.js is removed
 */
module.exports = {

  async up(db) {
    logger.info('Apply migration');
    mongoose.connect(config.mongoUri, config.mongodb.options);

    const User = require('@server/models/user')();
    const UserGroup = require('@server/models/user-group')();

    // retrieve all documents from 'myrelations'
    const relations = await db.collection('myrelations').find().toArray();

    for (let relation of relations) {
      const user = await User.findOne({ _id: relation.user });
      const group = await UserGroup.findOne({ _id: relation.group });

      ...

      await user.save();
      await group.save();
    }

    // drop collection
    await db.collection('myrelations').drop();

    logger.info('Migration has successfully applied');
  },

  async down(db) {
    logger.info('Undo migration');
    mongoose.connect(config.mongoUri, config.mongodb.options);

    const User = require('@server/models/user')();
    const UserGroup = require('@server/models/user-group')();

    const insertDocs = ...;
    ...

    await db.collection('myrelations').insertMany(insertDocs);

    logger.info('Migration has successfully undoed');
  }

};

コード解説

上記コードでは、 myrelations という関連テーブル風のコレクションで管理していた情報を破棄する、というシナリオを想定しました。myrelations コレクションは当初は Mongoose によって管理されていたモデルに対応する形で生成されたはずですが、モデルを廃止する場合は当然モデルクラス・モジュールも削除されますので、スキーマファイルを require してアクセスすることができません。そのため、Raw な MongoDB Driver API を用いてデータマイグレーションを行う必要があります。

まとめ

まだまだ発展途上の JavaScript、Node.js の世界では、MongoDB を利用しながらデータマイグレーション機構をプロジェクトに導入できるデファクトスタンダードがまだありません。しかしながらコミュニティは精力的に npm ライブラリを開発・追加し続けていますので、今回のように慎重に選定を行い、時にはいくつかの実装を実際に試しながら導入することで、他の枯れた言語・エコシステムに劣らないプロジェクト構成を実現できます。

今回の Tips が、JavaScript と MongoDB を好むプロジェクトたちの一助となれば幸いです。

Dev in Container on WSL2 でボリュームのパフォーマンスを比べてみる

f:id:yukitakei:20200613040453j:plain

こんにちは。武井です。

2020年5月末に待望の Windows 10 May 2020 Update が公開され、WESEEK, Inc. で開発中の GROWI でも WSL2 と Docker Desktop を利用して Dev in Container を実現した新しい開発スタートアップ を採用しました。

本日のエントリーでは、その開発時に利用するストレージ(Docker volume)の種類によってパフォーマンスがどれくらい変わるのかを実験した際の結果を紹介します。

続きを読む

Raspberry Piと温度・湿度・気圧センサーと天気予報APIでペット環境を見守る

こんにちは、 takayuki です。

最近、知り合いのインコ部屋にRaspberry Piと温度・湿度・気圧センサーを設置し、天気予報APIも使用して、お留守番中の環境を確認できるようにしました。

動機

インコはもともと暖かく湿気のある地域に生息しており、寒さに弱い生き物です。
日中に家を空けていると、インコ部屋の温度・湿度が保たれているか、心配になります。
以前、部屋の温度を数日低くしてしまったことで、インコが体調を崩してしまったことがありました。

現在は、家電メーカーから発売されているペットカメラを設置していますが、これは微妙にかゆいところに手が届かないと感じていました。
ペットカメラには、標準カメラ、赤外線カメラ、温度センサー、動作センサー、音センサーが搭載されています。

標準カメラ、赤外線カメラは、外出先でも様子が見られるため、非常に便利です。
温度センサーは、ペットカメラの画角の関係で距離を取る必要があり、インコ部屋の外に置いています ※。
そのため、計測される温度は人がいるところの室温で、インコが過ごしているケージ内の室温はわかりません。また、温度センサーしかないため、湿度はわかりません。

※インコ部屋はアクリルケージで囲い、ペット用のヒーターで暖めています。

そこで今回、温度・湿度・気圧センサーをインコ部屋の中に設置し、環境を計測できるようにしました。

構成

今回構築した構成は下記の通りです。

f:id:Aqutam:20200303020524p:plain

ハードウェア

# 品名 数量
1 Raspberry Pi 4 Mlodel B 1
2 OKdoブラック2ピース構造スライドケース 1
3 TOSHIBA microSDHC EXCERIA 16GB 100MB/s 1
4 BME280 温湿度・気圧センサ 2
5 タカチ SWプラケース SW-50B 2
6 6極4芯モジュラーケーブル 2m 3
7 モジュラーローゼット MJ-4S-MG 1
8 モジュラーソケット6極4芯 TM5RJ3-64 3

ソフトウェア

ネットワーク

  • インコの家は、 LTE を経由してインターネットに接続しています。
  • インコの家と自宅は相互に通信できるよう、 IPsec で拠点間接続しています。

センサーの製作

温度・湿度・気圧センサーは、秋月電子通商で販売されている BME280 を使用しています。このセンサーは、I2Cで制御できます。

Raspberry Pi と BME280 を下記のように配線していきます。

f:id:Aqutam:20200309234805p:plain

BME280は、ジャンパを設定することで、 0x760x77 の2つのI2Cアドレスを使用できます。
今回、2つセンサーを接続するため、それぞれのアドレスになるようにジャンパをはんだ付けします。

I2Cは、電源に2本、信号に2本 (SCL, SDA) の合計4本の配線が必要になります。
Raspberry PiとセンサーをつなぐI2Cバスは、容易に取り回しができるようにしたいと考え、4芯モジュラーケーブルを採用しました。
モジュラーケーブルは家電量販店で調達できるものなので、最適な長さのものに交換して、センサーの位置を自由に変更できます。

タカチ SWプラケースを削り、センサーと2つのモジュラージャックを収めました。
2つのモジュラージャックは導通しているため、他のセンサーをデイジーチェーンできるようにしています。
写真では穴が空いていませんが、温度・湿度・気圧を計測できるよう、ケースの前面には通気孔を空けています。

f:id:Aqutam:20200302232643j:plain f:id:Aqutam:20200302232624j:plain

Raspberry Pi からはI2Cで使用する4本の線を引き回し、モジュラーローゼットを配置しています。

f:id:Aqutam:20200302232435j:plain f:id:Aqutam:20200302232512j:plain

設置

下記の写真のように、インコ部屋に温度センサーを設置しています。
手作業で穴を空けたため、ガタガタになってしまっています。

f:id:Aqutam:20200302232558j:plain f:id:Aqutam:20200302232609j:plain

実装

センサーからのデータ取得

センサーからのデータの取得は、こちらのコードを参考にさせていただきました。 samplecodes/BME280 at master · SWITCHSCIENCE/samplecodes · GitHub

こちらのコードをベースに、下記CSVを出力するように実装します。

Date,GR Temp,GR Press,GR Hum,OR Temp,OR Press,OR Hum
2020-03-01 17:00:00,23.94,1004.81,53.46,21.53,1003.36,62.13
2020-03-01 17:30:00,25.75,1005.25,50.85,22.10,1003.73,62.95
2020-03-01 18:00:00,26.55,1005.34,48.78,22.22,1003.83,62.06
2020-03-01 18:30:00,26.58,1005.70,47.49,22.00,1004.25,62.20
2020-03-01 19:00:00,26.41,1005.88,42.79,21.68,1004.29,50.83
2020-03-01 19:30:00,26.18,1006.09,40.66,21.65,1004.61,51.54

天気予報APIからの実績と予報データの取得

天気予報データは、 Rakuten RapidAPI を使用し、このマーケットプレイスで公開されている、 Dark Sky API を使用しました。
Rakuten RapidAPI は、1つのAPIキーを使用して世界中の様々なAPIを利用できるようにした、プロキシサービスです。

様々なAPIをWebで簡単にテストでき、Pythonをはじめいろいろな言語に対応したAPIをコールするためのコードスニペットも用意されているため、結構便利です。

f:id:Aqutam:20200310003313p:plain

Pythonコードスニペットをダウンロードし、パースして下記CSVを出力するためのコードを追加実装しました。

Date,WT Temp,WT Press,WT Hum,WT Sum,WT PrecipIntensity,WT PrecipProbability,WT ApparentTemperature,WT WindSpeed,WT WindBearing,WT CloudCover
2020-03-01 17:00:00,14.79,1015.4,42.0,曇り,0,0,14.79,2.66,167,77
2020-03-01 17:30:00,14.2,1015.7,44.0,曇り,0,0,14.2,2.74,180,90
2020-03-01 18:00:00,13.65,1016,47.0,曇り,0,0,13.65,2.89,191,94
2020-03-01 18:30:00,13.14,1016.6,50.0,曇り,0,0,13.14,3.13,187,96.0
2020-03-01 19:00:00,12.73,1017.2,52.0,曇り,0,0,12.73,3.51,180,92.0
2020-03-01 19:30:00,12.46,1017.6,55,曇り,0,0,12.46,3.93,177,91.0

グラフの描画

グラフの描画は、まず Jupyter Notebook で行いました。
Jupyter Notebook はブラウザ上で Python のコードを記述でき、グラフなど視覚的な結果を確認しながらプログラミングできるツールです。

センサーからのデータ取得天気予報APIからの実績と予報データの取得 で出力していた2つのCSVを読み込ませ、 Pandas で加工していきます。
そのデータを Matplotlib でグラフを描画するようにしました。

f:id:Aqutam:20200310004559p:plain f:id:Aqutam:20200310004610p:plain

Slackへのアップロード

Jupyter Notebook でグラフが描画できたら、ここから Python のコードに落とし込みます。
Pythonコード上で、 Slack files.upload API を使用して、Slack の特定 channel にグラフをアップロードするようにします。
cron に登録し、定期的にグラフをアップロードするようにします。

動作画面

下記のように、定期的に1週間ごと、1日ごとの温度・湿度・気圧の推移をグラフに描画しています。
緑色、黄色の線はセンサーのデータで、グレーの線は DarkSky API から取得した天気データです。

f:id:Aqutam:20200314120352p:plain

DarkSky API は、1時間ごとの天気予報も取得することが可能です。
これを使用して、1日先の1時間ごとの温度・湿度・気圧・降水量・降水確率を取得して、下記のようにグラフをプロットしています。
毎晩1回Slackにアップロードし、翌日のインコ部屋の温度管理の計画を立てるための情報として使用しています。

f:id:Aqutam:20200314120359p:plain

使用してみての感想

今回は温度・湿度・気圧センサーと天気予報APIから取得したデータをグラフに描画する仕組みを構築しました。

使用を開始してからは1ヶ月弱が過ぎました。
今までは、インコ部屋に温湿度計を立てて、ペットカメラ越しに確認しに行っていたのですが、このシステムを構築してからは定期的にSlackに情報が通知されるため、だいぶ把握が楽になりました。
湿度・温度・気圧・降水確率・降水量のグラフは、それぞれ別の画像で書き出しSlackにアップロードしているのですが、短時間に連続してアップロードしているためか、たまにアップロードに失敗します。
そんなに頻度は多くないので、失敗したときは手動でグラフ描画を再実行しています。

製作時の感想は、ケースの加工がなかなか大変でした。バンドソーやボール盤など電動工具がなく手作業で削ったり穴を空けていたので結構疲れました。
ソフトウェアの実装は、参考にできるコードがすでにあったり、便利なAPIサービスがあったりと、すんなり実装できました。

インコ部屋の環境は、今後下記のように改良してみたいと考えています。

  • Prometheus・Grafana 環境の構築
  • 温度のしきい値に応じて Slack への通知 (Prometheus Alertmanager)
  • 温度に応じて赤外線でエアコンの電源ONにする
  • I2Cバスのノイズ対策