WESEEK Tech Blog

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

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

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

こちらは 「Kubernetes 時代の CI/CD「Jenkins X」とは? 〜前編〜」からの転載です。



2018年3月に Jenkins を開発している CloudBee から「Jenkins X」というプロダクトが発表されました。

jenkins.io

社内で検証する機会がありましたので、この記事では前後編に渡って、CI/CD の解説から、Jenkins X を実際に使うまでに必要となった道のりや、使ってみての感想を書いていきたいと思います。

CI とは?

CI とは、Continuous Integration (継続的インテグレーション)の略です。
インテグレーション、と一言で言われても初めて聞く人にとっては何のことやらわからないと思いますが、アプリケーション開発中にエンジニアが頻繁に実施する以下の様なことを指します。

  • ビルド
    • ex.) コンパイルしてエラーなくバイナリなどの成果物を得る
  • テスト
    • ex.) 開発したアプリケーションをテストするコードを作成し、テストに記載された通りの動きになることを確認する
  • lint
    • ex.) 開発チーム内で予め決めたコーディングルールに則って、コードが書かれていることを確認する

ある機能を開発する際に Git などのブランチを新規に作成してから開発する、という昨今では当たり前のフローを取っていた場合、インテグレーション作業を以下の様なタイミング・内容で行いたくなるはずです。

  • master ブランチにブランチがマージされるたびに、ビルド、テスト、lint の実行結果が正常であることを確認したい
  • ブランチを切ってコードをコミット・プッシュするたびに、逐一ビルド、テスト、lint が正常であることを確認したい
  • テスト実行時には、モックではなく本番と同様のミドルウェアへ接続した上で、統合テストを実施したい

このような作業は、作成するアプリケーションの品質を向上させる効果があるため、可能であれば全て実施するべきです。

しかし、これだけの量の作業を毎度開発する人たちが手動で実行するのはとても大変です。
人間が手動で全てを実行する場合、開発に利用している端末のリソースを取られるので開発が止める必要があるかもしれません。また手動で実行しなければならないので、実行すること自体をそもそも忘れてしまうかもしれません。統合テストを実施するために、毎回環境を準備する作業も必要になります。テストを実行する環境(OS のバージョン、ミドルウェアのバージョン…)が厳密には揃っておらず、実行結果が異なるものになるかもしれません…などなど

これらの作業を自動化して、開発スピードの向上、成果物の品質向上を継続的に実現できるようにするものが、CI です。

CI を実現するプロダクトは本記事でも紹介する Jenkins や、SaaS としてサービスしている Travis CICircle CI など様々なものがあります。

CD とは?

CD とは、Continuous Delivery (継続的デリバリ)の略です。
前項で説明した CI を実施しているアプリケーションに対して、さらに頻繁なデリバリ(リリース)の実施作業を追加することによって、以下の様な効用が得られる、というソフトウェア開発における手法の1つです。

  • 一回のリリースでの変更点が少なくなり、リリース作業のリスク、リリースに伴うコスト、時間を抑えられる
    • リリース作業の自動化が実現している = リリース作業の定型化ができているとなるため、コスト・時間を大幅に抑えられます
  • アプリケーションを任意のタイミングでリリースできるようになる

アプリケーションのデリバリを頻繁に実施できるようにするためには、そのアプリケーションに関して CI が実施できていることが必須条件となり、それに加えて開発環境や本番環境などの異なる環境へのデプロイも自動化、または人間が判断できるタイミングを設けて、方針の決定後に素早く実行できる状態にしておく必要があります。

上記のような CD を実現するプロダクトは、Jenkins のほかに、SpinnakerGoCD などがあります。

Jenkins とは?

最初は Hudson という名前で Sun Microsystems の配下で OSS として開発されていましたが、Sun Microsystems がオラクルに吸収された後の 2011 年に Jenkins という名称でフォークし、現在は CloudBee の配下で OSS として活発に開発されています。

Jenkins は CI を実現するプロダクトとして人気を博しており、以下のような特徴を有しています。

  • Java Servlet アプリケーションとして開発されており、war ファイル 1 つで起動できる
    • docker イメージも存在し、公式にメンテナンスされているため、起動は本当に簡単です。
  • Jenkins 起動後に設定した内容は 1 ディレクトリ内に全て収まるため、バックアップが非常に用意しやすい
  • GUI 操作によるジョブの設定やテストレポートの表示が可能
  • 設定するジョブに対して、柔軟な設定が可能
    • ex.) リポジトリが更新されたら/X時間ごとにジョブを実行する、ジョブ実行後にメールや Slack で結果を通知する、など
  • 豊富なプラグインによる機能拡張が可能
  • Jenkinsfile によるジョブのコード化(Infrastructure as Code)を実現可能
  • ジョブの設定次第で CD ツールとしても運用可能
  • 動作が非常に安定している
    • 筆者の経験上の話ですが、ジョブの量が多かったり、プラグインが大量にインストールされていても、Jenkins 自体が原因で動作が不安定になったり遅くなったりしたところに遭遇したことがありません

Jenkins を CD ツールとして利用した場合の難点

上記で紹介した通り Jenkins は非常に柔軟で、CD ツールとしても利用可能ではありますが、実施したいことは自前でプラグインなりスクリプトなりを組み合わせてジョブとして設定する必要があります。

昨今流行している Kubernetes 環境への CI/CD を Jenkins で実現しようとした場合、以下の作業を自分たちで設定する必要が出てきます。

アプリケーションの開発者の範疇では、アプリケーションのビルド・テストまでは考えることができても、それ以外の項目を準備するのは非常に工数がかかる作業となってしまいますので、できればあまり手をかけずに準備できないものかと考えるはずです。

上記の問題を解消すべく、CloudBee が新たに発表したものが今回ご紹介する「Jenkins X」です。

Jenkins X とは?

Jenkins X は、以下のような環境でアプリケーションを開発する場合に限り、従来自前で考えて自動化しなければならない箇所をある程度簡単に実現できるようにしてくれます。

Jenkins X には、従来の Jenkins に加えて以下の機能が標準装備されています。

  • Jenkins 以外に CD として扱う上で必要となるツールのインストール
  • Git/Docker/Kubernetes を扱うためのプラグイン・Jenkins 設定のプリセット
  • Jenkins X を CLI から叩けるようにした jx
    • Kubernetes クラスタ・Jenkins X が動作する環境構築から、Jenkins X に設定したプロジェクトのビルド状況を確認するところまで CLI でできます

まとめ

本記事では、CI/CD 自体の解説、必要性から Jenkins/Jenkins X の概要について簡単に説明しました。

次回以降、実際に Jenkins X をインストールする手順を解説しつつ、何がインストールされるのか、どういう形で動くのかを記載していきたいと思います。