WESEEK Tech Blog

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

Kubernetes 時代の CI/CD「Jenkins X」とは? 〜中編 〜

Kubernetes 時代の CI/CD「Jenkins X」とは? 〜前編〜 で予告した通り、本記事では実際に Jenkins X をインストールし、どのような構成でクラスタが構築されるのかを見ていきたいと思います。

前提

本記事では、GKE (Google Kubernetes Engine) へ Jenkins X をデプロイする手順を紹介していきますので、まず Jenkins X をインストールする前に以下の事項を確認してください。

  1. GKEクラスタを作成できるアカウント・権限を持っていること
  2. MacOSLinux などのターミナル環境上で gcloud, kubectl コマンドがインストールされていること
  3. gcloud コマンドで 1. のアカウントでコマンドが実行できる状態になっている
    • gcloud auth login でログインている状態であれば問題ありません
    • 2., 3. については、Google Cloud Console 上で提供されている Google Cloud Shell を使うことで代替することもできます

この辺については、以下の記事が参考になります。

また、本記事では Git Provider として Jenkins X インストール時のデフォルト設定である GitHub を利用します。jx コマンドは、Jenkins X のインストール途中で、構築した Kubernetes クラスタ上に staging/production 環境としてアプリケーションを起動できるようにするための情報を GitHub 上のリポジトリとして登録するため、GitHub アカウントが必要となります。

インストールしてみよう

jx のインストール

jx コマンドとは、Jenkins X のインストールやプロジェクト管理などを行える、CLI ツールです。Jenkins X を使い始めるためには、まずこのツールを用意する必要があります。

jx コマンドをインストールするには、gcloud/kubectl が揃っている環境で以下を実行します。

$ curl -L https://github.com/jenkins-x/jx/releases/download/v1.3.167/jx-linux-amd64.tar.gz | tar xzv 
$ sudo mv jx /usr/local/bin
  • 1.3.167 の部分については、jx コマンドのリリースページから最新のものを選択してください

上記を実行すると、システム上で jx コマンドを実行することができるようになります。

ついでに、bashzsh の Tab キーを用いた補完機能にも対応させてみましょう。 Ubuntubash 環境下では以下を実行してログインしなおすと、jx コマンドと打った後に Tab キーを押すとコマンド候補が表示され、便利です。

$ jx completion bash | sudo tee /etc/bash_completion.d/jx
$ exit
(再度ログインする)
$ jx [Tab]
cloudbees    compliance   context      create       edit         gc           import       install      namespace    preview      prompt       rsh          start        step         sync         uninstall    upgrade
completion   console      controller   delete       environment  get          init         logs         open         promote      repository   shell        status       stop         team         update       version

Kubernetes クラスタの構築

それでは、クラスタを構築していきましょう。

まず、以下のコマンドを打ちます。

$ jx create cluster gke --skip-login

すると、以下のように GCP 上のプロジェクト選択肢が表示されますので、クラスタを構築するプロジェクトを矢印キーで選択して Enter を押します。

? Google Cloud Project:  [Use arrows to move, type to filter, ? for more help]
  project-a
  project-b
❯ project-c

いくつか jx 側で処理が実行されたのちに、以下のようなゾーン選択画面が表示されます。 東京リージョンであれば、asia-northeast1 のいずれかのリージョンを選択しましょう。

? Google Cloud Zone:  [Use arrows to move, type to filter, ? for more help]
  asia-east1-a
  asia-east1-b
  asia-east1-c
❯ asia-northeast1-a
  asia-northeast1-b
  asia-northeast1-c
  asia-south1-a
  asia-south1-b
  asia-south1-c
  asia-southeast1-a

次に、起動するマシンタイプを選択画面が表示されます。Jenkins X では、最小でも n1-standard-2 を推奨しているようですので、n1-standard-2 を選択します。 (「?」キーを押すと、選択肢に関する情報が出てきます)

ⓘ We recommend a minimum of n1-standard-2 for Jenkins X,  a table of machine descriptions can be found here https://cloud.google.com/kubernetes-engine/docs/concepts/cluster-architecture
? Google Cloud Machine Type:  [Use arrows to move, type to filter]
  g1-small
  n1-standard-1
❯ n1-standard-2
  n1-standard-4
  n1-standard-8
  n1-standard-16
  n1-standard-32
  n1-standard-64
  n1-standard-96
  n1-highmem-2

次に、Kubernetes Node 最小数・最大数を入力します。Jenkins X では 3 Node を推奨していますが、今回はお試しのため最小 1・最大 2 に設定します。

ⓘ We recommend a minimum of 3 for Jenkins X,  the minimum number of nodes to be created in each of the cluster's zones
? Minimum number of Nodes (3)
? Maximum number of Nodes [? for help] (5)

ここまで入力すると、 Creating cluster... と表示され、実際にクラスタの構築が開始されます。(数分で終わると思います)

GKE 管理画面 を確認すると、クラスタが構築されている様子を確認できると思います。

Kubernetes クラスタの設定

クラスタ作成が完了すると、jx はすかさずクラスタの準備に取り掛かってくれます。 この準備作業では、Kubernetes クラスタ上で Jenkins X が稼働するために必要となる Role/ServiceAccount の用意や、helm のインストールが行われます。

Initialising cluster ...
Git configured for user: Hoge Hogeo and email *******
Trying to create ClusterRoleBinding **********-cluster-admin-binding for role: cluster-admin for user *******
: clusterrolebindings.rbac.authorization.k8s.io "**********-cluster-admin-binding" not foundCreated ClusterRoleBinding **********-cluster-admin-binding
Created ServiceAccount tiller in namespace kube-system
Trying to create ClusterRoleBinding tiller for role: cluster-admin and ServiceAccount: kube-system/tiller
Created ClusterRoleBinding tiller
Initialising helm using ServiceAccount tiller in namespace kube-system
helm installed and configured

それが終わると、次は以下のような質問を聞かれます。ingress controller とは、Kubernetes クラスタ内で稼働するアプリケーションを、外部に公開するために必要なアプリケーションのことです。今回は、Jenkins X をインターネットからアクセスできるようにするため、 y と入力しましょう。

(GKE の場合は本来 ingress-gce が稼働しているはずなので、nginx-ingress-controller は不要なはずですが、ここで n を選択するとクラスタ上にインストールされたサービスへのアクセス経路がなぜか設定されないため、ここでは y とします)

? No existing ingress controller found in the kube-system namespace, shall we install one? [? for help] (Y/n)

入力すると、ingress controller のインストールが行われた後、構築した Kubernetes クラスタに対してアクセスするためのドメイン名について聞かれますが、今回はそのまま [Enter] を押します。

Waiting for external loadbalancer to be created and update the nginx-ingress-controller service in kube-system namespace
Note: this loadbalancer will fail to be provisioned if you have insufficient quotas, this can happen easily on a GKE free account. To view quotas run: gcloud compute project-info describe
External loadbalancer created
Waiting to find the external host name of the ingress controller Service in namespace kube-system with name jxing-nginx-ingress-controller
You can now configure a wildcard DNS pointing to the new loadbalancer address XXX.XXX.XXX.XXX

If you do not have a custom domain setup yet, Ingress rules will be set for magic dns nip.io.
Once you have a customer domain ready, you can update with the command jx upgrade ingress --cluster
If you don't have a wildcard DNS setup then setup a new CNAME and point it at: XXX.XXX.XXX.XXX.nip.io then use the DNS domain in the next input...
? Domain [? for help] (XXX.XXX.XXX.XXX.nip.io)

Jenkins X のインストール

ここまで完了すると、次はいよいよ Jenkins X のインストールです。

GitHub アカウント名について聞かれるので、前提で用意した GitHub アカウント名を入力します。

Lets set up a git username and API token to be able to perform CI/CD

? GitHub username for CI/CD pipelines:

次に、API token を発行するよう表示されるので、出力された URL へブラウザでアクセスして token を作成し、作成した token を入力しましょう。

To be able to create a repository on GitHub we need an API Token
Please click this URL https://github.com/settings/tokens/new?scopes=repo,read:user,read:org,user:email,write:repo_hook,delete_repo

Then COPY the token and enter in into the form below:

? API Token:

