データをいい感じに活用する文化を育てる - アジャイルな言語化とその取り組み

JX通信社Advent Calendar 2019」3日目の記事です.*1

昨日は, @kimihiro_nさんの「Scala で書いたマイクロサービスを Go で書き直した話」でした.

改めまして, こんにちは. JX通信社でエンジニアをしています, @shinyorke(しんよーく)と申します.

今年の10月に入社してから, CTO室*2のデータ・エンジニアとして基盤の構築から運用のサポートまで色々しています. ちなみに趣味は野球のデータ分析です.*3

入社後最初のエントリーとして, 私が好きかつ情熱を持って育てているデータ基盤の話を語りたい...ところですが!?

  • 入社してプロジェクト内でやった言語化は記録・知見として残しておきたい.
  • 世の中, アジャイルなやり方・プラクティスは多数あれどデータ・サイエンスや分析という文脈での言及って案外少ない.
  • チームメンバー以外からの評判も良かったし今後もやりたい.

と思い, 今回は,

「データをいい感じに活用する文化を育てる」ためのアジャイルな言語化とその取り組み

というテーマでお送りしたいと思います.

なお, 予め断っておくと,

ほとんどがアジャイルのお話で, データサイエンス・データ基盤の話は一切出てきません.*4

文化作りのためにやったこと, これから育てるぞ!という意思表示として見て頂けると幸いです(&期待されてた方申し訳ない).

TL;DR

  • 「Be Agile(アジャイルな状態)」を目指す価値とし, 言語化の手段・プラクティスを状況によって変えていく, そして計測可能であることが重要.
  • 「アジャイルな状態」かどうかは, 常に「アジャイル・マニフェスト」と向き合う.
  • 手段・プラクティスは「言語化が必要な事柄の濃淡」に合わせていい感じに(選球眼が大切). 最後は見るべきメトリクス・データが言語化されるように引っ張る.

目次

Who am I ?(お前誰よ)

  • @shinyorke(しんよーく)
  • Pythonと野球, そしてアジャイルの人. *5
  • 認定スクラムマスター(CSM)持ってます&過去に在籍したチーム何社かでスクラムマスターをやってました.

アジャイルな言語化 #とは

アジャイルな状態とはなんぞや?という話を元にアジャイル・マニフェストをふりかえりつつ, 自分が心がけていること・やっていることを紹介します.

アジャイルな状態

これはシンプルに言うと,

黙っていても対話・動くソフトウェアを元に変化への対応ができている状態(なお手段は問わない)

と私は解釈しています. もうちょっと具体例を上げると,

  • フレックスやリモートで働いていても, チーム・メンバー間の対話・コミュニケーション(対話)が取れている
  • CIやテスト自動化等の手段で常に「動くソフトウェア」が手元に届く状態である
  • ダッシュボード(例えばRedash)もしくはBigQuery等のデータを見ながら, 対象ユーザーさん(つまり顧客)の状態を把握し, 上手く変化できている

という世界に近づいているか?ということです.

今挙げた例だと実現しているところもあればまだまだストレッチ・改善できる所もありますが, 日々起こる状況・ISSUEが何処に刺さる話か?を私は重要視してみています.

また, 手段を問わないのは重要で,

アジャイルな状態ができていれば, 手段はスクラムだろうがXPだろうがモブだろうがなんだっていいのです, 自分で作っても構わないし.

ぐらいに考えて手段・施策をアレンジして適用しています.*6

