TECH PLAY

BASE株式会社

BASE株式会社 の技術ブログ

587

この記事は、「BASE Advent Calendar 2018」の5日目の記事です。 devblog.thebase.in Backend Engineer の田中( @tenkoma )です。 「BASE」の裏側で動いているアプリケーションは CakePHP 2 を使っています。 そのCakePHP 2にPHP7.3対応のプルリクエストを送ろうとしたけど先を越されてしまった話をします。 CakePHPのPHP7.3対応状況 PHPはPHP7.0以降、大きな機能追加のあるバージョンが年1回リリース *1 されるようになりました。 リリースサイクルは公開されており( PHP: todo:php73 )、毎年9月中までにはRC版が公開されているようです。活発なPHP製OSSプロジェクトなら、ビルドスクリプトにPHPの新しいバージョンを追加して、互換性が確認できているか確かめられるようになっているかもしれません。 CakePHP 3はテストスイートがすべてパスしたブランチを2018年10月2日に master マージしていたようです。 参考: Pull Request #12488 Add php7.3 to build matrix in allow failure mode. ではCakePHP 2の状況は? PHP 7.2でだいたい動作することは自分で確かめていたので、PHP 7.3 RC3が出た頃に状況を確認しました。 すると、PHP 7.3互換性対応のプルリクエストがマージされたことがあるが、全テストケースがパスしているかはわからない状況みたいでした。 PHP 7.3 互換性対応: Pull Request #12487 Fixes: a few issues found when running PHP 7.3 メンテナーがローカルでテスト実行して問題ないことを確認したようです。 PHP 7.3の勉強にもなるし、やってみるか〜ということで作業することにしました。 ほぼ同じ修正をコミットしてたのに…悔 まずは、テストケースが失敗するとしたら、どれが失敗するか把握する必要があるので、PHP 7.3でテストを実行するためのプルリクエストをつくり、マージしてもらいました。 Pull Request #12707 [2.x]PHP7.3 added to the build setting. マージ後のビルドログ を見ると、270 Errorsと表示されましたが、ほぼ同種のエラーがいろいろなテストに発生しているだけで、修正はたいしたことありません。 自分のアカウントにフォークしたリポジトリもTravisの設定を追加していたので、一つ一つ修正してはプッシュしてエラーを確認していったところ、残り1箇所まで減らすことができました。 エラーが残り1箇所に減ったビルドログ: Job #90.13 - tenkoma/cakephp - Travis CI 残る一つは mb_strtoupper() のPolyfillのテストでした。 CakePHP 2には、mbstring拡張が利用できない場合でも mb_strtoupper() , mb_strtolower() が使えるよう、関数が用意されています。(実際のコードは Multibyte クラスで実装されています。) mb_strtoupper() のテストも用意されていますが、Travis CIでmbstring拡張が有効になっており、組み込み関数が呼び出されて、そこでテストが失敗していました。 There was 1 failure: 1) MultibyteTest::testUsingMbStrtoupper Failed asserting that two strings are equal. --- Expected +++ Actual @@ @@ -'ԱԲԳԴԵԶԷԸԹԺԻԼԽԾԿՀՁՂՃՄՅՆՇՈՉՊՋՌՍՎՏՐՑՒՓՔՕՖև' +'ԱԲԳԴԵԶԷԸԹԺԻԼԽԾԿՀՁՂՃՄՅՆՇՈՉՊՋՌՍՎՏՐՑՒՓՔՕՖԵՒ' PHP 7.3 RC版の不具合かとも思いましたが、これは文字ケース変換の機能拡張のようです。 https://t.co/TbDfTo9ycq 仕様っぽいな…7.2.9 と 7.3.0RC5 で挙動が変わる — Koji Tanaka (@tenkoma) 2018年11月11日 PHP 7.3で仕様が変わることはわかりましたが、どう直すか悩みました。いまさら Multibyte クラスの仕様をPHPにあわせて拡張するのもどうかと思いましたし(実装難しそう…)互換性を諦めて、テストを削除してしまえばいいか、とも考えました。 それから何もせず1日経ったところで次のプルリクエストを見つけました。 Pull Request #12718: Fix some PHP 7.3 errors コミットの内容はほぼ一緒で、 mb_strtoupper() のテスト失敗はそのままでしたが、見事に先を越されてしまいました。 うお〜先を越された。ともあれPHP7.3もサポート出来そうですね https://t.co/dA56Rm9zDw — Koji Tanaka (@tenkoma) 2018年11月12日 先にプルリクエストを出して、スレッドで相談しながら決めれば良かったんですよね。 行動指針「Move Fast」にしたがって、早く出せば良かったな、と思いました。 補足: PHP 7.3対応で修正されたCakePHP 2の互換性 実装コードの修正を2点紹介します。 compact() で未定義の変数を配列にしようとするとNotice Errorになる 7.2 まで、 compact() では未定義変数名を含めることができました。(戻り値にキーは無い) $defined = 'defined'; $arr = compact ('defined', 'undefined'); var_dump($arr); // output: // array(1) { // 'defined' = > // string(7) "defined" // } 7.3からは以下のようなNotice Errorになります。 PHP Notice: compact(): Undefined variable: path in /home/travis/build/tenkoma/cakephp/lib/Cake/Utility/Debugger.php on line 255 switch文の break; の代わりに continue; を使うとWarning Errorになる 7.2 まで switch文ではbreak文の代わりにcontinue文を使うことができましたが、switch文の外側のループを対象にした命令のつもりでも、switchへのbreak文になる問題がありました。 foreach($items as $item) { switch($item['type']) { case 'A': continue; // foreachループの先頭に戻らず、switch文を抜けるだけ } } PHP 7.3ではWarning Errorとなります。PHP 8では構文エラーになるそうです。 参考: PHP 7.3 曖昧なcontinue文の禁止 – yohgaki's blog まとめ プルリクエストはある程度できたら出しちゃったほうがいいこともあるよ! 明日は id:pigooosuke です。お楽しみに! 「BASE Advent Calendar」の記事一覧はこちら。 devblog.thebase.in *1 : 毎年12月初旬頃
これは、「BASE Advent Calendar 2018」4日目の記事です。 devblog.thebase.in BASEでサーバーサイドエンジニアをやっている、東口( @hgsgtk )です。 BASE BANK というBASEの子会社にて 金融事業の立ち上げ を行っています。 以前投稿した、 Goを運用アプリケーションに導入する際のレイヤ構造模索の旅路 | Go Conference 2018 Autumn 発表レポート という記事の中で、レイヤ構造を成長させていくためのユニットテストについて言及させていただいていました。こちらのエントリーにて進めていたコードベースでは、全体で約85%程度のテストカバレッジとなっています。本日は、そんなGoのユニットテストについての内容です。 ユニットテストの知見 現在、Goのユニットテストについての知見は数多く見られ、 テーブル駆動テスト ・ サブテスト などといった基本的なところから、 非公開(unexported)な機能を使ったテスト や GoのAPIのテストにおける共通処理 で紹介されているようなインテグレーションテストの知見など、充実した情報量となっていると思います。 ただ、資料を漁っていて「どのようなテストヘルパーを作っているか」・「テーブル駆動テストをどのように活用しているか」といった、現場のノウハウはまだ探しづらいなと感じています。 そのため、今回の記事では、筆者がAPI開発のために使っていたテストTipsや共通化のためのテストヘルパーについて紹介してみます。 具体的には、「 http.Handlerのテスト 」・「 実行ごとに結果が異なる処理のテスト 」・「 外部ライブラリの活用 」の3点についてそれぞれ紹介していきます。 http.Handlerのテスト http.Handlerのユニットテストでは、 httptest パッケージを活用して、ハンドラーに対してリクエストを送り、レスポンスを検証するといったテストを書きます。その際、期待値定義をどのようにするか分かれるところかと思いますが、筆者は対象ハンドラーに対する リクエスト・レスポンスの期待値をJSONで定義する ようにしています。 実際に次のように、ユーザーのリソースの取得を行うようなHandlerコードをのテスト例を紹介します。 . ├── interfaces │   └── controller │ ├── testdata │   │ └── user_controller │   │ ├── request.json │   │ └── response.golden │   ├── user_controller.go │   └── user_controller_test.go └── testutil   └── handler.go 筆者のプロジェクトでは、現在、テストヘルパー群を testutil というサブパッケージにまとめており、各テストケースからは、 testutil パッケージが提供する関数を使用する構造となっています。 実際に、ユーザーのリソース取得を行うHandlerを例に見てみます。 func TestUserController_Handler(t *testing.T) { // prepare http.Request and http.ResponseWriter w := httptest.NewRecorder() r := httptest.NewRequest( "GET" , "/v1/users" , nil ) // execute the target method c := controller.UserController{} c.Handler(w, r) res := w.Result() defer res.Body.Close() testutil.AssertResponse(t, res, http.StatusOK, "./testdata/user_controller/response.golden" ) } testutil.AssertResponse というResponse Assertion用のテストヘルパーを提供しています。 // AssertResponse assert response header and body. func AssertResponse(t *testing.T, res *http.Response, code int , path string ) { t.Helper() AssertResponseHeader(t, res, code) AssertResponseBodyWithFile(t, res, path) } testutil パッケージで定義した AssertResponse は、レスポンスヘッダー・ボディを検証する機能を持っています。 下記がヘッダー検証関数 AssertResponseHeader の中身です。 // AssertResponseHeader assert response header. func AssertResponseHeader(t *testing.T, res *http.Response, code int ) { t.Helper() // ステータスコードのチェック if code != res.StatusCode { t.Errorf( "expected status code is '%d', \n but actual given code is '%d'" , code, res.StatusCode) } // Content-Typeのチェック if expected := "application/json; charset=utf-8" ; res.Header.Get( "Content-Type" ) != expected { t.Errorf( "unexpected response Content-Type, \n expected: %#v, \n but given #%v" , expected, res.Header.Get( "Content-Type" )) } } ステータスコード・Content-Typeをチェックしています。レスポンス検証の際、おまじないのようにこちらのアサーションは書くことになるので、共通化しておくと新規のhandler実装も楽になるかと思います。 次にBody検証関数 AssertResponseBodyWithFile です。 // AssertResponseBodyWithFile assert response body with test file. func AssertResponseBodyWithFile(t *testing.T, res *http.Response, path string ) { t.Helper() rs := GetStringFromTestFile(t, path) body, err := ioutil.ReadAll(res.Body) if err != nil { t.Fatalf( "unexpected error by ioutil.ReadAll() '%#v'" , err) } var actual bytes.Buffer err = json.Indent(&actual, body, "" , " " ) if err != nil { t.Fatalf( "unexpected error by json.Indent '%#v'" , err) } assert.JSONEq(t, rs, actual.String()) } 筆者のプロジェクトでは、 別定義したJSON文字列との比較 によってレスポンスボディを検証しています。使い方の流れは下記になります。 testdata 以下に期待値となるJSONを .golden 拡張子で配置 テストケースにファイルパスを指定。(今回の場合、 "./testdata/user_controller/response.golden" となります。) AssertResponseBodyWithFile にて対象ファイルと実レスポンスを検証 .golden 拡張子の使用方法については、 Testing with golden files in Go を参考にさせていただきました。テストのOutputの期待値を testdata 内に別ファイルとして定義する際に、標準ライブラリ内でよく使用されている方法のようです。 { " user ": { " id ": 1 } } この際、JSON同士の比較の可視性のために、部分的に stretchr/testify の assert.JSONEq を利用しています。 そして、ファイルから文字列を取得する実装 GetStringFromTestFile では、 ioutil.ReadFile を利用することで実現しています。 // GetStringFromTestFile get string from test file. func GetStringFromTestFile(t *testing.T, path string ) string { t.Helper() bt, err := ioutil.ReadFile(path) if err != nil { t.Fatalf( "unexpected error while opening file '%#v'" , err) } return string (bt) } これまで紹介していたレスポンスボディを別ファイル定義する手法は、リクエストボディが必要なテストケースでも同様に使用しています。リクエストで必要なjsonファイルは、Outputの期待値として定義していないため、 .json 拡張子としています。 func TestUserController_Handler(t *testing.T) { // prepare http.Request and http.ResponseWriter w := httptest.NewRecorder() r := httptest.NewRequest( "POST" , "/v1/users" , strings.NewReader(testutil.GetStringFromTestFile(t, "./testdata/user_controller/request.json" )) // execute the target method c := controller.UserController{} c.Handler(w, r) res := w.Result() defer res.Body.Close() testutil.AssertResponse(t, res, http.StatusOK, "./testdata/user_controller/response.golden" ) } 実行ごとに結果が異なる処理のテスト UUIDや時刻を扱うコードなど実行ごとに結果が異なる処理の場合は、実装自体を「 テスト可能な実装 」にする必要があります。 時刻を扱うテスト 本章冒頭でも述べたとおり、時刻を使う処理がある場合はテストごとに結果が変わってしまいます。そのため、筆者は、サブパッケージとして clock パッケージを作成し、テストケース内で実行時刻を固定できるようにしています。 clock パッケージの中身はこれだけです。 package clock import "time" // Now is current time. // テスト時に差し替え可能にするため、グローバル変数として定義している var Now = time.Now テスト時に差し替え可能とするため、グローバル変数にtime.Nowを設定しています。テスト時には、こちらの変数に任意の time.Time を設定することで時間を固定します。 なお、この実装の方法については、 外部環境への依存をテストする という資料を参考にさせていただきました。そして、時間を期待値に固定するテストヘルパーを提供しています。 package testutil // SetFakeTime set fake time to clock package. func SetFakeTime(t time.Time) { clock.Now = func () time.Time { return t } } テストケース内では、 testutil.SetFakeTime を使用することでテストケース内で任意の時間に設定できるようにしています。 UUID UUIDに関してもテスト実行ごとに値が変わってしまいます。筆者はUUID生成に、 github.com/satori/go.uuid を利用していますが、本ケースでは、go.uuidから使用するメソッドのみを必要としたInterfaceを作成しています。 package uuidgen import ( "github.com/satori/go.uuid" ) // UUIDGenerator is interface to generate uuid. type UUIDGenerator interface { V4() string } // UUID has generating method. type UUID struct { } // V4 wrap satori.uuid.NewV4() func (*UUID) V4() string { return uuid.NewV4().String() } そして、UUID生成する実装では、 UUIDGenerator interfaceを受け入れる型として定義することでモック差し替え可能にしています。 例えば、次のように引数に UUIDGenerator interfaceを指定した場合は、 func GetSampleUUID(g uuidgen.UUIDGenerator) { return g.V4() } 次のようにモック実装を作成して差し替えます。 type mockUUID struct {} func (*mockUUID) V4() string { return "sample-uuid-string" } func TestGetSampleUUID(t *testing.T) { actual := GetSampleUUID(&mockUUID{}) if diff := cmp.Diff( "sample-uuid-string" , actual); diff != "" { t.Errorf( "differs: (-want +got) \n %s" , diff) } } 外部ライブラリの活用 Goのユニットテストを書いていくにあたり、標準の testing パッケージでどれだけやるのか、テストフレームワークを利用するのかは序盤で検討するかと思います。筆者は、基本的に標準ライブラリを使用していますが、補助的に次のライブラリを利用しました。それぞれ使用感も含めて紹介いたします。 google/go-cmp github.com/google/go-cmp はGoogle非公式の値比較のライブラリです。構造体などの大きめな値比較をする際に使っている方が多いかと思います。 実際の使い方は 公式のexample が参考になりますが、例えば、構造体のアサーションをする場合には、次のように書くことができます。 func TestFuncHoge(t *testing.T) { expected := Hoge{ Moji: "hogehoge" , AnotherStruct: Huga{ Moji: "hugahuga" , }, Num: 1 , Flag: false , } res := FuncHoge() if diff := cmp.Diff(res, expected); diff != "" { t.Errorf( "Hogefunc differs: (-got +want) \n %s" , diff) } } テスト結果が FAIL の場合は次のような表示になります。 === RUN TestFuncHoge --- FAIL: TestFuncHoge (0.00s) main.go:56: Hogefunc differs: (-got +want) {Hoge}.AnotherStruct.Moji: -: "hugahuga" +: "hugahuga_diff" FAIL 構造体をそのまま比較できるので便利です。特に使って不便になることはないので、迷っていらっしゃる方がいれば使っていくのが良いかと思います。 DATA-DOG/go-sqlmock.v1 gopkg.in/DATA-DOG/go-sqlmock.v1 は、DBを使うコードをテストする際にsql.Driverをモックするものです。 実際にSQLを実行してデータを取得するような技術的実装を行う関数のテストにおいて、こちらのライブラリを使っています。 go-sqlmockは、各人が愚直に使うと少々辛いところがあったので、こちらのライブラリを不便少なく使うためにヘルパーを作成しています。 実際には、SQL自体をGoファイル内で定義していくのは見通しが悪く感じたので、次のようにSQLファイルを testdata ディレクトリに配置してテストで利用しています。 下記より、user情報を保存するコードに対するテストコードを例に紹介します。 . ├── infrastructure │   └── datastore │ ├── testdata │   │ └── user │   │ └── save.sql │   ├── user.go │   └── user_test.go └── testutil ├── error.go   └── sqlmock.go まず、テストヘルパーを利用したテストコードは次のようになります。 package datastore_test func TestUserStore_Save(t *testing.T) { // create database mock db, mock := testutil.NewMockSQLDB(t) // Helper 1 defer db.Close() // set expectation of mock query := testutil.GetSQLFromFile(t, "./testdata/user/save.sql" ) // Helper 2 mock.ExpectPrepare(query). ExpectExec(). WithArgs( 1 , 1 , testutil.GetTestTime(t), testutil.GetTestTime(t)). WillReturnResult( 1 ). WillReturnError( nil ) // execute the target method s := datastore.UserStore{} result, err := s.Save(db, 1 , 1 ) // assertion testutil.AssertMockExpectation(t, mock) // Helper 3 testutil.AssertError(t, err, tt.expectedErr) if tt.expected != result { t.Errorf( "expected: %#v, \n given: %#v" , tt.expected, result) } } 上記のテストコードに対して、関連するもので3つHelperを提供しています。 package testutil // Helper 1 // NewMockSQLDB create a new instance sql.DB for test mocking func NewMockSQLDB(t *testing.T) (*sql.DB, sqlmock.Sqlmock) { t.Helper() db, mock, err := sqlmock.New() if err != nil { t.Fatalf( "failed to create sqlmock '%#v'" , err) } return db, mock } 1つ目が、sqlmockのinstanceを生成するヘルパーです。ライブラリ系では、どうやって生成するか毎回思い出すのも面倒なのと、テスト用インスタンスの生成のエラーハンドリングにてテストコードが冗長になりがちなため、テストヘルパーとして提供しました。 次に、期待値となるSQL文を準備するためのヘルパーです。 package testutil // GetSQLFromFile get sql string which is quoted meta to use in sql-mock. func GetSQLFromFile(t *testing.T, path string ) string { t.Helper() mq := GetStringFromTestFile(t, path) return regexp.QuoteMeta(mq) } こちらでは、別ファイルのパスを受け取り、string型の文字列を返すヘルパーを作成しています。 sqlmockは、テスト時のSQLを正規表現で期待値との比較するのですが、「正規表現での比較」という点をテストコード実装者に意識してほしくないので、このようなヘルパーを用意しています。 テストコード実装者は、次のように testdata ディレクトリ以下に期待値となるSQLをSQLファイルとして設置すればOKです。 INSERT INTO users (last_name,first_name,created,modified) VALUES (?,?,?,?) 最後に、sqlmockで設定した期待値と一致しているかを確認するテストヘルパーを提供しています。 // AssertMockExpectation assert mock expectation func AssertMockExpectation(t *testing.T, mock sqlmock.Sqlmock) { if err := mock.ExpectationsWereMet(); err != nil { t.Fatalf( "there were unfulfilled expectations: %s" , err) } } これは、sqlmockの sqlmock.Sqlmock の持つ関数をwrapしています。ライブラリ系の特殊なAssertionも、書き方を忘れがちなのと、設定するエラーメッセージは共通ですので、テストヘルパーとして仕様をWrapすると、各人でばらつきも少なくなるかと思います。 sqlmockの使用感ですが、昨今実データベースでテストするべきという言説が増えている通り、sqlmockで保護できるのが「期待したSQLが発行されているか」のみとなり、「そのSQLが正しいかどうか」についてはテスト保護対象外となる点が難点です。 筆者は、「SQLの妥当性」をチェックしやすいように、SQLのみ別ファイル定義する方法で、ミスを見逃すリスクの軽減に努めましたが、時間が許すのであればFixture機構を用意して実データベースでのテストを行うのがより安全かと思います。 実データベースを用いたテストについては、 Golang API Testing the HARD way という資料にて紹介していただいています。 golang/mock github.com/golang/mock は、テスト時のモックを生成する選択肢として使われることが多いかと思います。 基本的な使い方として、下記のようなコマンドを実行することでモックを自動生成してくれます。 mockgen -source hoge.go -destination mock_hoge.go 自動生成したモックは、本ライブラリが提供するモックコントローラによって期待値を設定することができます。 ctrl := gomock.NewController(t) mockHoge := mock_hoge.NewMockHoge(ctrl) mockHoge.EXPECT(). Fuga( "hoge" , "huga" ). Times( 1 ). Return( "hogehuga" , nil ) 筆者は、テーブル駆動テストでgomockを利用しているのですが、それぞれのテストケースで設定したい期待値が異なるため、期待値設定用の構造体を定義して動的に期待値を変えています。 type mockExpectedHogeFuga struct { callTime int arg string result string err error } func TestHoge_Fuga(t *testing.T) { tests := [] struct { name string mockExpected mockExpectedHogeFuga expected bool }{ { name: "success_to_print_hogehuga" , mockExpected: mockExpectedHogeFuga{ callTime: 1 , arg: "hoge" , result: "hogefuga" , err: nil , }, expected: true , }, } for _, tt := range tests { t.Run(tt.name, func (t *testing.T) { ctrl := gomock.NewController(t) mockHoge := mock_service.NewMockHoge(ctrl) mockHoge.EXPECT(). Fuga(tt.mockExpected.arg). Times(tt.mockExpected.callTime). Return(tt.mockExpected.result, tt.mockExpected.err) // テスト対象関数へモック注入し実行... }) } } テストケースごとに期待値を変えることによって、テーブル駆動テストでも使いやすいようにしています。この手法を使うことでモックをテストケース内で柔軟に使うことができるため重宝しています。 gomock自体の使用感ですが、モック量が多くなってきたタイミングでは、gomockに任せてしまえばよいので、テスト効率は上がりました。ただし、Interfaceに対するモック実装に慣れていない状態では使わないほうが良いかと筆者は思います。モック実装を自前で作れる状態になった上で 効率化のためのモック自動生成手段 として導入するプロセスが、Goらしいコードを学ぶ上で有益だったと実感しています。 まとめ 今回は、Goのユニットテストの実践しているTipsについて書きました。テストヘルパーは、ライブラリ使用状況・コード構成によって、提供の仕方はそれぞれ微妙に異なるかもしれませんが、一つアイデアとして参考になれば幸いです。 明日は、 id:tenkoma です!お楽しみに!
この記事は、「BASE Advent Calendar 2018」の3日目の記事です。 devblog.thebase.in 前の日 は id:yaakaito でした。Suggested change、便利ですよね。プルリクエストのレビューであまり重要ではないけれど気になる些細なコメントをするかどうか迷うというときに、悩まずにコメント代わりにパッチをサクッと送れるので、レビューに関わる人たちみんなが幸せになれるすごく良い機能だと思いました。ガンガン使っていきたいです! 3日目の今日は、エンジニアリングマネージャを担当している加賀谷が、「新任エンジニアリングマネージャ(私自身)を支えてくれた本と言葉」と題して、いくつか紹介していきたいと思います。 駆け出しマネジャーの成長論 - 7つの挑戦課題を「科学」する https://www.amazon.co.jp/dp/4121504933/ 駆け出しマネジャーの成長論 - 7つの挑戦課題を「科学」する (中公新書ラクレ) 作者: 中原 淳 発売日: 2014/05/09 メディア: 新書 私は2018年、エンジニアのグループマネージャとして活動してきました。それまではサーバサイドエンジニアとして、アプリのバックエンドを作ってきました。なので、まずはマネージャとは何をすればいいのか?というところからのスタートでした。そんなときに救われたのが人事をしている同僚の言葉と、その時にオススメされたこの本でした。 「例えるなら、サッカー選手から管理栄養士へのジョブチェンジです。加賀谷さんは今までサッカー選手としてメンバーとともに試合に出て、時に自分で点を取り試合に勝ってきました。これからは管理栄養士としてチームを勝たせて下さい。」 選手ではないことはまだ受け止められたのですが、チームの管理栄養士です。まだ、監督だったら戦術をどうしようとか、直接ボールを蹴らなくてもまだ試合を動かす術があります。それもできません。試合(開発現場)からかなり遠いイメージを持ちました。 結果的に「サッカー選手から管理栄養士へのジョブチェンジ」という比喩が、すごくしっくりきました。本の中で語られていた、マネージャの仕事 = Getting things done through others(他者を通じて何かを成し遂げる仕組みをつくること)とリンクしたからです。 手を動かさなくていいのか。自分でコードを書いた方がはやいのでは。その葛藤、分かるよ。というところから始まり、マネジメントとはなんぞや、全部並べるとこういう仕事がある、ただいきなり全部やらなくていいんだぞ(むしろやれないし、今まで周りにそんなマネージャいなかったでしょ)、これからマネージャになる君とその周りにはこういうことが起きるぞ、だからこうやって乗り越えていこうー。 少しチクっとするけどこれから現れる症状を和らげてくれる、ワクチン接種のような優しい本です。 エンジニアが「明日からマネジメントして」と言われたら – FiNC Engineering Blog https://medium.com/finc-engineering/12908151a41e BASEのプロダクトマネージャーは思想を持って戦略を立て、何を作り何を作らないのかをずっと意志決定し続けています。ディレクターはプロジェクトチームが最速で目的達成するために、やりたいことを具体的な戦術として言語化とタスク化し、チームの今とこれからの課題を明確にして優先度を決め進捗を管理しています。 そんな中、具体的な数値目標をOKRとして設定して日々追っていく事業系グループのマネージャと、私が担当しているエンジニアのグループのマネージャでは少し違うような気がしていました。グループ単体で達成できるようなOKRの設定が難しく、具体的に何をマネージメントするのかがまだピンとはきていませんでした。 Getting things done through others、だとしたらエンジニアのグループのマネージャとして何をすればいいのか。迷っていたときに、こちらの記事と図を見てすごくしっくりきました。 私がやるべきことは、この図の中のピープルマネジメントでした。各メンバーが活躍できる場所を整え、フロー状態に持って行くのです。そのためにまず、プロダクトマネージャーと密にコミュニケーションを取り、立ち上げるべきプロジェクトの検討とアサインを考える必要がありました。そして、我々が向かう目的地と次の電柱の場所が見えるように目標を咀嚼して伝え、メンバーにかける期待を上司や役員とすりあわせた上で伝えること、プロジェクト内のメンバーが健全な対話と共同作業ができているか見続けて、お互いの期待のズレがないかすりあわせることが仕事のうちの1つと考えました。 OKR(オーケーアール) シリコンバレー式で大胆な目標を達成する方法 https://www.amazon.co.jp/dp/4822255646 OKR(オーケーアール) シリコンバレー式で大胆な目標を達成する方法 作者: クリスティーナ・ウォドキー 発売日: 2018/03/15 メディア: 単行本 BASEでは目標設定のフレームワークとしてOKRを導入しています。各マネージャが全社OKRを因数分解するようにOKRを設定します。OKRとはなんなのか、良いOKRとはを知るためにはこの本が読みやすくて良かったです。茶葉を販売する架空のスタートアップに様々な困難が起き、エンジェル投資家のすすめでOKRを導入したがうまくいかず、そこからだんだんと変わっていくストーリーにも親近感が持てました。 Team Geek ―Googleのギークたちはいかにしてチームを作るのか https://www.amazon.co.jp/dp/4873116309 Team Geek ―Googleのギークたちはいかにしてチームを作るのか 作者: Brian W. Fitzpatrick , Ben Collins-Sussman 発売日: 2013/07/20 メディア: 単行本(ソフトカバー) 優れたソフトウェアはチームで作る、だとしたら優れたチームはどうすれば作れるのか。ソフトウェア開発はチームスポーツであり、技術と同じように人を考えることが大事。謙虚(Humility)、尊敬(Respect)、信頼(Trust)の3本柱を身につけよう、という本です。マネージャになって読み返したのですが、自分がいちエンジニアの時に読んだのとは見え方が全く異なっていました。1章に書いてあるうちの、優れたプロダクトを作る上で必要なプロセスの一部である、建設的な批判の仕方と受け入れ方、失敗からの学び方、集団のエゴを考える大切さについては特にです。 採用基準 https://www.amazon.co.jp/dp/B00B42SX70/ 採用基準 作者: 伊賀 泰代 発売日: 2012/11/09 メディア: 単行本(ソフトカバー) リーダーとマネージャの境が分からなくなってきたときに気づきを与えてくれました。 リーダーは引っ張る人でも何でもなく、チームの使命を達成するために必要なことをやる人のことで、そのリーダーが持つ問題解決リーダーシップは、メンバー全員が持つべき。その方が高い成果を生み出せるチームになる。なぜなら、お互いに背中を任せた上で何をすべきか自発的に動けるからです。 Team Geekにも書いてありましたが、時にマネージャはリーダーであることも期待されますが、リーダーは必ずしもマネージャである必要はないです。マネージャはメンバーのリーダーシップを育んでいくことで、よいチームになりプロダクトにより大きく寄与できるのではないかと思います。 エラスティックリーダーシップ ―自己組織化チームの育て方 https://www.amazon.co.jp/dp/4873118026 エラスティックリーダーシップ ―自己組織化チームの育て方 作者: Roy Osherove 発売日: 2017/05/13 メディア: 単行本(ソフトカバー) 同時に複数のエンジニアグループのマネージャをさせていただいたのですが、時にグループごとに課題がちがい、同じ振る舞いではうまくいかないことを学びました。この本ではチームの状態を、サバイバルフェーズ、学習フェーズ、自己組織化フェーズ3つで定義し、それぞれのフェーズでとるべきリーダーシップの方法を紹介しています。 ヤフーの1on1―――部下を成長させるコミュニケーションの技法 https://www.amazon.co.jp/dp/4478069786 ヤフーの1on1―――部下を成長させるコミュニケーションの技法 作者: 本間 浩輔 発売日: 2017/03/25 メディア: 単行本(ソフトカバー) メンバーと定期的に1on1をすることになり、1on1ってなんぞ?と読み始めた本です。マネージャになって最もお世話になった本かもしれません。 そもそも何を目的としてどうやったらいいかを良く理解していなかったので、この本のまんま通りにやってみて場数をこなすところから始めました。90度に近い配置で座り「最近話したいこと話せてますか」と話をしてみるところからでした。 最初は全くうまくいきませんでした。何を話していいのか分からず、場は沈黙ばかりでしたし(最初の頃、その沈黙が経験学習を促すために必要と勘違いもしました)、メンバーには1on1の必要性すら届きませんでした。 1on1は1回あたり30分、頻度は多いときは1週間に1度行っています。話している内容をMarkdownでメモでとりながら話し、最後に必ずマネージャにできることがないかリクエストの確認をします。1on1の最後にはメモをSlackのDMで本人だけに送り、気づきの確認と、次のアクションの確認をします。もらったリクエストはその日のうちに行動して、どうなったかを本人に連絡しています。 1on1のメモは MacのメモツールのDayOne でとっていたのですが、そのメモを数えたら311回分ありました。1on1をしていく上で大切だと思った準備や質問がいくつかできてきたので、それはまた別の機会に紹介したいと思います。 Getting things done through others、自分が直接携わらないサッカーの試合を勝利に導く上で、1on1はマネージャの強力なツールの1つだと思います。いいプロダクトを作りショップオーナーの支援につながる事が上のサッカーの試合の勝利だとすると、そのためにはチームメンバーがそれぞれ活躍していて、高く評価されていることが必要です。 エンジニアがやりたいことやできることの円と、プロダクトづくりでやりたいことの円があったとき、両方の円が重なったところが組織と個人の課題や目的が一致したところで、メンバーが活躍できることと考えることができます。なので、メンバーが活躍するためには、この両方の円を近づけていくように整えていく事がエンジニアリングマネージャの仕事の1つかなと、今は思います。また、両方の円を大きくすることでも重なる面が増えるので、こちらも仕事の1つと考えています。 これがエンジニアリングマネージャとしてのレバレッジの効かせ方のひとつ、自身のアウトプットの与える影響範囲を広範囲に及ばせる、ということなのではないでしょうか。 全然別の話ですが、日々1on1をしていくなかで金八先生やルーキーズの川藤先生を思い出しました。赴任してきていきなりみんなの前でああしようこうしようと理想を語ったところで、次の日皆がそれぞれの居場所を見つけて充実した顔をしている卒業式のようにはならないわけです。ドラマで1話ごとに1人と向き合い、強みと課題を見つけて成果を出せるように行動変容を促すようにサポートし続ける姿勢はマネージャとして見習わなければならないと思いました。 今年1年、気付けば1on1を通じてたくさんの経験学習をさせてもらっていたのは自分のほうでした。メンバーには感謝の気持ちでいっぱいです。ありがとうございました。 明日は id:khigashigashi です。お楽しみに! 「BASE Advent Calendar」の記事一覧はこちら。 devblog.thebase.in
これは「BASE Advent Calendar 2018」の2日目の記事です。 devblog.thebase.in Native Application Group の右京です。 ネットショップ作成サービス「BASE」では日々機能追加や改善の為、無数の Pull Request が作成され、レビューされています。今日はそんな中で知っているととっても便利な機能を紹介します。 レビューをしていたら typo をみつけてしまった... ありませんか?前後に真面目なレビューがあるとなんとなく指摘するのを躊躇してしまいますね...。シレッと修正するにも編集するなりブランチを切るなりして...はコンフリクトの可能性などもあり、コストが高く面倒ですよね。 そんなときはこれ、「Suggested change」です。 Incorporating feedback in your pull request - User Documentation これは最近 GitHub に追加された機能(public beta)なのですが、僕はこれを知ってからというものおすすめの機能の一つになりました。では、実際にこれを使って見つけてしまった typo をそっと修正してみます。 修正を送る側はとっても簡単 レビューでコメントをつけるときと同じ要領で行を選択して、ここクリックすると... 変更点に対してこのようなコメントが挿入されます ```suggestion function niec() { ``` この状態ではまだ typo したままなので、正しい内容に修正してプレビューしてみると... このように「Suggested Change」が作られます。そしてこれをコメントかレビューとして送信すると... こうなります!これで、この変更をパッチとして送っている状態になりました、取り込まれるのを待ちましょう。 取り込む側もすごく簡単 パッチをもらった側は、変更が妥当ならそれを取り込むことができます。「Commit Suggestion」から「Commit changes」で変更をコミットできます。 変更が取り込まれて、コミットが作られました。今回は自作自演したので僕だけがコミットしたことになっていますが、実際はこんな風に提案した側、取り込んだ側の両方がコミットに記録されます。 お互いに手間も無駄な気遣いも少なく、コードを少し良くすることができました。小さなことからコツコツと! Move Fast 今回は typo を例に出しましたが、文章の推敲、設定ファイルに対する小さな修正など、BASE のコードレビューシーンではすでにいろんなところで「Suggested changes」が活用されています。「提案を即座に取り込んでいける」という面が本当便利で、行動指針である「Move Fast」を体現した小回りの効く機能だと感じています。パッチをもらった側は uniposで #MoveFast すれば完璧 ですね! 以上、「Suggested changes」の紹介でした、この機能であなたも BASE にパッチを送りませんか? 明日は id:yuhei_kagaya です!楽しみですね! 「BASE Advent Calendar」の記事一覧はこちら。 devblog.thebase.in
これは BASE アドベントカレンダー 1日目の記事です。 devblog.thebase.in CTOの藤川です。ネットではえふしんと名乗っていて、会社でもえふしんさんと呼ぶ人が大多数です。 今年はテックリードの働きかけをきっかけとして、BASE社でもアドベントカレンダーを書いてみよう!という話になりました。皆様よろしくお願いいたします。 最近、エンジニアリングマネージャという役割が急速に普及し始めているので、今回はマネジメントの話について書いてみたいと思います。 IT業界にはエンジニア35歳定年説という都市伝説がまかり通っています。開発技術が進化してコードを書く労力が劇的に下がったにも関わらず、このことに恐怖してる人は少なくないようですし、何よりエンジニアがコードを書かなくなるような状態は望ましい状態ではないとされています。 一方で、成長するビジネスにおいては組織を作っていかないといけないのですが、エンジニアは、エンジニア出身のリーダーに評価してもらいたいという部分と、コードを書かないとエンジニアとして死ぬというイメージに矛盾があって、35歳定年説の不安と相まって、エンジニアのマネジメントのキャリアパスというのはどこか淀んでいる印象すらあります。 要は、マネージャの存在を絶対的に必要としているのに、自分がやるのは不安だという矛盾をはらんでいます。 これまでの転職志望者でも、現職で管理職になってしまったが、まだ技術の最先端に携わりたいと言う理由で面接にお越しいただく方もいて、転職後にマッチングに失敗するかもしれないリスクを取ってでも、転職せざるを得ない状況に追い込まれるというのは、エンジニアである僕らのキャリアにとっては辛い状況とも言えなくもないです。 この状況をポジティブに解決するのがエンジニアリングマネージャという役割だなと思い始めています。 エンジニアリングマネージャとは? エンジニアリングマネージャとは、ピープルマネジメントの一形態です。エンジニアリングマネージャの仕事を一言で言うと、 事業、プロダクトに貢献しながら、チームのエンジニアの活躍にコミットすることで、メンバーの評価を上げる仕事 だと考えています。チームのメンバーに活躍してもらって、高い評価を得られることが、マネージャ自身も評価され、それができなかったらエンジニアリングマネージャとしては評価が下がる仕事として考えられます。 チームを持ってる以上、事業の推進やプロダクトの開発責任も担うわけですが、自分がコードを書いて問題を解決してもエンジニアリングマネージャとしては評価されないわけです。 会社によっては、そもそもエンジニアリングマネージャは組織図上の上長ではない立ち位置であったりもするようです。メンターみたいな立ち位置でしょうか。 当社においては、組織図上の上司にあたるピープルマネージャがその任を担います。いずれにせよ、ネット系企業におけるピープルマネージャの役割は、サーバント・リーダーシップにのっとりメンバーを下支えすることで、メンバーの活躍をメイクできなければ、すぐにでも他社に転職されてしまう昨今の状況において、チームを支える、メンバーの活躍を導くという役割を担っていることを自覚しているかどうかこそが重要です。 メンバーの給料は成果に応じて昇給する会社がほとんどだと思います。その前提において、エンジニアリングマネージャはメンバーの状況を常にトラッキングし、1on1などを通じてメンタリングしながら、活躍をメイクし、どういう成果が評価可能なのか?を明確にし、今後はどういう期待をかけていくべきか?を共有し、メンバー自身がそれを実現していきます。 昇給というものは、結果そのものに支払われるのではなく、結果によってもたらされる次なる期待に対し人的投資として支払われるものですので、そのお膳立てをする役割だと考えます。評価権限を持っていることで、メンバーと踏み込んだ話をすることができるし、それだけの責任を負うことになります。 エンジニアリングマネージャは常に挙動をメンバーから評価されている エンジニアリングマネージャがメンバーの評価権限を持つ以上は、メンバーからすると、評価する人に値するか否かというのは常に評価されることになります。 その基準は、 評価する人としての人間性は期待できるか 自分のスキルを判断できる技術力、技術判断力を持っているか 日常の活躍をしっかり観察してくれている人か しっかり自分を高めてくれる指導をしてくれるか など、緩すぎても厳しすぎても成立しませんし、日常の言葉一つ一つにおいて常にチェックされていると言っても過言ではありません。 コードを書くような知的労働においては、ポジティブに脳内物質の分泌が行われるか否かが生産性の大多数を支配します。目的意識を持っていないとか、楽しくないという状況で良いアウトプットができるわけはありません。それが一文字直すだけの簡単な仕事でも、アーキテクチャから作り変えるような難しい仕事でも、やる意義を引っ張ったり、勇気を振り絞って困難に立ち向かうように問題をほぐしていくのはエンジニアリングマネージャの重要な仕事です。 このマネージャの下にいると自分は評価されないと思った時に取りうる行動は、他部署に異動するか、会社を辞めるかのどちらかであることが多く、マネージャと戦い上司を教育してまで状況をよくしようとしてくれる人は稀であることを考えると、マネージャは常に緊張感を持ってメンバーから信頼に値する評価を得られているのか?について考えていなければいけません。 よく言われる、マネージャになるとコードを書く書かない問題は、チームメンバーの成長にコミットする役割において、メンバーや会社から自分が評価されるために、目の前のコードを自分が書くことのトレードオフを考えて、状況状況でマネージャが最適な手立てを判断してもらえば良いと思います。どうしても危機的状況や問題がある時、または背中を見せる時に率先してコードを書くシーンもあるでしょうから、采配として手段を使い分けるのはエンジニアのリーダーだからこそできることです。 また、技術はどんどん変わってもいくので、メンバーから評価されるためのキャッチアップが必要と考えると、むしろメンバーのプルリクエストを通じて最新技術やエンジニアリングを学ぶ姿勢が必要であるとも考えます。そのためにもプロダクトや事業と連動した技術マネジメントを行う必要も出てくるハズで、基本的にエンジニアとして前向きな役割でしかないと僕は考えますが、皆さんはいかがでしょうか? エンジニアリングマネージャは「偉い」立場ではない ここまで書けば、エンジニアリングマネージャは、シニアなエンジニアみたいな役割に近く、旧来型の組織である「上司・部下」「偉い人」というイメージではないことに気がついてもらえるのではないでしょうか? むしろプロジェクトマネージャやディレクターに近い役割で、とても現場感が求められる仕事で、エンジニアリングにより踏み込んだ形でエンジニアの成長にコミットする役割であると考えます。 この役割になれるスキルはエンジニアとしてのスキルや事業に対する姿勢が信頼されていなければ、エンジニアであるメンバーから評価されることはないわけで、エンジニアのキャリアパスの一つであると考えられます。 そう書くと、この役割にはエンジニアのスキルがものすごく必要なのでは?と思われがちですが、ミニCTOとして技術で引っ張るテックリードとは違います。しかし、テックリードは卓越した技術力でチームへの影響を与え、如何に高いアウトプットを出すか?と言う部分にコミットする役割であると考えると、人を導くという点においてはエンジニアリングマネージャと向いているベクトルには大差はなく、何を「とくいわざ」とするかだけの話で、それぞれがチームで働くエンジニアに求められる職能として考えることが可能です。 そして、ここからが大事なことですが、必ずしもエンジニアリングマネージャは永遠にその立場でいる必要もなく、一定期間で別の人に入れ替わっても良いと考えています。期待するのは大人としてチームの活躍を支えるチームディレクションとしての役割です。ディレクションかプレーヤーかというのは、その都度、役割を入れ替えることはあってもよいと思います。 なによりそうすることで、たくさんのエンジニアが人の成長を支える仕事の難しさを知ることができるし、一度、エンジニアリングマネージャを経験した人は、より広い視野での仕事を期待できるわけなので、改めて現場でコードを書く役割にコミットしながら、開発プロジェクトマネジメントやメンバーの育成においてもエンジニアリングマネジメント力を発揮することが期待できます。 そうこうしてるうちに新しい事業アイディアが出てきた時には、そういう人が率先してエンジニアリングマネージャとしてチームを作れるようになることで組織のスケーラビリティは向上します。 このようにエンジニア組織全体としては、立場を入れ替え可能であることと、マネージャという役割をエンジニアとしてのキャリアの幅や柔軟性を高める役割として定義することで、プログラマ35歳定年説に代表されるような、コードを書かなくなって、エンジニアとしての死を迎えるみたいな不安な世界を終わらせることができるのではないかと考えています。 なお、これは、今自分が携わっているCTOにおいても全く同じです。あくまでも役割分担の一つとして柔軟に人を入れ替え、抜擢可能なチャレンジ文化を作ることは組織の柔軟性とスケーラビリティに影響が出るし、新たな役割への期待を常に作り続けることで、ビジネスが成長することを大前提とした組織運営の考え方として捉えています。 以上、えふしんでした! 明日は id:yaakaito です!お楽しみに! 「BASE Advent Calendar」の記事一覧はこちら。 devblog.thebase.in
こんにちは、BASEでサーバーサイドエンジニアをやっている、東口( @hgsgtk )です。つい最近、 Goを運用アプリケーションに導入する際のレイヤ構造模索の旅路 | Go Conference 2018 Autumn 発表レポート - BASE開発チームブログ というGoに関するエントリを書きましたが、PHPもしっかり書いてます。 今回は、特に弊社のメインプロダクトであるネットショップ作成サービス「BASE」のフレームワークの大部分を占めるCakePHPの国際カンファレンス CakeFest の協賛についてです。 さて、この度、BASE株式会社は、 CakeFest 2019 に、シルバースポンサー・ランヤードスポンサーとして協賛いたします。 cakefest.org CakeFest 2019 とは、PHP製フレームワークの一つであるCakePHPの国際カンファレンスです。年に一回開催されており、今回は初めて日本での開催です。 冒頭でも述べましたが、弊社はCakePHPをメインプロダクトのフレームワークとして採用し、日々CakePHPをベースとしたプロダクト開発を行っています。そのため、CakePHPコミュニティに貢献したいという想いから、初の日本開催となるCakeFest 2019に協賛することを決定しました。 また、CakeFestについて、日本で開催されると決定される前から、弊社の @tenkoma が、PHPカンファレンス関西2018にてCakeFestを紹介しておりました。 私も日本開催を待望しており、今回協賛という形でCakeFestに参加できることをとても嬉しく思います。 speakerdeck.com CakeFestに参加しましょう! 肝心な開催時期ですが、 @CakeDC の発表では、2019年11月の開催を予定しているとのことです。 We will be sponsoring #CakeFest2019 (November in Japan!) - and we hope that you will join us. Sponsorship opportunities are available! Get details here: https://t.co/TrndH05dtD #cakephp #cakefest #cakedc #php #phpconference pic.twitter.com/3BYksH59j6 — CakeDC (@CakeDC) 2018年9月19日 国際カンファレンスが日本開催される機会はそこまで多くないと思いますので、普段CakePHPを触っていない方もこれを機会に参加してみると新たな発見があるかと思います。 トーク応募始まってます! また、現在トーク応募が開始されています。 Have you submitted your talk for #CakeFest2019 ? Call For Speakers is now open!! If you are keen to be apart of the speaker line up for CakeFest next year, then NOW is the time to apply! CFP's close Jan 7th #php #phpc #Japan #CakePHP https://t.co/ghvbY0hho8 pic.twitter.com/3CPn12Wp2Y — CakePHP (@cakephp) 2018年11月27日 普段足が運びづらい国際カンファレンスが日本で開催されるということで、海外で英語で発表したいといった目標のある方はとても良い機会になるのではないでしょうか!
こんにちは。BASEで採用広報を担当している米田( @aiyoneda )です。 今年も残り一か月となりました。今年は、BASEとして初のAdvent Calendarを実施する運びとなりました。こちらの「BASE開発チームブログ」にて、12/1(土)から12/25(火)まで毎日記事を公開していく予定です! 記事一覧はこちらです。随時更新していきます。 devblog.thebase.in 初となる「BASE Advent Calendar」では、技術的な内容だけにとどまらず、BASEのプロダクトや開発方針、開発組織の運営に関わる内容を中心にカジュアルな内容も含めて幅広くお届けしようと思っています。 これをきっかけにBASEという会社やメンバーの雰囲気、またBASEの開発部門での取り組みについて知っていただき、興味を持っていただければ幸いです。 記念すべき初日は、取締役CTOの藤川( @fshin2000 )に筆を執ってもらいます!お楽しみに!
こんにちは、BASEランニング部で10kmマラソンなどに参加し、3kgほど体重が落ちたSRE Groupに所属しているデータベースエンジニアの植木です。おかげで甘いものが美味しいです。ちなみに次はハーフマラソンに挑戦です! 今回は会社のブログなどを書いてみます。弊社では、ネットショップ作成サービス「BASE」およびショッピングアプリ「BASE」を運営していますが、11月にメインDBをRDS for MySQL5.6からAurora(MySQL5.6コンパチブル)に変更しましたので、そちらの話を書かせていただきます。 何故Aurora? まず、弊社でAWSをメインに使っていたという背景があります。入社した際にはRDS for MySQLを使用しており、CTOの藤川も「AWSを使うならAuroraにしたい」という要望を持っていました。私自身、AWSをメインに使い続けるつもりであればAuroraは良い選択だと思っていました。 MySQLを使う場合、オンプレミスやMySQL on EC2の方がスピードや機能面でメリットがありますが一定以上のスキルのエンジニアを複数人確保し続ける必要があり、運用にそれなりの稼働が取られます。RDSを選択して運用負荷を下げる方針であるならば、いっそRDSだからこそ使えるAuroraのメリットを受ける方が良いと考えています。下記2点がOKならAuroraの選択を候補に入れて良いかと思います。 ・AWSにベンダーロックインしても大丈夫か ・MySQLの最新バージョンの機能よりもRDBの基本処理のスループットを重視するか 弊社運用を考えて、特にメリットと思った点 レプリカラグが発生しにくい ストレージの使い方が大きく変わっていますのでレプリケーションラグが発生しにくいです。 クラスターエンドポイントとカスタムエンドポイント HAproxyを使っての複数台レプリカのロードバランスを検討していたため、カスタムエンドポイントが移行直前にリリースされた事は助かりました。リーダーエンドポイントは弊社のように分析用レプリカを作っている場合にはそちらも参照先に入ってしまいます。書き込みはクラスタエンドポイントを指定して、読み込みはサービス用インスタンスだけを指定したカスタムエンドポイントを作成して使用しましょう。 クエリキャッシュ クエリキャッシュの作りがMySQLとは全く違うためかなり有用になっています。 無停止パッチ 稀にAWS側要件のパッチが発生するので助かります。 スループットの向上 MySQLの5倍程度との事。そこまでのスループットはまだ必要ではないですが、後述のログや監視機能を有効にするためにスループットが向上されている事は良いです。 Performance Insights MySQL Performance Schema設定を有効にする必要があるので、スループットに15%程度の影響が出るとの事ですが、専門じゃなくても負荷原因特定などが簡単にできるようになっていてとても便利です。MySQLでも使えるのですがスループットへの影響があるので躊躇っていたものをAurora移行を機に利用開始しました。 監査ログ ECというサービスを考えると監査は必要です。スループットに影響するのですがMySQLの監査設定と比べるとAuroraでは格段に影響が少なくなっているため利用する決断がしやすくなりました。ただし、ユーザー単位での記録くらいしかログ対象の選択設定ができないためCloudWatch Logが増える事は認識しておきましょう。 費用面 一台ずつの料金はMySQLの方が安いですが、AuroraのマルチAZは参照可能になっています。MySQL利用時にはサービス用のマスターレプリカ以外に分析・集計用DBを用意していましたが、AuroraではマルチAZ用インスタンスをそちらに割り振ることができます。そのため4台分必要だった費用が3台で良くなり費用削減ができます。 高速クローン 本番相当のDBをQA用に用意する事を進めているため、そちらで高速クローンが利用できると考えています。 Aurora注意点 続いて、Auroraの注意点です。基本的にMySQL互換ではありますが、少しAuroraで利用できない操作や違いがあります。 テーブルの圧縮機能が無い 明確に圧縮非サポートと書いてあるドキュメントは無かったのですが、非サポートです。移行時のスナップショットからのリストア時に非サポートと書いてあるドキュメントはあります。 圧縮コマンドは通るのですが、実際には圧縮されず気づきにくいのでご注意ください。 Parameter Groupが複数存在する インスタンス用とクラスタ用とに分かれます。Auroraはインスタンスとストレージが分かれているのでクラスタ用=ストレージ用と考えるとわかりやすいです。 Aurora Serverlessはどうなのか? テスト環境としてAurora Serverlessを利用できないか考えましたが、仕組み的にクラスタ用パラメータグループしか存在せずインスタンス用パラメータグループにあるinnodb_file_formatでbarracuda指定ができないのが痛いです。おそらく、アクセス時にdefaultパラメータグループを利用してインスタンスが立ち上がってくるためAWS側でdefaultの設定を変えてくれるか、barracudaがデフォルトになっている5.7互換のAuroraでServerlessを出してくれるかに今後期待です。 Aurora MySQL5.7互換はどうなのか? Aurora最新は5.7互換ですがPerformance Insightsなど新しい機能リリースが5.7にはまだされていません。また、移行パスがスナップショットからのリストアしかないため後述のレプリケーションを利用した移行ができず、移行時間が長くかかります。そのような状況ですので、今回は5.6互換のAuroraが最適だと判断しました。 事前作業 作業は大きく事前作業と当日作業とに分けられます。比率としては事前作業が8、9割で移行当日はそれほど作業がありません。重要なのは事前作業です。 テストに関して テストに関しては、開発環境・Staging環境を事前に移行してそこを使用して開発をやってもらう事で網羅テストとしました。開発環境を移行後に最低限の確認を各開発チームでしてもらい、その後二ヶ月ほど開発環境として使ってもらいました。 また、開発・Staging環境については後述のような移行構成は作らずに空いている時間にデータベースの変更処理でアップグレードするのが簡単で良いです。 本番環境事前構成の作成 オンプレミスでMySQLをある程度扱っていた方ならわかると思いますが、レプリケーションを利用した移行の事前構成です。下記のような構成を作ります。RDS for MySQLでは子レプリカまでしか作成できませんが、Auroraでは孫以下のレプリカ作成が可能になっています。そのためこのような構成を作る事でサービスに利用しているMySQLには影響を与えずに事前にAuroraの構成を作っておく事が可能です。 この際にほんのちょっと工夫するところとして、テンポラリの書き込み可能MySQLレプリカを間に入れる事をお勧めします。サービスに影響しない書き込み可能レプリカを入れることで、「mysql.rds_stop_replication」と「mysql.rds_start_replication」コマンドにてレプリケーションを操作し、レプリケーションを止めている間にテンポラリに対して巨大テーブルへのALTER文等を実施して事前にAuroraのテーブル定義をカスタマイズできます。弊社で言えば大きめのテーブルへのインデックス追加はこのやり方で実施しておきました。row_formatをDynamicに書き換えておきたい場合などはこの方法で実施すれば夜間メンテなどで長い時間サービスを停止する必要なく実施できます。 当日作業 当日の流れ 他社事例を見ると、ノンストップでの移行なども見受けられましたが、ネットショップ作成サービス「BASE」は60万店舗のショップに使っていただいているサービスのため、一時的に参照だけ可能になる事などが許される状況ではありません。そのため各ショップに連絡し、夜間メンテナンスタイムを入れさせてもらいました。ざっと下記のようなスケジュールです。 2:00:メンテイン作業開始 2:30:移行処理およびその他の追加作業開始 4:00:メンテ明け作業開始 4:30:社内限定アクセステスト 5:00:メンテ明け&最終テスト 5:15:終了 当日の移行作業 AuroraのMasterインスタンスを昇格します。その後、CNAME内のエンドポイント情報を全てAurora側に向ければ完了です。また、旧MySQLインスタンスは万一の場合にすぐに戻しができる必要があるため当日の削除はしません。どこからもアクセスできないSecurity Groupを作って入れ替えるかインスタンス名自体を変更してエンドポイント情報が変わるようにしてあげると良いです。弊社では後者を実施しました。インスタンス名変更は短時間でできますし、AWSコンソール上から見て名前が変わっている事が一目でわかるところが良いです。こちらで無事5時過ぎには作業完了しました。 使用感 マスターはCPU利用率が高くなります。CPUを使い切る最近の流れですので、そこはあまり気にせず。レプリカのCPUはそんなに変わりませんが、タイムラグは本当に減りました。レプリケーションの仕組みが以前と違うのでわかっていたとは言え、ここはとても嬉しいところです。マスターインスタンスのCPU利用率が高めになったため、監視システムのワーニング値はレプリカと分けるなど見直しをした方がいいですが、全体に急激なコネクション増加なども減り、良い結果に繋がっております。 下記が移行前後のCPU利用率の変化です。 緑:旧マスター 赤:旧レプリカ 青:新マスター 黄:新レプリカ また、下記がレプリカのラグです。単位がミリセコンドなので、非常に安定しています。 最後に 入社後、二ヶ月ほどでRDS for MySQL5.5からRDS for MySQL5.6への移行を実施し、それから1年経ってAuroraにたどり着きました。ようやく、一つ重めの荷物を下ろす事ができるような気がしています。まあ、そう言いながら来年はAurora MySQL 5.7互換やってたりしそうな気もしますが・・・。データベースとの付き合いはコードとの付き合いより長いと言います。今後もうまく回していきたいと思います。 BASEのSREチームではお客様に安心して買い物をしていただくために、サービスの安定性に責任を持ち、インフラ・アーキテクチャの改善に日々取り組み続けております。 ご興味を持たれた方いらっしゃいましたら是非ご連絡いただければ幸いです。 jobs.binc.jp
お久しぶりです、BASEでサーバーサイドエンジニアをやっている、東口( @hgsgtk )です。 BASE BANK というBASEの子会社にて 金融事業の立ち上げ を行っています。今回は、BASE BANKで行っているGo言語でのチーム開発について書こうと思います。 なお、このエントリーの内容について、2018年11月25日に開催された Go Conference 2018 Autumn にてLT登壇してきました。 発表資料については次のURLで公開しています。 https://go-talks.appspot.com/github.com/higasgt/gocon-lt/architecture.slide#1 今回、LTをCFPで2つ提出して、偶然2つ採択いただいたので、併せてDocker開発環境の整備についても次のスライドで発表させていただきました。 https://go-talks.appspot.com/github.com/higasgt/gocon-lt/realize.slide#1 Go製のApplicationをDocker上で開発する際に環境整備が喫緊の課題としてある方がいらっしゃればこちらの資料も併せてご覧いただくと参考にしていただけるかもしれません。 社内のGo言語の使用状況 今年の5月23日、「Go言語勉強会を始めたよ」といったエントリーを投稿していました。 devblog.thebase.in その際には、基本的にBASEのプロダクトはほとんどがPHPで構成されており、各開発者が一時的に使用するスクリプトをGoで書くケースがあるよといった使用状況でした。 現在は、新規機能をGo製APIとして実装・運用するプロダクト機能が出てきており、実際に運用開始しているものもあります。今回紹介する事例についても運用開始に向けて進めているものになります。 レイヤ構造模索の旅路 Go言語で運用を前提としたサービスを実装する場合に最初に頭を悩ませる、レイヤ構造実現のためのプロセスについてです。世の中には、Clean Architectureなどレイヤ構造についての解説記事が多数ありますが、結局は自分たちの要件にあったものを実現していく必要に迫られます。それをどう探っていったかという少し泥臭い話をしようかと思います。 なお、注記しておきますが、今回紹介する構成が「本当に正しいLayered Architectureである」などといった概念を正確に実現できているかという点に関しては保証できませんので予めご了承ください。 なぜレイヤ構造が必要だったか 様々必要・不要意見があるかと思いますが、筆者の環境では次のような理由にて必要だと感じておりました。 チームの開発成果物に統一的な構造をもたせて、どこで何が書いてあるかわかりやすくしたい 運用後のプロダクト改善を見越して、ビジネスロジックを入出力から分離した変更容易な設計としたい 運用を前提としない単純なスクリプトなどであれば、コードベースにレイヤ構造をもたせるといった検討は不要かもしれません。 しかし、今回は実際に運用しながらプロダクトとしてブラッシュアップしていく必要があるものであったため、上記の理由からレイヤ構造を考えていきました。 どのように進めたか 構造の概念図を仮定義 まず、最初に概念的な構成を図として定義するところからはじめました。実際に次のような簡易的な概念図を作成しています。 Layered Architecture を参考にしつつ、単一な依存方向となる構成とし、HTTPなどアプリケーションの外側の処理とビジネスロジックを分離する当初の目論見を形にできそうな構成を作成しています。 作っては壊す 上記の図を書いたからと言って実際に実装に落とし込み、かつ機能をそこに乗せてみないと本当に自分たちにとって必要なものだったのかわかりません。そのため、概念図をチームに説明した上で、作っては壊していくことを宣言していました。 スキルを上げていく チームが扱えるレベルを上げていかないことには、変更容易な設計となる成果物は難しいと考えます。そのため、その時点のチームで、ちょっとずつ背伸びしていく方針としました。そのため、後述しますが最初はシンプルな構成から仮定義した地点に近づけていく作業をしています。 実装を進めるにあたって ユニットテストに対する意味付け 今回、進めるにあたってユニットテストに対して次の意味付けをしていました。 設計に対するフィードバック材料 ユニットテストを書くことによって、「疎結合な実装になっているか」といったコード設計に対するフィードバックがある程度得られると考えます。そのため、「実装の肌感を増やすためのユニットテスト」という意味合いをもたせました。 大胆なリファクタリング 「作っては壊す」と上で書いたとおり、都度都度大胆なリファクタリングが発生しうります。大胆にコードベースをいじるためにユニットテストを書いています。(しかし、これは同時に筆者に大きく反省を残す瞬間が後ほど現れます。) パッケージの使用 Go言語を実際に扱うに当たり、標準パッケージでどれだけやるのかという議論はよく耳にするかと思います。筆者のケースでは、Go言語自体を覚えれば書ける状態にしたかったため、なるべく標準パッケージを用いる方針にしています。 後続のエンジニアに対する配慮という意味合いもありますが、同時に自分たちが、インターフェースの使い方など「Goらしく」実装するために必要な技法をプロジェクトを通じてある程度体得している状態としたかったためです。 そのため、最初は愚直に書いていき、理解度に合わせて順次便利になるライブラリ・機構を導入していっています。 実際のレイヤ構造 初版:Simple Model-Controller 一番最初の構造です。非常にシンプルなcontrollerとmodelしかない構造です。 ├── model └── controller この際は、GoのAPI開発にまずチーム全体で慣れるというところから始めたい意図があり、最小限の構成となっています。 model内には、データベースに対する技術的実装なども含まれており、インターフェースの使い方はまだこれからです。 第二版:Model-Controller + DIP 次に移動した構造です。repositoryとdatastoreという2つのパッケージが増えています。 ├── domain │ ├── model │ └── repository // + ├── infrastructure │ └── datastore // + └── controller ここで、model内にべた書きされていた、データベースに技術的実装をdatastoreパッケージとよんでいる箇所に移動し、repositoryという抽象レイヤを挟む構成となりました。実装イメージとしては次のようになります。 package repository type UserRepository interface { Save(userID int ) error } package datastore type UserStore struct { DB *sql.DB } func (s *UserStore) Save(userID int ) error { // 処理 } ここにて、インターフェースの扱い方について習熟するため、ユニットテストでも自前でモック実装を作成してもらい実装の流れを掴むということをしていました。 第三版:Layered Architecture + DIP ├── config ├── domain │ ├── model │ └── repository ├── infrastructure │ ├── datastore │ └── router ├── interface │ ├── controller │ └── middleware └── service // + layered architectureのapplication layerに該当 ここで、最初の概念図の構成要素がほとんど用意された状態になります。単一な依存方向となるようなLayered Architectureの構成とし、レイヤ間をより疎結合にするためドメインレイヤにインフラストラクチャが依存する抽象を設置しています。 また、処理の複雑化に伴いアプリケーションレイヤに相当する(※1)serviceパッケージを作成しています。これはトランザクションを扱うことが喫緊の課題として取り組みましたが後に記述する反省点の一つとなりました。 ※1 MVC + Serviceと捉えた場合はServiceレイヤと同等とも言えるかも知れません。 現在:Layered Architecture + DIP 2018年11月26日現在の構成です。 ├── common // + 全レイヤー共通で利用する機能群 │ ├── uuidgen │ └── validation ├── config ├── domain │ ├── model │ └── repository ├── infrastructure │ ├── clock │ ├── datastore │ ├── logger │ └── router ├── interface │ ├── controller │ └── middleware ├── service └── testutil // + テストヘルパー群 全レイヤ共通で利用するUUID生成などをcommonディレクトリとして集約し、テストヘルパー群をtestutil packageに集約しています。 実践した上での反省点 トランザクションの扱い トランザクションをどう扱うかを最初から考えておいたほうが良かったと反省しております。トランザクションをどう扱うによって大きくインターフェースが変わってくるかと思います。筆者の例の場合は次のような扱い方にしています。 package service type SimpleServiceImpl struct { DB repository.DBConnector User repository.UserRepository } func (s *SimpleServiceImpl) Run(userID int ) error { tx, err := s.DB.Begin() if err != nil { return errors.Wrap(err, "failed to begin transaction" ) } if err = s.User.Save(tx, userID); err != nil { // 引数に*Txを渡す wrapRollback(tx) return errors.Wrap(err, "failed to save user" ) } if err = wrapCommit(tx); err != nil { wrapRollback(tx) return errors.Wrap(err, "failed to commit transaction" ) } return nil } トランザクションはservice packageで扱います。その際、service package内で生成された*sql.Tx構造体をそれぞれのrepositoryの引数に渡す構造としています。 package repository type DBHandler interface { Execer Querier Preparer } type UserRepository interface { Save(db DBHandler, userID int ) error } その上で、repositoryでは、*sql.DB/Txをwrapした抽象型を作成し利用しています。なお、この構成については、 @codehex さんの もう一度テストパターンを整理しよう(WebApp編) - Speaker Deck という資料を大変参考にさせていただいています。 そして、この実装構成にするために、本当に大胆なリファクタリングが必要になり、チーム全員でそれぞれ自身の実装した機能をリファクタリングするという工数が発生してしまいました。 世の中で公開されているレイヤ構造のサンプルコードでなかなかトランザクションを扱っているものは少ない気がしていますが、実際にやるのであれば、最初にトランザクションを扱うコードをサンプルとして作ってみることはおすすめしておきます。 追記:Contextの扱いについて 頂いたリアクションの中で、トランザクションに合わせてcontextの扱いについての話が出ておりましたので追記しておきます。 こちらもトランザクションと同様にメソッドの第一引数に context.Context を渡す実装にしたほうがよいかと思います。 筆者が実際に context パッケージを用いた実装を行う際には、 @deeeet さんの次の記事を非常に参考にさせていただきました。 Go1.7のcontextパッケージ | Taichi Nakashima Golangのcontext.Valueの使い方 | Taichi Nakashima テストヘルパーを先回りして用意しておく ユニットテストを愚直に書いていくという方針は当初理解度を深める上ではよかったのですが、進んでいく中で共通で毎回やるアサーションなどがコピペ作業になっている状態が発生し、「テストを書くのが億劫になる」といった状態が生まれていました。 先回りして共通的なものをテストヘルパーとして提供して開発体験を損ねないということが重要だったなと思います。 実際例として、ResponseHeaderのアサーションなどのヘルパーなどを随時用意していっています。 // Example assertion response header func AssertResponseHeader(t *testing.T, res *http.Response, code int ) { t.Helper() if code != res.StatusCode { t.Errorf( "expected status is '%#v', \n but actual is '%#v'" , code, res.StatusCode) } if expected := "application/json; charset=utf-8" ; res.Header.Get( "Content-Type" ) != expected { t.Errorf( "expected Content-Type is '%#v', \n but given '%#v'" , expected, res.Header.Get( "Content-Type" )) } } また、テストに関するTipsは今回のGo Conference 2018 Autumnにて、 @ timakin さんが発表されていた、 Golang API Testing the HARD way - Speaker Deck という資料が大変勉強になるかと思います。 まとめ 今回のエントリでは、最初に構想を決め、実践しながら拡張して構想に近づけていく事例を紹介させていただきました。 すでにGo言語をバリバリ使用している現場の方であれば過程の振り返りに、これから使用を検討されている方にとっては一歩踏み出す参考となれば幸いです。
どうもお久しぶりです。BASEビール部部長の氏原です。最近急に涼しくなりましたね。ハイアルなベルギービールでも飲んで温まるといい季節ですよ。 さて、今回もビールの話はとりあえず置いておいて現在Data Storategy Groupで取り組んでいる内容として、今年に出たらしい論文「 Adversarially Learned One-Class Classifier for Novelty Detection 」を実装して商品画像フィルタにならないか試してみたことについてお話しようと思います。 One-Class Classifierとはあるクラスに属するか否かの判別器です。例えばある画像に写っているものがQRコードか否かとか、水着か否かとかです。 今回の話は一行で言えば、ショッピングアプリ「BASE」の商品検索などから意図と異なる画像をフィルタリングするためにAdversarially Learned One-Class Classifier(ALOCC)が使えないか試したという内容です。 背景 皆さんショッピングアプリ「BASE」を使ったことはおありでしょうか?数多くの商品が並んでいて、私は米とか肉とか買ってます。そんな中こんな商品たちを見かけたことはないでしょうか? またはおすすめショップのところのこういうのとか こちらは、お知らせをひとつの商品として登録するとこのように表示されます。本来的な商品画像の使用法とは異なりますが、登録自体は可能です。 これらの商品ではない商品がショップのページに出てくるのは特に問題ないのですが、もし検索とかで出てきたら探している商品と違うなぁと思ってしまうのではないでしょうか? この問題を解決するために商品ではない商品を自動で発見して検索等には出ないようにする機能を開発しています。そしてQRコード画像については現在検索等からの除外を開始しています。 今回お話するのはお知らせ等の文字画像の検出について今取り組んでいることの紹介です。 Adversarially Learned One-Class Classifier(ALOCC) for Novelty Detectionについて 単純に文字画像か否かの判別器を作ることを考えると以下のような問題があります。 文字画像ではない画像、という教師データをどう集める? 文字画像以外の画像では幅が広すぎる 文字画像とはなにか?がはっきりとわからない。人によって違う。 文字中心のポスターや、文字っぽいロゴや、サイズ表はどう扱うべきか そもそも文字画像のサンプルが少ない 圧倒的に教師データが不足している これらの問題が Adversarially Learned One-Class Classifier for Novelty Detection では軽減できそうだったので、使えそうかどうか試してみました。 実験結果に行く前にこの手法を簡単に解説しておきます。 ネットワーク構造 論文に乗ってた画像そのまま上げます。 ( M Sabokrou, "Adversarially Learned One-Class Classifier for Novelty Detection", arXiv.org, 2018 ) 構造としてはGANにインスパイアされたものになります。 D がDiscriminatorなのはGANと同じで、GANでGeneratorだった部分がReinforcerとなっています。ReInforcerは画像(元の画像にノイズを追加したもの)を与えられて、何かしらの画像を出力します。 DiscriminatorはReinforcerが出力した画像と元の画像を見分けるように学習させ、ReinforcerはDiscriminatorを騙すように学習します。 学習の目的 さて、では上記ネットワーク構造は何を意図しているのでしょうか?これは元画像との違いを検出する検出器を作ろうとしているのです。 学習に使う画像は検出したいターゲットのクラスの画像だけ、今回の私の用途で言えば文字画像だけいいのです。文字画像以外の画像というものを教師として揃える必要がありません。これはありがたいです。 教師を作るためにこれは文字画像、これは文字画像ではない…と延々とやってるとこれはどっちだろうかという画像が出てきてだんだん混乱してきます。なのでこれは文字画像にしたいなってものを集めるだけでいいのはとても楽です。 R はターゲットに似た画像を生成するようになります。でも特定のターゲットクラスの画像しか学習してないので、ターゲットのクラスではない画像を渡すとぐちゃぐちゃな画像が生成される、ということを期待しています。今回は文字画像を学習させるので文字画像っぽいものは綺麗に再構築されるけど、それ以外はぐちゃっとなってほしいというわけです。 D は再構築された画像、つまりターゲットから少しでも違う画像を見分けようとするため、違いに敏感になるように学習されます。この結果 D はターゲットとの違い、つまり新規性の検出器となります。 文字画像を1として学習させれば、 D が返す結果が1に近いほど新規性がない、つまり学習した文字画像に近い画像ということになり、0に近いほど新規性の高い見たことがない画像ということになります。 ( M Sabokrou, "Adversarially Learned One-Class Classifier for Novelty Detection", arXiv.org, 2018 ) 論文ではペンギン画像を学習させています。 画像を R に通した結果、ペンギン画像はペンギン画像として再構築されていますが、ペンギンでない画像はなにか汚い画像になっています。この再構築した画像を D に渡すと元の画像を渡すよりも綺麗にペンギンとそれ以外を判別できるようになると主張されています。 実装 さて、一応論文の 著者の方のgithub はあったのですが、tensorflowがゴリゴリでわかりづらかったのでpytorchを使って実装してみました。 Reinforcer Reinforcerの中身は受け取った画像をConv2dで畳み込むencoderと、Deconvするdecorderです。GANだとパラメータ受け取ってDeconvしていくだけなので、ここがちょっと違います。 import torch.nn as nn class Reinforcer (nn.Module): def __init__ (self): super (Reinforcer, self).__init__() self.encoder = nn.Sequential( nn.Conv2d( 3 , 64 , 3 , stride= 1 ), nn.ReLU(), nn.BatchNorm2d( 64 , 0.8 ), nn.Conv2d( 64 , 128 , 3 , stride= 1 ), nn.ReLU(), nn.BatchNorm2d( 128 , 0.8 ), nn.Conv2d( 128 , 256 , 3 , stride= 1 ), nn.ReLU(), nn.BatchNorm2d( 256 , 0.8 ), nn.Conv2d( 256 , 512 , 3 , stride= 1 ), nn.ReLU(), nn.BatchNorm2d( 512 , 0.8 ), ) self.decoder = nn.Sequential( nn.ConvTranspose2d( 512 , 256 , 3 , stride= 1 ), nn.ReLU(), nn.BatchNorm2d( 256 , 0.8 ), nn.ConvTranspose2d( 256 , 128 , 3 , stride= 1 ), nn.ReLU(), nn.BatchNorm2d( 128 , 0.8 ), nn.ConvTranspose2d( 128 , 64 , 3 , stride= 1 ), nn.ReLU(), nn.BatchNorm2d( 64 , 0.8 ), nn.ConvTranspose2d( 64 , 3 , 3 , stride= 1 ), nn.Tanh(), ) def forward (self, x): x = self.encoder(x) x = self.decoder(x) return x Discriminator DiscriminatorはGANと何も変わりません。受け取った画像を畳み込んで全結合層に渡すだけですね。 import torch.nn as nn class Discriminator (nn.Module): def __init__ (self): super (Discriminator, self).__init__() self.model = nn.Sequential( nn.Conv2d( 3 , 64 , 3 , stride= 2 ), nn.BatchNorm2d( 64 , 0.8 ), nn.LeakyReLU( 0.2 , inplace= True ), nn.Dropout2d( 0.25 ), nn.Conv2d( 64 , 128 , 3 , stride= 2 ), nn.BatchNorm2d( 128 , 0.8 ), nn.LeakyReLU( 0.2 , inplace= True ), nn.Dropout2d( 0.25 ), nn.Conv2d( 128 , 256 , 3 , stride= 2 ), nn.BatchNorm2d( 256 , 0.8 ), nn.LeakyReLU( 0.2 , inplace= True ), nn.Dropout2d( 0.25 ), nn.Conv2d( 256 , 512 , 3 , stride= 2 ), nn.BatchNorm2d( 512 , 0.8 ), nn.LeakyReLU( 0.2 , inplace= True ), nn.Dropout2d( 0.25 ), ) self.adv_layer = nn.Sequential( nn.Linear( 512 * 9 , 1 ), nn.Sigmoid(), ) def forward (self, img): out = self.model(img) out = out.view(out.shape[ 0 ], - 1 ) validity = self.adv_layer(out) return validity 学習 学習の仕方もGANとほぼ変わりません。 import torch import torch.nn as nn import torch.optim as optim import torchvision.datasets as dset ... device = torch.device( "cuda:0" ) netR = Reinforcer().to(device) netD = Discriminator().to(device) dataset = dset.ImageFolder(....) dataloader = torch.utils.data.DataLoader(dataset) criterion = nn.BCELoss() optimizerR = optim.Adam(netR.parameters(), lr=..., betas=...) optimizerD = optim.Adam(netD.parameters(), lr=..., betas=...) ... for epoch in range (n_epochs): for data in dataloader: images = data[ 0 ] # dset.ImageFolderのtransformでtorchvision.transforms.Lambdaつかって # 元画像とノイズ画像両方とれるようにごにょごにょしてる real = images[ "real" ].to(device) # 元画像 noised = images[ "noisy" ].to(device) # ノイズかけた画像 batch_size = real.size( 0 ) ############################ # Update D network ########################### netD.zero_grad() # 本物 label = torch.full((batch_size, 1 ), 1 , device=device) output = netD(real) errD_real = criterion(output, label) errD_real.backward() # 偽物 fake = netR(noised) label.fill_( 0 ) output = netD(fake.detach()) errD_fake = criterion(output, label) errD_fake.backward() optimizerD.step() ############################ # Update R network ########################### netR.zero_grad() # 偽物でDを騙す label.fill_( 1 ) output = netD(fake) errR = criterion(output, label) errR.backward() optimizerR.step() 結果 文字画像を学習させてみた結果、文字画像はそこそこ綺麗に再構築され、それ以外は結構ぐちゃっとなるようになったようです。 accuracy false positive false negative D ( X ) 0.92 0.06 0.02 D ( R ( X )) 0.87 0.03 0.10 むしろ判別性能は R をかました方が落ちてます。ただ、false positive、つまり本来文字画像じゃないのに文字画像と認識してしまった率は多少改善されました。文字画像は出したくないのですが、本来出したい商品画像が落とされてしまうのは避けたいのでfalse positiveはできる限り抑えたいところです。そういう意味ではそこそこ意味はあるのかなと思います。 感想 文字画像はそこまで綺麗にいかない、というか論文の結果の画像が綺麗にできすぎ。何か書いてない(もしくは私がちゃんと読めてない)工夫があるのかも。 Reinforcerは論文にはない工夫を追加した(まだ実験中なので内容は内緒)。そうしないとただのGANにしかならない。ターゲット画像以外の画像渡しても普通にそこそこ綺麗な画像が再構築される。 Batch Normarizationは偉大。 まとめ 文字画像を正確に判別できるようになるまではまだまだ道のりが長そうです。 false positive 3%といっても画像は1日に何万枚と上げられてきますので、間違いの絶対数はそこそこ多くなります。理想的には桁をもう一つ下げたいところです。 でも今回の判別器では、単純に文字画像判別器を作ったときに弾かれがちだった文字Tシャツはかなり高精度で文字画像ではなく商品であると認識できるようになりました。 こういうやつ ( RACCOONS ONLINE STORE より) 引き続き良きユーザー体験を提供できるように頑張っていきます。
BASEでエンジニアリングマネージャーを担当している加賀谷です。普段は採用に携わったり、1on1での経験学習の促進などを通じて、個人と組織のアウトプットが大きくなるようにサポートする仕事をしています。また、サービス開発に関わる体験を良くしていくこともしています。その中で今回は、静的コンテンツのCI/CDでしていることを紹介したいと思います。 静的コンテンツのホスティング 静的コンテンツは、サーバサイドでリクエストに応じてレスポンスする内容を作成しないデータです。主に、サイト内で使う画像、CSS、JS、ランディングページなどのHTMLファイルになります。これらのファイルはよく、AWSのS3に置いてホスティングして前段にはCDNを配置し、Webブラウザの同時接続数を考慮してサービスとは別のホストに分散したりしますが、BASEでもそうしています。 静的コンテンツ用のGitリポジトリを用意 CSSやJSが成果物となる開発はBASEにおいては主にデザイナーとフロントエンドエンジニアが担っています。以前はPHPもJSもCSSも同じリポジトリで開発していたのですが、今ではサービスのメインGitリポジトリとは別のリポジトリで開発〜デプロイをするようにしています。もちろんメインGitリポジトリからいっさいのJSやCSSを無くしているわけではなく、TwitterのBootstrapのようにコンポーネントとなるCSSやJS、ランディングページなど、メインから独立できるファイル群をこのような別リポジトリに入れています。 git pushでCircleCIからaws s3 syncする デプロイ先はS3で、 git push を契機にCircleCI上から aws s3 sync しています。以前は手動で本番S3に画像をアップロードする属人的な場面も少なからずあったのですが、今ではリポジトリの中に画像を入れてCircleCI経由でS3に配置するようにしています。 開発ワークフローと環境別のURL 静的コンテンツリポジトリの開発ワークフローはメインのGitリポジトリと同じようにしています。 develop ブランチから開発用ブランチ feature/xxx を切って開発 develop へプルリクエスト&マージ リリース時は develop から master へ git-pr-release でリリース用プルリクエストを作成 master を本番環境へデプロイ 環境ごとに静的コンテンツのURLが欲しいので、S3はそれぞれ用意してCircleCIが回るブランチで aws s3 sync 先を変えています。 develop ブランチの時にはステージング用のS3、 master ブランチの時には本番、それ以外のブランチは開発用S3へ対応させています。 .circleci/config.yml CircleCIではだいたい以下のようなことをしています。 checkout awscliをインストール リポジトリ内の特定ディレクトリ配下の各ファイルにACLとcache-control、content-typeをつけて aws s3 sync syncしたファイルのパスのCloudFrontのキャッシュを削除 また、アップロードされたファイルのURLなどをSlackへ通知して気付けるようにもしています。 version : 2 jobs : build : docker : - image : docker:17.12.0-ce-git environment : - TZ : "/usr/share/zoneinfo/Asia/Tokyo" - SYNC_OPTIONS : "--cache-control \" max-age=86400 \" --acl public-read --size-only --no-progress --delete" - S3 : "static-example-net" steps : - checkout - run : name : Install dependencies command : | apk add --no-cache \ py-pip=9.0.1-r1 \ curl \ curl-dev \ openssl pip install \ awscli==1.14.40 - run : name : Upload files to S3 command : | set -x tmpfile=`mktemp` for i in `cat .s3ignore | grep -v "^#" ` do IGNORE_OPTIONS="${IGNORE_OPTIONS} --exclude \ "**/${i} \" " done # リポジトリ内webroot/配下の各ファイルを適切なcontent-typeをつけてsync eval `printf "aws s3 sync webroot/ s3://%s/ %s %s --exclude \" * \" --include \" %s \" %s --content-type \" %s \" " "${S3}" "${AWS_PROFILE}" "${SYNC_OPTIONS}" *.css "${IGNORE_OPTIONS}" text/css` | tee -a $tmpfile eval `printf "aws s3 sync webroot/ s3://%s/ %s %s --exclude \" * \" --include \" %s \" %s --content-type \" %s \" " "${S3}" "${AWS_PROFILE}" "${SYNC_OPTIONS}" *.js "${IGNORE_OPTIONS}" application/javascript` | tee -a $tmpfile eval `printf "aws s3 sync webroot/ s3://%s/ %s %s --exclude \" * \" --include \" %s \" %s --content-type \" %s \" " "${S3}" "${AWS_PROFILE}" "${SYNC_OPTIONS}" *.json "${IGNORE_OPTIONS}" application/json` | tee -a $tmpfile eval `printf "aws s3 sync webroot/ s3://%s/ %s %s --exclude \" * \" --include \" %s \" %s --content-type \" %s \" " "${S3}" "${AWS_PROFILE}" "${SYNC_OPTIONS}" *.html "${IGNORE_OPTIONS}" text/html` | tee -a $tmpfile eval `printf "aws s3 sync webroot/ s3://%s/ %s %s --exclude \" * \" --include \" %s \" %s --content-type \" %s \" " "${S3}" "${AWS_PROFILE}" "${SYNC_OPTIONS}" *.png "${IGNORE_OPTIONS}" image/png` | tee -a $tmpfile eval `printf "aws s3 sync webroot/ s3://%s/ %s %s --exclude \" * \" --include \" %s \" %s --content-type \" %s \" " "${S3}" "${AWS_PROFILE}" "${SYNC_OPTIONS}" *.jpg "${IGNORE_OPTIONS}" image/jpeg` | tee -a $tmpfile if [ -s $tmpfile ] ; then tmpdir=`mktemp -d` split -l 30 $tmpfile $tmpdir/ for splited in `find $tmpdir -maxdepth 1 -type f`; do # CloudFrontからのキャッシュを削除 PATHS=`grep -ao "s3://${S3}/.*$" ${splited} | sed "s/^s3:\/\/${S3}//g" ` aws cloudfront create-invalidation ${AWS_PROFILE} --distribution-id ${CDN_DISTRIBUTION_ID} --paths ${PATHS} done fi サーバサイドのリポジトリと分けて開発、デプロイするメリット 1日に何度も本番環境へデプロイをする状況下においても、1度にデプロイするコード量が減り確認範囲が狭くなることで、よりカジュアルにデプロイできることがメリットかなと思います。サーバサイドと同じリポジトリで開発していたときには、例えばランディングページのちょっとしたスタイル変更にも、CircleCIでサーバサイドの全テストを回してBlue-Green Deploymentリリースフローをする流れを必要とするために比較的変更の反映に時間がかかっていましたが、これもなくなり手続き的にも早いデプロイができるようになりました。 Slackからgit-pr-release リリース用プルリクエストを作るときに、 git-pr-release コマンドを実行していますが、これをSlack Botでもできるようにしています。スマホのSlackアプリからも実行できるので便利です。GitHubのアカウントをSlackのアカウントに変換してメンションさせたり、リリースのサマリをSlackへ通知して変更がざっくり共有できるように工夫しています。 1. Botにメンションするとセレクトボックスを返すのでデプロイするリポジトリを選ぶ 2. 選択したリポジトリをその場で確認される 3. Yesを押すとgit-pr-releaseを実行 4. スレッドでメンションが返ってくる 5. 作成されたリリース用プルリクエストのURLが通知されるので、確認してmasterマージするとCircleCI経由で本番デプロイ まとめ 今回は、GitHubとCircleCIでS3へデプロイするワークフローを紹介しました。まだリポジトリ内には画像のようなバイナリファイルがあまり多くないのでリポジトリが肥大化してgit pull/pushが苦になることはありませんが、多くなってきたら Git LFS も検討してみたいと思います。 BASEの開発チームでは良いサービスをつくっていくために開発体験の改善も楽しみながら活動しています。ご興味を持たれた方いらっしゃいましたら是非ご連絡いただければ幸いです。 jobs.binc.jp
DataStrategyの齋藤( @pigooosuke )です。 ネットショップ作成サービス「BASE」は60万店舗のショップが利用しており、ショッピングアプリ「BASE」のユーザーは、新着商品、キーワード検索、関連商品、商品特集などを介して気になる商品を見つけることができます。今回、新機能として、検索ワードに関連するキーワードを表示することで、ユーザーの興味のありそうな商品にたどり着ける動線を機械学習を活用して実装しました。 DataStrategyチームは発足して間もなく、サービスドメインに適応した単語辞書がなかったので、新規で作成するところから始まりました。機械学習におけるデータセットのアノテーションについての知見が共有される機会が少ない印象もあり、折角なので今回私達が行ったデータ作りから実装までの流れをご紹介します。 概要 今回、どんなキーワードも意味的に近ければ、サジェストしても良いとはせず、ホワイトリストに登録されたキーワードのみをサジェストし、サービス品質を保てることを最低条件としています。また、関連キーワードにはいくつか定義が存在しますが、今回は類義語をサジェストする関連キーワードの開発を行いました。 大まかな手順は以下となります。 「BASE」で過去検索されたキーワードのデータからキーワードのリストを取得 前処理として、不適切な検索キーワード、優先度が低いキーワードを一定のルールで削除 有用なキーワードに対して、アプリに表示可能な単語かをアノテーション チェックがOKだったキーワードを登録したMeCab辞書を活用し、商品のタイトル・紹介文を分かち書きに分解 商品情報(タイトル+説明文)に対してWord2vecを通し、類似度の高い単語を調査し、関連キーワードとして表示 Word2vecについて 文字の通り、word(文字)をvector(ベクトル:n次元)で表現するために深層学習を行うモデルです。それぞれの単語はその周辺に出現する単語によって決められているという仮説に基づいて学習を行います。今回の記事のテーマではないので詳細は割愛します。 # 学習語彙での類似度の高い単語を出力 model.wv.most_similar(positive= 'ワンピース' , topn= 6 ) > [( 'フレアワンピース' , 0.8115994334220886 ), > ( 'ロングワンピース' , 0.7681916952133179 ), > ( 'ミニワンピース' , 0.7396647930145264 ), > ( 'aラインワンピース' , 0.7346851825714111 ), > ( 'vネックワンピース' , 0.7129236459732056 ), > ( 'レースワンピース' , 0.7060198783874512 )] アノテーション 実際にアノテーションを進めていく前に、きちんとルールを決めておくことがかなり重要です。 いくらルールを厳格に決めておいても、「このケースはOKで通してみたけれど、アノテーションを進めて全体感がつかめてくると、あれはNGのケースだった。」という手戻りは少なからず発生してしまいます。 ましてや、集団でアノテーションを実施する時の難易度はかなり高くなると感じています。 今回は、(幸いなことに?)アノテーション作業を一人で行ったので、可能な限りルールの統一が出来たと思います。 参考までに、今回設定したルールとしては下記の通りになります。 今回は、各キーワードについて3つのラベルをつけていきます。 NG単語ラベル アプリで表示するに耐えうる表現・ユーザーのUXを損なうことのない表現かをチェックしていきます。 OKとなった(ホワイトリストに登録された)キーワードのみアプリ上で表示されます。 基本的に最小商品単位になりうるものはOK 修飾語が3語以上つながっているものはNG(バックロゴ付き薄手コート: バック , ロゴ付き , 薄手 ) ブランド名が含まれているものは、社内ルール的にNG( ルイヴィトン , ディズニー ) 年号・数字・期間限定のキーワードが含まれているものはNG(例外で、季節感のあるものはOKとしている サマーサンダル , スプリングコート ) 色を表すものはNG(全色対応など、候補のノイズになる可能性が高いため) 口語表現がついているものはNG( ゆったり , もこもこ ) 単語単体で何を検索しているのか不明瞭なものはNG( 星 , クーポン , 名刺 ) ゆらぎ表現ラベル 単語表現のゆらぎは検索エンジンにはつきものです。類似表現が出てきた時、両単語を表示することはせず、どちらかの単語のみを表示します。これを ゆらぎ表現 として別途登録して入出力制御に活用します。 例: Pコート , ピーコート , キャミソール , キャミ など。 ゆらぎ表現 は検索件数が多いものを正解とする 鞄でいう、 〇〇バッグ , XXバック のように語尾が統一されないジャンルが存在するが、検索実績が多いキーワードを正解とする カテゴリーラベル これは個人の主観が大きく入るのと、全体のバランスを見つつゼロからラベルをつけていくので、大きく手戻りが発生する作業なかなか難しい作業です。こちらのラベルも出力制御に活用します。BASEで設定しているカテゴリーを参考にしつつ作業を進めました。 各単語について、何のカテゴリーなのかを手動でラベリング( コート , 皿 , 肉 ) カテゴリー階層は最大3階層まで アノテーションデータはこのように構成されています 入出力の制御(アノテーションラベルの活用) Word2vecは学習に使用された語彙に対して、それぞれベクトルが設定されています。上図のようにアノテーションで属性が判明している単語もあれば、まだ調査していない未知語も入力として入ってくる可能性があります。未知語はシャットアウトすれば良いという判断もありますが、対応語を増やすため未知語でも入力を認めることにしました。ただ、Word2vecにより大量の文章を学習しており、未知語の関連語として何が出力されるのかブラックボックス化されてしまう懸念がありました。この不透明さを解消するために活用したのがこれまでアノテーションしてきたラベルです。 NGフィルタは、ホワイトリストに登録された単語のみを出力するように配置されています。ゆらぎ表現フィルタは、ホワイトリスト内の単語を片寄せするために配置されています。カテゴリーフィルタは、出力するカテゴリーを統一させ、出力ノイズを減らすために設置されています。カテゴリーフィルタは、特に未知語や学習が不十分な語彙に対して大きく影響を与えます。 下でカテゴリーフィルタ設定前後を比較しています。 # カテゴリー補正前 model.wv.most_similar(positive= 'ポット' , topn= 6 ) > [( 'ピッチャー' , 0.6643306016921997 ), > ( 'ティーポット' , 0.62236487865448 ), > ( 'ボウル' , 0.6212266087532043 ), > ( '鉢' , 0.5690277814865112 ), > ( 'ティーカップ' , 0.5430924296379089 ), > ( '花器' , 0.5429580211639404 ), # カテゴリー補正後 model.wv.most_similar(positive= 'ポット' , topn= 6 ) > [( 'ピッチャー' , 0.6643306016921997 ), > ( 'ティーポット' , 0.62236487865448 ), > ( 'ボウル' , 0.6212266087532043 ), > ( 'ティーカップ' , 0.5430924296379089 ), > ( 'コーヒードリッパー' , 0.5426920056343079 ), > ( 'トレイ' , 0.5033057332038879 ), 鉢 や 花器 といった、容器ではあるものの、キッチン周りにはふさわしくない園芸向けのキーワードの出現が抑制されるようになっています。結果、類似度の閾値制御だけでは弾くことが難しいケースもカテゴリーを設定することによって解決することができました。 まとめ 今回は、アノテーションを機械学習の制御に活用してみたという内容をお送りしました。機械学習の学習結果をそのまま信用して活用するのではなく、アノテーションの結果を活用することで品質向上を行うことができました。ただ、アノテーションを闇雲に行うのではなく、どう活用していくのか事前の課題設定をきちんと立てることも重要そうですね。 BASEでは一緒にネットショップ作成サービスを開発・改善するエンジニアを募集してます。 機械学習のチームでは、様々なデータや技術を使ってECならではの開発を続けています。 ご興味のある方はぜひ遊びにきてください!! jobs.binc.jp
こんにちは、BASEのDesign Groupに所属している 吉岡 です。 ネットショップ作成サービス「BASE」のデザインや、2018年1月に設立されたBASE株式会社の100%子会社であるBASE BANKの株式会社立ち上げにデザイナーとして携わっています。 BASE BANK株式会社は、「銀行をかんたんに、全ての人が挑戦できる世の中に」をミッションとし、現在は関連事業の立ち上げを行っています。代表はBASE社と同じく鶴岡裕太が務め、メンバーはBASEと兼務している者もいます。オフィスも同じフロアにあり、カルチャーや思想なども含めてBASE社とかなり近いと言えます。 先日、BASE BANK社のロゴを作成しましたのでデザインプロセスの経緯を書き留めておきたいと思います。この記事を通じてBASE BANKがどのような会社なのか伝われば嬉しいです。 デザインの経緯 はじめに仮置きされていたロゴは下記画像の右のロゴでした。 左側がBASEのコーポレートロゴで、BASEロゴに合わせて、タイポグラフィをアウトライン化して入れたものでした。 'SE'に対して、BANKの'NK'がフォントボディが大きく、上手くはまりません。 綺麗にデザインしよう 、ということでここからロゴの作成をスタートします。代表の鶴岡に時間をもらいながらヒアリング/調整を行なっていきました。 まず、BASE BANKの立ち位置と印象を考える ヒアリングしたところの概要をまとめると、 「銀行をかんたんに」をミッションとするので安心感を与えるロゴにしたい ただ、固すぎないような自由なデザインも欲しい 印象としてはストリート感が少し欲しい という内容でした。 それを受けてデザインした初稿がこちらです。 なんとなく、Aが良いかな...という感じで、もう少し安定感のあるフォントが良いという流れに。ただ、この段階では、 しっくりくる提案ができていないような感触 がありました。 迷うロゴデザイン ここから2週間ほどかけて3回ほどアップデートしていくのですが、なかなか良い形で決めることができませんでした。 フィードバックとしては、 BASEとの親和性が感じられない ロゴの立ち位置が中途半端になっているのでフォントを変える意味がなさそう などでした。 この時点でスタートから既に3週間は掛かっていました。しかし、BASE社のタイポグラフィやテイストに捉われすぎて、壊せていない部分があったので、 今まで作ったロゴは全て壊してしまおう という結果に。 他のプロジェクトと並行して行なってきたのですが、この段階でロゴの作成に注力していきます。 コンセプト設計へ立ち返る BASE BANK のロゴは どういう立ち位置なのか。パーソナリティとしてどういう人なのか? を定めて、それに対しロゴを考えるフローに戻しました。 BASE BANKは何をする会社なのか? MTGやブレストで上がった内容から、BASE BANKはどういう会社なのかまとめました。 もっと銀行を簡単にしたい ITとデータを活用する 多くの人にチャレンジしてほしい 成長をフォローしていく 新しく立ち上げた会社 パーソナリティを決める どういう性格なのか、を決めます。 革新的(銀行を簡単にする、新しく立ち上げた会社) クール(ITとデータの活用) 安心感(チャレンジして欲しい、成長をフォローしていく) 以上の3点を踏まえた上で、ロゴを再考しました。 ラフスケッチしていく まずは手書きでラフスケッチしていきます。 パーソナリティを考慮し、すっきりとしつつ、安定感のあるようなロゴの形を簡単にスケッチしていきます。一旦思いつくままに書き込みます。 あまり綺麗ではありませんが...100~150個位は書いたかと思います。 後々の閃きにも繋がってくるので、どんなアイデアでも書き留めることが重要です。 データに落とし込む 手書きラフから、イメージに合うものを選び、Illustratorで描いていきます。手書きでは問題なさそうな案も、データにしてみるとイメージが違った..などの案はこの段階で絞り込んでいきます。 ベースと、方向性の決定 brandon grotesqueという割と新しいフォント。1930年代のgeometricフォントの歴史的なものを継承しつつ、ディテールに新しさを感じます。可読性もあり、安定感と革新性を感じられるものを選びました。 このベースを元にデザインの方向性を決定しました。 洗練された印象を持たせる白をベースに、フォントの持つ安定感は残し、角丸は消す BASEとの親和性を持たせるためにスクエアにこだわる 成長をフォローしていくイメージで、斜めのラインモチーフを入れる ロゴだけに注力し始めて2週間...出来上がったアウトプットがこちらです。安定感のあるボディと革新的な部分を共存させるようなイメージで作り上げました。 ここから、角度をつけた部分やディテールを調整していきました。 角度をつけた部分が急に見えるので30度から15度に変更したり、安定感が欲しい...ということで'A'のバー位置を下げたり、もう少し落ち着かせたい...角度をつけるロゴは1つに限定する、などの変更をしました。 スタートから1ヶ月半、遂に完成したロゴはこちらです...! アウトプットとしては、単にロゴだけなのですがBASE BANK社のこれから成し遂げたい事や思いを全て詰め込んでロゴを作れたかな..と思います。会社について、ひたすら考えることができた1ヶ月半でした。 文字領域と四角の領域は黄金比率1:1.618を使ってレイアウトを組んでいます。 全てをグリッドに合わせ過ぎると、小さく見える文字があるので若干調整しつつ、斜めの角度を0.5%単位、線幅など最終0.4ptの細かい調整をしています。 最後に コーポレートのロゴを作成する、という経験はインハウスデザイナーでもなかなかできないので良い経験になりました...!自ら手を上げれば、色々なデザインに携わることができる環境だと思います。 この記事でBASE BANKのチームで一緒に働くことに興味が出たという方は以下の募集からご連絡ください! jobs.binc.jp
はじめまして、BASEでSREに所属している浜谷です。現在は主にAWSを使用したインフラ構築と運用を担当しています。 そこで今回は前回好評だったBASEビール部部長が語ってくれた「 Yahoo!の近傍探索ツールNGTを使って類似商品APIをつくる 」のインフラ環境の構築についてお話をしようかと思います。 1. 背景 BASEでは機械学習の環境以前に今本番で何が動作しているのか、又その全体を把握するにはAWSのコンソールにログインして調査する方法しかありませんでした。 AWSの運用をしているとよくある事かと思います。 そんな中BASEではシステム全体構成図の見直しやサーバの一覧を自動化したりとインフラの見える化を進めています。 そこで次はインフラの構成管理を行っていこうといった流れがあり、まずはData Strategyチームの環境をTerraformで構成管理しようと相成りました。 2. 機械学習の環境 環境をサクッと説明するとS3に画像がアップロードされたら、SNSにイベントを送付します。 SNSからSQSやLambdaに振り分けて情報を蓄積し、蓄積したデータを元にECSのDockerで機械学習した結果をAPIで返すといったインフラ環境となります。 類似商品APIって何と詳細が気になった人は BASEビール部部長の記事 を読んでみてください。 3. そもそも構成管理は必要? 運用をしていく為に構成を把握する事は必要不可欠です。 ただ私自身構成管理ツールの必要性をあまり感じていませんでした。 というのはSIerの出身なので設計書、手順書、運用のドキュメントは納品物なので時間を掛けて作るのは当たり前でした。 SIerのエンジニアはoffice製品でのドキュメント作りや成果物の承認に時間を割くことが多く、その上構成管理のツールまで必要なのって感じている人は少なくないと思います。 またドキュメントでの構成管理は構築を始めるまでに時間が掛かるのは当然といった考え方が前提としてあります。 しかし、BASEの行動指針の一つに「MOVE FAST」があります。 このMOVE FASTを実現するためには、時間が掛かるのは当然といった考え方を捨てる必要があります。 そこで構成管理ツールは必要不可欠なのです! 構成管理ツールを使用するとインフラをコードで管理できるので設計から構築までを一気通貫に実現できます。 4. なぜTerraformなの? 現在BASEではAWSをメインに利用しているので、AWS CloudFormationの方が良いかもしれません。 機械学習の環境においてGCPのBigQueryを使用する場面が今後発生してくるかと考えています。 その際にマルチプラットフォームに対応した構成管理ツールを選ぶ必要がありました。 5. コードでのインフラ管理 AWSを管理コンソールで構築しているとGUIベースで確認するか、ドキュメントベースで環境を把握する必要があります。 しかし、Terraform等の構成管理ソフトを使用するとコードベースで管理できます。 VPCをTerraformでコード定義すると以下のようになります。 resource "aws_vpc" "ds-image-processing-vpc" { cidr_block = "10.1.0.0/16" tags { Name = "ds-image-processing-vpc" } # We explicitly prevent destruction using terraform. Remove this only if you really know what you're doing. lifecycle { prevent_destroy = true } } コードで管理することでGitHubを利用した管理が出来ます。 GitHubはアプリエンジニアだと日常的に使用しますが、インフラエンジニアにとっては少し敷居が高く感じます。 私もBASEに入社するまでは殆どGitHubは使用していませんでしたが、今では普通に使用してレビューなどの履歴も残すことが出来てとても便利に感じています。 今まではインフラ構築のレビューをシステム構成図や設計書ベースでのレビューをしていたので、別途レビュー票を起こしたり等の手間が大幅に減りました。 またインフラ構成の変更をAWSのコンソールでを漏れなくチェックするのは非常に困難です。 しかし、GitHubを使用することで差分の確認をすることで、何を変更したのかも一目瞭然です。 6. 実際にTerraformを使ってみよう! それではいよいよ実際にTerraformを使用してみましょう。 今回はハンズオンとして、RDSのAuroraとEC2のLinuxインスタンスを実際に作ってみます。 ①まずはTerraformを実行出来る環境の準備をします。  Terraformのダウンロード(URL: https://www.terraform.io/downloads.html )   ※バージョンは適時ダウンロードしたファイルに読み替えてください。  ダウンロードしたファイルを展開して、環境変数にパスを通す。 $ mv terraform_0. 11 .4_linux_amd64.zip /usr/ local /bin $ unzip terraform_0. 11 .4_linux_amd64.zip $ ls -l terraform  ※実行権限があることを確認 $ env | grep PATH  ※PATHに/usr/ local /binが通っていることを確認 $ mkdir -p < 作業ディレクトリ > $ cd < 作業ディレクトリ > ②GitHubにサンプルを用意しましたので、ダウンロードします。  GitHubのリポジトリ: https://github.com/baseinc/Hands-on-Terraform  Terraformのドキュメント: https://www.terraform.io/docs/providers/aws/ $ git clone git@github.com:baseinc/Hands-on-Terraform.git $ terraform init $ vi terraform.tfvars  必要に応じて以下の内容を書き換えてください。 aws_access_key = "<AWSのアクセスキー>" aws_secret_key = "<AWSのシークレットキー>" aws_region = "ap-northeast-1" main_aurora_root_user = "<auroraへのアクセスユーザ>" main_aurora_root_password = "<auroraへのアクセスパスワード>" external_ip = "<EC2にアクセス出来るIP>" host_ssh_key = "<EC2にアクセスする際のキーペア名>" ③準備ができたので、Terraformを実行するだけです。 実行前の動作確認(DryRun): $ terraform plan Plan: 20 to add, 0 to change, 0 to destroy. ------------------------------------------------------------------------ Note: You didn't specify an "-out" parameter to save this plan, so Terraform can't guarantee that exactly these actions will be performed if "terraform apply" is subsequently run.  上記の様に「20 to add」と表示されればOKです。  20個のリソースがコマンド一つで作成されます。 実行: $ terraform apply Plan: 20 to add, 0 to change, 0 to destroy. Do you want to perform these actions? Terraform will perform the actions described above. Only 'yes' will be accepted to approve. Enter a value: yes  「yes」と回答します。 aws_iam_role.db-main-aurora-monitoring: Creating...  リソースの作成が開始します。 ④リソース作成完了までは少し待ちます。 aws_rds_cluster_instance.db-main-aurora-instance.0: Still creating... (12m50s elapsed) aws_rds_cluster_instance.db-main-aurora-instance[0]: Creation complete after 12m57s (ID: db-main-aurora-instance-0) Apply complete! Resources: 20 added, 0 changed, 0 destroyed.  これでAWS上にAuroraとLinuxのインスタンスが立ち上がりました。  どうですか、サクッと作れてしまいましたね。  リソースが出来たかどうか、実際にAWSコンソールにログインして確認してみましょう! 6. リソースを作った後は。。。 こんなに簡単に作れてしまうと、次から次へとリソースを作ってしまいますね。 そして放置なんてことになったら、AWSに多大な貢献をしてしまうことになりかねません。 ですので不要になったらきちんとお片付けを実施します。 削除 : $ terraform destroy コマンドを実行して削除完了と思ったら。。。。 Error: Error running plan: 1 error(s) occurred: aws_route.db-vpc-route-external: aws_route.db-vpc-route-external: the plan would destroy this resource, but it currently has lifecycle.prevent_destroy set to true. To avoid this error and continue with the plan, either disable lifecycle.prevent_destroy or adjust the scope of the plan using the -target flag. あれ?なんでだろう。。。エラーが出てしまいます。 理由は削除保護が有効になっていたからです。 VPCやDB等は間違えて消してしまうと想定外の影響が出てしまう可能性がある為、簡単に消せない様に削除保護 prevent_destroy = true をリソースに定義していました。 ただ、今回は消してしまいたいのでリソース上の定義を全て prevent_destroy = false に変更して再度実行してみましょう。 Do you really want to destroy all resources? Terraform will destroy all your managed infrastructure, as shown above. There is no undo. Only 'yes' will be accepted to confirm. Enter a value: yes 「全てのリソースを削除しますが、良いですか」と聞かれます。 「yes」と回答します。 … aws_subnet.db-vpc-subnet-d1: Destroying... (ID: subnet-0bae4cb4032d56f93) aws_subnet.db-vpc-subnet-a1: Destroying... (ID: subnet-0a9d2118812674f12) aws_subnet.db-vpc-subnet-c1: Destruction complete after 0s aws_subnet.db-vpc-subnet-d1: Destruction complete after 1s aws_subnet.db-vpc-subnet-a1: Destruction complete after 1s aws_security_group.db-main-aurora-security-group: Destruction complete after 2s aws_vpc.db-vpc: Destroying... (ID: vpc-0790adf505d9e6ac3) aws_vpc.db-vpc: Destruction complete after 0s Destroy complete! Resources: 20 destroyed. 今度は消えましたね。 また必要になったら、 $ terraform apply で作り直しましょう。 まとめ Terraformを使用することで新規作成から削除までコマンドで簡単に出来るようになりました。 今後、既存の本番環境のTerraform化や新規にプロダクトを開発している子会社のBASE BANKもTerraformを利用したりしているので、また機会があれば続きを書ければなぁと思います。
こんにちは!BASE Product Division サーバーサイドエンジニアの東口( @Khigashiguchi )です。主にEコマースプラットフォーム「BASE」の決済領域の開発や、 BASE BANK というBASEの子会社にて 金融事業の立ち上げ を行っています。 さて、2018/8/7(火)に、ランサーズ、ReBuild、BASEの3社でエンジニア向けの勉強会「 レガシーコード改革!UT/CIでWebサービスの技術的負債を解消する取り組み 」を開催しました。 www.wantedly.com そこで「 PHPバージョンアップと決済テストを支えたユニットテスト 」というタイトルで発表させていただきました。 「レガシーコードを取り扱うためにどのようなユニットテストを書いていったか」について実体験ベースでの取り組みと得られた課題について話しています。 参加者の方からも、「 実体験ベースでの知見が参考になった 」といったフィードバックもいただけました。 登壇資料 この発表は、 PHPカンファレンス関西2018 にてLT発表させていただいた、 テストを書いたことがないエンジニアがテストを書けるようになるまでやったこと の詳細版として、以下のポイントについて言語化させていただきました。 テストがないコードに対して、どういう考え方でテストを書いていったか チームとしてどうやってテストを広めようとしたか 結果としてどのような課題があったか 実際にコードレベルでの説明にまで落とし込んでいるので、同じ課題を持つ方にとって一つの有益な知見として参考になれば幸いです。 また、今回はイベントタイトルが「 レガシーコード改革!UT/CIでWebサービスの技術的負債を解消する取り組み 」というものなので、「 レガシーコード 」をテーマとした発表になりました。 そのため、「レガシーコード」というテーマで発表するに当たり、主に下記の2点の書籍を改めて読み直して発表のコアとなる部分の言葉をお借りさせていただいています。 www.shoeisha.co.jp www.shoeisha.co.jp とても参考になるので、今回の資料を入り口にそちらもご参考にしていただければと思います。 まとめ ランサーズ、ReBuild、BASEの3社でエンジニア向けの勉強会「 レガシーコード改革!UT/CIでWebサービスの技術的負債を解消する取り組み 」を開催しました。 レガシーコードを扱うためのユニットテストの考え方と書き方について話しました。
はじめまして、BASEビール部部長の氏原です。BASEのData Strategy Groupで機械学習エンジニアをしています。 今回初登場ということで、暑いときにいいサワーエールのお話でも......といきたいところですが、ここは開発ブログということなので仕方ありません。開発のお話をしましょう。 現在私は商品の画像に基づいて、その商品に似た商品を類似商品として提示するAPIの開発を行なっています。今回はこのAPIをYahoo!さんの NGT(Neighborhood Graph and Tree for Indexing) を使って作成したことについて書いてみようと思います。 背景 BASE株式会社はネットショップ作成サービス「BASE」を運営しています。ここで作成されたショップはそれぞれ別のWEBサイトとして公開されていますが、ショッピングアプリ「BASE」では作成されたショップを横断して商品を検索し購入することができます。しかしショップの数は公称で50万店舗、非常に多くの商品がありますのでユーザーさんが興味を持つであろう商品をいかにして探しやすくするかはサービスにとって喫緊の課題と言えます。その取り組みの一環として、私は商品詳細ページにある関連商品の改善を目的に類似商品APIの作成を行っています。 全体の概観 類似商品APIは以下の3つの構成要素で成り立っています。 画像の特徴量抽出 新しい商品画像がサービスに登録された際に特徴量を計算して保存する 特徴量のindexing 関連商品として出す商品の画像を選別して近傍探索用のindexを作成する API 検索元商品の画像から近傍の画像を探索しその画像に対応する商品を類似商品として返す ではそれぞれについて解説していきましょう。 画像の特徴量抽出 BASEには毎日結構な量の商品が新しく登録されていきます。既存の商品の画像を差し替えたりすることもあります。それらの新しい画像の特徴量は随時計算しておかないといけません。 特徴量抽出の全体構成 BASEでは商品画像をS3に保存しています。S3ではファイルが登録されると、そのことをイベントとして通知できます。特徴量抽出ではそれを利用して以下のようにシステムを組んでいます。 S3, SNS, SQS S3で画像が登録されると、そのイベントをまずSNSに投げます。そしてSNSはそのイベントをそのままSQSに投げます。 一旦SNSを通しているのは、画像登録を契機に何かしらの処理を行いたいという要望は今後他にも出てくることを想定しているためです。SNSであればsubscriberを増やせばイベントをbroadcastできます。 こうしてS3に登録された画像はイベントとしてSQSに溜まっていきます。 ECS SQSに溜まったイベントを取りに行くのがECS配下で稼働しているServiceです。ここではSQSからイベントをpollingして、取得したイベントから画像を取得して一枚づつ特徴量を計算してDBに保存していきます。ECSはAuto Scalingと組み合わせればSQSが溜まってきたときにServiceを増やすのが簡単です。 画像の特徴量 みなさん、画像の特徴量といえば何を思いつきますか?SIFTとかHOGでしょうか。最近ですとDeepLearningでしょうか。今回、画像の特徴量の抽出にはMobileNetを利用しました。 いちからMoblieNetを作るのではなく学習済みモデルをそのまま利用しました。 Kerasのやつ ですね。ホント楽になりましたね。 from keras.applications.mobilenet import MobileNet model = MobileNet(weights= 'imagenet' , include_top= False , input_shape=( 224 , 224 , 3 ), pooling= "max" ) include_topはFalseにしてクラス分類のネットワークは外して特徴量を抽出する部分だけ使います。画像はもう単純に224×224にリサイズして使います。これで224×224のRGB画像からfloat32の1024次元のベクトルが得られます。 from keras.applications.mobilenet import preprocess_input from keras.preprocessing import image import boto3 import keras.applications.mobilenet import numpy as np import io # S3から画像を取ってくる s3 = boto3.resource( 's3' ) img_object = s3.Object(bucket, object_key) response = img_object.get() # 画像を読み込む img_data = io.BytesIO(response[ "Body" ].read()) img = image.load_img(img_data, target_size=( 224 , 224 )) # numpyのarrayにしてMobileNetの前処理をする x = image.img_to_array(img) xs = preprocess_input(np.array([x]) # 特徴量を計算する vec = model.predict(xs).flatten() 特徴量を保存するDB 上で得られた特徴量はDBに保存しておきます。 今回特徴量を保存するのにはAuroraを利用しました。1024次元のfloat32のベクトルを保存するのに容量あまり気にしないでもいい場所が欲しかったためです。でもRDSとか単にベクトル保存する場所としては機能過多ではあります。必要な機能を考えると単なるKey-Value Storeでいいんですが、ここは今後も要検討です。 特徴量のindexing 画像登録に連動してDBに特徴量を保存できるようになりましたが、サービスで使うにはある画像の特徴量vectorの近傍にあるvectorがどれなのかを知ることができるようにしなくてはいけません。これを実現するために今回は NGT を利用しました。 NGTとは Yahoo!さんの説明をそのまま引用させていただきます。 NGTは任意の密ベクトルに対して事前に登録した(同次元の)ベクトルから最も距離が近いベクトルの上位数件(k件)を高速に近似k最近傍探索(k-Nearest Neighbor Search)するためのソフトウエアです。 高次元ベクトルデータ検索技術「NGT」の性能と使い方の紹介 超高速に近傍のベクトルを探せるソフトです。本当に超高速です。350万件の1024次元のベクトルを登録してみたところ、メモリを15G程度食いますが近傍1000件取ってくるのに数十msくらいしかかかりません。これならキャッシュを併用すれば十分使えると判断しました。 python wrapperもあるので使うのも簡単です。 index作成の全体構成 ECS Taskを利用したバッチ処理でNGTのindexを作成しています。 ECS indexの作成は日次バッチで行うのでCloudWatch Eventをdailyで投げるようにして、ECSがそれを受け取ってTaskを実行するようにしました。 TaskはAuroraからindexingする特徴量を取得してNGTに登録します。このとき全部の特徴量を使うのではなく、古い商品は登録しないなどある程度の取捨選択をしています。 NGTはpython wrapperを使ってこんな感じです。(適当に簡略化してます) from ngt import base as ngt ngt_index = ngt.Index.create(b "any/where/you/want/to/save/index" , 1024 ) # 結果は大きいのでサーバーサイドCursorつかう conn = MySQLdb.connect(..., cursorclass=MySQLdb.cursors.SSCursor) cursor = conn.cursor() cursor.execute(......) # 画像の特徴量とかとってくる # 大きくて全部持ってこれないから一個づつ処理 for row in cursor: image_id, vec = row oid = ngt_index.insert_object(vec) # NGT内でのobject idと画像のIDの紐付けは自分で覚えておく必要あり ... # indexの作成 ngt_index.build_index(num_threads= 8 ) S3 作成されたNGTのindexはS3に保存しています。indexは毎日作成され、ある程度の期間保存して古いものは捨ててます。 API NGTのindexが日々作成されるようになりましたので、今度はそれを利用する部分を用意しましょう。 API提供部分の全体構成 構成としてはAPI Gatewayを入り口としたECSの二段構えになっています。 これは類似商品を取得するという機能と類似画像を取得するという機能を分離させておくことで、類似画像APIを利用した他の機能の開発を簡単にするためです。例えば、現在BASEの商品の画像検索APIも開発中ですが、これはこの仕組みにそのまま乗っかっています。 類似商品API ここは以下のような役割を受け持ちます。 API Gatewayから商品IDを受け取る 商品IDを対応する画像IDに変換する 画像IDを類似画像APIに投げ、類似画像のIDを受け取る 類似画像のIDを対応する商品IDに変換する 類似商品のIDをAPI Gatewayに返す やってることはシンプルです。 構成のところには出ていませんが、毎度類似画像APIを呼ばなくてもいいようにElastiCacheを設置して、類似画像APIを呼ぶ前にそっちを確認しています。 類似画像API ここはNGTの薄いwrapperになってます。やっているのは画像IDや画像の特徴量そのものを受け取り、近傍の画像のIDを返すだけです。 ここはindex作成が終わったときに新しいindexを読むように再起動されます。この再起動はECSの仕組みを使って複数動いているECS Serviceを少しづつ再起動させていくことでサービスとしては無停止で行われるようになっています。 実際にどう変わるか では、構築したAPIで類似商品がどの程度改善したかみてみましょう。 この洋服の関連商品は以前のロジックだと以下のようになってました。 うん、服ですらないですね。 正直どうしてこれらが関連商品に上がってきたのか謎です。名前だろうか。 で、それが今回のAPIで以下のように改善しました。 おお!服だ! ......まあそれだけで感動できるほど前のが悪すぎたという話もあります。 雰囲気もなんとなく似てますかね。ファッション系は概ね良い感じに類似商品を出せるようになりました。 まとめ 私の所属しているData Strategyチームは最近できたばかりで、まっさらなAWS環境を渡してもらってインフラ含めて一から類似商品APIを構築するというなかなか楽しい経験をさせてもらいました。まだまだ精度を上げたいところではありますが、そこそこ良いものができたのではないかと思っています。 実際関連商品のタップされる率は今までの2倍ほどに伸びており一安心というところです。 今回構築した類似商品APIでは今のところ画像しか見ていません。そのため少々コンテキストから外れたものが出てくることもあります。そこで現在商品のタイトルや説明も考慮するようにAPIをアップデートしようとしているところです。もっと精度を上げて、私がいいビールを探せるように......いえ、ユーザーさんがいい商品をみつけられるようにこれからも頑張ります。 開発した成果は、ショッピングアプリ「BASE」で見ることが出来ます。個別の商品を見るビューをスクロールして「関連する商品」をぜひご覧下さい。 iOS https://itunes.apple.com/jp/app/base-beisu-guo-nei-zui-da/id661263905?mt=8 Android https://play.google.com/store/apps/details?id=in.thebase.base P.S. 長くなるんで省きましたが、実は今回作ったAPIのAWSの構成管理はTerraformで書いて1発で構築できるようにしています。その話はまたの機会にしたいと思います。
こんにちは!BASE Product Division サーバーサイドエンジニアの東口( @Khigashiguchi )です。主にEコマースプラットフォーム「BASE」の決済領域の開発をしています。 さて、この度、BASE株式会社は7月14日(土)に開催された「 PHPカンファレンス関西2018 」にシルバースポンサーとして協賛いたしました。 私は当日スポンサー担当として参加し、前日に開催された「 PHPカンファレンス関西2018非公式前夜祭 」と合わせて2夜連続でLT発表してきました。 また、同じくBASE Product Divisionの田中( @tenkoma )も本編の懇親会にてLT発表いたしました。 PHPカンファレンス関西2018 2018.kphpug.jp PHPカンファレンス関西は、大阪・梅田のグランフロント大阪で開催され、全国のPHPerが集結したイベントになりました。 メイン会場の様子 BASEは、シルバースポンサーとして協賛いたしましたので、オープニングにてBASEを紹介していただきました。 発表内容 本編 本編では、「 テストを書いたことがなかったBASE入社前から現在に至るまでテストに対してどういう取り組みを行ってきたか 」をぎゅっと5分に詰めて話しました。 LT発表中の様子 内容を詰めたので、ある意味「LTっぽく」スピード・テンポ感重視のプレゼンではありましたが、要点と随所のポイントは無事伝わったようで「一部実践してみよう」という反応をいただけたので良かったです。 懇親会 こちらは、先ほどご紹介した田中( @tenkoma )の発表です。 CakeFest2019 というCakePHPにフォーカスしたカンファレンスの候補地に日本が入っており、 「みんなで日本開催に投票しよう!」 という話をしました。 懇親会LTの様子 7月30日まで投票中 なので私もこのブログが書き終わったら投票します! 非公式前夜祭 非公式前夜祭では、 PhpSpreadsheet というライブラリを扱う上でトラブルシューティングについて話しました。  非公式前夜祭での様子 「PHPでExcelを扱ったことがある方」が参加者の方に多かったので、 「あるある」 という共感をいただきながら、ラフに話させていただきました。 まとめ PHPカンファレンス関西2018にスポンサーし、せっかくの大阪遠征なので張り切って2夜連続LTにチャレンジしてみましたが、充実感があって楽しかったのです。 ただ、個人的な反省としては両日資料完成が発表直前になるくらい追い込まれたので資料準備はより計画的にしないとです。 もし来年参加される方はチャレンジしてみてはいかがでしょうか。 続きは8/7 (火)の勉強会で 今回、PHPカンファレンス本編で5分に詰め込んだテスト関連の話を15分に伸ばした詳細版を、8月7日(火)に開催するエンジニア向け勉強会にてお話しします。ランサーズさん、ReBuildさんにもお越しいただき、CIや技術的負債を解消する取り組みについてお話ししていただきます。 ご興味のある方はぜひ下記のページよりお申込みください。 base.connpass.com
こんにちは、BASEのDesign Groupに所属している 北村 です。 現在は主にプラットフォームとアプリのデザインを担当しています。 BASEのデザインチームはここ数年で一気に人が増え、社内でも大所帯の部署へと成長しました。今回は、BASEのデザイナーが日々の開発でどんなことを行い、どんなことを考えているのかについてご紹介したいと思います。 1. BASEのデザイナーの役割 プロジェクトの共通認識を作る仕事 BASEでは仕様検討の段階からデザイナーが入り、プロジェクトの担当者と一緒に開発要件を詰めていきます。ときには箇条書きの要件書からワイヤーを起こし、UIをアウトプットしていきます。画面デザインが先にあると、メンバー同士のコミュニケーションがスムーズになり、かつ他部署との認識合わせも円滑になるため、BASEの開発フローでは まずデザインありき という文化が浸透しています。また単純に、先にデザインがあると実際の画面をどう作っていけば良いのかチームの皆で把握しやすい、という利点があります。 プロジェクト内で共通認識となるアウトプットを担うのが、BASEのデザイナーの仕事のひとつです。 BASEにおけるデザイナーの責任 BASEでは基本的に、プロジェクトにアサインされたデザイナー本人がデザインクオリティのチェックを行います。 さらに、担当デザイナーが自分で画面のUIを実装することも多いです。最近はフロントエンドエンジニアも増えたので分業が進んではいますが、例えば細かなインタラクションの実装やUIの改修などはデザイナー自身が積極的に行っています。 画面のUIを設計するだけでなく、最終的なUXの担保まで担うのが、BASEのデザイナーの責任範囲になります。 2. BASEのデザイン思想と、大切にしていること 「お母さんも使える」という共通言語 BASEには社内全体の共通言語として「 お母さんも使える 」という思想が存在しています。 これは、BASE創業当時から変わらない哲学で、代表取締役CEOである鶴岡の強いポリシーであり、「BASE」の重要なコンセプトです。 というのもこのコンセプトは、「BASE」のサービス立ち上げのストーリーにまつわります。鶴岡の母は、大分の商店街で洋品店を営んでいますが、「ネットで服を売ってみたい」と鶴岡に相談があったので、楽天のような大手のECモールを使ってみるよう勧めましたが、使い方が難しくてできないと母から返ってきます。 その際に鶴岡は、母と同じような悩みを持っている人が世の中にたくさんいるのではないかと感じ、「BASE」の開発を始めた、という背景があります。 お母さんのために、良くも悪くも「機能をつけない」 便利だけれど使い方が難しい機能よりも、いかにクリック数を少なくして、簡単にショップを開設・運営してもらうかが最優先事項になります。 4、5クリックしなければならない機能より、2、3クリックで完了する機能を開発・提供する。たとえば、ユーザが一番使用する管理ページには、あえて機能をつけないなど、設計については、当初から「ユーザの手間を減らすための開発」を意識していました。 現在、BASEチームには13人ほどのエンジニアがいますが、新しい機能ができてもチーム内で「この機能は、お母さんたちには使えないのでは」となったら、たとえ便利そうな機能でもリリースしていません。 出典: お母さん目線で作ってみたら、「BASE」はシンプルなサービスになった BASEではプロダクトの開発中、あらゆる場面・文脈で「 お母さん 」という単語が出てきます。「これって複雑すぎてお母さんは使えないよね」や「この説明、お母さんに伝わるのかな?」など、議論の主語となることがとても多いです。 複雑な仕組みだったとしても、お母さんも使えるくらい簡単なサービスとして提供するのが、プロダクトを作るときに最も大切にしている思想です。 誠実さを大切にしている、BASEのデザイン思想 「お母さんも使える」を実現するために、デザイナーで作成したデザイン思想があります。 誠実であること あらゆるユーザに寄り添える、柔軟な拡張性 価値を最大化させる新しいショップ体験 「お母さんも使える」親しみやすいサービス 簡単じゃないものを簡単に BASEのデザイナーはなによりも「誠実」さに重きをおいています。 例えばユーザが何かものを売りたいと思ったときに、今までだとハードルが高くて気軽にできなかったものを、誰でもできるようにするのがインターネットの力で、BASEはそのインターネットのポジティブな力を信じてサービスを作っています。 一方で、インターネットがインフラとして広まっていく上で負の側面もあり、伝え方次第では誤った情報が多くの人に広まってしまったり、誰かを傷つけてしまうことがあったりします。 そういった意味で、デザインの役割は非常に重要です。多くの人に影響を与えるインターネットというインフラ上でサービスをデザインする以上、誠実さを忘れてはいけない、という思想を大切にしています。 3. デザイン思想を実現するワークフロー では実際に、2 のデザイン思想を実現するために行っているワークフローをご紹介したいと思います。 BASEにおけるデザインのフロー PMやディレクターから機能要件がくる 要件・仕様を元にデザイナーがワイヤー、またはプロトタイプを起こす 起こしたデザインを「 UI/UXミーティング 」でレビューする 実装に入る 週2回おこなわれるUI/UXミーティング BASEでは週に2回、UI/UXミーティングという場が設けられています。 このミーティングでは、各デザイナーそれぞれが担当しているプロジェクトのUIをレビューしてもらいます。 (プロジェクトの粒度によってまちまちですが)鶴岡、PM、ディレクター、プラットフォームデザイナー、アプリデザイナー、フロントエンドエンジニア、CSが一堂に会し、デザイナーの作成したUIを見ながらみんなで議論し合います。 他のデザイナー目線でのUIに対するダメ出しも含め、ビジネス視点からの要望や、実装に対するツッコミ、全体のUXへの指摘など、多角的な意見が入ります。もちろん「そもそもその機能って必要なの?」という意見が出ることも多々あります。そこをまとめて取捨選択するのもデザイナーの役目です。 たとえ途中で仕様が変わろうとも プロジェクトのメンバーだけでは把握できなかったより大きなユーザ体験が、UI/UXミーティングでの議論で見えてくる部分もあります。先日公開されたエンジニアの 日比野 、 柳川 の記事でも書かれていましたが、BASEではプロジェクトを進行してゆくなかで「ちゃぶ台返し」が起こるポイントがしばしば発生します。 デザイン目線でいうと、例えば 「簡単じゃないものを簡単に」が満たされてない 「親しみやすさ」が満たされてない 「価値を最大化させるショップ体験」が満たされていない 「お母さんも使える」が満たされてない この部分の踏み込みが甘く、議論が充分でないままプロジェクトが進んでしまった時に、UI/UXミーティングで指摘を受けることも多いです。 これはBASE全体にデザイン思想が浸透しているからこそ起こる議論で、たとえもう一度仕様を練り直すことになったとしても、より良いものを作る努力は惜しまない、納得するまで議論する、というBASEの「SPEAK OPENLY」のあらわれだと思っています。 まとめ BASEのデザイナーは裁量が多く、担う範囲も広いですが、開発の上位レイヤーから関われる環境というのはやはりやり甲斐があります。 誠実にサービスを作る、というBASEのデザイナーが大切にしている思想に共感してくださった方、一緒サービスを作りましょう! デザイナーを随時募集しています! jobs.binc.jp
こんにちは、Back-end Engineer の田中 @tenkoma です。ショッピングアプリ 「BASE」向けAPIの開発を担当しています。 去る6月16日に福岡で開催された「 PHPカンファレンス福岡2018 」に参加してきました。当日は一般参加でしたが、前日に開催された 非公式前夜祭 リジェクトコン で発表してきたのでブログでもご紹介します。 BASEのPHPアプリケーションは2018年5月にようやくPHP7化が完了しました。 アップデートプロジェクトの中でユニットテストしやすくするためにテストスイートをカスタマイズしたので、発表で3つほどをTipsとして紹介しました。 紹介した3つのTipsについてこの記事でも説明します。 Tips 1. assertSame() で配列のdiffを出力できるようにする PHPUnit 7 で assertSame() は 配列のdiff を出力できる ようになりました。CakePHP2アプリ(データ形式として配列を多用する)で使えるとありがたいのですが PHPUnit 7 の利用は難しそうです。そこで、配列のdiffを出力できるように移植してみました。以下のように出力されます。 SampleTest.php <?php App :: uses ( ' ExtendTestCase ', ' ExtendTestSuite.TestSuite ' ) ; class SampleTest extends ExtendTestCase { public function testAssertArray () { $ actual = [ ' User ' => [ ' id ' => 1 , ' name ' => ' Taro ' ]] ; $ expected = [ ' User ' => [ ' id ' => ' 1 ', ' name ' => ' Taro ' ]] ; $ this -> assertSame ( $ expected , $ actual ) ; } } 出力 There was 1 failure: 1) SampleTest::testAssertArray Failed asserting that Array &0 ( 'User' => Array &1 ( 'id' => 1 'name' => 'Taro' ) ) is identical to Array &0 ( 'User' => Array &1 ( 'id' => '1' 'name' => 'Taro' ) ). --- Expected +++ Actual @@ @@ Array &0 ( 'User' => Array &1 ( - 'id' => '1' + 'id' => 1 'name' => 'Taro' ) ) テストが失敗したときかなり原因がわかりやすくなるので、すぐにPHPUnit 7にアップグレードできない場合は以下のコードを実装すると便利です。 assertSame ExtendTestSuiteConstraintIsIdentical.php Tips 2. テストスイート向けにユニットテストを書く setUp() , tearDown() などに記述したコードをどうテストするか、という話で test for test cycle(setUp/tearDown) #1 のようなテストコードを書きました。 スライドの最後で、CIで複数のPHPバージョンでテストした話をしましたが、そのためのCircleCIの設定は .circleci/config.yml のようになっています。 Tips 3. runkit, php-timecopのコードをIDEで補完するためのスタブ PhpStorm組み込みのスタブファイル に無い runkit , php-timecop のようなPHP拡張のコードを補完できるようにするものです。以下のリポジトリを git clone してIDEの include_path に追加してお使いください。 tenkoma/php-stubs-mighty-magic カンファレンスレポート 以下はカンファレンス当日・非公式前夜祭後夜祭のレポートです。来年も開催されるらしいので、参加したい方の参考になれば幸いです。 前日に福岡入り (6月15日・金) 非公式 PHPカンファレンス福岡2018前夜祭リジェクトコン PHPカンファレンス福岡を楽しむには前日入りが必須。13:30頃に福岡空港に到着しました。 このときまだリジェクトコンの資料が仕上がっていなかったので博多駅近くにとったホテルで資料の手直しをしました。 リジェクトコンの会場であるLINE Fukuoka株式会社と同じビルの1Fで知り合いの方と一緒に食事を取り、19:00 に会場到着しました。 以下、発表前の現場の様子です。 会社に報告しないといけないので写真撮ってる pic.twitter.com/amuPYMhBaX — Koji Tanaka (@tenkoma) 2018年6月15日 発表順は1番目。印象に残るよう GitHub Username Shirt を着て発表しました。 #phpconfuk_rej @tenkoma さんー! pic.twitter.com/l0xKtavLKp — 青ごへいもち (@blue_goheimochi) 2018年6月15日 カンファレンス本編より多い同時3セッション。だれも聞きに来なかったらどうしようと思いましたが、多くの人に聞いていただけたようです。 PHPカンファレンス福岡2018 当日 (6月16日・土) PHPカンファレンス福岡2018 PHPカンファレンス福岡は去年初参加でした。そのときは前日午前2:00くらいまで懇親会に参加した結果当日1時間以上遅刻してしまいましたが、今年は午前10時に間に合いました。 「PHPカンファレンス福岡2018」は同時2セッションです。2つの会場は両方とも席に余裕がある感じで出入りがしやすかったです。 参加したセッションについて紹介します。 MySQLで画像 を扱うメリット・デメリットと障害・解決事例 資料: MySQLで画像 を扱うメリット・デメリットと障害・解決事例 - Speaker Deck 以前実装してみたことがあり、メリット・デメリットについて実感したので、さらに学ぼうとおもい参加。 発想としては思いつきがちですが、適用するメリットの大きいケースは限られそうですね。 ログの設計してますか?PSR3とログ設計の話 資料: ログの設計してますか?PSR3とログ設計の話 - Speaker Deck 何をログに残せば良いかで迷うことが未だにあり、周辺知識を強化したくて参加。ログのリスクの部分を一番集中して聞いてました。 Event Sourcing,CQRS For PHP Application 資料: Event Sourcing,CQRS For PHP Application - Speaker Deck 複雑化するサービスのための設計パターン、アプリケーションとミドルウェアの組み合わせ例紹介。扱うデータやトランザクションが増えたり、集計分析などのシステムがあると必要とされそうに思いました。 Testing Live!!! 資料: Testing Live!!! 実際のWebサービスに対して探索的にテストを行い、問題を探すデモでした。5分という短い時間でアプリケーションに潜む不整合が見つかり、会場がすごく盛り上がってました。 PHP 5.3 + CakePHP1.3 バージョンアップ報告 資料: PHP 5.3 + CakePHP1.3 バージョンアップ報告 - Speaker Deck 去年、同じくPHPカンファレンス福岡で発表された計画の現状報告。BASE もCakePHPを利用しているので、気になりました。 物理層のこと、時々でいいから、思い出してください。 資料: 物理層のこと、時々でいいから、思い出してください。 - Speaker Deck システムの物理層というより物理インフラ構築運用について面白おかしく紹介。失敗談が聞いていて面白すぎました。 その他 スポンサーブースも参加者に足を運んでもらえるようAsk The Speakerコーナー(セッション後に発表者に直接質問できる場所)とコーヒーが用意されており、いつ行っても賑わっていました。 翌日 (6月17日・日) いっぱいご参加頂けて嬉しい限り! #phpconfuk_after #fusic pic.twitter.com/CEMKSoHJit — せいけ しろー (@seike460) 2018年6月17日 非公式 PHP Conference Fukuoka After Hack!! 去年同様、株式会社Fusicのセミナールームで開催されたので参加しました。参加者各自で自由に作業しながら、発表したい人が自由に発表するようなゆるやかな集まりでした。 僕は前夜祭で発表した内容のコード tenkoma/extend-cakephp2-testsuite-example の準備をしていたのですが、飛行機の都合で2時間ほどしか参加できませんでした。 来年もし開催されるなら、絶対フルで参加できるように調整します! まとめ 非公式前夜祭 リジェクトコンで発表しました PHPカンファレンス福岡、フルで楽しむなら前後のイベントもチェックしてみるのがオススメです。
こんにちは、BASEのPayment Engineer Groupに所属している 柳川 です。 先日BASEではショップコインという新機能をリリースしました。ショップコインの説明を簡単にすると、BASEをご利用いただく各ショップさんが、独自にショップで使えるコインを発行することで、ショップさん独自の経済圏を作れる機能です。詳しい説明は こちら をご覧いただけると幸いです。 私はこの機能の開発でサーバーサイドエンジニアとして設計、開発、リリースを行いました。 今回は開発者ブログの記事ということで、開発していて気がついたことをまとめてみたいと思います。 事前情報 プロジェクトの特徴 修正範囲がでかい 新規コード+既存の決済コードに手を入れる 端的にいうと新しく決済方式を作るということ 開発量に対して実装者が少ない サーバーサイドエンジニア:1人 デザイナー:2人 私の特徴 大規模開発をしていたSIer出身。 プロジェクトの情報と私の特徴から、SIer時代の開発経験をうまく活かせば、上手にプロジェクトを回せるのではとうっすら考えながら開発に挑むのでした。 やったこと UMLを書いた 開発が始まる前に、自分の中で以下のことを確認したいと考えUMLを書きました。 ヒト モノ コト カネ このあたりのことを詰めると、実装の漏れが防げると考えてのことでした。 SIerからスタートアップに転職してからは、関係者の人数が少ないことや、スピードが求められることからこの工程を省くことも多かったのですが、今回はそうも言ってはいられない物量であると考えてSIer時代の知識を引っ張り出してきました。このとき書いたUMLが、人に設計を共有する上でも役に立ちました。少人数で開発していると、開発中の内容がブラックボックスと化してしまうきらいがありますが、その点を少なからず軽減できたのではないかと思います。 また図に起こすことで、エンジニア以外も直感的に機能の把握が行えたようです。弊社がドキュメント管理ツールとして使用している DocBase にて PlantUML の記法が使用できるため、そちらを使用してUMLを記述しました。文章でダイアグラムの作成ができるので、通常のドローイングツールに比べ、あとからの変更が非常に容易でした。おすすめです。 設計書を書いて設計書のレビューをした 転職後はあまり行ってこなかった、設計書自体のレビューをしました。 小さな機能開発であれば 頭の中や手元のメモで設計→実装→コードレビュー→リリース この流れで、進めて大きな問題はないように思います。 しかし今回は開発量の多さが懸念されたため、SIer時代を思い返して、設計書をある程度書いて、なおかつ設計書のレビューもしようと考えました。致命的な指摘等炙り出せたので、本当にやってよかったと思いました。 ちゃぶ台返しポイントを積極的に作った SIer時代の経験を思い出して辛いのが大幅な手戻り、いわゆるちゃぶ台返しというやつです。 残念ながら自社プロダクトの開発でもちゃぶ台返しは起こります。大切なのはいかにちゃぶ台返しのインパクトを少なくして、プロダクトのクオリティの向上に活かすか。 ちゃぶ台返しは敵ではなく、味方なのです。 今回はちゃぶ台返しへの対抗手段として、いろいろなところで細かくちゃぶ台返しを起こさせるという作戦を取りました。 設計書のレビュー 複数回のコードレビュー 出来ている場所までをデプロイして操作してもらいながらのレビュー 上記のように開発期間中に数々のレビューをはさみました。 ちゃぶ台返しの回数こそ多くなったような気はしますが、結果的に工数が収まった上でクオリティを上げられたのではないかと思います。ポイントとしてはSlackでレビューを依頼をするだけではなかなか見てもらえないので、必要と思うタイミングで必要な人には明確に時間を取ってもらいレビューを行うことです。他力本願は駄目! リリース前にQAプロセスを入れられたこと 今回少人数での開発、かつ開発量が多かったこともあり、明示的にQA期間を設け、リリース前の検証を行いました。機能の開発に関わっていないメンバーの新鮮な目で、QAをおこなってもらうのは、実際にバグが潰せるのはもちろん、精神的にも大きな助けとなりました。 うまくやれなかったこと リリースの単位がでかくなってしまった この 発表資料 を読みながら反省したのですが、リリースの単位がでかくなってしまったのは反省点かなと思います。後で確認する範囲がでかくなるだけなので、できるだけ細かく早く出すべきだったと思います。ただすぐには本番稼働しないコードを本番に組み込むのも影響確認とロールバックという点でどうだろう、というのはあるので難しいところかなとも思います。 リリース前の忙しい時期がうまく回らなかった 開発自体は、少ない人数でも段取りをしながら進めることが出来たのですが、リリース直前に立て込んでくると、開発作業とそれ以外作業の帽子をかぶり直すことが出来ませんでした。特にリリース前のQA作業を行うことと、バグを修正することを同時に行うのが厳しかったです。リリース直前に忙しくなることがわかっているなら、極力帽子をかぶり直すことなく進められるように、人をアサインしてもらうなどで準備するべきでした。 開発範囲が少ない場合は、できるだけ一人でやったほうがスピーディーに進められますが、ある程度開発範囲が大きくなると、各人が各ロールに分かれた体制を作ることに強みが出てくるのだろうなと思いました。このあたりは今後の課題かなと思います。 また、自分が今何に集中すべきかを客観的に考えるのが大切だと感じました。テンパらない。 まとめ 大きめの機能開発を、主導する立場として担当させていただき、非常に勉強になりました。まるごと任せていただけたからこその気付きが多かったように思います。特に設計書を作る、開発プロセスを明確化する等のSIerで培った技術が役に立ったのはいい経験でした。 SIer時代は、最初から決まりとしてあった開発の手順に窮屈感を感じることもありましたが、実際になんのために行うのかということを考えて、開発中の課題と照らし合わせていくと、力となる部分は無数にあるなと感じました。 当たり前ですがSIerでもスタートアップでもアプリケーションを作るという上では同じです。アプリケーション開発の道具として使えるソフトウェアの開発手法を、積極的に取り入れていくべきだと感じました。すべての開発工程が最初に描いたとおりに進んだわけではありませんでしたが、それも含めいい経験でした。忙しく開発作業を終えたあとは、知識の吸収が早い気がするので、振り返られなかった分をしっかり振り返って、学ぶべきものを見定め学んで、次に活かしたいです。日々勉強。 最後に BASEでは、様々な経験を活かしながら一緒に未来を作っていく仲間を募集しています。前向きかつ自由にやっていけるのは間違いない環境です。 ご興味を持たれた方は以下の採用情報から! jobs.binc.jp