入力すると以下のように、Jenkins X が予め用意したクラウド環境向けの設定が入ったリポジトリが手元にクローンされ、Jenkins X のインストールが開始されます。

Cloning the Jenkins X cloud environments repo to /home/hoge/.jx/cloud-environments
Counting objects: 805, done.
Compressing objects: 100% (6/6), done.
Total 805 (delta 2), reused 6 (delta 2), pack-reused 797
Generated helm values /home/hoge/.jx/extraValues.yaml
Installing Jenkins X platform helm chart from: /home/hoge/.jx/cloud-environments/env-gke
waiting for install to be ready, if this is the first time then it will take a while to download images

ここまでメッセージが出ると少し時間がかかります。(10分程度)

やがて、インストールが終わると、以下のようなメッセージが続いて出力されます。指示の通り URL へアクセスし、admin/表示されているパスワードで Jenkins へログインしてください。ログイン後、ページ内に「APIトークンを表示」というボタンがありますので、それを押して出てきた API トークンを入力してください。

Jenkins X deployments ready in namespace jx


        ********************************************************

             NOTE: Your admin password is: XXXXXXXX

        ********************************************************

Getting Jenkins API Token
using url http://jenkins.jx.XXX.XXX.XXX.XXX.nip.io/me/configure
unable to automatically find API token with chromedp using URL http://jenkins.jx.XXX.XXX.XXX.XXX.nip.io/me/configure
Please go to http://jenkins.jx.XXX.XXX.XXX.XXX.nip.io/me/configure and click Show API Token to get your API Token
Then COPY the token and enter in into the form below:

? API Token:

API token を入力し終わると、staging/production 環境用の設定ファイルが置かれるリポジトリGitHub 上に作成されます。

Created user admin API Token for Jenkins server jenkins.jx.XXX.XXX.XXX.XXX.nip.io at http://jenkins.jx.XXX.XXX.XXX.XXX.nip.io
Updating Jenkins with new external URL details http://jenkins.jx.XXX.XXX.XXX.XXX.nip.io
Creating default staging and production environments
Using git provider GitHub at https://github.com


About to create repository environment-speakerglow-staging on server https://github.com with user XXXXXX


Creating repository XXXXXX/environment-speakerglow-staging
Creating git repository XXXXXX/environment-speakerglow-staging
Pushed git repository to https://github.com/XXXXXX/environment-speakerglow-staging

Created environment staging
Created Jenkins Project: http://jenkins.jx.XXX.XXX.XXX.XXX.nip.io/job/XXXXXX/job/environment-speakerglow-staging/

Note that your first pipeline may take a few minutes to start while the necessary images get downloaded!

Creating github webhook for XXXXXX/environment-speakerglow-staging for url http://jenkins.jx.XXX.XXX.XXX.XXX.nip.io/github-webhook/
Using git provider GitHub at https://github.com


About to create repository environment-speakerglow-production on server https://github.com with user XXXXXX


Creating repository XXXXXX/environment-speakerglow-production
Creating git repository XXXXXX/environment-speakerglow-production
Pushed git repository to https://github.com/XXXXXX/environment-speakerglow-production

Created environment production
Created Jenkins Project: http://jenkins.jx.XXX.XXX.XXX.XXX.nip.io/job/XXXXXX/job/environment-speakerglow-production/

Note that your first pipeline may take a few minutes to start while the necessary images get downloaded!

Creating github webhook for XXXXXX/environment-speakerglow-production for url http://jenkins.jx.XXX.XXX.XXX.XXX.nip.io/github-webhook/

ここで作成される GitHub リポジトリには、今回インストールした Jenkins X の webhook 用エンドポイントにアクセスするような webhook 設定が予め追加されます。 つまり、このリポジトリに対して push すると、自動的に Jenkins X 上のジョブが実行されるように設定されます。

Jenkins X installation completed successfully


        ********************************************************

             NOTE: Your admin password is: XXXXXXXX

        ********************************************************


Your kubernetes context is now set to the namespace: jx
To switch back to your original namespace use: jx ns default
For help on switching contexts see: https://jenkins-x.io/developing/kube-context/

To import existing projects into Jenkins:       jx import
To create a new Spring Boot microservice:       jx create spring -d web -d actuator
To create a new microservice from a quickstart: jx create quickstart

そして、ここまでで、インストールは完了です!お疲れさまでした!!

Jenkins X へのアクセス

インストール完了時に出るメッセージ中に記載された http://jenkins.jx.XXX.XXX.XXX.XXX.nip.io/ にアクセスすると、Jenkins X へアクセスすることができます。

アクセスすると、ユーザ名・パスワードを求められますが、ユーザ名は admin、パスワードはインストール完了後に出てくるパスワードを入力すればログインできます。

もし万が一パスワードを忘れたとしても、jx コマンドで構築した際に以下のディレクトリに設定ファイルが残っていますので、そのファイルを見ることでパスワードを確認できます。

$ cat ~/.jx/jenkinsAuth.yaml
servers:
- url: http://jenkins.jx.XXX.XXX.XXX.XXX.nip.io
  users:
  - username: admin
    apitoken: XXXXXXXXXX
    bearertoken: ""
    password: XXXXXXXXXX
    ^^^^^^^^^^^^^^^^^^^^ これが admin のパスワードです
  name: jenkins.jx.XXX.XXX.XXX.XXX.nip.io
  kind: ""
  currentuser: ""
defaultusername: ""
currentserver: http://jenkins.jx.XXX.XXX.XXX.XXX.nip.io

インストール後の Jenkins X・その他コンポーネントの構成

jx コマンドでクラスタ構築が完了した後、クラスタ上では以下の様な Pod が稼働し、Ingress 経由でサービス公開されます。

$ kubectl get pod --all-namespaces
NAMESPACE     NAME                                                   READY     STATUS      RESTARTS   AGE
jx            jenkins-6dc7ff9cfd-f28gf                               1/1       Running     0          2d
jx            jenkins-x-chartmuseum-7b95b777b-r7qhs                  1/1       Running     0          2d
jx            jenkins-x-docker-registry-dcb6d6d44-w9c6q              1/1       Running     0          2d
jx            jenkins-x-gc-activities-1534714200-rwm2p               0/1       Completed   0          4h
jx            jenkins-x-gc-activities-1534723200-bcdwj               0/1       Completed   0          2h
jx            jenkins-x-gc-activities-1534725000-5pk5c               0/1       Completed   0          1h
jx            jenkins-x-gc-previews-1534701600-bqcbn                 0/1       Completed   0          8h
jx            jenkins-x-gc-previews-1534712400-5kd6j                 0/1       Completed   0          5h
jx            jenkins-x-gc-previews-1534723200-wprdp                 0/1       Completed   0          2h
jx            jenkins-x-heapster-96bd95dcf-bgxps                     2/2       Running     0          2d
jx            jenkins-x-mongodb-6c84f866dc-hp2bc                     1/1       Running     0          2d
jx            jenkins-x-monocular-api-8499d45598-s5gxn               1/1       Running     3          2d
jx            jenkins-x-monocular-prerender-6d8897856-x87lh          1/1       Running     0          2d
jx            jenkins-x-monocular-ui-967d8d8d9-h7h6p                 1/1       Running     1          2d
jx            jenkins-x-nexus-7c6c4579fd-9jws7                       1/1       Running     0          2d
jx            pipelinecontroller-5dbc96bbd4-hmrsr                    1/1       Running     0          2d
kube-system   event-exporter-v0.1.9-5c8fb98cdb-zrbs8                 2/2       Running     0          2d
kube-system   fluentd-gcp-v2.0.17-gsw5d                              2/2       Running     0          2d
kube-system   fluentd-gcp-v2.0.17-pbdd6                              2/2       Running     0          24m
kube-system   heapster-v1.5.2-5fc46bc685-mz9bj                       3/3       Running     0          2d
kube-system   jxing-nginx-ingress-controller-86c769695f-ndkw4        1/1       Running     0          2d
kube-system   jxing-nginx-ingress-default-backend-5dbcb4b48b-csjzn   1/1       Running     0          2d
kube-system   kube-dns-5dcfcbf5fb-9z8bm                              4/4       Running     0          2d
kube-system   kube-dns-5dcfcbf5fb-9zbkr                              4/4       Running     0          24m
kube-system   kube-dns-autoscaler-69c5cbdcdd-kgtm8                   1/1       Running     0          2d
kube-system   kube-proxy-gke-speakerglow-pool-1-6c92678d-cjxx        1/1       Running     0          2d
kube-system   kube-proxy-gke-speakerglow-pool-1-6c92678d-j1c2        1/1       Running     0          24m
kube-system   kubernetes-dashboard-bf9c699db-64df7                   1/1       Running     0          2d
kube-system   l7-default-backend-57856c5f55-8r9dx                    1/1       Running     0          2d
kube-system   metrics-server-v0.2.1-7f8dd98c8f-9jlrf                 2/2       Running     0          2d
kube-system   tiller-deploy-5cd7c76b66-pph9h                         1/1       Running     0          22m