言語化も同様で,

  • 運用しているのでメトリクスを監視している
  • いま一生懸命開発してる
  • スケジュールは決まったけど何をどの順番で(ry
  • 何をやればいいんだったっけ?

という, チーム及びプロジェクトの解像度の違いでいい感じに変えていけばいいかなという感じでやりました.

言語化のフェーズを絵にした

そんなJX通信社のチーム&メンバーに合わせて言語化のフェーズ・選ぶ視点がこちらの絵です.

f:id:shinyorke:20191202153715p:plain
フェーズによる言語化の区分け

ここから先は, 実際に自分が入社してからやったこと(もしくはこれからやること)ベースで書いていきます.

言い換えると, 「オンボーディングされた側の記録」です笑

1. 「さっぱりわからん」期

やったこと:「ひたすらヒアリングとメモ」

入社した直後(10月), 初日と2日目ぐらいまで.

通常通りの入社手続きにPCの設定をしながら, MTGに参加してた時期です.

幸いにも, 同じデータ基盤をやるメンバーが既に関係各所にヒアリング*7したり, 必要な技術・使う予定のFWやライブラリをまとめていてくれたので,

  • ヒアリングの結果および, 関連する文章・データをひたすら読む
  • 上記結果で(自分的に)腹落ちしていない・理解していない箇所をSlackで聞く
  • 公開メモは自分のtimes(times_shinyorke)やちょっとした独り言を自分宛てDMで記録

といったことを繰り返し, 次のフェーズにつなぐ言語化をしました.

2. 「断片的に理解した」期

やったこと:「一枚の絵にする, 議論する. 具体的にはインセプションデッキを書いた」

これは入社2日目でした.

色々と読んだり聞いたりして,

「断片的に理解した, だが全体像(ry」

という感じだったので次のアクションを,

  • ひとまず体系化されたドキュメントにする
  • テンプレに埋めるだけかつ, あまりお絵かきとかしなくていいもの*8
  • 途中段階でも議論できるもの, つまり完成を目指さない

という所に目標を置きました.

「埋めるだけのテンプレかつ, アジャイルな状態に持っていくことができて完成を目指さなくてもいい感じにまとまる」ドキュメントといえばインセプションデッキやろ!

と思いついたのですぐ書きました.

github.com

インセプションデッキ自体は, ずっと昔から使っていて書き慣れているので, 割とすぐ書けました&物事を説明するのに,「エレベーターピッチ」のスライドがいい感じの「一枚の絵(見ればわかるだろというスライド)」になるので実にやりやすかったです.

これを元に, チームメンバー・関係者とMTGしたりSlack上で議論したりといい感じに成熟しました.

なお, 今回は「データ基盤としてDWHとかWorkflowを何とかする」とゴールがある程度決まっていたので使いませんでしたが, もっとフワッとしたお題目だった場合は,

  • ビジネスモデルキャンバス
  • リーンキャンバス

などで状況を整理するつもりでした.*9

3. そろそろキックオフを...

やったこと:「議論した結果を閉じる. インセプションデッキをブラッシュアップ&関連ドキュメントを作る」

これは入社3, 4日目くらい.

議論したり新たに出たイシューを,

  • 技術調査したり
  • ちょっとお試ししたり(プロトタイピングなど)

しながら一個ずつ解くような事をしていました.

ただ, これをずっとやってるといつまで経ってもキックオフができないので,

  • インセプション・デッキのフワッとした箇所をクローズする
  • チームのOKRとインセプションデッキをつなぐ*10
  • インセプションデッキでは足りない箇所を補足するドキュメントを書く

といったことをしました.

これで試すことをやりつつも, データ基盤としての開発をスタートしました.

4. 具体化しないと開発できないよ

やったこと:「コードを書ける, すなわち開発できる状態にする. ざっくりなデザインドックを書く」

開発可能な状態(これは入社数週間ですね)になった時点で,

  • プロダクトの構成図だったりなんの技術使うかを言語化する
  • 試してOKな事, NGだったこと整理
  • そもそも仕様を書かないと(ry

というわけで,

  • 必要な項目のみに絞ったデザインドックを書く*11
  • 完成したもの, トライアルで動かせるものから順に仕様書を書く(例:テーブル定義など).

を作り, 順次公開しました.

この当たりのコツは, 「必要になったら作りはじめる, 早すぎる最適化はしない」といったあたりで,

  • 変更の可能性があるところはギリギリまで「言語化作業」の着手を遅らせる
  • 時が来たら, テンプレを決めて(作って)言語化

最後の数日でまとめてやったジャスト・イン・タイムで出していきました.*12

5. データ活用に向けて - ローンチから計測へ

これからやる:「監視可能な指標を提示し常に可視化できる状態をつくる」

お察しの通り, 今と未来のはなし, です笑

先月ようやっとデータ基盤がいい感じに使える状態になりました.

この先は,

  • プロダクト・サービスなど, 分析・監視対象に対してメトリクスを定義する
  • メトリクスを常に見れる状態にする
  • メトリクスを元に対話し, 変化に順応する

状態をデータ基盤を中心に作っていくことになります.

これらの打ち手はまだふわっとしていますが,

  • 既存のダッシュボード(Redash)をいい感じに使うとか
  • もっと別の可視化・言語化手段を活用するとか
  • 可視化やデータ分析をいい感じに楽しくやる習慣を作る

がメインになっていくかなと思っています.

一言で言うと,

計測可能なメトリクスをファクトとしていい感じに回せる世界

を作っていけたらと思います, 今もできてる所ありますがまだまだストレッチできるかなと.

この辺は未来の話なのでワクワクと苦労両方あると思いますがやっていきたいお気持ちです.

まとめ

というわけで, 自分がやってきたこと・これからやることを言語化しました.

この先も色んな状況やワクワクが待っていると思いますが,

「Be Agile(アジャイルな状態)」を目指す価値とし, 言語化の手段・プラクティスを状況によって変えていく, そして計測可能であることが重要.

という価値観・想いを元にやっていきたいと思います.

最後までお読みいただきありがとうございました.

次の方へ

次は@YAMITZKYさんの「Apollo、Prisma」あたりの話です.

【Appendix】参考文献

これらの思想の・施策の元になったものをいくつかご紹介します.

*1:同時に, 「DevLOVE Advent Calendar 2019」3日目でもあります.

*2:SRE(インフラ)やデータ基盤といった, サービス・プロダクトに紐付かない全社的な機能をサポートする横断的な部門です.

*3:自分のことはだいたいこの辺に書いてます.

*4:データ基盤やサイエンスの話は後日別エントリーで数本書く予定です, そちらを乞うご期待.

*5:この後いくつか引用も出てきますが, 「Lean Baseball」というブログでこの辺を色々と書いてます.

*6:これは実は前職から心がけていることで, 一つの例として昨年こんな発表をしました.

*7:これがホントに大ファインプレーでした, 一緒にやったチームメンバーに感謝しかない.

*8:余談ですが私はスライドを作るのが死ぬほど嫌いなので, なるべく手抜きしたい・コード書きたいという気持ちもありました.

*9:社内基盤だとユーザーが社員もしくは関係者, 解くべき課題も大体明確なのでインセプションデッキがいきなり作れそう, と判断しました.

*10:JX通信社では全社的にOKRを採用しています.

*11:文脈的には過去にやった仕事を参考にしました.

*12:必要になるまで読まれないのは辛いし何より「作ってから変更」が入ると, コードを書くのとドキュメント更新するのと二重苦を味わうので勘どころ大事です. なお誤解を招かないように言うと「ドキュメントはいらない」という意味ではありません.

Scala で書いたマイクロサービスを Go で書き直した話

この記事はJX通信社 Advent Calendar 2019 2日目の記事です。
昨日は、たっちさんの「Kubernetes Admission Webhookでリソース作成を自在にコントロールする」でした。

f:id:nsmr_jx:20191129095710p:plain

こんにちは、サーバーサイドエンジニアの @kimihiro_n です。 今回は長年動かしてた Scala のマイクロサービスのリビルドを行った話をしようと思います。

TL;DR

  • 新しい言語を投入するのにマイクロサービスは便利
  • Scala で感じていた問題点を解消しつつ Go へ移行できた
  • 消費メモリが大きく減って安定稼働できるようになった 

予防線を貼っておきますと、Scala より Go のほうがいいよね、といった本旨ではありません。

Scala で書いたマイクロサービス

弊社のマイクロサービスの一つにカテゴリ分類専用のサービスが存在します。 カテゴリやキーワードを登録しておくとルールベースでカテゴリ分類を行うもので、自然言語やMLを使うほどでもない大雑把な分類を低コストで実現するためのサービスです。

持ってる機能としては

  • カテゴリやキーワードを REST 形式で CRUD 操作できる
  • テキストを分類エンドポイントへ POST するとカテゴリ分類が実行される

というシンプルなものです。2015年4月に作り始めたのでもう4年以上稼働しているシステムになります。

このシステムは最初 Scala で作り始めました。当時社内で Scala 勉強会というのが行われており、そこで学んだ知識の実践のために Scala を使って実装をはじめました。 独立したサービスなので新しい言語を持ち込むには最適のタイミングだったと思います。 また計算的な処理も多いのでメインで使っている Python よりもパフォーマンス出せるのではないかという期待もありました。

いろいろあったものの (本筋ではないので省略) 無事サービスとしてリリースでき、今日まで稼働してくれました。 ただ4年間動かす上でインフラ周りの大きな変遷がありました。

インフラの変遷

f:id:nsmr_jx:20191129100411p:plain:w120 f:id:nsmr_jx:20191129100459p:plain:w100

当初は専用の EC2 を建てて、そこに Ansible でデプロイするという形式で動かしてました。インスタンス自体の管理をしなくてはならないものの、サーバーのリソースを専有して動かせるので安定して動かす事ができていました。

その後、Docker が全社的に流行りだし、このサービスも Docker の上で走るよう変更しました。ビルドした war ファイルを Docker Image に乗っけるだけだったので移行はスムーズでした。

Docker の運用も最初は ElasitcBeanstalk を使って専有インスタンスで稼働していましたが、会社の ECS の基盤が整ってからは共有のサーバーの上で動くようになりました。

共用サーバーになって顕在化してきたのが Scala のメモリ使用量の問題です。 1タスクをメモリの割当量が256MBでは安定して動かすことが出来ず OOM が頻発してしまいました。512MBや1024MB消費することもあったので実装上のメモリリークも疑わしかったのですが、そもそも最低専有量が大きくて共用サーバーでは扱いづらいサービスとなっていました。 Docker の上に JVM を載せてその上にアプリケーションを載せる2階建ての構造になっているので、どうしてもオーバーヘッドが大きくなってしまうみたいです。 (最近は GraalVM のようなものが出てきて事情が変わりつつあります。)

その他 Scala で発生していた問題

またメモリ以外に Scala を扱う上で発生していた問題がいくつか。

コンパイルが遅い

開発のタイミングで一番の課題はこちらでした。インクリメンタルビルドがあるものの、多くの場合コンパイル待ち・テスト待ちで手が止まってしまっていました。フルにビルドが走ってしまうと10分くらい待たされることもありました。普段はインタプリタの Python メインで書いていたのでなおさら待ち時間への戸惑いを感じました。

書き方の統一が難しい

Scala はとても高機能な言語です。オブジェクト指向でありながら関数型のパラダイムを取りこんでいる特徴を持ってます。 それ故にプログラマの Scala 熟練度によって書き方が変わってしまう問題がありました。 Better Java 的な書き方から、より Scala らしい書き方、関数型を意識した書き方などいろいろな表現方法があります。 プログラムを書く上で「こういう書き方あるよ」と教えてもらってスキルアップできるのはとても楽しいですが、システムを保守する上でネックになりがちでした。 全社的に Scala の知見があって、コーディングの方針が統一できていればよかったのかもしれませんがそこまでの体制は作れませんでした。

JVM の知見が少ない

先程のメモリの話にも共通することですが、会社として JVM のサーバー運用に対して知見が溜まってなかったというのもあります。メインで使っている Python であればサーバーを動かすための知見が溜まっていて安定的に動かすことができたのですが、JVM のシステムは社内初だったため不安定になったり、問題発生時に適切な対処が取りづらかったです。

一時期新サービスは Scala で書いていこうみたいな勢いがありましたが、これらの問題のためか結局 Python メインに戻ってしまいました。

Go でリビルド

Python は個人的にもしっくりきていてとても扱いやすい言語だと思っているのですが、プロジェクトの規模が大きくなってくるにつれ「型」の重要性が増してきました。静的な型付けがあると、それ自身がコードのテストになり実行時の予期せぬエラーを減らしたり、型の情報を生かして IDE による効率的な開発が出来たりします。 Python にも Type Hints と呼ばれる型のサポートがありますが、エディタによってサポートのばらつきがあったり、間違った型付けをしていても mypy を通さない限り気づかなかったりしてイマイチです。

そこで目をつけたのが Go でした。 静的型付け言語の候補はいくつもあるのですが、コンパイルが速く Docker とも相性がよい言語を探してるうちに Go がよさそうなのでは、と思うようになってきました。 言語仕様もシンプルで書き手に左右されづらかったりと Scala のときに感じていた課題点を解消できそうな点もよさそうでした。あとコンパイルの速さとかコンパイルの速さなんかも大事ですね。 最終的に Go やろうと決め手になったのは社内で Go を利用しているプロジェクトがすでにある点でした。社内にすでに触れる人が複数名いるというのは非常に心強いです。

Tour of Go をこなして基本を覚えたあと、言語を覚えるには実践が一番ということで Go でかけそうなシステムを探してました。 そこで出てきたのが例の Scala のマイクロサービスです。独立したサービスで改修しやすく、APIの仕様も固まっており規模もそんなに大きくないという理由から、カテゴリ分類サービスを書き直してみることにしました。

やったこと

仕様が固まっていたので BDD (振る舞い駆動開発) を採用してテストコードを充実させながら進めていきました。

BDD のフレームワークを使うと

    シナリオ: カテゴリ一覧がみれる
    前提 insert_categories.sqlの中身がDBにセットされている
    前提 メソッドがGET
    もし /v1/categoriesへアクセスする
    ならば ステータス200が返ってくる
    かつ レスポンスのカテゴリが1件入っている

みたいな形で振る舞いを記述することができます。 この形式自体は Gherkin と呼ばれる形式で、プログラムの実装言語によらず共通して定義することができます。 日本語のドキュメントのような形なので非エンジニアでも何をしたいのかが明確になるメリットがあります。

Go の場合 DATA-DOG/godog gucumber/gucumber といった BDD のテストフレームワークが存在します( Python だと behave というものがあります)。 今回はDATA-DOG/godog を利用してテストを書いていきました。

実際テストを書くときは、上記のシナリオの 1 行 1 行(=step) に対応するコードを準備します。

たとえば「ステータス200が返ってくる」に対応するコードだと以下のようになります。

func (c *Context) CheckStatus(status int) error {
    if c.resp.Code != status {
        return fmt.Errorf("status_code %d is not %d", c.resp.Code, status)
    }
    return nil
}

200 の部分はパラメータとして利用できるので汎用的なテストコードを実現することができます。 BDD のフレームワークを利用すると嬉しいのが、振る舞いの記述を追加してもその分だけテストコードが増えるわけではないという点ですね。同じ名前の step は同じ関数が再利用できるので、開発が進むほどテストコードに割く時間が減ってくる特徴があります。

Web のフレームワークは Echo を使ってみました。Go の場合フレームワークを使わなくてもいいみたいな話を聞きましたが、REST API の場合、GET や POST, PUT... といったメソッドレベルでのルーティングが必要となってくるため、フレームワークを利用して見通しをよくするようにしました。

Go を書いていて思うのはやはりコンパイルが速くて快適ということですね。テスト込みでも数秒で終わってしまうので TDD、BDD といった手法と相性がいいです。 あとは Go 言語は筋肉が要求されるというのが実感できました。例外処理もないので、愚直にエラーをチェックして返り値にセットするみたいなのを頻繁に書く必要があります。 ただ慣れてくると返り値を多値にして error を返すという仕様がとても自然に思えてくるようになってきました。 例外で横道から抜けられてしまうよりはちゃんと関数の結果として返ってくるほうが考慮漏れずに記述できる感じがします。 記述が長くなってしまうことについては「筋肉、筋肉…」と思うことで納得しています。

サービスリプレース

業務の合間の気分転換にちょっとづつ進めること約半年、ついにサービスが完成しました。 DB まわりのテストどうするか悩んだりきれいな設計になるようリファクタしてたりしてたら結構かかってしまいました。

Docker 化

FROM golang:latest as builder

ENV CGO_ENABLED=0 \
    GOOS=linux \
    GOARCH=amd64 \
    GO111MODULE=on

WORKDIR /opt/app
COPY . /opt/app
RUN go build

# runtime image
FROM alpine
COPY --from=builder /opt/app /opt/app

CMD /opt/app/basic_classifier

ECS に載せるため、Docker のイメージを作ったのですが Dockerfile がとてもシンプルでした。 バイナリを生成できてしまうと強いですね…。最終的なイメージサイズもかなり軽量です。 ECS だと起動のタイミングでイメージの Pull が走るので、イメージが軽いことは起動の高速化にも繋がります。

デプロイ

いよいよ実戦投入です。ECS で別タスクとして作っておき簡単にロールバックできるようにして切り替えました。

問題なく動いてる…と思いきや大きな問題が。 カテゴリ分類サービスを利用してるシステムの一部で予期せぬ挙動が起こってしまいました。 原因を精査してみたところ、API の仕様が変化しているのが元凶でした。 パラメータ名がスネークケースではなくキャメルケースという…。 サービス作成時に策定した仕様書に沿って実装していたのですが、Scala 版の実装のほうが間違っていて実装しており、利用側も実装を正に進めていたため、切り替えのタイミングでトラブルが起きてしまってました。

切り戻してシステムを改修したところ無事動くようになりました。

パフォーマンスの変化

実際 Scala から Go に置き換えてみてメモリ消費量がどうなったかというと

f:id:nsmr_jx:20191129101331p:plain

のようにリリースを境に大きくメモリ消費量を減らすことが出来ました。
(Scala 版右肩上がりなのでメモリリークも疑わしいです…) クラスタに余裕ができたので台数減らしたり、その分他のサービスを載せることができそうです。

一応 Apache Bench を使って簡易的なベンチマークもとってみました。

f:id:nsmr_jx:20191129101309p:plain

軽めのテキストの分類
Scala: Requests per second:    97.24 [#/sec] (mean)
Go: Requests per second:    118.89 [#/sec] (mean)

重めのテキストの分類 
Scala: Requests per second:    20.37 [#/sec] (mean)
Go: Requests per second:    29.34 [#/sec] (mean)

Go 書くときパフォーマンスはあまり意識せず書いていたので、Scala より性能が上がるのは嬉しい誤算でした。 分類結果は一致しているので大きくロジックを変えたつもりはないですが、コード的に完全移植したわけではないのでこのベンチマークは参考程度に見てもらえると。 Scala 強い人が書いたら十分逆転もありえそうです。

おわりに

1機能が独立して動くマイクロサービスみたいな構造だとこういった置き換えが気軽にできていいですね。 サービスまるまる実装してみて Go への理解がだいぶ進んだように思います。 パフォーマンスや使い勝手も悪くないのでこれから Go どんどん触っていきたいです。

明日は@shinyorkeさんの「データをいい感じに活用するためのアジャイルな言語化とその取り組み」です。

Kubernetes Admission Webhookでリソース作成を自在にコントロールする

この記事はJX通信社 Advent Calendar 2019の1日目の記事です。

こんにちは、SREのたっち(TatchNicolas)です。

先日開催されたKubeCon 2019でもセッションで紹介されていた、Admission Webhooksについて書きます。

Admission WebhooksとはKubernetesリソースを操作(CREATE/UPDATE)する時に、作成や変更の内容をチェックしたり、書き換えたりすることができる機能です。

TL;DR

  • Admission Webhooksを使うと、あらゆるKubernetesリソースの操作をトリガーに 「チェック(Validation)」「変更(Mutation)」 を行える
    • 身近なところでは、Istioでサイドカーのauto-injectionで使われています
  • どの種類のリソースにどんな操作をするときにWebhookを呼ぶかは細かく条件指定ができる
  • 最低限自分で実装すべきは非常にシンプルなWebサーバのみで、簡単に試すことができる
  • 実運用するには冪等性、可用性、適用範囲など考えることが色々

サンプルコードは以下に置いてあります。

github.com

やってみる

f:id:TatchNicolas:20191129002303p:plain
Kubernetes Admission Webhooksの位置付け

今回は、以下の二種類のAdmission Webhooksを作ってみます。

  • 「Pod作成時にenv ラベルにdev,stg,prdのいずれかが付与されているかチェックする、なかったらリソース作成を拒否する」 = ValidatingWebhook
  • 「Pod作成時にリソースの名前にある接頭辞を付与する」 = MutatingWebhook

Kubernetes環境はAdmission Webhooksのbetaが取れている v1.16を使います。今回はMinikube v1.4.0を利用しました。*1

Webhookを書いてServiceとして動かす

では早速、Webhookを実装しましょう。要は、kube-apiserverからJSONを受け取ってJSONを返すことができれば良いので言語やフレームワークは何でも良いです。 前述の通り今回は手軽さ最優先でFlaskを使います。

最低限のValidationは非常にシンプルで、以下のようなレスポンスを返すだけです。allowedtrue であればValidationは成功し、false であれば失敗します。

{
  "apiVersion": "admission.k8s.io/v1",
  "kind": "AdmissionReview",
  "response": {
    "uid": "<value from request.uid>",
    "allowed": false
  }
}

実際に書いたコードはこちら。今回はstatus フィールドにメッセージを入れてみました。たとえばkubectlによるPod作成時にValidationに失敗した場合、このメッセージが返ってきます

これをDeploymentとしてデプロイし、Serviceとして叩けるようにします。*2

今回は同じクラスタ内から利用するのでClusterIPだけで十分です。 真面目にやるなら、目的の異なるWebhookは別にデプロイしたほうが良いと思いますが、今回はお試しということで一つのアプリケーションとして書いてしまいました。

Admission WebhooksはHTTPS必須なので、Podを作る前にまずcfsslを使ってオレオレ証明書を作成します。

$ cat <<EOF | cfssl genkey - | cfssljson -bare server
{
  "hosts": [
    "sample-admission-webhook.default.svc"
  ],
  "CN": "sample-admission-webhook.default.svc",
  "key": {
    "algo": "ecdsa",
    "size": 256
  }
}
EOF
2019/11/27 18:28:38 [INFO] generate received request
2019/11/27 18:28:38 [INFO] received CSR
2019/11/27 18:28:38 [INFO] generating key: ecdsa-256
2019/11/27 18:28:38 [INFO] encoded CSR

server-key.pemserver.csr が作成されます。続いて、Webhookに持たせるための証明書を作ります。

$ cat <<EOF | kubectl apply -f -
apiVersion: certificates.k8s.io/v1beta1
kind: CertificateSigningRequest
metadata:
  name: sample-admission-webhook.default
spec:
  request: $(cat server.csr | base64 | tr -d '\n')
  usages:
  - digital signature
  - key encipherment
  - server auth
EOF
certificatesigningrequest.certificates.k8s.io/sample-admission-webhook.default created
$ kubectl certificate approve sample-admission-webhook.default
certificatesigningrequest.certificates.k8s.io/sample-admission-webhook.default approved

base64エンコードしてダウンロードし、そこからSecretリソースを作ります。

$ kubectl get csr sample-admission-webhook.default -o jsonpath='{.status.certificate}' | base64 --decode > server.crt
$ kubectl create secret tls --save-config sample-admission-webhook-secret --key server-key.pem --cert server.crt
secret/sample-admission-webhook-secret created

最後に、このSecretを使ってDeploymentを作成し、それに紐づくServiceも作成します。

apiVersion: apps/v1
kind: Deployment
metadata:
  name: sample-admission-webhook
spec:
  replicas: 1
  selector:
    matchLabels:
      app: sample-admission-webhook
  template:
    metadata:
      labels:
        app: sample-admission-webhook
    spec:
      containers:
      - name: app
        image: tatchnicolas/sample-admission-webhook:0.10
        imagePullPolicy: Always
        ports:
        - containerPort: 8080
        volumeMounts:
        - name: tls
          mountPath: /tls
        command: ["gunicorn"]
        args: 
        - "main:app"
        - "--bind"
        - "0.0.0.0:8080"
        - "--access-logfile"
        - "-"
        - "--certfile"
        - "/tls/tls.crt"
        - "--keyfile"
        - "/tls/tls.key"
      volumes:
      - name: tls
        secret:
          secretName: sample-admission-webhook-secret
---
apiVersion: v1
kind: Service
metadata:
  name: sample-admission-webhook
spec:
  selector:
    app: sample-admission-webhook
  ports:
  - port: 443
    protocol: TCP
    targetPort: 8080

Webhookの設定を登録する

さて、それではいよいよAdmission Webhooksの設定を登録します。必要なManifestは至ってシンプルです。

caBundle フィールドにてクライアント(=kube-apiserver)がWebhookへリクエストを送る際に信用する証明書を指定することができます。

まず、ValidatingWebhookのほうから登録してみましょう。

apiVersion: admissionregistration.k8s.io/v1
kind: ValidatingWebhookConfiguration
metadata:
  name: "sample-validating-webhook"
webhooks:
- name: "sample-validating-webhook.hoge.fuga.local"
  failurePolicy: Fail
  rules:
  - apiGroups: [""]
    operations: ["CREATE"]
    apiVersions: ["v1"]
    resources: ["pods"]
    scope: "Namespaced"
  clientConfig:
    caBundle: <server.crtの中身をbase64エンコードして貼る>
    service:
      namespace: default
      name: sample-admission-webhook
      path: /validate
  admissionReviewVersions: ["v1", "v1beta1"]
  timeoutSeconds: 5
  sideEffects: None

続いてMutatingWebhookも同様に

apiVersion: admissionregistration.k8s.io/v1
kind: MutatingWebhookConfiguration
metadata:
  name: "sample-mutating-webhook"
webhooks:
- name: "sample-mutating-webhook.hoge.fuga.local"
  failurePolicy: Fail
  rules:
  - apiGroups: [""]
    operations: ["CREATE"]
    apiVersions: ["v1"]
    resources: ["pods"]
    scope: "Namespaced"
  clientConfig:
    caBundle: <server.crtの中身をbase64エンコードして貼る>
    service:
      namespace: default
      name: sample-admission-webhook
      path: /mutate
  admissionReviewVersions: ["v1", "v1beta1"]
  timeoutSeconds: 5
  sideEffects: None

Manifestを適用したら、リソースが作成されたことを確認しましょう

$ kubectl get validatingwebhookconfigurations
NAME                        CREATED AT
sample-validating-webhook   2019-11-28T05:58:28Z
$ kubectl get mutatingwebhookconfigurations
NAME                      CREATED AT
sample-mutating-webhook   2019-11-28T05:39:28Z

動きを確認する

では、実際に動作確認してみましょう。

apiVersion: v1
kind: Pod
metadata:
  name: webhook-demo
  labels:
    env: stg
spec:
  containers:
  - name: nginx
    image: nginx

こんなManifestからPodを作ってみましょう。

$ kubectl apply -f pod.yaml
pod/dummy-prefix-webhook-demo created
$ kubectl get pod
NAME                                        READY   STATUS              RESTARTS   AGE
dummy-suffix-webhook-demo                   0/1     ContainerCreating   0          2s
sample-admission-webhook-7ddfc8c784-6pf7k   1/1     Running             0          6m6s

Pythonで書いた処理の通り、名前が書き換わっています。

Validationのほうもチェックするために、env ラベルが dev,stg,prd のいずれにも一致しないPodを作ってみましょう。

apiVersion: v1
kind: Pod
metadata:
  name: validation-demo
  labels:
    env: willberejected
spec:
  containers:
  - name: nginx
    image: nginx

設定した通りのエラーメッセージが返ってきており、Podも作られていないことが確認できます。

$ kubectl apply -f invalid_pod.yaml
Error from server: error when creating "invalid_pod.yaml": admission webhook "sample-validating-webhook.hoge.fuga.local"
denied the request: Required label env is not set or its value is invalid

はまったところ

  • k8s v1.14.8で利用できるバージョン(admissionregistration.k8s.io/v1beta)では、リクエスト先 portオプションを指定するとUnknown fieldって怒られる(サンプルには書いてあるのに...)
  • Podに対してWebhookを書くと、DeploymentやReplicaset経由で作られるPodについてもValidation/Mutationの対象となってしまうので、Webhookを開発中にそのDeploymentを更新したらWebhook用のPod自体がValidationに引っかかってコケてしまった
    • あとは kube-system ネームスペースのPodに対して悪さをされると困るので、GitHubのサンプルコードでは namespaceSelector objectSelector を使って対象を絞っています

感想

オレオレ証明書の準備等の手間はかかりましたが、Webhook自体は非常にシンプルに作ることができました。Kubernetes自体にコンパイルされるAdmission controllerを作るのは非常にハードルが高いですが、こうして疎結合に機能を拡張できるのはとても魅力的ですね。

Webhook自体の可用性が低いと条件に合致するリソースの操作全体に影響してしまったり、MutatingWebhookConfigurationのほうは「変更を加える」という性質上、複数回呼ばれてしまった場合に備えて処理を冪等にする必要があったり、本番運用の際は慎重になる必要があります。

しかし、HelmやKustomize等を使うのに比べてより根っこに近いところでガバナンスを効かせることができるので、Admission Webhooksは非常に強力な機能だと思います。上手に使って、リリースや運用の負荷を下げていきましょう。

参考資料

*1:執筆時点で最新のMinikubeにはregistry-credsのaddonに不具合があり、Docker Desktopではk8sバージョンがv1.16いなかったため

*2:ServiceのほかにURLによる指定でクラスタ外のWebhookを呼ぶこともできます

potatotips #66 (iOS/Android開発Tips共有会) に登壇してきました

f:id:numatch-jx:20191120000627j:plainこんにちは、Androidエンジニアのぬまっちです。(@nuMatch)
半年ほど前からFlutterエンジニアとしてもデビューしてるので、今はAndroid/Flutter (iOS) エンジニアという感じでしょうか。

あっという間に冬が近づいてきました。 夏にはFlutter布教の為、社内でFlutterモブプログラミング回をしたのが懐かしくなります。 tech.jxpress.net

今回はyappliさんで開催されたpotatotips #66 (iOS/Android開発Tips共有会)にてLTしてきましたので、その紹介をさせて下さい。

potatotips.connpass.com

登壇前

今回、自分がLTで紹介したかったメインテーマは「怖くないよFlutter」でした。
iOS/Android開発におけるTipsがメインの勉強会なので、
「Flutterって最近良く聞くけど、まだ片方のプラットフォームしか触ったことないな」
って方を対象に、Androidエンジニア視点でFlutterに入門したらクロスプラットフォームが身近になった、という紹介をしたいと思いました。

という事でタイトルは
「Android エンジニアが Flutterに入門して驚いたこと3点」
です!

今回、久しぶりの登壇(JXエンジニアになってからは初)という事もあって準備・練習の段階から社内エンジニアに手伝って頂きました。
この場を借りて感謝!です!

f:id:numatch-jx:20191120000627j:plain

LT

実際に登壇で使った資料はこちらになります。

speakerdeck.com

怖くないよ flutter doctor

Flutter入門する方がほぼ必ず叩くだろうコマンド「$ flutter doctor」について紹介しました。

FlutterSDKのインストールが完了し、PATHが通っていれば flutter doctor コマンドを実行することが出来ます。
flutter doctor コマンドは、開発環境がFlutterで開発できるか診断してくれる便利な機能です。

で、試しに叩いてみるとかなり怒られると思います。
AndroidまたはiOSどちらかのプラットフォームのみの開発者なら、Android Studio / Xcodeが片方しか入ってない事も多いかと思いますが、両方揃えないといけません。
特にAndroidエンジニアの方はiOSの開発環境が整っていないと思います。
HomebrewやCocoaPodsも足りないと怒られますが、ちゃんと順番に何をすればいいのか指示してくれるので怖がる必要はありません。

言われた通りにコマンドを叩いていくとFlutter開発の為の環境が作られるはずです。
自分はAndroid StudioでiOSのシミュレータが立ち上がるのを体験した時、軽く感動を覚えました。

入れやすいよ Plugin

自分が声を大にして言いたい、

「FlutterにおけるPluginの導入にしやすさ」

について語らせて頂きました!
何故ここまでピックアップするかというと、理由が大きく2つあります。

  1. 検索のしやすさ

使いたいPluginを探したい時は、まずpub.dev/flutterで検索しましょう。( Flutter packages

LTではAndroidにおける値の永続化の入門であるSharedPreferenceを例にとって紹介しました。

(紹介したPluginはこちらになります↓)

shared_preferences | Flutter Package

検索窓でSharedPreferenceと入力すれば、現在導入可能なSharedPreference用Pluginの一覧が出てくると思います。
似たような名前のPluginが沢山あると思いますが、選ぶポイントとしては

  • 「flutter.dev」の公式であるかどうか
  • 「performance score」が100、または100または100に近いかどうか

が大事かと。

  1. クロスプラットフォームのしやすさ

導入したいPluginが決まったら「pubspec.yaml」に追加記述しましょう。
記述したらflutter pub getコマンドを叩く事をお忘れなく。

基本的にはこれだけです。

PluginによりますがSharedPreference用Pluginではプラットフォーム固有の設定は不要で、導入の準備が完了になります。
AndroidのManifestファイルへの記述や、iOSのPodfileへの書き込みいらずになる事が多いのは大変ありがたいです。
「pubspec.yaml」で両プラットフォーム対応が完結するのはFlutterの大きな強みになると思います。

Flutter と Firebaseの相性の良さ

クロスプラットフォーム入門としてFlutterとFirebaseは抜群の相性を誇ります。
Flutter公式で専用のPluginを用意されていますし、
(他にも各FrebasePackage用のPluginが用意されています。)

ドキュメントは日本語が用意されています。
Flutter アプリに Firebase を追加する  |  Firebase

今回のLTでは時間が足りずに詳細は語れませんでしたが、

  • Firebase Authentication
  • Firebase Cloud Messaging

については後日、JX通信社のアドベントカレンダーにて紹介したいなと思っています!

qiita.com

@nuMatchの担当する回をお待ち頂ければ幸いです!

登壇してみて

振り返ってみると、今回のLTでFlutterのTipsを紹介していたのが自分だけだったのが印象的でした。 また、Twitterで参加者の方の感想で
「聞きやすかった」、「Flutterはまだ入門したことないので、定期的に紹介されるのは嬉しい」
との声を見かけたので、準備した甲斐があったなと嬉しく思いました。

今回はFlutterにおけるクロスプラットフォーム入門についての登壇でしたが、次は実際にプロトアプリ開発で出会った事例を元にした内容でLT出来ればと思います!

開発合宿@箱根湯本

f:id:jxpress:20191008062416j:plain

はじめに

こんにちは!CTOの柳です。今回のブログでは、9月某日に行われたJX通信社恒例の開発合宿について紹介したいと思います。

これまで、JX通信社では年1回のペースで開発合宿を実施してきました。

tech.jxpress.net

www.wantedly.com

合宿のテーマ

毎年、弊社の開発合宿では、さまざまなテーマに取り組んできました。社内環境・ツールの整備、CI/CDの導入、去年はビジネスサイドのメンバーを巻き込んだデータ分析講習会や普段できない業務改善について取り組みました。

今年はCTOである私が幹事を引き受けたので、職権乱用メンバーを説得し、より尖ったテーマに取り組みました。ずばり「rebuild」です!

組織とプロダクトが大きくなっていく中、普段の業務では慣れた技術スタックばかり使うようになってしまいます。世の中に面白い技術が続々と登場しても、中々触れる機会がありません。

そこで、合宿では「新しい技術スタックで、既存プロダクト1つを0から作り直すこと」(= rebuild)にチャレンジしました。

合宿までの準備

実りのある合宿にするために、準備は怠りません。

合宿参加者を開発領域(フロントエンド、バックエンド、アプリ、etc..)ごとにチームを分け、それぞれの領域の技術選定や設計をしてもらいました。

また、定期的に、合宿参加者が全員集まり、

  • 目指すべきMVP(Minimum Viable Product)はどういったものか

  • 合宿中に取り組むPBI(Product Backlog Item)は何か

について話し、意識のすり合わせを行いました。

箱根温泉「ホテルおかだ」

今年は箱根湯本にある「ホテルおかだ」さんにお邪魔しました。宿を決めるにあたり、株式会社Loco Partnersさんと株式会社FiNCさんのブログを参考にしました!この場を借りて御礼申し上げます。

relux 開発合宿 in 箱根湯本 - wootan's diary

FiNC 開発合宿 in 箱根! | FiNC Developers Blog

合宿の様子

f:id:jxpress:20191008062706j:plain

ロマンスカーに乗っていざ出発。

f:id:jxpress:20191008063003j:plain f:id:jxpress:20191008063015j:plain f:id:jxpress:20191008063027j:plain

宿到着。

f:id:jxpress:20191008063158j:plain

到着後、インフラチームから、デプロイ方法について説明を受けました。

弊社では、コード管理にGitLab、デプロイにGitLab CIを、普段は使っていますが、今回はGitHub Teamと、発表されたばかりのGitHub Actionsを使って、CI/CDのパイプラインを組んでみました。

f:id:jxpress:20191008145143j:plain

開発中。

f:id:jxpress:20191008063721j:plain

開発中。

f:id:jxpress:20191008145154j:plain

開発中。

f:id:jxpress:20191008063744j:plain

開発中。

f:id:jxpress:20191008063800j:plain

開発中。

f:id:jxpress:20191008145131j:plain

開発中。

f:id:jxpress:20191008063908j:plain

夕食・朝食は共にバイキング。種類豊富でした。

f:id:jxpress:20191008064133j:plain

ご飯から戻ってまた開発。深夜遅くまで続きました。

f:id:jxpress:20191008064148j:plain

開発中。

f:id:jxpress:20191008064212j:plain

そろそろ限界。

f:id:jxpress:20191008064313j:plain

合宿最終日は会議室を借りて、成果発表会を開きました。

f:id:jxpress:20191008145121j:plain

合宿の成果

最終日ギリギリまで粘ってなんとか、バックエンドからWebフロント・アプリまで疎通でき、3泊4日の日程内にプロダクトのrebuildができました!

WebフロントエンドはTypeScriptを、アプリはFlutterを使い、BFFとしてGraphQLのAPIを作りました。 バックエンドは、普段使ってるAWSではなくGCPを採用し、GCPの各種サービス(GAE、Cloud Functions、Cloud Pub/Sub)やKubernetes / Knativeにチャレンジしました。

まとめ

今回の合宿では、新しい技術にガッツリ取り組めました。また、3泊4日という期限内に、全員で1つのプロダクトの0→1開発に取り組めたことは、チームとして得難い経験になったと思います。

最後に、我々を快く受け入れてくれた「ホテルおかだ」さんに感謝!

www.hotel-okada.co.jp