$ kubectl get ing --all-namespaces
NAMESPACE   NAME              HOSTS                                       ADDRESS          PORTS     AGE
jx          chartmuseum       chartmuseum.jx.XXX.XXX.XXX.XXX.nip.io       XXX.XXX.XXX.XXX   80        2d
jx          docker-registry   docker-registry.jx.XXX.XXX.XXX.XXX.nip.io   XXX.XXX.XXX.XXX   80        2d
jx          jenkins           jenkins.jx.XXX.XXX.XXX.XXX.nip.io           XXX.XXX.XXX.XXX   80        2d
jx          monocular         monocular.jx.XXX.XXX.XXX.XXX.nip.io         XXX.XXX.XXX.XXX   80        2d
jx          nexus             nexus.jx.XXX.XXX.XXX.XXX.nip.io             XXX.XXX.XXX.XXX   80        2d

簡単に見ていくと、稼働しているソフトウェアの概要は以下です。

まとめ

本記事では、Jenkins X をインストールするために必要な jx ツールをインストールし、実際に GKE 上に Jenkins X が稼働するクラスタを構築するステップをご紹介しました。

次回は、構築した Jenkins X を利用し、サンプルとして用意されている quickstart プロジェクトを立ち上げて、Jenkins X がどのように CI/CD を実施するのかを見ていく予定です

関連記事

tech.weseek.co.jp

GROWI のユーザーズガイドをリニューアルしたお話

WESEEK の kouki です。GitHub 個人アカウントは kaishuu0123 で活動しています。

今回は SEROKU からの引用ではなく、オリジナル記事として「GROWI のユーザーズガイドをリニューアルした話」を書かせていただこうと思います。

ドキュメントサイトはこちらです: https://docs.growi.org

リニューアルしようとしたモチベーション

WESEEK では最近 GROWI.cloud をリリースいたしました。

growi.cloud

GROWI.cloud リリース以前では、Geek な方たちが GROWI を触っているという状況で、Issue も上げてもらいつつ、ポジティブなご意見をいただいて、特定のクラスタで盛り上がりを見せていました。

しかし、今後 GROWI.cloud と共に GROWI のコミュニティを広めていくには Geek な方も含め、 GROWI でページを書く側に寄り添ったドキュメントが必要になるだろうと考えました。

そこで、ユーザーズガイドのリニューアル提案に至ったわけです。

高尚なことを掲げていますが、正直なところ 「GROWI の機能一覧を紹介する場所を自分が作りたかった」というのが本音だったりします。 「こんな機能があったんだ~」と後になって気付くより、俯瞰して機能を把握する方が使っていて楽しいので。

提案からリニューアルまで

提案フェーズ

まずはメインコミッターである弊社武井に対して、提案ページを見せるところから始めました。

dev.growi.org

ついでに一応 GROWI はオープンなものなので、自身のアカウントでも呟いてみたりもしました。

そこから意外にもサラッと OK をもらいました。(口頭でですが)

リニューアルフェーズ

ここからは単純作業ですが、スクショを撮って、ガリガリとドキュメントを書いていきます。 リニューアル以前は「管理者ガイド」相当は無かったので、まずは以前の「ユーザーズガイド」を「管理者ガイド」に移動し、 中のコンテンツを充実させていく作業を進めました。

変更が大きくなるため、作業途中の様子を見てもらうために、タイトルに [WIP] (Work In Progress) という文字列を付与して作業していました。 実際の PR は下記の URL です。

github.com

ファイル変更が 160 と多いですが、ほぼほぼリネームです。 主要な変更点は /guide/ 以下にドキュメントを追加する作業でした。

無事マージ

PR を送ってから、slack の GROWI workspace 上で改善点を指摘してもらって、すぐにマージされました。

f:id:weseek:20191028182549p:plain

今後考えていること

まだ構想段階ですが、下記のような流れで情報をまとめていけたら、と考えています。(お手伝いいただける方募集中です)

  1. GROWI 本体から GROWI Docs (https://docs.growi.org) へのリンクを追加
  2. github にある weseek/growi の README.md に掲載されている情報、wiki を docs.growi.org にまとめていく
  3. growi.org も併せた情報の整備 (i18n 対応)
  4. 海外の方に知っていただきたいので、もっと発信は増やしていきたい

最後に

正直、独断と偏見でユーザーズガイドを用意したので、「読みにくい」とか「大して役に立たないのではないか」など不安はありますが、今まで機能を紹介する場所が無かったので、その場所を作ることができたことは個人的に満足しています。

多少なりとも GROWI を使ってくださっている方、これから GROWI を使おうとしている方に GROWI docs を一目でも見ていただけたら嬉しいです。

Slack の GROWI workspaceWESEEK の Twitter アカウント などでご意見、ご感想をお待ちしています。

蛇足

しれっと awesome-vuepress にも PR しておきました。

github.com

自作で Video Chat 環境を作って導入した話〜選定編〜

こちらは 「自作でVideo Chat環境を作って導入した話〜選定編〜 」からの転載です。



「SEROKU フリーランス(以下、SEROKU)」の中の人をやっている ryosuke です。 前回の経緯編に続き、今回は Video Chat サービスや機器の選定についてお伝えします。

Video Chatサービス比較

さて、使用する Video Chat 用のプロダクトを用意する必要があります。

以前はビデオ会議システムといえば Polycom 社製品などを代表する、高価な専用のハードウェアを購入したり、ビデオ会議システム用の SIP, H.323, RTP といった特殊なプロトコルの通信ができるようにファイアウォールの設定が必要だったりと、コストと手間がかかる手段が大半を占めていました。しかし最近では普通の PC とカメラ、マイク、スピーカさえそろえば、あとは適当なブラウザベースの Video chat サービスを利用すれば、だれでもすぐに簡単に高品質な Video Chat サービスができるようになっています。ラップトップ PC ならばカメラ、マイク、スピーカも内蔵されているものが多くありますから、より手軽に利用できますね。

今回はできるだけ安価に環境を整えたいのもあり、PC で使える VideoChat サービスを利用する方針としました。

では、利用する Video Chat サービスを選定しましょう。選定の条件として下記の 4 つの観点を挙げました。

  1. 長時間接続ができる
    • 業務中のコミュニケーション手段として利用したいので、会議の時に限らず業務時間中は常時接続した状態で運用しようと考えていました。話しかけたいときに毎回接続操作をすることなく、すぐに会話ができる状態にするのが理想だからです。業務時間中に接続し続けることを考えると、 8 時間程度は連続利用ができることが要求されます。
  2. 多人数が参加できる
    • 現在は 2 拠点間で使用できれば良いですが、将来拠点が増える可能性も考慮し、複数人での Group Video Chat ができることが望ましいです。また、Group Video Chat ができるサービスを選択すれば、例えばある日に在宅ワークをする社員がいたとしても、その社員も Video Chat に参加できれば現状よりコミュニケーションロスを減らすことが期待できます。
  3. 画面共有ができる
    • 会議などでは共通の画面を見ながら話を進める場面が多々あります。また、それ以外の業務においても遠隔地と画面を共有して、実際に行っている操作を見せながら話を進める様な使い方もできそうです。せっかく PC で使える Video Chat サービスを選定しているので、この機能も欲しいところです。
  4. 実質無料で利用できる
    • Video Chat サービスには無料で利用できるものが数多くありますが、最も使える機能が少ないプランが無料で、そのほかの付加価値の高い機能を使おうとするとサービスの使用料が必要な(いわゆるフリーミアム)ビジネスモデルを採用しているものが多いです。もちろん実際にかかるコスト次第ではありますが、可能であれば上記に挙げた条件をクリアしつつ、無料で利用できると嬉しいです。

この条件を基にいくつかの Video Chat できるサービスを比較し、以下にまとめました。

サービス 長時間接続 多人数 画面共有 料金(左記要件を利用する場合)
Skype △※1 25人まで 10人まで 無料
Hangouts 25人まで 無料
appear.in 4人まで 無料
Slack 15人まで ¥850/月/人
ChatWork × 無料

※1 グループ通話の場合、最長時間に制限がある (グループビデオ通話公正使用ポリシー) ※2 無料版だと画面共有時に自動的にフルスクリーン表示をしない制限がある

比較したところ、Hangouts と appear.in であれば無料で要件を満たすことができます。Hangouts の方がより大人数で同時に接続できるようですので、今回は Hangouts を採用することにしました。

なぜ chromebox for meeting を買わないのか?

Hangouts を採用することにしましたが、実は Google は Hangouts を会社のテレビ会議システムとして利用するためのソリューションを提供しています。 Chrome devices for meetings という名前で複数の代理店を経由して PC、カメラ、マイク、スピーカなどハードウェアを含めて購入することができます。これを使えば簡単に Hangouts を使う環境が整いますが、今回は採用しませんでした。

その理由ですが、安価ではないという点です。手ごろなプランでも1台当たり初期設備費が約 11.5 万円となるだけでなく、年間保守費が約 3.5 万で、初年度を含め 1 年ごとに保守費用が発生します。これでも大企業が導入するような専用システムと比べればかなり安いほうなのですが、「自前で PC を揃えれば使える」と考えている我々にとっては割高になってしまいます。

そのため、今回はコストを抑えつつ Hangouts を使うのに十分なスペックを揃えた PC と周辺機器を別途調達することにしました。

機器選定・構築

PC の調達ですが、 WESEEK では普段使用している PC を自作(組み立てだけ外注)しているため、今回も自作することにしました。 PC のスペックについては下記の点を考慮して選定しました。

  • Hangouts が快適にできるスペック
  • OS は Windows である必要はない
    • PCを自作する場合、Windows だと OS を購入するコストも考慮しなければならなくなるので、買わずに済むならそちらの方がよいです。 Hangouts はブラウザである Google Chrome 上で動作するサービスのため、Chrome さえ動作すれば OS には制約がありません。今回は無料で利用できる Linux の主要なディストリビューションの一つである Ubuntu Desktop を採用することにしました。
  • 常時使用している状態でも冷却を維持しつつ比較的静かである
    • 一日中使用することになるため、PC の騒音はできるだけ小さく抑えたいところです。かといって闇雲にファンレス構成にしてしまうのも問題があります。実はこの PC 構築を検討する前に、一時的に既存の安価なラップトップ PC で Hangouts を試用していました。しかし、しばらく使用し続けていると、映像がコマ落ちしたり音声がぶつぶつと切れてしまう問題が発生していました。原因を調査してみると Hangouts を使用し続けたためにPCの冷却が追い付かず、Thermal throttling という CPU の性能を低下させて冷却を間に合わせている状態になっていたためということが分かりました。同じ問題を起こさないためにも十分に冷却しつつできるだけ静音であることを目指します。
  • まあまあ安い
    • Hangouts 専用機にするため、その他のアプリケーションを同時に利用することはありません。過剰なスペックにする必要はないのでその分安価に仕上げることを考えます。
  • エコーキャンセラーのついたスピーカーマイクを使う
    • Hangouts 自身にもエコーキャンセル機能がついていますが、専用のハードウェアで処理できたほうが品質向上が期待できるため、エコーキャンセラーのついたスピーカーマイクを使用することを検討します。
  • できるだけ大勢が画面に映るようにする
    • 常時接続していつでも話しかけられるような運用を想定しているため、オフィスにいる多くの人が何をしているか見渡せるぐらい広角に撮ることができるカメラが望ましいです。

これを基準に選定した結果が下記のとおりです。

部品 品名
CPU Intel Pentium Dual-Core G4560
RAM 4GB
Storage SSD 32GB SATA3.0
MotherBoard Intel H370搭載 Mini ITXマザーボード
電源 SFX 300W 80PLUS:Bronze
ケース 筒形状の Mac Proと似たケース
ディスプレイ 27inch Full HD ディスプレイ
カメラ 視野角120度 Full HDカメラ (Buffalo BSW200MBK)
スピーカマイク エコーキャンセラー搭載WEB会議小型スピーカーフォン (サンワサプライ MM-MC28 )

この構成で 1 セット当たり合計 7 万円強で調達ができました。

まとめ

いかがでしたか?今回は Video Chat サービスと使用機器の選定についてお話ししました。Video Chat サービスの導入を検討している方にご参考になれば幸いです。

次回は hangouts をベースにもう少し WESEEK 社での用途向きに使いやすくした話と、失敗談、今後の展望についてお伝えする予定です。

関連記事

tech.weseek.co.jp

社内 Kubernetes トラブルシュート 前編

こちらは 「SEROKUを支える技術〜社内 Kubernetes トラブルシュート前編〜」からの転載です。



「SEROKU フリーランス(以下、SEROKU)」の中の人をやっている kouki です。

今回は 社内 Kubernetes 実験環境をRancher 1.6から 2.0にアップデートして復活させた話 の中でお話しした「2.0で行ったトラブルシューティング」の「グローバル IP とプライベート IP 2つの足(NIC)を持つサーバを Kubernetes クラスタのネットワークに所属させることができない (Calico ネットワークが確立されない)」という件についてお話させていただきます。

経緯

経緯としては、 Rancher を利用した Kubernetes クラスタに対してインターネットからリーチャビリティを持たせるためにグローバル IP アドレスと社内通信用のプライベート IP 2つの足(NIC)を持つサーバをクラスタに参加させようとしました。

その際に、Kubernetes クラスタ間の通信を行う Canal ネットワークの確立がうまくいかない状況に遭遇しました。

接続イメージとしては以下の通りです。

f:id:weseek:20191017143107p:plain

Host A にグローバル IP アドレスとプライベート IP アドレスを持たせます。 また、正常なケースでは Host A、Host B、Host C、Host D と Kubernetes クラスタを組むための Canal (Calico + flannel) (Canal 以外のネットワークも選択可能です) ネットワークを確立します。

ですが、Host A はインターネットにも接続を持たせたいため、デフォルトゲートウェイを Internet 側(グローバル IP 側) に向ける必要があります。 そうすると、Host B、Host C、Host D との Canal (Calico + flannel) のネットワーク確立に失敗する、という事象に遭遇しました。

直後は「HostA は Host B/C/D にはプライベート IP で到達可能なのに、ネットワーク確立に失敗する理由がまったく分からん」という状態でした。

トラブルシューティング 初動

まずは Host A の中に SSH でログインし、Canal ネットワークを確立しようとするコンテナを調べてみることにしました。 そこで以下のようなログが記録されていました。

$ docker logs k8s_kube-flannel_canal-wtqnx_kube-system_0f6345ce-7b54-11e8-bf7c-525400638f33_18
I0629 14:44:25.191200       1 main.go:487] Using interface with name eth0 and address 192.168.XX.XXX
I0629 14:44:25.191314       1 main.go:504] Defaulting external address to interface address (192.168.XX.XXX)
E0629 14:44:55.192923       1 main.go:231] Failed to create SubnetManager: error retrieving pod spec for 'kube-system/canal-wtqnx': Get https://10.43.0.1:443/api/v1/namespaces/kube-system/pods/canal-wtqnx: dial tcp 10.43.0.1:443: i/o timeout

IP アドレスは伏せてあります。 Host A を追加する際に Kubernetes クラスタ通信用の I/F を指定することができるのですが、無事 192.168.XX.XXX が利用されています。 この点は問題無さそうで、 10.43.0.1:443 への通信がうまくいっていないようでした。

この失敗している状態でルーティングテーブルを参照しても 10.43.0.1 宛のルーティングは登録されている気配がありません。

「どこで通信を曲げているのだろう?」ということがまったくハッキリせず、「一度社内ネットワークにデフォルトゲートウェイを曲げて、その後、グローバルにデフォルトゲートウェイを向けなおす」という workaround を実施し、後日調査することにしました。

トラブルシューティング iptables

ふと思いつき、 iptables で通信を曲げているのではないか、ということが思いつきました。そこでまた Host A にログインし、iptable の nat テーブルから調べることにしました。

$ sudo iptables -L -t nat
...
Chain KUBE-SEP-OT4MDINTM57KEHPZ (2 references)
 pkts bytes target     prot opt in     out     source               destination         
    0     0 KUBE-MARK-MASQ  all  --  *      *       192.168.YY.YYY       0.0.0.0/0            /* default/kubernetes:https */
    2   120 DNAT            tcp  --  *      *       0.0.0.0/0            0.0.0.0/0            /* default/kubernetes:https */ recent: SET name: KUBE-SEP-OT4MDINTM57KEHPZ side: source mask: 255.255.255.255 tcp to:192.168.YY.YYY:6443
...

非常に長い出力になるので省略していますが、Kubernetes master である Host B (192.168.YY.YYY) へ通信を曲げている箇所が見つかりました。

このエントリは POSTROUTING として登録されており、おそらく初回起動時にネットワーク設定などを Kubernetes master へ取得しに行っているのだろう、と予想を立てました。

こちらの設定も192.168.YY.YYY へ到達するためには正しいように見えます。

トラブルシューティング 解明編

あと疑う点としては、Kubernetes master へ通信する際に source I/F が加味されているのだろうか、という点でした。

flannel が一番最初に通信するタイミングなども気になったので、 coreos/flannelソースコードを読むことにしました。

まずソースコードを読むにあたって、当たりを付けたのは、前述した error retrieving pod spec for というメッセージでした。親切に文章として出されているので、おそらくソースコード中に記述されているだろうと、検索をかけてみました。

予想は的中し、メッセージを出しているコードが見つかりました。

       pod, err := c.Pods(podNamespace).Get(podName, metav1.GetOptions{})
        if err != nil {
            return nil, fmt.Errorf("error retrieving pod spec for '%s/%s': %v", podNamespace, podName, err)
        }
        nodeName = pod.Spec.NodeName
        if nodeName == "" {
            return nil, fmt.Errorf("node name not present in pod spec '%s/%s'", podNamespace, podName)
        }

このコードを見ると、おそらく c.Pods(podNamespace).Get(podName, metav1.GetOptions{}) で失敗しているようです。 c の出所を探るため、更に調べると以下のような記述が見つかりました。

c, err := clientset.NewForConfig(cfg)

clientset の NewForConfig を利用して、クライアントを作成しているようです。 この clientset は "k8s.io/client-go/kubernetes" を利用しているようでした。

そこからさらに kubernetes/client-go に潜っていきます。

(ここからコードを追う作業は煩雑になるので省略します)

kubernetes/client-go を読むと、 HTTP リクエストを出す際に Source Address の指定はされていないようでした。

また、 kubernetes/client-go を利用している coreos/flannel 側も HTTP リクエストを出すための source I/F を指定しているようには読み取れませんでした。

これらの調査結果から、「Kubernetes master に HTTP でリクエストを行う際に使われるルーティングはデフォルトゲートウェイが用いられる」という裏打ちが取れました。

具体的な workaround

原因が分かれば対処はシンプルで、Kubernetes master (社内のネットワーク) への通信を社内のゲートウェイに向けてあげるだけです。

$ sudo ip route add 10.43.0.1 via 192.168.XX.X

このコマンドを起動時に実行するように設定するだけでトラブルは発生することはなくなりました。

宛先を 10.43.0.1/32 にしている理由は単純に 10.43.0.1 への通信に失敗していた事実からです。

まとめ

Canal ネットワークのトラブルから、 flannel, client-go へのソースコードに潜ることになるとは思いませんでしたが、go のソースコードリーディングも含めて、良い勉強になりました。

バッドノウハウな気がしなくもないのですが、こういった未知の領域のトラブルをきっかけにノウハウを蓄積することは続けていきたいと思っております。

関連記事

tech.weseek.co.jp

Go 言語での開発を試してみる 〜調べる編〜

こちらは 「Go言語での開発を試してみる 〜調べる編〜」からの転載です。

2018 年 8 月時点での Go 言語関連の調査結果を紹介しています。


こんにちは。haruhikonyan です。 自分 Go 言語というものを実はこれまで触ったことが無かったのでちょいと触ってみることにしました。 とはいえ何がやりたいとかそういうことは無いのでとりあえず Hello World と開発のための道具としてはどういうものがあるかを調べてみようと思います。

Go 言語のインストール

まずはインストールから。 調べるにもとりあえず手元で go コマンド自体が動かないとつらいものがあると思うのでインストールします。

windows

  1. インストーラダウンロード
  2. インストール
  3. バージョン確認

     > go version
     > go version go1.10.3 windows/amd64
    

無事インストールされて PATH も自動的に通してくれています!

mac

  1. homebrew でインストール

     $ brew install go
    
    • 自分は普段から homebrew を使っているのでインストーラは使わずに homebrew でインストールします。
  2. バージョン確認

     $ go version
     go version go1.10.3 darwin/amd64
    

こちらも無事インストール完了です。

Hello World

書いてみる

とりあえず公式のトップにある下記コードを拝借し、保存。

package main

import "fmt"

func main() {
  fmt.Println("Hello, 世界")
}
$ go run hello.go
Hello, 世界

簡単ですね!

コンパイル

Go 言語で書いたコードはデフォルトでクロスコンパイルに対応しているということで早速試してみます。

Windows

windows環境で試しているのでそのままビルド

> go build hello.go

なんとexeが吐き出されます! それをcmd上で実行してみると?

> hello.exe
hello, 世界

ちゃんと出力されました!

Mac

次は mac 用に出力してみることにします。

$ SET GOOS=darwin
$ SET GOARCH=amd64

$ echo %GOOS%
darwin
$ echo %GOARCH%
amd64

上記のように環境変数をセットしてあげます。 すると、hello というバイナリファイルが出力されました。 そいつを手元の mac に送ってターミナルで実行してみると?

$ ./hello
hello, 世界

どんな環境でも同じコードで動く!楽しい!

本格的に開発するために必要なうんちく

この章は Go 言語あまり関係ないんで飛ばしてくれて構いません。 Go 言語開発を始める上で他言語で開発を始める上で気にしていることを述べています。

フレームワークの有無

開発を行う上で言語の選定と同じ、それ以上に重要なこととしてフレームワークの選定があると思います。 何を作るにしても大体は先人が同じようなことを行なっていることがほとんどで、そのライブラリであったり、フレームワークを選定し、優れたフレームワークのある言語で開発を行うという選択が開発を成功させることであったり、工期を短くする秘訣であったりします。

例えば Ruby には Ruby on Rails という優れた Web フレームワークがあったからこそ莫大な人気が出たと言っても過言ではありません。 他にも Java の Spring や PHP の Laravel、 PythonDjango といったフレームワークが人気が高く幅広く使われていると言えるでしょう。

また、フレームワークを採用することでチーム開発をする上でのコーディング規約などが自然と揃い、読みやすくなるという利点もあると言えるでしょう。 車輪の再開発を悠長にやっている暇は無いのです。使えるものを使いましょう。

開発エディタ候補

開発環境においても VSCodeAtom のようなテキストエディタ拡張機能を追加して機能を充実させていくようなタイプのエディタもあれば、Java でいう Ecripce や IntelliJ のようなすでにデバッガ機能などが充実した IDE (統合開発環境)という大きな二つの選択に分かれると言えます。

前者のテキストエディタがベースのものではまず軽いといったメリットがありますが、機能を増やすには拡張機能を探して自分で入れる必要があったり、全ての要件を満たそうとすると自分で色々調べたりしなければならないのが欠点とも言えると思います。また、全てを詰め込もうとするとエディタ自体も重くなってしまうとうことも十分考えられます。 後者の IDE では専用ということもあり、機能は一通り揃っているが、エディタ自体が重いということもよくあることです。

エディタはよく Vim vs Emacs というような宗教戦争などと揶揄されるようにエンジニア個人の好みも大きく分かれるところです。開発効率にも大きく影響する部分だと思うのでベストな選択をしたいところです。

パッケージ管理

開発を行う上でライブラリやパッケージの依存管理はほぼ必須と言ってもいいでしょう。Node.js には npm や yarn といったものがあったり、Ruby では RubyGems (gem) であったり、他にもさきほど Mac で Go をインストールするために使った Homebrew もパッケージ管理システムです。

これらにパッケージ管理を任せることで、だれがアプリをビルドしても必ず同じパッケージ構成にできるというのも大きな利点の一つです。 開発をする上で依存関係を人間が細かく気にすることなく進める上では欠かせない存在であると言えます。

Go で本格的に開発するための道具候補

フレームワーク

Web フレームワークばかりになってしまいましたが、フレームワークの紹介です。

Gin

beego

Echo

iris

Revel

Martini

Gorilla

開発エディタ候補

公式ページによると4つのエディタが紹介されていました。

vim

Visual Studio Code

GoLand

Atom

パッケージ管理

dep

glide

vgo (Versioned Go)

使わない

  • パッケージ管理しないといけないような粒度の成果物つくるのは Go じゃなくていい説という話も。。。

次回予告

次回は後編ということで実際にこれらの調べた要素を組み合わせて簡単な何かを作ってみたいと思います。

Angular Tips その3

こちらは 「SEROKUを支える技術 Angular Tips編 その3」からの転載です。

記事の最後に関連記事を掲載しています。よろしければご参考にどうぞ。




さて、今回は前回予告したとおり ng-content を使った、テンプレートに対して外部から子要素を突っ込むような処理を書いてみようかと思います。

Angular に置いてあまりサンプル記事がない印象ですが、使い方さえ覚えておけばきっと役に立つはずです!

Tips 4 :ng-content を使った子要素への注入

ng-content とは、作成したコンポーネントに対して外部から要素を注入するときに使うタグです。

ng-content を使ったシンプルな例

まずは一番簡単な例を、コードベースで書いていきたいと思います。

selector: ‘foo’ とする Foo コンポーネントを定義

<div>
  <h1>header要素</h1>
  <!-- 以下ng-contentを配置し、外部から注入するための要素を宣言 -->
  <ng-content></ng-content>
</div>

外部のコンポーネントで foo を呼び出し、さらに要素を注入

    <foo>
      <div>test</div>
      <span>test2</span>
    </foo>

上記の foo コンポーネントを呼び出した結果の html

    <div>
      <h1>header要素</h1>
      <!-- angular によって展開され、foo タグに書いたタグが全て ng-content に展開される -->
      <div>test</div>
      <span>test2</span>
    </div>

foo タグ内部に書いた要素が、まるまる ng-content と置き換わりますね。 上記の簡単な例を実装する上ではうまくいきましたが、では複数の要素を別々の場所に注入したい場合はどうすればよいでしょう?

ng-content を使い複数の要素を注入する例

ng-content では select アトリビュートを使い、注入するコンテンツを指定することができます。 以下のコードで複数の要素を注入したいと思います。

selector: ‘foo’ とする Foo コンポーネントを定義

<div>
  <!-- ng-contentを宣言(selectでh1を指定) -->
  <ng-content select="h1"></ng-content>
  <!-- ng-contentを宣言(selectで.headerを指定) -->
  <ng-content select=".header"></ng-content>
  <!-- 以下ng-contentを宣言(select無し) -->
  <ng-content></ng-content>
</div>

外部のコンポーネントで foo を呼び出し、さらに要素を注入

    <foo>
      <h1>test</h1>
      <div class="header">test2</div>
      <div>test3</div>
      <span>test4</span>
    </foo>

上記の foo コンポーネントを呼び出した結果の html

    <div>
      <!-- select で h1 を指定しているので h1 タグの要素が展開される -->
      <h1>test</h1>
      <!-- select で .header を指定しているので header クラスを指定した要素が展開される -->
      <div class="header">test2</div>
      <!-- 上記指定に当てはまらないタグが全て ng-content に展開される -->
      <div>test3</div>
      <span>test4</span>
    </div>

foo タグ内部に書いた要素の中で、Foo コンポーネントの ng-content の select で指定した箇所にそれぞれ置き換わりますね。

ちなみに、上記の例で言うと h1 タグが複数あった場合、または header クラスが複数あった場合でも、select にマッチする要素分 ng-content は置き換わります。

ng-content を使った例 応用編

上記の例はあくまでただの使い方の例だったので、どういう時に役に立つかをユースケースを交えながら書いていきたいと思います。

一つの例として、コンポーネントを作る際に、あるボタンイベントを親コンポーネントに EventEmitter を使って通知し、実際の処理は親コンポーネントでやらせているケースがあるとします。

selector: ‘foo’ とする Foo コンポーネントを定義

// テンプレート側
<div>
  <button (click)="clickHandler()"></button>
</div>


// コンポーネント側
@Output()
onClicked = new EventEmitter();

clickHandler() {
    onClicked.emit();
}

外部のコンポーネントで foo を呼び出し onClicked に対してイベントハンドラを仕掛けている

// テンプレート側
<foo (onClicked)="clickHandler()" ></foo>

// コンポーネント側
clickHandler() {
    // 実際の処理
}

Foo コンポーネント側の clickHandler で、なにか特別なことをしているのなら別ですが、ただ emit しているだけならば以下のように書いたほうが、Foo コンポーネントはロジックを気にせずにただの View テンプレートとしての役割に専念できるので見通しが良いです。

selector: ‘foo’ とする Foo コンポーネントを定義

// テンプレート側
<div>
  <ng-content select="button"></ng-content>
</div>


// コンポーネント側
処理がなくなる!!

外部のコンポーネントで foo を呼び出し click ハンドラを設定している button タグを要素として注入

// テンプレート側
<foo>
    <button (click)="clickHandler()"></button>
</foo>


// コンポーネント側
clickHandler() {
    // 実際の処理
}

上記は一例に過ぎませんが、同じような状況は結構ある気がします。 これ使ったほうがコードがキレイになりそうというのがもしあれば試してみると良いかもしれません。

まとめ

今回は ng-content に関して書きました。

前回の記事で ngTemplateOutlet にも少し触れましたが、ある特定の要素を置き換えるという場合にも複数のやり方がありますね。(もちろんそれぞれで適切な使い所は異なりますが)

吐き出したいネタが一旦尽きてしまったので、なにか思いついたらまたアップしたいと思います。

それでは皆さんごきげんよう

関連記事

tech.weseek.co.jp

tech.weseek.co.jp

イマドキの JavaScript 開発で使える、リモートデバッグとロガーの Tips (中編)

こちらは 「イマドキの JavaScript 開発で使える、リモートデバッグとロガーの Tips(2018年版-中編)」からの転載です。

SEROKU の開発を例に、弊社で使っているリモートデバッグとロガーの Tips をご紹介します。 当記事は 2018 年と過去の記事ですが、現在でも応用可能な Tips になっています。

記事の最後に関連記事を掲載しています。よろしければご参考にどうぞ。




案件としても OSS 成果物としても、JavaScript を利用するシチュエーションは増え続けています。まだまだ枯れた言語とは言い難い状況で、使われるバージョンも ES5 から ES7 まで進化を続け、新しい文字列リテラルや async/await のような「イマドキの JavaScript の書き方」を紹介する記事は多い中、デバッグはこうあるべきという情報は比較的少ないように思います。

本記事の前編にあたる「イマドキの JavaScript 開発で使える、リモートデバッグとロガーの Tips (前編)」では、システム開発に於けるデバッガ、ロガーの大切さと、他の言語・フレームワークと比べた際の JavaScript 開発環境に於けるビハインドについて説明しました。

本記事ではいよいよ JavaScript の世界での理想的なロガーの具体的な設定方法を紹介します。

理想の世界

まずは前編で掲げたゴールの再掲です。

対象システム

以下の3つのシナリオをカバーすることにします。

  • シナリオA
    • node.js (Express)
  • シナリオB
    • node.js (Express) + webpack/babel によるクライアントビルド
  • シナリオC
    • next.js on Express

やりたいこと

  • 記述側
    • サーバーサイド、クライアントサイド両方で同じログモジュールを利用できる
    • ログレベルを指定したログ出力行の記述ができる
  • 出力側
    • サーバーサイド、クライアントサイド両方で同じログモジュールを利用できる
    • development, production 等の環境用の設定ファイルでデフォルトの挙動を設定できる
      • 環境変数設定により、上記を一時的に変更できる
    • プロダクト全体でのデフォルトログ出力レベルを設定できる
    • ログのネームスペースごとにログ出力レベルを設定できる
    • development, production 等の環境に応じて出力フォーマットを変更できる
      • development 環境ではフォーマットされたログ出力
      • production 環境下では JSON フォーマット出力による高速なログ出力
  • 過去の資産の有効利用
    • 今まで記述された debug ライブラリによる記述を変更することなく、新しいロギングライブラリによる出力を可能にする

利用ライブラリ選定

node.js 環境向けのロガーでは、winstonBunyanlog4jspino あたりが有名処です。

npm の weekly download, Github のスター数で言えば、勢力図は 2018/08 現在以下のようになっています。

npm package License weekly download Github stars
Winston MIT 2,165,220 10,998
log4js Apache-2.0 1,199,467 3,321
Bunyan MIT 372,873 5,238
pino MIT 91,535 2,873

pino (選外)

上記のうち、pino は筆者も実際に Production 環境で利用したことがあり、その処理速度は素晴らしいものがあります。Extreme Mode の発想なども興味深く、設計と API 双方からスピードに対するこだわりがにじみ出ていました。 Github の Issue の開発スタッフのストイックさも特徴的です。

ただ、ログレベルと出力先(出力方法)の設定が他ライブラリと比べて貧弱で、今回の理想の世界を実現する上で足枷となったため、選外としました。

log4js (選外)

log4js は名前からも分かるとおり、log4j を意識したライブラリです。Appender や Layouts など、Java 界隈のエンジニアには概念的に馴染みやすいものと言えるでしょう。

こちらは機能的には問題ないものの、ブラウザ利用向けの周辺ライブラリが見当たらなかったことから選外としました。

Bunyan を採用

残る2つ、Winston と Bunyanは機能的にはどれも申し分ありません。今回は勢力図では3番手(Github stars では2番手)である Bunyan (バニヤンと読むらしいです) を採用することにします。

コラム: Winston と Bunyan

Winston と Bunyan の違いは何でしょうか?
比較記事としてはかなり古いのですが以下が有名です。

Comparing Winston and Bunyan Node.js Logging

こちらの記事からは、ストレージへのログ書き込みを主眼としていた Winston に比べて、Bunyan にはシンプルで魅力的なアイデンティティーを持っていることがわかります。

  • JSON 出力を標準とした作り
  • 書き込み対象として Writable Stream interface を意識した作り

これらはコンテナ時代にマッチしますし、出力先がどこであれストリームとして扱うというのは、よりクリーンな設計思想に見えます。

上記記事は執筆された時から長い時間が経過し、つい先日(2018.06)には Winston 3.0 がリリースされました。強力な Transport は Winston の大きな特徴でしたが、winston-transport という

Stream implementation & legacy Transport wrapper.

が外部モジュールとして作成され、従来の Transports エコシステムをそのまま利用可能なストリーム実装、という形式にコア部分が書き直されたようです。思想的には Bunyan に寄った形になったと見ていいでしょう。ただし、リリースされてまだ間もないため、信頼性という意味では v3 系の採用はもう少し時間をおいた方がいいかもしれません。

準備 ― 依存関係整備

ではここからは実際のコードの紹介です。 まずは package.json から。シナリオA~Cまで全てで必要になるものを全て挙げます。

"dependencies": {
    "browser-bunyan": "^1.3.0",
    "bunyan": "^1.8.12",
    "bunyan-format": "^0.2.1",
    "express-bunyan-logger": "^1.3.3",
    "minimatch": "^3.0.4",
    "module-alias": "^2.0.6",
    "next-bunyan": "^0.0.1",
},
"_moduleAliases": {
    "@services/logger": "lib/service/logger"
},

「"_moduleAliases" ってなんだよ??」と思われる方もいらっしゃるかもしれません。ひとまずおまじない的に書いておいてください。便利な使い方を後述します。

Let's Try! ― シナリオA

シナリオAは、node.js (Express) 環境向けのロガー設定です。

設定用ファイル

利用時のイメージを掴みやすいよう、コンフィグファイルから作っていきましょう。

config/logger/config.dev.js

module.exports = {
  default: 'info',

  //// configure level for name
  // 'myapp:*': 'debug',
  'myapp:utils:third-party': 'error',
};

config/logger/config.prod.js

module.exports = {
  default: 'info',
};

上記2ファイルは、それぞれ開発時の設定ファイル、本番環境用の設定ファイルです。
'${ログネームスペース}': '${ログレベル}' という書式の記述をカンマ区切りで列挙できるような形式です。ログネームスペースはここではコロン区切りの blob となっていますが、実際にはどんな文字列でも構いません。

デフォルトのログレベルは default というキーで指定できます。

そして以下は、今し方作成した設定を NODE_ENV 毎に読みわけるためのファイルです。

config/index.js

function envShortName() {
  switch (process.env.NODE_ENV) {
    case 'production':
      return 'prod';
    default:
      return 'dev';
  }
}

module.exports = {
  logger: require(`./logger/config.${envShortName()}`),
};

サービスモジュール

次に、ロガーインスタンスを作成するようなサービスモジュールを作成します。まずは上で定義したような設定ファイルを読み込む機能は考慮しないコードです。

services/logger/index.js--(WIP)

const bunyan = require('bunyan');   // will be replaced to browser-bunyan on browser by next-bunyan

const isBrowser = typeof window !== 'undefined';
const isProd = process.env.NODE_ENV === 'production';
const isTest = process.env.NODE_ENV === 'test';

let stream = isTest
  ? require('./stream.test')
  : isProd
    ? require('./stream.prod')
    : require('./stream.dev');

// logger store
let loggers = {};


/**
 * determine logger level
 * @param {string} name Logger name
 */
function determineLoggerLevel(name) {
  if (isBrowser && isProd) {
    'error';
  }

  return 'info';
}

module.exports = (name) => {
  // create logger instance if absent
  if (loggers[name] == null) {
    loggers[name] = bunyan.createLogger({
      name,
      stream,
      level: determineLoggerLevel(name),
    });
  }

  return loggers[name];
};

まだこのファイルだけでは動作させることはできません。このファイルから参照するストリーム設定用のファイルが必要です。

ストリーム設定ファイル

まずは開発環境用のファイル。

services/logger/stream.dev.js

const isBrowser = typeof window !== 'undefined';

let stream = undefined;

// browser settings
if (isBrowser) {
  const ConsoleFormattedStream = require('@browser-bunyan/console-formatted-stream').ConsoleFormattedStream;
  stream = new ConsoleFormattedStream();
}
// node settings
else {
  const bunyanFormat = require('bunyan-format');
  stream = bunyanFormat({ outputMode: 'short' });
}

module.exports = stream;

開発環境では、ブラウザ上ではフォーマット済み文字列をブラウザコンソールに、node.js 上では同じくフォーマットされた文字列を実行中のコンソールに出力します。

次に本番環境用。

services/logger/stream.prod.js

const isBrowser = typeof window !== 'undefined';

let stream = undefined;

// browser settings
if (isBrowser) {
  const ConsoleFormattedStream = require('@browser-bunyan/console-formatted-stream').ConsoleFormattedStream;
  stream = new ConsoleFormattedStream();
}
// node settings
else {
  // do nothing
  // output JSON to stdout
}

module.exports = stream;

こちらはブラウザ上の設定は開発環境用と同じですが、node.js 上は stream 変数を undefined のままにしています。Bunyan のデフォルト挙動となり、stdout に JSON 形式で出力されます。

最後にテスト用です。

services/logger/stream.test.js

const bunyanFormat = require('bunyan-format');
const stream = bunyanFormat({ outputMode: 'short' });

module.exports = stream;

ここまでで、require('services/logger')('myapp:mymodule') のように呼び出すことで、myapp:mymodule ネームスペース用のロガーインスタンスを生成することができます。

このままでは最初に作った設定ファイル群が活きないので、services/logger/index.js に手を加えます。

services/logger/index.js

const bunyan = require('bunyan');   // will be replaced to browser-bunyan on browser by next-bunyan
const minimatch = require('minimatch');

const isBrowser = typeof window !== 'undefined';
const isProd = process.env.NODE_ENV === 'production';
const isTest = process.env.NODE_ENV === 'test';

let config = require('../../config').logger;

let stream = isTest
  ? require('./stream.test')
  : isProd
    ? require('./stream.prod')
    : require('./stream.dev');

// logger store
let loggers = {};


// merge configuration from environment variables
const envLevelMap = {
  INFO:   'info',
  DEBUG:  'debug',
  WARN:   'warn',
  TRACE:  'trace',
  ERROR:  'error',
};
Object.keys(envLevelMap).forEach(envName => {   // ['INFO', 'DEBUG', ...].forEach
  const envVars = process.env[envName];         // process.env.DEBUG should have a value like 'growi:routes:page,growi:models.page,...'
  if (envVars != null) {
    const level = envLevelMap[envName];
    envVars.split(',').forEach(ns => {          // ['growi:routes:page', 'growi:models.page', ...].forEach
      config[ns.trim()] = level;
    });
  }
});


/**
 * determine logger level
 * @param {string} name Logger name
 */
function determineLoggerLevel(name) {
  if (isBrowser && isProd) {
    'error';
  }

  let level = config.default;

  // retrieve configured level
  Object.keys(config).some(key => { // breakable forEach
    // test whether 'name' matches to 'key'(blob)
    if (minimatch(name, key)) {
      level = config[key];
      return;                       // break if match
    }
  });

  return level;
}

module.exports = (name) => {
  // create logger instance if absent
  if (loggers[name] == null) {
    loggers[name] = bunyan.createLogger({
      name,
      stream,
      level: determineLoggerLevel(name),
    });
  }

  return loggers[name];
};

かなりごちゃっとしていますが、やっていることは環境変数や設定ファイルの map で定義しているネームスペースごとにログレベルを変更しているだけです。

使ってみよう!

ここまでのコードで、

  • シナリオA
    • node.js (Express)

環境はカバーできたことになります。それでは使ってみましょう。以下のように定義、利用します。

app.js

const logger = require('services/logger')('myapp:app');

(..snip..)

// listen
expressApp.listen(process.env.PORT, err => {
  if (err) {
    throw err;
  }
  logger.info('> Ready on http://localhost:' + process.env.PORT + ' [' + process.env.NODE_ENV + ']');
});

以下のようなショートフォーマットされたログが出力されるはずです。

10:00:00.000Z  INFO myapp:app: > Ready on http://localhost:3000 [development]

require('../../../../services/logger') …あれ、まだ足りない?」

Java と違ってパッケージの概念がない JavaScript では、require や import 時の階層の解決は頭の痛い問題です。

そこで、packages.json に記述した _moduleAliases の出番です。これによってどの階層にあるファイルからでも @services/logger で先ほど作成したロガーモジュールを参照できます。

利用側のコードとしては、どのファイルからでも

const logger = require('@services/logger')('myapp:...');

という書き方で参照可能になります。

環境変数で設定を上書きする

本番ディプロイ後に思わぬトラブルに見舞われ、特定のモジュールのログを吐き出したい、という要求もあることでしょう。以下のように環境変数を設定することで、一時的にログレベルを変更することができます。

  • DEBUG=myapp:app,myapp:routes:*
  • TRACE=myapp:services:passport

キーがログレベルの大文字表現、値がログネームスペースのカンマ区切りとなります。

debug ライブラリ代替

既存の debug ライブラリによるログ出力行が山のようにあるプロジェクトに対して、一つ一つを新しいロガーの記述に書き換えていくのは大変です。

ここでまたpackages.json に記述した _moduleAliases が活躍します。
まずは package.json_moduleAliases に以下を追加しましょう。

package.json

"_moduleAliases": {
    "debug": "lib/services/logger/alias-for-debug"
},

そして alias-for-debug.js を作成します。

services/logger.alias-for-debug.js

/**
 * return 'debug' method of bunyan logger
 *
 * This is supposed to be used as an replacement of "require('debug')"
 *
 * @param {string} name
 */
module.exports = (name) => {
  const bunyanLogger = require('./index')(name);
  return bunyanLogger.debug.bind(bunyanLogger);
};

これで、debug ライブラリを利用して出力されたログは、Bunyan 内でログレベル debug として取り扱われます。具体的には、

const debug = require('debug')('myapp:routes:page');

debug('Debug message!');

のように利用していた部分はそのまま Bunyan で処理されるので、myapp:routes:page ログネームスペースの出力レベルを debug 以上にすることでログが出力されるようになります。

この機能を利用することで、debug ライブラリを利用してログ出力しているサードパーティー製ライブラリのログも、同じ設定ファイル(環境変数)でログの出し分けを設定することができます。

(おまけ) 開発時の Express ログ向け設定

Express のログをどう出したいかは好みになりますが、「ヒューマンリーダブルかどうか」を重視すると、morgan の出力ほど見やすいものはありません。しかもほぼコンフィグレスです。一方Bunyan を使って同等の情報量にしようとすると、設定が煩雑になります。

以下は、低速かつ JSON 出力できない morgan は開発環境のみで利用し、一方で本番環境では express-bunyan-logger を利用して高速に JSON 出力するような設定を行うコードです。

/**
 * initialize logger for Express
 * @param {object} expressApp
 */
initExpressLogger(expressApp) {
  const isProd = process.env.NODE_ENV === 'production';

  // use bunyan
  if (isProd) {
    const expressBunyanLogger = require('express-bunyan-logger');
    const logger = require('@services/logger')('express');
    expressApp.use(expressBunyanLogger({ logger }));
  }
  // use morgan
  else {
    const morgan = require('morgan');
    expressApp.use(morgan('dev'));
  }
}

Let's Try! ― シナリオB

シナリオBは、node.js (Express) + webpack/babel によるクライアントビルド両方で利用できる設定です。

といっても、ここまでに作ったファイルは完全に流用できますので、必要なのは webpack の設定のみです。

webpack.js

(..snip..)

resolve: {
  alias: {
    '@services/logger': 'services/logger',
    // replace bunyan
    'bunyan': 'browser-bunyan',
  }
},

(..snip..)

node.js 向けには package.json_moduleAliases で行っていたことを、webpack でも設定するというだけです。ただし、require('bunyan')require('browser-bunyan') に置き換わることになります。

使ってみよう!

使い方は全く同じです。あえて ES Module での import 風に書くと以下のようになります。

components/Component1.mjs

import loggerFactory from '@services/logger';
const logger = loggerFactory('myapp:components:comp1');

export default class extends React.Component {

  render() {
    logger.debug('test: debug: render() about');
    logger.info('test: info: render() about');

(..snip..)

Let's Try! ― シナリオC

シナリオCは、next.js 向けの設定です。

ここでもシナリオAで作ったファイルはそのまま流用できます。シナリオBで行った webpack 用の設定を、next.js 専用の設定ファイルの書式で書き直すだけです。

next.config.js

const path = require('path');
const withBunyan = require('next-bunyan');

module.exports = withBunyan({
  webpack: (config, { dev }) => {

    (..snip..)

    // resolve
    config.resolve.alias = Object.assign({
      '@services/logger': path.resolve(__dirname, './services/logger'),
    }, config.resolve.alias || {});

    // Important: return the modified config
    return config;
  },

(..snip..)

require('bunyan') => require('browser-bunyan') への置き換えは、next-bunyan がやってくれます。

使ってみよう!

シナリオBと全く同じですので割愛します。

まとめ

中編ではロギングに関して実際のコードを紹介し、node.js, webpack/babel, そして next.js という3つのシナリオ、3つの環境で理想的なロギングを行うことができるようになりました。柔軟なログネームスペース、ログレベルの設定や運用時の一時的な変更だけでなく、これまで debug ライブラリを使っていたコードベースに対しても二重管理することなく美しいログ出力ができるシステムを実現可能です。

手前味噌ですがこれらのコードは移植性も高いので、全てのプロジェクトにファイルをコピーして持っていくだけで同じ効能を得ることができます。チーム内のロギングに関する学習コスト節約にもなるのではないでしょうか。

次回、後編では、デバッガ利用の「理想の世界」を実現する具体的なコードを紹介していきます。

関連記事

tech.weseek.co.jp