株式会社ZOZOのブログ - TECH PLAY

TECH PLAY

株式会社ZOZO

株式会社ZOZO の技術ブログ

1025

はじめに こんにちは! 2019年2月にZOZOテクノロジーズへサーバーサイドエンジニアとして入社した籏野( @gold_kou )と申します。 Qiita でも少し記事書いてます。 いきなりですが、皆さんはAPI仕様書をどのように管理されていらっしゃいますか? Confluence、Wiki、Markdown、Spreadsheet、Excelなど色々手段やツールはあると思います。私が担当しているプロジェクトではOpenAPIを導入しています。 この記事ではOpenAPIの基本と実際に導入して得られたノウハウをご紹介いたします。 OpneAPIの恩恵はただの管理の仕方にとどまらないので、ぜひこの記事を読んで開発効率化のお役に立てばと思います。 また、弊社のテックブログで以前、OpenAPI(Swagger)のバージョン2系に関する 開発効率を上げる!Swaggerの記法まとめ や 開発効率を上げる!Swaggerで作るWEB APIモック が投稿されておりますが、今回は対象バージョンが3系となります。 OpenAPI概要 OpenAPI Specification(OAS) OASはREST-APIの標準仕様です。OASのことを単にOpenAPIと呼ぶこともあります。 YAMLかJSON形式で記述します。 現在はバージョン3系が最新ですので、特別な事情がない限り3系を使いましょう。 2系から3系への変更点は様々あるのですが、一番大きな変更はComponentsオブジェクト(後述)が追加されたことです。 DRYにかけるため、OpenAPIが目指している "human readable" へ近づきました。 Swagger Toolsを活用することで効率的に記述できます。 OpenAPIを使うメリットとデメリット メリット 効率的に記述できる Swagger Editorのおかげ 3系からより効率的に human readable & machine readable APIクライアントとサーバースタブを自動生成できる OpenAPI Generatorのおかげ スキーマ駆動開発できる 開発工数を削減できる かっこいいビジュアルのAPI仕様書を作れる Swagger UIのおかげ バージョン管理しやすい 書き方に統一性を持たせられる 我々がOpenAPIを導入した理由は上記メリットのうち、特に「APIクライアントとサーバースタブを自動生成できる」点に魅力を感じたためです。 スキーマ駆動開発を実践しているわけではないのですが、APIを定義すればある程度のソースコードを自動生成できる一石二鳥感は充分な選定理由だと思います。 デメリット 学習コスト YAML/JSONの記法 自動生成のやり方 Swagger OpenAPIを勉強するうえで避けては通れないSwaggerについて説明します。 まず歴史的な話なのですが、もともとOpenAPIの前段としてSwagger Specificationというものがありました。 それがOpenAPI Initiativeという団体に管理が移ったことで、名称がOpenAPI Specificationに変更されました。 しかし、ツールセットの開発は現在もSwaggerで行われているものもあり、ツール名には「Swagger」が名残で残っています。 Swagger Tools OpenAPIを効率的に記載するためのOSSのツールセットです。 Swagger Editor ブラウザ上で記述するタイプのエディタです。インストール不要なので手軽に試せます。 リンクは こちら インターネット上でAPI情報を記載することに抵抗がある場合は、以下のようにローカルでDockerイメージをpullして、起動することもできます。 $ docker pull swaggerapi/swagger-editor $ docker run -d -p 80:8080 swaggerapi/swagger-editor ブラウザで localhost:80 にアクセスすれば、以下が表示されます。 また、 Visual Studio Code にはSwagger Viewer(プラグイン)が用意されています。 プログラミングと同じエディタで編集できるので便利です。 Swagger UI OpenAPIに則って記述されたスキーマをAPI仕様書化するツールです。 YAMLファイルやJSONのままでは人間には見るのが辛い部分もありますが、これを使えば統一されたカッコいいUIを提供します。 Swagger EditorやSwagger Viewerの右側はこれを利用しています。 APIクライアントツールとして利用することも可能です。認証まわりも対応していますので、トークンを埋め込んで実行することもできます。 Swagger Codegen OpenAPIに則って記述されたスキーマからAPIクライアントとスタブサーバーを自動生成するツールです。 自動生成により開発コストを削減するだけでなく、スタブサーバーがあることでフロントエンドの開発もバックエンドの開発を待たずに進めることができます。いわゆるスキーマ駆動開発というやつですね。 3系対応を進めるためSwagger Codegenをフォークした OpenAPI Generator の開発がコミュニティドリブンで進んでいるそうです。 後述ですが、私の担当プロジェクトではOpenAPI GeneratorのDockerコンテナを使用しています。 OpenAPIの基本記法(YAML) 公式サンプル を中心にYAMLでの基本記法をまとめます。 読めばなんとなくわかるのですが、一応1つずつ説明していきます。 また、サンプルには無くてもよく使う記法もいくつかピックアップします。 その他の記法や詳細は 公式ドキュメント をご参照ください。 ファイル名 ルートのファイル名は openapi.yml が推奨されていますが、それ以外に特に決まりはありません。 <システム名>.yml とかもよく見ます。 OpenAPIオブジェクト openapi フィールドでOpenAPIのバージョンを設定します。 openapi : "3.0.0" Infoオブジェクト メタ情報を設定します。 version フィールドでAPIドキュメントのバージョンを設定します。 title フィールドでAPIドキュメントのタイトルを設定します。 description フィールドで説明を設定します。 termsOfService フィールドでサービス規約を設定します。例では、内容が長くなるのでURLになっていますね。 contact フィールドで連絡先情報( name / email / url )を設定します。 license フィールドでライセンス情報( name / url )を設定します。 info : version : 1.0.0 title : Swagger Petstore description : A sample API that uses a petstore as an example to demonstrate features in the OpenAPI 3.0 specification termsOfService : http://swagger.io/terms/ contact : name : Swagger API Team email : apiteam@swagger.io url : http://swagger.io license : name : Apache 2.0 url : https://www.apache.org/licenses/LICENSE-2.0.html Serverオブジェクト APIサーバー情報を設定します。 url フィールドでURLを設定します。今回の具体例は1つだけですが、リスト形式で設定できるため例えば、「ローカル環境用」「ステージング環境用」「プロダクション環境用」などをそれぞれ設定することも可能です。 servers : - url : http://petstore.swagger.io/api Pathsオブジェクト 各エンドポイント仕様を設定します。 Path Item オブジェクト(/petsなど)で1つ以上のパスを設定します。 Operation オブジェクト(postなど)で1つのパスの1つのメソッドの単位を設定します。 operationId フィールドでOpenrationオブジェクトを一意にする識別IDを設定します。APIクライアントを自動生成する際に使用されます。 requestBody フィールドでリクエストボディを設定します。 required: true とすることでリクエスト時にこのボディがあることを必須とします。 content でボディの中身を設定します。 schema フィールドでは $ref でcomponents配下に定義したスキーマを読み込み、DRYな記述ができます。もちろんここに直接記述することもできます。また、 $ref は外部ファイルも読み込めるため、ファイルを分割することも可能です。 responses フィールドでレスポンスを設定します。ステータスコードをキーにして、その他はdefaultとします。こちらも schema を $ref できます。 こちらの例には無いですが、 Tags オブジェクトで Operation オブジェクトをグループ化するためのタグを設定します。 name フィールドでtag名を設定します。 paths : /pets : post : description : Creates a new pet in the store. Duplicates are allowed operationId : addPet requestBody : description : Pet to add to the store required : true content : application/json : schema : $ref : '#/components/schemas/NewPet' responses : '200' : description : pet response content : application/json : schema : $ref : '#/components/schemas/Pet' default : description : unexpected error content : application/json : schema : $ref : '#/components/schemas/Error' Parameter オブジェクトでパラメータを設定します。 name フィールドでパラメータ名を設定します。 in フィールドでパラメータの場所を設定します。 query/header/path/cookie のいずれかを選択します。 query: /items?id=### のようにURL末尾に ? でパラメータを設定する場合です。 path: /items/{itemId} のようにパス内にパラメータを埋め込む場合です。 required フィールドでパラメータが必須かどうかを設定します。 in フィールドの値が path の場合は必然的に true になります。 paths : /pets : get : parameters : - name : tags in : query description : tags to filter by required : false style : form schema : type : array items : type : string paths : /pets/{id} : get : parameters : - name : id in : path description : ID of pet to fetch required : true schema : type : integer format : int64 Componentsオブジェクト 再利用する部品を定義します。 また、再利用しないとしてもリクエストボディやレスポンスは極力Componentsオブジェクトに記載することで、記載方法に統一性を持たせ可読性を向上できます。 Schema オブジェクトでスキーマを設定します。スキーマ名(Petなど)は $ref で参照する際に使用されます。スキーマ内でさらに $ref して入れ子構造にすることも可能です。 properties フィールドでプロパティ(パラメータ)を設定します。 type フィールドでは integer(整数)/number(少数)/string/boolean/array/object のいずれかを設定します。 format フィールドでは int32/int64/float/double/byte/binary/date/date-time/password のいずれかを設定します。 type フィールドと組み合わせます。 required フィールドでプロパティ単位に必須パラメータを設定します。 こちらの例には無いですが、 minimum と maximum フィールドで数値の下限上限を設定します。 こちらの例には無いですが、 example フィールドでそのプロパティが取りうる値を具体例として設定します。 components : schemas : Pet : allOf : - $ref : '#/components/schemas/NewPet' - required : - id properties : id : type : integer format : int64 NewPet : required : - name properties : name : type : string tag : type : string Error : required : - code - message properties : code : type : integer format : int32 message : type : string APIクライアントとスタブサーバーを自動生成する OpenAPIを利用するメリットの1つである自動生成についてです。 いくつか手段はありますが、今回はDockerを使用する方法です。 APIクライアント 上記の具体例でも使用していた petstore-expanded.yaml からAPIクライアント(Go言語)を自動生成します。 $ docker run -v ${PWD}:/local openapitools/openapi-generator-cli:v3.3.4 generate -i /local/petstore-expanded.yaml -g go -o /local/out/go [main] WARN o.o.c.ignore.CodegenIgnoreProcessor - Output directory does not exist, or is inaccessible. No file (.openapi-generator-ignore) will be evaluated. [main] INFO o.o.c.languages.AbstractGoCodegen - Environment variable GO_POST_PROCESS_FILE not defined so Go code may not be properly formatted. To define it, try `export GO_POST_PROCESS_FILE="/usr/local/bin/gofmt -w"` (Linux/Mac) [main] INFO o.o.c.languages.AbstractGoCodegen - NOTE: To enable file post-processing, 'enablePostProcessFile' must be set to `true` (--enable-post-process-file for CLI). [main] INFO o.o.codegen.AbstractGenerator - writing file /local/out/go/model_error.go [main] INFO o.o.codegen.AbstractGenerator - writing file /local/out/go/docs/Error.md [main] INFO o.o.codegen.AbstractGenerator - writing file /local/out/go/model_new_pet.go [main] INFO o.o.codegen.AbstractGenerator - writing file /local/out/go/docs/NewPet.md [main] INFO o.o.codegen.AbstractGenerator - writing file /local/out/go/model_pet.go [main] INFO o.o.codegen.AbstractGenerator - writing file /local/out/go/docs/Pet.md [main] INFO o.o.codegen.AbstractGenerator - writing file /local/out/go/api_default.go [main] INFO o.o.codegen.AbstractGenerator - writing file /local/out/go/docs/DefaultApi.md [main] INFO o.o.codegen.AbstractGenerator - writing file /local/out/go/api/openapi.yaml [main] INFO o.o.codegen.AbstractGenerator - writing file /local/out/go/README.md [main] INFO o.o.codegen.AbstractGenerator - writing file /local/out/go/git_push.sh [main] INFO o.o.codegen.AbstractGenerator - writing file /local/out/go/.gitignore [main] INFO o.o.codegen.AbstractGenerator - writing file /local/out/go/configuration.go [main] INFO o.o.codegen.AbstractGenerator - writing file /local/out/go/client.go [main] INFO o.o.codegen.AbstractGenerator - writing file /local/out/go/response.go [main] INFO o.o.codegen.DefaultGenerator - writing file /local/out/go/.travis.yml [main] INFO o.o.codegen.AbstractGenerator - writing file /local/out/go/.openapi-generator-ignore [main] INFO o.o.codegen.AbstractGenerator - writing file /local/out/go/.openapi-generator/VERSION すると、カレントディレクトリ以下に下記のようなディレクトリやファイルが生成されます。 これらをもとに開発を進めていけば定型部分がだいぶ自動生成されているので、開発工数を削減できるはずです。 上記で実行したopenapitools/openapi-generator-cliイメージのgenerateコマンドのオプションは以下です。 g: 生成コードの種類の指定(言語やFWなど) i: yamlファイルの指定 o: 出力パスの指定 また、gオプションで指定できるクライアントとサーバーのgeneratorの種類は以下です。 $ docker run --rm openapitools/openapi-generator-cli:v3.3.4 list The following generators are available: CLIENT generators: - ada - android - apex - bash - c - clojure - cpp-qt5 - cpp-restsdk - cpp-tizen - csharp - csharp-dotnet2 - csharp-refactor - dart - dart-jaguar - eiffel - elixir - elm - erlang-client - erlang-proper - flash - go - groovy - haskell-http-client - java - javascript - javascript-closure-angular - javascript-flowtyped - jaxrs-cxf-client - jmeter - kotlin - lua - objc - perl - php - powershell - python - r - ruby - rust - scala-akka - scala-gatling - scala-httpclient - scalaz - swift2-deprecated - swift3 - swift4 - typescript-angular - typescript-angularjs - typescript-aurelia - typescript-axios - typescript-fetch - typescript-inversify - typescript-jquery - typescript-node SERVER generators: - ada-server - aspnetcore - cpp-pistache-server - cpp-qt5-qhttpengine-server - cpp-restbed-server - csharp-nancyfx - erlang-server - go-gin-server - go-server - haskell - java-inflector - java-msf4j - java-pkmst - java-play-framework - java-undertow-server - java-vertx - jaxrs-cxf - jaxrs-cxf-cdi - jaxrs-jersey - jaxrs-resteasy - jaxrs-resteasy-eap - jaxrs-spec - kotlin-server - kotlin-spring - nodejs-server - php-laravel - php-lumen - php-silex - php-slim - php-symfony - php-ze-ph - python-flask - ruby-on-rails - ruby-sinatra - rust-server - scala-finch - scala-lagom-server - scalatra - spring (以下省略) スタブサーバー APIクライアント同様に、スタブサーバー(Go言語)を生成します。 gオプションで指定するものが違うだけですね。 $ docker run -v ${PWD}:/local openapitools/openapi-generator-cli:v3.3.4 generate -i /local/petstore-expanded.yaml -g go-server -o /local/out/go [main] WARN o.o.c.ignore.CodegenIgnoreProcessor - Output directory does not exist, or is inaccessible. No file (.openapi-generator-ignore) will be evaluated. [main] INFO o.o.c.languages.AbstractGoCodegen - Environment variable GO_POST_PROCESS_FILE not defined so Go code may not be properly formatted. To define it, try `export GO_POST_PROCESS_FILE="/usr/local/bin/gofmt -w"` (Linux/Mac) [main] INFO o.o.c.languages.AbstractGoCodegen - NOTE: To enable file post-processing, 'enablePostProcessFile' must be set to `true` (--enable-post-process-file for CLI). [main] INFO o.o.codegen.AbstractGenerator - writing file /local/out/go/go/model_error.go [main] INFO o.o.codegen.AbstractGenerator - writing file /local/out/go/go/model_new_pet.go [main] INFO o.o.codegen.AbstractGenerator - writing file /local/out/go/go/model_pet.go [main] INFO o.o.codegen.AbstractGenerator - writing file /local/out/go/go/api_default.go [main] INFO o.o.codegen.AbstractGenerator - writing file /local/out/go/api/openapi.yaml [main] INFO o.o.codegen.AbstractGenerator - writing file /local/out/go/main.go [main] INFO o.o.codegen.AbstractGenerator - writing file /local/out/go/Dockerfile [main] INFO o.o.codegen.AbstractGenerator - writing file /local/out/go/go/routers.go [main] INFO o.o.codegen.AbstractGenerator - writing file /local/out/go/go/logger.go [main] INFO o.o.codegen.AbstractGenerator - writing file /local/out/go/go/README.md [main] INFO o.o.codegen.AbstractGenerator - writing file /local/out/go/.openapi-generator-ignore [main] INFO o.o.codegen.AbstractGenerator - writing file /local/out/go/.openapi-generator/VERSION カレントディレクトリ以下に下記のようなディレクトリやファイルが生成されます。 特にスタブとして重要なのは下記ファイルです。 リクエストが来たら Status.OK を返すようになっています。 当然ながらビジネスロジックは記述されていません。 /* * Swagger Petstore * * A sample API that uses a petstore as an example to demonstrate features in the OpenAPI 3.0 specification * * API version: 1.0.0 * Contact: apiteam@swagger.io * Generated by: OpenAPI Generator (https://openapi-generator.tech) */ package openapi import ( "net/http" ) // AddPet - func AddPet(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json; charset=UTF-8") w.WriteHeader(http.StatusOK) } // DeletePet - func DeletePet(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json; charset=UTF-8") w.WriteHeader(http.StatusOK) } // FindPetById - func FindPetById(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json; charset=UTF-8") w.WriteHeader(http.StatusOK) } // FindPets - func FindPets(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json; charset=UTF-8") w.WriteHeader(http.StatusOK) } サーバー起動します。 $ go run main.go 2019/03/20 21:28:21 Server started curlリクエストすると200が返ってきました。 $ curl -s http://localhost:8080/api/pets -o /dev/null -w '%{http_code}\n' 200 クライアント開発チーム用にこのスタブを残してスキーマ駆動開発にしたり、サーバーサイド側の開発工数をさげたりできます。 なお、私が所属するプロジェクトでは、一連のコマンドをmakeコマンドで実行できるようにしています。 現場からのTips ここからは実際の開発で得られたノウハウやつまずいたこと、もう少し良くしたいと考えていることをご紹介したいと思います。 定義したobjcet型のプロパティが自動生成されなかった 以下は実例を簡略化し、一部抜粋したものです。SampleBに関する記述であることに着目してください。 SampleB : type : object properties : status : type : integer example : 200 message : type : string example : successfully resource : type : object properties : count : type : integer example : 1 results : type : array items : type : string example : "1234567890ABCDEFGHIJKLMNOPQRSTUVWXYZ" 以下が自動生成されたモデルです。 Resourceの型に着目してください。なぜか、定義したSampleBでなく別のSampleAのポインタ型のフィールドが宣言されています。 package httpmodel type SampleB struct { Status int32 `json:"status"` Message string `json:"message"` Resource * SampleAResource `json:"resource,omitempty"` //あれ、なんでAなの? } 原因は、既にプロパティ名とexample値が同一のobject型のプロパティがあることでした。 どうやら、OpenAPI Generatorはモデル自動生成時に同一のものがある場合は、YAMLファイル上でより上に定義されたものをDRYに生成してくれるようです。 ちなみに、あえてDRYにしたくない場合は、 $ref を使ったり、exmaple値を異なるものにすることでも回避できます。 array型プロパティを持つモデルが期待通りに生成されなかった 以下のように type: array を持つスキーマを定義し、モデルを自動生成したところ、期待通りにスライスをプロパティとしてもつモデルを生成できませんでした。 (以下は実例を簡略化し、一部抜粋したものです) components : schemas : RequestA : description : こちらはサンプルです type : array items : properties : sku30 : description : こちらはサンプルです example : 123abc type : string required : - sku30 // RequestA - こちらはサンプルです type RequestA struct { Inner [] map [ string ] interface {} `json:"inner,omitempty"` } 原因は こちらのPR でしょうか。 トップレベルにarrayかmapのプロパティがあるとgenerateしてくれないようです。 解決策は2通りあります。 1つ目は、 .openapi-generator-ignore ファイルに自動生成を無視するファイルを指定し、手動で実装する方法です。これを多用しすぎると自動生成の恩恵を受けられないため、OpenAPIを利用するメリットがかなり薄れてしまいます。 2つ目は、下記のように、 type: array を type: object で包む方法です。モデルが1つ増え、独自型のスライスのプロパティを持つことになります。 components : schemas : RequestA : description : こちらはサンプルです type : object properties : inner : type : array items : properties : sku30 : description : こちらはサンプルです example : 1234567890ABCDEFGHIJKLMNOPQRSTUVWXYZ type : string required : - sku30 type : object // RequestA - こちらはサンプルです type RequestA struct { Inner [] RequestAInner `json:"inner,omitempty"` } type RequestAInner struct { // こちらはサンプルです Sku30 string `json:"sku30"` } 命名に気を使う必要がある スキーマ名や operationId フィールド名などは自動生成コードでのstruct名やフィールド名、ファイル名などに反映されるものです。したがって、一意に認識しやすい名前をつける必要があります。 これを怠るとソースコードの可読性低下につながってしまいます。 こちらはOpenAPIならではの悩みです。 バリデーションを手動で実装している これは、今後改善したいと考えていることです。 現在、 ozzo-validaiton というバリデーションのライブラリを使用して、API仕様書の情報を見ながらバリデーションを手動で実装しています。 これは二度手間感がある上、人が実装しているので、バリデーション漏れなどのミスが発生する可能性もあります。 例えば、Rubyであれば oas_parser と json_schema というgemを組み合わせる方法があります。OpenAPIで定義したファイルのrequiredやtype、example値などの情報を使って自動でバリデーションを自動生成できます。Go言語でも同様のことができるライブラリを探そうと考えています リクエストパラメータのバリデーションライブラリの選定 go-playground/validator は、最もポピュラーなGo言語のバリデーションライブラリです。しかし、今回は別のライブラリ(上述)を採用しました。 理由としては、OpenAPIとの相性が悪いと判断したためです。 go-playground/validator はstructにバリデーション用のタグを記述するスマートな方法です。しかしながら、モデルを自動生成した際に上書きされてタグの記述内容が消失するケースもあります。 自動生成した後にタグを記述し、 .openapi-generator-ignore にファイル名を追記すれば解決するのですが、開発時の運用が複雑になってしまうと判断し、採用を見送りました。ここでの連携ができれば利便性がすごく高いと思います。 API仕様書とソースコードの乖離 開発時にAPIの仕様変更にドキュメントが追従できず、API仕様とソースコードで乖離が発生する経験はありますでしょうか。これは実装者とレビュアが普段の開発で注意し、定期的に乖離の発生状況を確認するべきでしょう。 OpenAPIを有効活用すれば乖離の発生を抑えることができます。 なぜならば、リクエストとレスポンス用のモデルは定義ファイルにしたがって自動生成されるため、レスポンスでの乖離は発生しません。 リクエストに関しては、リクエストパラメータのバリデーションに関して自動生成を導入していないケースでは、乖離が発生し得ます。例えばGo言語などの静的言語で実装しているのであれば、型の確認は可能ですが、ビジネスロジック面でのチェック(値の範囲など)まではできません。 まとめ 今回はOpenAPIの基本記法と、実際の開発現場で得られたつまずきやTipsをいくつかご紹介しました。 OpenAPIは単にAPI定義をスマートに記述できるだけでなく、そこからある程度まで自動生成してくれます。 良さそうだなと感じたら、OpenAPIを使ってみてください。 さいごに ZOZOテクノロジーズでは、技術でファッションの世界を変える仲間を募集しています。 お洒落に自信がある方・無い方どちらも歓迎です。 ご興味のある方は、以下のリンクからぜひご応募ください! ちなみに、私が担当しているプロジェクトでは現在、下記のような技術スタックやツールを扱っています。 よろしければご参考ください。 Go OpenAPI GitHub CircleCI AWS JIRA/Confluence Gsuite tech.zozo.com
こんにちは! ZOZOテクノロジーズ開発部の中坊( e_tyubo )です。 私の所属しているマーケティングオートメーション(MA)チームでは、ZOZOTOWNやWEAR等の各サービスで蓄積されたデータを集約したデータ基盤の運用を行なっております。我々MAチームはこの集約されたデータを用いて顧客分析を行いマーケティングに活用しています。 今回はその運用の中で生じた問題とその解決方法ついて紹介致します。 概要 データ基盤には定期的にデータを流し込む処理がスケジューリングされており、膨大なデータが毎日転送されています。 現在このデータ転送処理はサービス側のデータベースから直接データを流しておらず、一度ハブとなるデータベースに集約しています。 ZOZOTOWNとWEARはどちらもSQL Serverを使っているため、ハブとなるデータベースもSQL Serverを使っています。 SQL Serverでデータを転送する方法はいくつかありますが、定期的にバッチ処理でデータをまとめて転送する場合に注意すべきポイントがあったため対応を行いました。 この記事ではMAチームが運用しているSQL Server同士のデータ連携で生じた問題と実施した改善内容をご紹介します。 前提 SQL Server同士の連携 まずはデータ連携のシステムを紹介します ハブとなるデータベースをマーケティング用途に使うことからMarkeデータベースと呼びます。 ZOZOTOWNやWEARのデータベースからMarkeデータベースに連携する流れは下図のようになります。 このようにbcpコマンドを用いてデータのエクスポートとインポートを実現しています。 どのようなクエリでbcp outするかは次に説明する管理テーブルで定義します。 管理テーブル データ連携はMarkeデータベース上にある管理テーブルの情報をもとに実行されます。この管理テーブルには下記の情報が含まれます。 接続先DBの情報 連携タイミング 連携時に実行するSQL 取得結果を保存するMarkeデータベース上のテーブル名 例えば下記のようなデータが入っていたとすると、指定したタイミングにデータベースに対してSQLが実行され、その結果をMarkeデータベースのテーブルに保存します。 接続先 タイミング クエリ 保存先テーブル名 zozotown_db_1 夜間1回 SELECT * FROM zozotown_db.dbo.TableA TableA wear_db_1 日中に定期実行 SELECT column1, column2 FROM wear_db.dbo.TableB TableB 現在この管理テーブルに約300テーブル分のデータが入っており、日々データが更新されています。 課題 さて、ここからが本題です。運用上問題になったことは2つあります。 テーブル定義が変わる サービスを運用しているとテーブル定義の変更は起こり得ます。例えばサービス側のテーブルにカラムを追加すると、エクスポートするデータの形式が変化してしまいます。具体的には下図のように、SELECT * FROM ~を使っているケースで問題が起きます。 カラム追加時はMarkeデータベースにも追加しておかないと、インポートするデータとテーブル定義が異なることでエラーになります。これは実際に何度も発生していました。 そこで、すべての管理テーブルのレコードのクエリをカラム指定することにしました。そうすることで仮にカラムが追加されてもクエリの結果は変化しないため、問題が生じなくなります。もしカラムのデータが必要な場合は追加対応を行えば良いため、要件的にもすぐには問題になりません。 カラムが削除されたりカラムの定義が変わった場合は別途対応が必要になりますが、発生頻度が低いため考慮していません。 ソートされていないデータの大量インサート データ連携ではデータを全て入れ替えていますが、SELECT時にクラスター化インデックスを定義するカラムと同じ並び順でソートしないとINSERTに時間を要することがあります。 以降「クラスター化インデックスを定義するカラムと同じ並び順でソート」は長いので、「クラスター化インデックスでソート」と呼びます。 INSERTが遅くなる原因について説明します。 SQL Serverはページという容量8KBのまとまりでデータを保存していて、レコードの情報はページの中に含まれています。INSERTする時はクラスター化インデックスで規定された順番通りにデータを挿入しますが、その順序はページの中で保つ必要があります。ページの容量に限界が来るとページの新規作成かページの分割が必要になり、その挙動の違いでパフォーマンスが変わります。 まずクラスター化インデックスでソートされたデータを挿入するケースを考えます。 分かりやすくするために、主キーである整数型のidがクラスター化インデックスになっているケースを想定します。 ページの中に4レコードしか入らないと仮定すると、5レコード目を挿入する時は下図のように新しいページを作成します。 挿入されるデータの順序がクラスター化インデックスの順序と一致しているため、INSERT時にはページの新規作成だけで完結します。 次にクラスター化インデックスでソートされていないケースを考えます。 5レコード目はidの値で並び替えた場合既存のページに含まれる必要がありますが、すでに容量がありません。この時は下図のようにページの分割が発生します。 分割されて新たに生成されたページ1'には空き容量があるため、適切な領域にデータが追加されます。 データの量が多い時はこのページ分割の頻度次第で、パフォーマンスに影響する可能性があります。 適切にソートされていない場合、INSERT済みのデータ量が多いほどページ分割が発生しやすくなります。大量データをbcpでインポートする際にINSERT速度が低下する場合は、今回ご説明したページ分割が大量に発生している可能性を疑います。 ページ分割の発生を最小限にするためには全てのSELECT文にクラスター化インデックスを用いた適切なORDER BY句を設定する必要があります。 現状主キーがクラスター化インデックスに指定されているケースで対応が可能なため、主キーを基準にソートしました。 2つの課題をまとめると、下記の対策が必要です。 SELECT句を全てカラム指定にする ORDER BY句を主キーのカラムと同じ並び順で指定する 実際に対応の必要なレコードがかなりの数存在していたため、手動で書き換えると大変手間になることが予想されました。そこで管理テーブルに設定されてるクエリを更新するためのUPDATE文を自動生成することにしました。以下でその手法を紹介します。 改善方法 方針 すでに管理テーブルに入っているレコードを正しい形に修正する必要があるため、UPDATE文を対象となるレコード全てに対して作成します。 実現のためにPythonを使ってODBCドライバー経由でSQL Serverに接続できる pyodbc というライブラリを使いました。これは Microsoftの公式ドキュメント で言及されているSQL Serverにアクセスする方法です。 Pythonを使った理由はDigdagで利用することを念頭に置いているからです。 MAチームではワークフローを管理するツールとしてDigdagを利用しており、今後ワークフローに乗せることを想定しています。 実装の流れは下記のようになります。 現在連携されているテーブルの連携情報の一覧を取得 対象テーブルの定義を取得 対象テーブルの主キー情報を取得しORDER BY句を生成 既存のクエリにWHERE句が指定されていたら継承する 用意した情報からUDPATEクエリを生成しsqlファイルに出力 テーブル一覧を取得する まずは管理テーブルから同期対象のテーブル一覧を取ってきます。 この時に対象テーブル名とクエリを全て取得します。 対象テーブルの定義を取得 information_schemaを使ってカラム情報を取得します。 bcpコマンドを使ってインポートする際は、エクスポート時に指定するSELECT句のカラム順序を、インポート先のテーブルのカラム順序と合わせておく必要があります。そのため、ordinal_positionを基準にソートしています。 SELECT table_name, column_name FROM information_schema.columns ORDER BY ordinal_position 取得した情報からテーブル名毎のカラム定義のdictionaryをPythonのロジックの中で用意しておきます。 対象テーブルの主キー情報を取得しORDER BY句を生成 下記のシステムカタログビューを経由してテーブル毎に主キーで使われるカラム名を取得します。 それぞれの名前と用途と取得用クエリは下記の通りです。 名前 用途 sys.tables テーブル名の取得 sys.key_constraints 主キー制約の取得 sys.index_columns 制約の中で使われるindexの取得 sys.columns indexで使われるカラム名の取得 SELECT tbls.name, cols.name FROM sys.tables AS tbls INNER JOIN sys.key_constraints AS key_const ON tbls.object_id = key_const.parent_object_id AND key_const. type = 'PK' INNER JOIN sys.index_columns AS idx_cols ON key_const.parent_object_id = idx_cols.object_id AND key_const.unique_index_id = idx_cols.index_id INNER JOIN sys.columns AS cols ON idx_cols.object_id = cols.object_id AND idx_cols.column_id = cols.column_id ORDER BY tbls.name, idx_cols.key_ordinal 複合主キーの場合index内で使われるカラムの順序を合わせる必要があるため、テーブル毎にkey_ordinalを基準にソートしています。このクエリを使うと主キーに使われるカラム名を全て取得できるので、テーブル名毎にORDER BY句を作ります。 既存のクエリにWHERE句が指定されていたら継承する 既存のクエリのロジックを変えないために、既に指定されているWHERE句を抽出して新しいSELECT文に適用する処理が必要になります。 例えば下記のようなクエリがあった場合は、WHERE column2 >= 1を切り取る必要があります。 SELECT column1, column2 FROM db.schema. table WHERE column2 >= 1 Pythonを使うと下記の様な処理でクエリの中で存在するWHERE句以下を抽出できるので、保持しておきます。 where_start_pos = query.upper().find( 'WHERE' , 0 ) if where_start_pos >= 0 : where = query[where_start_pos: len (query)] また、WHERE句以降にORDER BYが使われているケースが存在する時は、同様のロジックでORDER BY以降を切り取って適切なものに置き換えを実施しました。 用意した情報からクエリを生成しsqlファイルに出力 これまでの処理でクエリを生成するために必要な情報は揃いました。これらを使ってUPDATE文を生成します。 用意したデータを下記の変数で保持しているとします。 target_tables: 連携しているテーブルのリスト column_map: テーブル毎のカラム定義のリスト primary_key_map: テーブル毎の主キーに使われるカラムのリスト where_map: テーブル毎の既存クエリのWHERE句 また、テーブル名毎の更新対象レコードidをrecord_id_mapという変数で持っているものとします。 Pythonの中でのクエリ生成処理の流れは簡略化すると下記の様になります。 update_query_format = "UPDATE manage_table SET select_query = %s WHERE id = %d" for table in target_tables: # テーブル毎に必要な情報を変数に入れる column_list = column_map[table] primary_key_list = primary_key_map[table] where = where_map[table] record_id = record_id_map[table] # 用意した情報を整形してSELECT文を生成(メソッドの詳細は省略) select_query = create_select_query(table, column_list, primary_key_list, where) # 用意したフォーマットを置換してUPDATE文を作成 update_query = update_query_format % (select_query, record_id) # 生成されたSQLを必要な場所に出力 最終的に出力されたUPDATE分を実行したら以降はカラム指定でテーブルが連携されるため、対策は完了です。 改善後の成果 まだ実施から3か月ほどしか経っていませんが、以降カラム追加に伴うエラーは発生しておりません。さらに実施後は新しく連携テーブルを追加する際はこの仕組みを使ってSELECT文を生成できるので、人為的なミスがおきにくくなっているだけでなく作業も効率化されています。 ORDER BY句を指定する等のクエリ生成時のルールをロジックに内包できるので、今後仕様の追加があっても容易に仕組に取り込むことが可能になったこともメリットだと思います。 今後の展望 本記事で紹介した機能の延長で、いずれはテーブル定義の差分を自動検知する仕組みを作りたいと思っています。Digdag上でワークフローとして定義し、定期的に必要なALTER文を生成して通知する仕組みを作ることでテーブル定義を常に正しい状態に維持しやすくなると考えています。 まとめ 本記事ではデータ統合のためのデータ連携の仕組みの一部と、それを安定化させるための取り組みを紹介しました。データ連携を改善する1つの方法として参考になれば幸いです。 MAチームではMA基盤上のアプリケーションの開発・運用だけでなく、データ連携の仕組みの開発・運用も行なっており、ビッグデータを活用したデータ基盤の改善に取り組んでいます。目立たない分野ですが非常にサービスへの影響は大きく、挑戦のしがいがあるチームです。 ZOZOテクノロジーズでは、一緒にサービスを作り上げてくれる方を募集中です。ご興味のある方は、以下のリンクからぜひご応募ください。 https://www.wantedly.com/companies/zozo-tech/projects
はじめまして! ZOZOテクノロジーズ開発部の平田( @TrsNium )と申します。 業務ではデータ基盤の開発・運用を行っています。 よろしくお願いいたします。 今回複数のツールが混在していたデータ基盤を「Digdag・Embulk」に統一したので、その取り組みを紹介します。 概要 弊社のデータ基盤は注文情報や顧客情報などをSQL Serverから取得しBigQueryに転送しています。 以前のデータ基盤では「Talend」と「Embulk・Digdag」でデータの収集と転送をしていました。 Talendは、タスクのスケジューリングとデータ転送を行うツールです。 Digdagはタスクのスケジューリングをするツールで、Embulkはデータを転送を実行するツールです。 「Talend」と「Digdag・Embulk」は別々のチームが管理・運用をしており、運用負荷が高いという問題がありました。 そこでデータの収集と転送をするツールを、「Digdag・Embulk」に統一することで運用負荷を下げました。 はじめに、移行前後のデータ基盤について紹介します。 以前のデータ基盤 以下の図が移行前のデータ基盤です。 TalendがSQL Serverからデータを収集し、S3にデータを配置します。 その後、DigdagとEmbulkがS3に配置されたデータをBigQueryに転送します。 なお、途中にS3を介しているのはDWHとしてRedshiftを使用していた時代の名残です。 現在はDWHとしてBigQueryを使用しているので、S3が存在する必然性はありません。 移行後のデータ基盤 以下の図が移行後のデータ基盤です。 SQL ServerからS3、S3からBigQueryまでの転送をDigdagとEmbulkで行います。 ツールの紹介 Talendとは ここで、Talendが何かご存じでない方もいると思うので、紹介させていただきます。 TalendとはETLツール兼ワークフローエンジンです。 ETLとはあるシステムからデータの抽出・加工をし、別のシステムにエクスポートする工程のことです。 またTalendには以下の様な特徴があります。 ビジュアルスクリプトにより、プログラミングの知識がなくとも直感的にデータ転送をできる 多種多様なDBやクラウドストレージ、csvなどのファイルフォーマットをサポートしている データ加工処理をJavaで拡張可能 ビジュアルスクリプトにより、プログラミングの知識がなくとも直感的にデータ転送をできる Talendは主にビジュアルスクリプトで操作を行うことができます。 ビジュアルスクリプトは、エンジニア以外の人にも直感的で分かり易いマウスのドロップ&ドラッグで操作ができます。 多種多様なDBやクラウドストレージ、csvなどのファイルフォーマットをサポートしている Talendは主要なクラウドストレージ(S3, GCS, Azure Data Lake Storeなど)に対応しています。 それに加え、データフォーマットも多岐にわたり対応しているため、クラウドストレージにあるデータのフォーマットを気にせず利用できます。 そして様々なデータベース(MySQL, SQL Server, Netezzaなど)をサポートしています。 これらを組み合わせることで、データ転送や、データベース・クラウドストレージへのデータの参照と書き込みができます。 データ加工処理をJavaで拡張可能 Talendではデータ転送だけではなく、ルーチンと呼ばれるデータを加工する機能があります。 例えば日付型の値に対しては、 TalendDate.diffDate で2つの日付データから時間の差を求められます。 多くのルーチンがデフォルトで提供されているため、様々なデータの加工ができます。 またルーチンはユーザーが独自定義できるようになっています。 Javaでルーチンを拡張できるため、より柔軟にデータの加工ができます。 先の通り、拡張性・柔軟性・敷居の低さからTalendが利用されていました。 Digdag・Embulkとは DigdagとEmbulkの紹介をさせていただきます。 Digdagとはオープンソースのワークフローエンジンです。 複数のタスクをワークフローとして定義し、Digdagがワークフローを実行します。 Digdagには以下の様な特徴があります。 YAMLフォーマットでワークフローを定義する タスクに依存関係を持たせることができる タスクの実行環境が柔軟である DigdagはYAMLフォーマットでワークフローを記述ができるため、Git上でDigDagのワークフローを管理できます。 タスクは依存関係を持つことができ「データの加工→データの集計→通知」の様なワークフローを定義をできます。 この機能により、どの様なタスクでもワークフローとして定義できます。 またDigdagのタスク実行環境は自由に選択をできます。 これはタスクをDockerコンテナ内で行うことができるためです。 Embulkはバルクデータをバッチ処理するオープンソースのバルクデータローダーです。 Embulkはプラグインベースのバルクローダーであり、以下の様な特徴があります。 バッチ処理をYAMLフォーマットで定義をできる 様々なプラグインがオープンソースとして公開されている EmbulkはYAMLフォーマットでバッチ処理を定義できるため、Git上でワークフローの管理をできます。 また多くのプラグインがオープンソースとして公開されています。 プラグインは「input」「filter」「output」「parser」「formatter」などの複数種類があります。 これらのプラグインを組み合わせることで、多様なのデータソースからデータを読み込み、加工し、ターゲットソースに書き込むことができます。 更にEmbulkはデータを並列処理をするため、バッチの実行時間を短くできます。 弊社には embulk-output-bigquery や embulk-filter-column 、 embulk-filter-timestamp_format などの作者でもある @sonots 氏が所属しています。 Embulkに詳しいエンジニアが在籍していることで、Embulkに関する問題解決速度が速くなりました。 Talendの運用上の問題点 Talendを使ってきましたが運用する上で問題がありました。 実際に起きた問題は以下の通りです。 無償版のTalendはビジュアルスクリプトで書かれている部分をGitで管理できない CUI操作を行うことができない Talendには製品版のTalend PlatformとTalend Enterprise、無償版のTalend Open Studioがあります。 弊社で運用されていたTalendは無償版のTalend Open Studioでした。 無償版のTalendでは、開発管理の機能やジョブ実行機能などに制限があります。 そのうちの一つにプロジェクトの管理をGitで行うことができません。 Git上でプロジェクト管理ができないために「どのETLを変更したのかの把握」や「機能の追加・変更による障害の対応」などの難しさがありました。 また、GUI操作で基本操作をするため、反復的な操作を行う業務が困難でした。 この様なことを踏まえて他のツールへ置き換えることを検討しました。 EmbulkとDigdagでは以上の問題を解決できることや、Talendと同様にデータを処理できる点から選択しました。 またチーム内で、で複数の運用実績があるのも選択の決め手となりました。 Digdag・Embulkへの移行 Digdag・Embulkへの移行手順について紹介します。 Talendのリプレイスは以下の様な手順で行いました。 TalendのETLを把握する Talendと同様にEmbulkでデータを転送する Digdagでデータ転送のタスクをスケジューリングする Talendと並走してDigdagとEmbulkを実行する 移行前後のデータの差分を確認する DigdagとEmbulkへ移行する 1. TalendのETLを把握する TalendのETLを把握するため「Talendが参照しているテーブル」「どの様なルーチンを使用しているのか」「データ転送スケジュール」を調べました。 参照しているテーブルは200程度でした。 ルーチンは14種類あり「対象のカラムをコピーし別のカラムへ移す」「データの秘匿化」「文字列の正規化」「日付のフォーマットを変更する」などがありました。 またS3へのデータ転送は、1日に1回AM4:00〜AM7:30の間に終えられるようにスケジューリングされていました。 Embulkでは、Talendで行っていた14のルーチンと同様な処理をしなければなりません。 Digdagでは、Talendと同様にAM4:00〜AM7:30の間にデータを転送できるようにスケジューリングする必要がありました。 2. Talendと同様にEmbulkでデータを転送できるようにする データ転送ではEmbulkを使用します。 EmbulkでSQL Serverのデータを参照するには、 embulk-input-sqlserver プラグインを使用しました。 ルーチンで行われている処理は、 embulk-filter-ruby_proc プラグインで代替えしました。 embulk-filter-ruby_procでルーチンと同じ処理ができているかは、手順5の方法で確認をしました。 またEmbulkの設定ファイルはYAMLで記述されます。 約200のテーブルを手書きでEmbulkのYAMLフォーマットに変換するのは、現実的ではないと考えました。 そこでTalendのプロジェクトファイル中にある、マークアップ言語で記述されているETLの情報を利用しました。 TalendのETLの情報をパースするスクリプトを書き、EmbulkのYAMLをしました。 副次的なメリットとしてデータを参照するためのSQLや、データ加工の対象となるカラム名のtypoが発生しませんでした。 3. Digdagでデータ転送のタスクをスケジューリングする Digdagは、Embulkが行うデータ転送をスケジューリングするために使用します。 Digdagでは繰り返し処理を並列に実行をできます。 繰り返し処理は複数のテーブルを転送する際に利用されています。 しかし、並列にテーブルのデータ転送を行う処理を行うことは、SQL ServerのDBリソースを占有してしまう恐れがあります。 そこで以下の様なことに気をつけました。 データベースの許可されている同時接続数や計算リソースを使い切らないようにする 同時に並列で処理する各テーブルのデータサイズが均一になるようにグルーピングすることで、従来よりデータの転送所要時間を少なくする SQL Serverの同時接続数を超えないように処理するために、以下の図のようにスケジューリングしました。 上の図では、複数のデータ転送を纏めて幾つかのグループにしています。 グループ内のデータ転送は並列に処理されますが、グループ自体は逐次的に処理されます。 つまり、同時にデータベースへアクセスするEmbulkプロセス数の上限は、1グループあたりのタスク数に制限されます。 グループ内の数を減らすことによりデータベースへの負荷を軽減ができますが、同時に全体を通したデータ転送の時間は長くなるトレードオフの関係になります。 更に最適化されていないスケジューリングでは以下の図の様に、データ転送の所要時間が大きくなります。 この図のテーブルサイズは、各テーブルのデータサイズを表しています。 Costは全テーブルの転送にかかる時間を示しています。 1グループの転送時間はグループ内の最大転送時間に依存するため、Costは並列で転送されるグループの最大テーブルサイズに依存します。 つまり、全体のグループ内のテーブルサイズを均一にすることでCostの最適化ができます。 しかし上の図では、グループ内のテーブルサイズが均一になってないため最適化されていません。 そこで、テーブルサイズで全体をソートすることで、Costを以下の図のように最適化しました。 テーブルサイズでソートすることによりCostが最適化されました。 これによりデータベースの負荷を制限しつつ、転送時間を短くできました。 4. Talendと並走してDigdagとEmbulkを走らせる 次に、Digdagサーバーにワークフローを登録します。 ワークフローは以下の図のようになっています。 以上の図では「Talend」と「Digdag・Embulk」はデータの干渉をしないために別々のバケットとデータセットを利用しています。 またDigdagとEmbulkを動作させる際には以下のことを気をつけました。 SQL Serverから読み込んだデータをS3にデータを転送できること S3からBigQueryにデータを転送できること SQL ServerからS3にデータを転送できない場合には、YAMLのフォーマットエラーやembulk-filter-ruby_procで使用するRubyコードが原因でした。 例えば、S3からBigQueryでデータの転送できないケースとして、日付型のデータフォーマットの違いによりデータを転送できないことがありました。 Talendでは 2019/04/01 12:00:00 の様にフォーマットされているのに対し、Embulkでは 2019/04/01 12:00:00.000 の様にデータを送っていました。 Talendの 2019/04/01 12:00:00 の様なフォーマットで送られることが期待されているので、Embulkで送ったデータは日付をうまく読み取れず転送に失敗します。 これらの問題がないことを確認することで、正常にDigdag・Embulkが動作していること、Talendとのデータフォーマットに違いがないことを把握できます。 5. 移行前後のデータの差分を確認する データに差分がある場合、分析で行なっている集計処理が正しく行われないため、差分がないことを確認します。 データの差分の確認はBigQuery上で行いました。 BigQuery上でデータの差分の確認を行うのは、以下の様なメリットがあります。 差分があった際に、どのカラムから差分が発生しているかの確認が容易 複数の差分を同時に求めることができる 当初は、データの差分をS3上にPutされているデータをもとにPythonスクリプトで求めていました。 しかし、データサイズの大きなテーブルでは差分を求めるのに時間がかかることや、一度に大量のテーブルの差分を求めることができませんでした。 そこで、BigQueryへ複数リクエストすることで、一度に複数テーブルの差分結果を短時間で取得をできました。 BigQueryでは、以下のクエリで差分結果を取得しました。 ( select * from `production. Order ` except distinct select * from `test. Order `) union all ( select * from `test. Order ` except distinct select * from `production. Order `) production.OrderはTalendで転送しているデータセットのテーブルで、test.OrderがEmbulkで転送しているデータセットのテーブルです。 except演算子では、左側の入力クエリに存在し、右側の入力クエリには存在しない行を返します。 例えば select * from `production.Order` except distinct select * from `test.Order` をベン図で表すと以下の様になります。 production.Orderにのみあるデータ 以上の図の斜線部ではproduction.Orderにはあるが、test.Orderにはないレコードを表しています。 全体の差分を求めるには上記クエリのExcept前後のテーブルを入れ替え、unionで両方の差分を結合し求めることができます。 6. TalendのジョブをDigdagとEmbulkに置き換える TalendをDigdagとEmbulkに移行する際の手順を紹介します。 移行手順は以下の2つです。 Talendの設定を変更して、移行前ジョブを無効化する EmbulkのS3へ転送するBucketを変更する 以上の手順は以下の図の様なイメージです。 以上の図ではTalendの設定を無効化した後に、Embulkのデータ転送の向き先をTalendが転送していたProductionバケットに向けています。 また、安全に移行するために、1日で移行するのではなく複数日に分けました。 なぜなら、移行を全て同日にするのはリスクがあるからです。 例えば、1日で移行する際に複数の折り重なった障害が起きた場合、本番環境に与える障害の影響が大きくなります。 そこで、同日にすべての転送するのをやめ、複数日に分けることでリスクを分配しました。 統一したことによるメリット EmbulkとDigdagに統一することで、以下の様なメリットがありました。 Gitを用いてDigdagとEmbulkの設定が管理できるようになった 時間に余裕を持ってデータ転送を終了できるようになった Git上でDigdagとEmbulkの設定を管理できるようになった DigdagとEmbulkに置き換わったことにより、スケジューリングやデータ転送の設定をGit上で管理できるようになりました。 Git上で管理できるようになったことで、複数人に情報共有をしやすくなりました。 またGitのリポジトリ運用はGitHubFlowのため、レビュー段階でミスや改善点に気付けます。 時間に余裕を持ってデータ転送を終了できるようになった EmbulkとDigdagに置き換えることで余裕を持って、バッチを終了できるようになりました。 TalendがAM4:00 ~ AM7:30の間にデータ転送を終えていたので、それと同様にできる必要がありました。 結果的にデータ転送を1時間30分程度で終了できるようになり、データ転送に余裕を持つことができました。 これはEmbulkとDigdagがデータ転送を並列処理できるからです。 次のデータ基盤のステージ 今回データ基盤のツールの統一をしました。 しかし、データフローの最適化がまだなされていません。 現在のデータ基盤のデータフローを最適化した図は以下のようになります。 以上の図ではデータフローを最適化することで、SQL ServerからS3を経由するフローがなくなりました。 データフローの最適化を行うことでデータフロー全体の転送時間を短くでき、ワークフローの数が減るためDigdagとEmbulkの設定ファイルを少なくできます。 S3を経由するのを止め直接BigQueryにデータを転送すれば、全体のデータ転送時間は1〜2時間程度短くできます。 そしてEmbulkとDigdagの設定ファイルを少なくできるため、新規テーブルの転送設定を行う手間を軽減できます。 今回は紹介ができませんが、データベースのレプリケーションなどまだまだ改善の余地があります。 まとめ 本記事では、「Talend」を「Digdag・Embulk」に移行したことについて紹介しました。 データ基盤の構築や、リプレイスをする際の参考になれば幸いです。 ZOZOテクノロジーズでは、一緒にサービスを作り上げてくれる方を募集中です。ご興味のある方は、以下のリンクからぜひご応募ください。 www.wantedly.com .visual_script{ font-size: 123%; }
こんにちは、サーバーサイドエンジニアの竹若です。今回GraphQLにおけるエラーハンドリングを調査、Ruby on Railsと graphql-ruby を使って実装する機会があったので、そこで得られた知見を共有させていただきたいと思います。(なお今回の実装はプロダクション環境には出ていません) GraphQLの仕様とプラクティス それではまず初めに、GraphQLが仕様に定めているレスポンスの返し方を見ていきましょう。 レスポンスのフォーマットに関するプラクティス GraphQLのプラクティスの1つに、レスポンスのhttp statusを200で統一し、レスポンスの errors キーにエラーの詳細な情報を持たせるというものがあります。 なぜならGraphQLではリクエストに複数のクエリを含めることができるからです。 https://www.graph.cool/docs/faq/api-eep0ugh1wa/#how-does-error-handling-work-with-graphcool Since GraphQL allows for multiple operations to be sent in the same request, it's well possible that a request only partially fails and returns actual data and errors. これはあくまでプラクティスであり仕様ではないのですが、周辺ツール( Apollo やgraphql-ruby)がこのプラクティスに従っているため私たちも基本的には従うことになります。 レスポンスのフォーマットに関する仕様 ではGraphQLはどのようにしてエラーを表現するのでしょうか? GraphQLの仕様を見てみましょう。 https://facebook.github.io/graphql/June2018/#sec-Errors GraphQLの仕様ではレスポンスのフォーマットはハッシュであり、中に data と errors というキーを含みます。 { " errors ": [ { " message ": " hogehoge ", " extensions ": { " bar ": " bar " } } ] , " data ": { " user ": { " name ": " takewaka " } } } data クエリの実行結果が入るキーです。 クエリの実行前にエラーが発生した場合、 data キーはレスポンスに含まれません。 errors クエリの実行中に発生したエラーが入るキーです。 クエリの実行中にエラーが発生しなかった場合、 errors キーはレスポンスに含まれません。 またレスポンスに data キーが含まれない場合、 errors キーは必ずレスポンスに含まれている必要があります。 なお errors のフォーマットも仕様で定められていて、中に message , location , path というキーが含まれます。( location と path はクエリの中にエラーの該当箇所が存在する場合にのみ含まれます) 上記3つのキーの他にキーを追加したい場合は、 extensions というキーを用意してその中に追加する仕様です。 なぜなら message や path などのキーと同じレベルにオリジナルのキーを追加してしまうと、そのオリジナルのキーと将来的に仕様に追加されるキーがバッティングを起こす可能性があるからです。 https://facebook.github.io/graphql/June2018/#sec-Errors GraphQL services should not provide any additional entries to the error format since they could conflict with additional entries that may be added in future versions of this specification. GraphQLを使っていて発生するエラーとその分類 さて、GraphQLがどのようにしてエラーを表現するかはわかりました。 次にGraphQLを使っていて起こるエラーにはどのようなものがあるのか見ていきましょう。 エラーを以下の2つの観点で見ていきます。 エラーの原因はクライアントなのか、サーバーサイドなのか エラーはどこで発生したか クライアントが原因のエラーは主に以下の3つに分類できます。 パースエラー クエリのシンタックスエラー バリデーションエラー クエリが型チェックで引っかかる クエリ実行時エラー 認証失敗など サーバーサイドが原因のエラーはRailsで実装したロジックのエラーです。 どのようなエラーが存在するかわかったところで、それぞれのエラーをどのような形のレスポンスで表現するのか見てみましょう。 Apolloなどのクライアントがレスポンスをパースしやすいように、レスポンスのフォーマットはGraphQLの仕様に準拠した形で統一したいです。 そこで errors キーの中の message キーにエラーの詳細なメッセージを入れて、 extensions キーの中の code キーにステータスコードを入れる方法をここでは見ていきます。 これは GraphQLの仕様書に載っているエラーレスポンスの例 と同じ方法です。 また Apollo Server では、 extensions キーの中に code をはじめとしたエラーに関するキーを含める方法を採用しています。 例えば認証エラーであればこのようにcodeの中に AUTHENTICATION_ERROR というステータスを入れます。 " errors ": [ { " message ": " permission denied ", " locations ": [] , " extensions ": { " code ": " AUTHENTICATION_ERROR " } } ] サーバーエラーの場合はこのようにcodeの中に INTERNAL_SERVER_ERROR というステータスを入れます。 " errors ": [ { " message ": " undefined method 'hoge' for nil ", " locations ": [] , " extensions ": { " code ": " INTERNAL_SERVER_ERROR " } } ] graphql-rubyで実装する方法 エラーをどのような形のレスポンスで表現するか決まったところで、graphql-rubyで実際に実装していきましょう。 graphql-rubyではエラーをどう拾ってどう返すか graphql-rubyでは GraphQL::ExecutionError かもしくはそのサブクラスをraiseすることで errors にエラーを含めることができます。 def resolve(name:) user = User.new(name: name) if user.save { user: user } else raise GraphQL::ExecutionError, user.errors.full_messages.join(", ") end end 認証エラー 例として認証エラーの実装を載せます。 この例ではログインをセッションで管理していてcurrent_userメソッドを呼ぶことでユーザーオブジェクトが取得できる設定です。 コントローラーで GraphQL::Schema#execute を実行する際にユーザーのログイン情報を context に入れて引数として持たせておきます。 class GraphqlController < ApplicationController def execute variables = ensure_hash(params[:variables]) query = params[:query] operation_name = params[:operationName] context = { current_user: current_user } result = SampleSchema.execute(query, variables: variables, context: context, operation_name: operation_name) render json: result #... end #... end resolve メソッドの中で context に入っているユーザーのログイン情報を見て認証エラーを吐かせています。 GraphQL::ExecutionError はキーワード引数として extensions を持っているのでオリジナルのキー(ここでいう code )を渡すことができます。 def resolve(name:, sex:) raise GraphQL::ExecutionError.new('permission denied', extensions: { code: 'AUTHENTICATION_ERROR' }) unless context[:current_user] #... end そうすることで認証エラーが発生した場合、このようなフォーマットでレスポンスを返すことができます。 " errors ": [ { " message ": " permission denied ", " locations ": [ { " line ": 3 , " column ": 3 } ] , " path ": [ " createUser " ] , " extensions ": { " code ": " AUTHENTICATION_ERROR " } } ] セーフティネット GraphQLのレスポンスはクライアントがパースしやすいようにフォーマットを統一することが重要です。 発生したエラーが最後まで rescue されずにいるとRailsの一般的な500エラーが返ってしまいます。 そうなるとクライアント側はhttp status 200で返ってくるGraphQLのエラーと、Railsの一般的な500エラーの両方をパースする準備をしなければなりません。 そこでサーバーサイドでエラーを最終的に受け止めるセーフティネットを用意したくなります。 graphql-ruby 1.8までは rescue_from メソッドを使ってこれを実現できます。 以下に rescue_from メソッドを使った実装例を示します。 class SampleSchema < GraphQL::Schema rescue_from(StandardError) { 'INTERNAL_SERVER_ERROR' } #... end こうすることでRailsの一般的な500エラーではなく、以下のようなGraphQLのエラーを返すことができます。 " errors ": [ { " message ": " INTERNAL_SERVER_ERROR " , } ] ただ rescue_from の欠点として、 errors 内の message キーの内容しか指定できないという点があります。 これはgraphql-errorsというgemを使って rescue_from にエラークラスのオブジェクトを渡すことで解決します。 GitHub - exAspArk/graphql-errors: Simple error handler for GraphQL Ruby しかしgraphql-rubyの機能として rescue_from にエラーオブジェクトを渡せてもいいのではないかと考えたのでパッチを書きました。 extend GraphQL::Schema::RescueMiddleware#attempt_rescue by masakazutakewaka · Pull Request #2140 · rmosolgo/graphql-ruby · GitHub このパッチは以下のようにブロックに GraphQL::ExecutionError オブジェクトを渡せるようにすることで extensions キーを使えるようにするものです。 class SampleSchema < GraphQL::Schema rescue_from(StandardError) do |message| GraphQL::ExecutionError.new(message, extensions: {code: 'INTERNAL_SERVER_ERROR'}) end #... end またこの rescue_from メソッドはgraphql-ruby 1.9から使えなくなります。 理由は rescue_from メソッドが定義されている GraphQL::Schema::RescueMiddleware クラスがgraphql-ruby 1.9から使えなくなるからです。 GraphQL - Interpreter graphql-ruby 1.9では現状 rescue_from メソッドに変わる何かは存在せず、どのような実装が追加されるかも未定というステータスです。 GraphQL::Execution::Interpreter and rescue_from compatibility · Issue #2139 · rmosolgo/graphql-ruby · GitHub 個人的にはgraphql-errorsがgraphql-rubyに上手く取り込まれてくれたらいいなと思っています。 複数エラー クエリを実行して発生した複数のエラーを1つのレスポンスに含めたい場合があります。 例えばユーザー登録などの複数の入力項目を持つMutationがあったとします。 入力内容が不正であった全ての入力項目にエラーメッセージを表示したい場合、複数のエラーをレスポンスに含めたくなります。 graphql-rubyにおいては GraphQL::Schema::Context#add_error を使うことで複数エラーをレスポンスに含めることができます。 https://www.rubydoc.info/github/rmosolgo/graphql-ruby/GraphQL%2FQuery%2FContext%2FFieldResolutionContext:add_error 以下は実装例です。 module Mutations class CreateUser < GraphQL::Schema::RelayClassicMutation argument :name, String, required: true argument :sex, String, required: true field :user, Types::UserType, null: true def resolve(name:, sex:) user = User.new({ name: name, sex: sex }) if user.save { user: user } else build_errors(user) return # これがないとrescue_fromに拾われてしまう end end def build_errors(user) user.errors.map do |attr, message| message = user[attr] + ' ' + message context.add_error(GraphQL::ExecutionError.new(message, extensions: { code: 'USER_INPUT_ERROR', attribute: attr })) end end end end 複数のエラーを含んだレスポンスは以下のようになります。 " errors ": [ { " message ": " hoge はすでに存在します ", " extensions ": { " code ": " USER_INPUT_ERROR ", " attribute ": " name " } } , { " message ": " fuge は一覧にありません ", " extensions ": { " code ": " USER_INPUT_ERROR ", " attribute ": " sex " } } ] また独自のエラータイプを定義してエラーの内容を data に含めるという方法も存在します。 https://github.com/rmosolgo/graphql-ruby/blob/master/guides/mutations/mutation_errors.md#errors-as-data しかし GraphQL::Schema::Context#add_error を使う方法を以下の理由で採用しました。 GraphQLの仕様上 errors は data と同じレベルに位置してる data の中にエラーを入れる場合、クエリ内にエラーのフィールドを明示的に書かないとエラーの情報を得ることができないのでエラーの受け渡し方として優れていない 独自のエラータイプを定義してエラーの内容を data に含める方法にも以下のような利点があると思います。 エラータイプを定義するのでエラーの構造をスキーマで共有できる クライアント側でエラーメッセージを表示したい場合に、エラーの情報をレスポンスから取り出すのが楽 まとめ この記事ではGraphQLにおけるエラーハンドリングの仕方とgraphql-rubyを使った実装例を紹介しました。 この記事の初めにGraghQLの仕様に軽く触れましたが、GraphQLの仕様はとても簡潔にまとめられているので読むことをお勧めします。 また紹介した graphql-ruby には見やすい場所にドキュメントされていない隠れAPIがあったりするので、ソースコードやissueを読んでみると色々発見できると思います。 この記事の最後の方で紹介した add_error メソッド が隠れAPIの1つです。 将来的にはプロダクションに出してから得られる知見も発信したいです。 GraphQLを使った開発に興味のある方がいましたら、ぜひ以下のリンクからご応募ください。お待ちしております! www.wantedly.com 参考 GraphQL API | Graphcool Docs Full Stack Error Handling with GraphQL + Apollo 🚀 – Apollo GraphQL graphql-ruby/overview.md at master · rmosolgo/graphql-ruby · GitHub graphql-ruby/execution_errors.md at master · rmosolgo/graphql-ruby · GitHub graphql-ruby/mutation_errors.md at master · rmosolgo/graphql-ruby · GitHub
DroidKaigiで展示したファッションチェックアプリについて こんにちは。ZOZOテクノロジーズ開発部山田( @yshogo87 )です。 DroidKaigi 2019ではプラチナスポンサーとして、ブースを出展させていただきました。 DroidKaigi 2019 そのコンテンツとしてファッションチェックアプリを展示させていただきました。 今回はファッションチェックアプリがどのような仕組みになっているかを説明させていただきます。 ファッションチェックアプリとは ファッションチェックアプリとは、ユーザーが撮影した全身の写真について、WEARに投稿されたコーディネートを元に作成した学習モデルを使用して採点を行うものになっています。 技術的構成 技術的な構成は下記のようになっています。 フロントエンド: Flutter バックエンド: Firebase(ML Kit、Cloud Firestore)、GCP(Cloud Vision API) このファッションチェックアプリは、別のイベントでも使う予定があり、iOSでも動かせるようにする必要があったため、クロスプラットフォームで開発できるFlutterを選択しました。 また、バックエンドもTensorFlow Liteで作成した学習済みモデルをFirebaseにアップロードすることで特別なAPIを作成することなく、SDK経由で簡単に使うことができるためFirebase ML Kitを選択しました。 Cloud Firestoreは検出された結果をログとして保存しています。 FlutterからFirebase ML Kitを使う Firebaseの導入 FirebaseはFlutterから使用することができます。 導入手順は こちら の公式ページをご参照ください。 Firebase ML KitをFlutterで使う Firebase ML KitをFlutterで使うための プラグイン もOSSで公開されています。 今回使用するFirebase ML Kit Custom Modelもこのプラグインに内包されています。このプラグインを使うと、AndroidとiOSを同時に一つのコードで動かすことができるので非常に便利です。 ただしCustom ModelをFlutterから使用する場合には一部状況で注意が必要です。今回はこのプラグインを使用せず実装しました。 FlutterからFirebase ML Kit Custom Modelを使うときの注意点 Custom ModelをFlutterから使用する場合、返却される型に注意が必要です。Dartではfloat型が存在しないので、Custom Modelからの結果がfloat型の場合うまく受け取れません。そこで、KotlinでFirebase ML Kitとのやりとり部分を書いてFlutter側に結果を返すコードを書きました。 実装 FlutterからKotlinのコードを実行する 次のように invokeMethod によってFlutterからKotlinのコードを呼び出し、その実行結果が result に返ってきます。 const platform = const MethodChannel( "firebaseCustomModel#run" ); try { final String result = await platform.invokeMethod( "getResult" , < String , dynamic > { 'imageFile' : cameraImage.path, 'gender' : widget.gender, }).catchError((err) { print( "エラーが発生しました" ); setState(() { _isError = true ; }); }); 次にKotlin側でFlutterからきたデータを受け取ります。 MethodChannel クラスを使ってコールバッククラスを設定することでFlutterから getResult という文字列でリクエストがくるとKotlin側で受け取れます。 override fun onCreate(savedInstanceState: Bundle?) { super .onCreate(savedInstanceState) GeneratedPluginRegistrant.registerWith( this ) MethodChannel(flutterView, "firebaseCustomModel#run" ).setMethodCallHandler( object : MethodCallHandler { override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) { val camera = call.argument<String>( "imageFile" ) val bitmap = BitmapFactory.decodeFile(camera) val gender = call.argument< Int >( "gender" ) startCustomModel(bitmap, gender, result) } }) } Flutterから設定されたパラメータは下記のコードで取得しています。 val camera = call.argument<String>( "imageFile" ) val bitmap = BitmapFactory.decodeFile(camera) val gender = call.argument< Int >( "gender" ) Flutterからネイティブのコードを呼び出す方法については公式ページがあるので詳しくは こちら をご覧ください。 Firebase ML Kitからデータを取得する ここからはFirebase ML KitからCustom Modelをダウンロードし、結果を取得します。 下記のコードは Firebase ML Kitのドキュメント を参考に実装しています。 fun startCustomModel(bitmap: Bitmap, gender: Int ?, result: MethodChannel.Result) { var conditionsBuilder: FirebaseModelDownloadConditions.Builder = FirebaseModelDownloadConditions.Builder().requireWifi() if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { conditionsBuilder = conditionsBuilder .requireCharging() .requireDeviceIdle() } val conditions = conditionsBuilder.build() val cloudSource = FirebaseCloudModelSource.Builder( "batai" ) .enableModelUpdates( true ) .setInitialDownloadConditions(conditions) .setUpdatesDownloadConditions(conditions) .build() FirebaseModelManager.getInstance().registerCloudModelSource(cloudSource) val modelName = if (gender == 0 ) { "wear_model" } else { "wear_model_women" } val options = FirebaseModelOptions.Builder() .setCloudModelName(modelName) .build() val firebaseInterpreter = FirebaseModelInterpreter.getInstance(options) val inputOutputOptions = FirebaseModelInputOutputOptions.Builder() .setInputFormat( 0 , FirebaseModelDataType.FLOAT32, intArrayOf( 1 , 224 , 224 , 3 )) .setOutputFormat( 0 , FirebaseModelDataType.FLOAT32, intArrayOf( 1 , 1 )) .build() val batchNum = 0 val input = Array( 1 ) { Array( 224 ) { Array( 224 ) { FloatArray( 3 ) } } } for (x in 0 .. 223 ) { for (y in 0 .. 223 ) { val pixel = bitmap.getPixel(x, y) input[batchNum][x][y][ 0 ] = ((Color.red(pixel) - 128 ) / 128 ).toFloat() input[batchNum][x][y][ 1 ] = ((Color.green(pixel) - 128 ) / 128 ).toFloat() input[batchNum][x][y][ 2 ] = ((Color.blue(pixel) - 128 ) / 128 ).toFloat() } } val inputs = FirebaseModelInputs.Builder() .add(input) // add() as many input arrays as your model requires .build() firebaseInterpreter !! .run(inputs, inputOutputOptions) .addOnFailureListener { e -> // Task failed with an exception // ... e.printStackTrace() }.continueWith { task -> val labelProbArray = task.result !! .getOutput<Array<FloatArray>>( 0 ) result.success(getTopLabels(labelProbArray)[ 0 ]) } } private fun getTopLabels(labelProbArray: Array<FloatArray>): List<String> { val data = labelProbArray[ 0 ][ 0 ] val list = ArrayList<String>() list.add( " $data " ) return list } 下記のコードでは、先ほどの説明した通りFlutterではfloat型を受け取れなかったので、KotlinでString型に変換してFlutter側に返却しています。 val labelProbArray = task.result !! .getOutput<Array<FloatArray>>( 0 ) result.success(getTopLabels(labelProbArray)[ 0 ]) private fun getTopLabels(labelProbArray: Array<FloatArray>): List<String> { val data = labelProbArray[ 0 ][ 0 ] val list = ArrayList<String>() list.add( " $data " ) return list } Cloud Vision APIを叩く 開発期間内では学習済みモデルの精度が高められるか不安があったので、Cloud Vision APIも叩いて加点することにしました。 Cloud Vision APIでは写真の情報から写っている画像に対して得られる情報を返すGCPのサービスの1つです。 cloud.google.com ファッションチェックアプリでは、Cloud Visionから「cool」や「fashion」などのファッションチェックとしてプラスになりそうなキーワードが検出されると加点しています。 加点するキーワードについてはCloud Firestoreで管理しています。 Cloud Vision APIを叩くコードは下記になります。 _requestCloudVision(File cameraImage, String result) async { String url = "https://vision.googleapis.com/v1/images:annotate" ; String apiKey = "api key" ; List < int > imageBytes = cameraImage.readAsBytesSync(); Map json = { "requests" : [ { "image" : { "content" : base64Encode(imageBytes)}, "features" : [ { "type" : "LABEL_DETECTION" , "maxResults" : 100 , "model" : "builtin/stable" } ], "imageContext" : { "languageHints" : [] } } ] }; Response response = await http.post(url + "?key=" + apiKey, body : jsonEncode(json), headers : { "Content-Type" : "application/json" }); var body = response.body; print(body); var bodyJson = jsonDecode(response.body); List < dynamic > responces = bodyJson[ "responses" ]; if (responces == null || responces.length == 0 ) { _showErrorDialog( "Label not found" ); return ; } Map < String , dynamic > labelAnnotations = responces[ 0 ]; if (labelAnnotations != null && labelAnnotations.length != 0 ) { List < LabelAnnotationModel > list = [] ; for ( dynamic label in labelAnnotations[ "labelAnnotations" ]) { LabelAnnotationModel model = LabelAnnotationModel.fromJson(label); list.add(model); } } } このコードではlistに検出されたラベル一覧が格納される方法になります。 結果はログとしてCloud Firestoreに保存 撮影した写真以外のデータはCloud Firestoreに保存していてリアルタイムでログを見れるようにするためのアプリも実装しました。 こちらのアプリでは、Flutterだけで実装しているため、AndroidでもiOSの動かすことができます。 ログとして保存したのは下記です。 男女ごとのスコア Cloud Visionから検出されるキーワード キーワードにヒットした時の加点数 Firebase ML Kitから返却される数値の平均点と、Cloud Vision APIからよく出力されるキーワードを監視していました。 開発当初、学習済みモデルの出来が悪く全員が同じ点数になることが危惧されたのでCloud Vision APIからキーワードで加点するように対策をしていましたが、結果的に危惧していた事態は起きず、この機能の出番はありませんでした。 まとめ 本記事ではファッションチェックアプリの仕組みと、Firebase ML Kit、 Cloud Vision APIの簡単な使い方について紹介させていただきました。 ZOZOテクノロジーズでは、技術でファッションを盛り上げてくれる方を募集しています。ご興味のある方は、以下のリンクからぜひご応募ください! www.wantedly.com www.wantedly.com
こんにちは。ZOZOテクノロジーズ開発部の田島です。 今時のシステム開発ではさまざまなツールを利用することが当たり前になっています。 そして各種ツールは日々新しいものが開発され、今まで当たり前だったものがレガシーなツールと呼ばれることも珍しくありません。 弊社では、GitHubやCircleCI・Slackなど様々なツールを利用しています。 私達のチームでもこれらのツールを利用していますが、それ以外にもGitBucketやJenkins・Redmineを独自で管理し利用していました。 今回ある理由からそれらのツールをSaaSへ移行しました。その経緯と移行手順を紹介します。 概要 開発支援サーバの紹介 利用しているGitBucket・Jenkins・Redmineは開発支援サーバと呼ばれる一台のEC2インスタンスの上で動作していました。 やったこと これらのツールを以下の図のように、「GitBucketをGitHub」・「JenkinsをCircleCI」・「RedmineをGitHub issue」へ移行しました。 ツール移行の経緯 なぜ開発支援サーバーが必要だったのか まず開発支援サーバーが必要だった理由を紹介します。以下のような理由から開発支援サーバーの運用を行っていました。 プロジェクト発足当時、全社的に利用するツールが統一されていなかった セキュリティのルールが定まっていなく、SaaSを利用することがためらわれていた 開発支援サーバを利用しているプロジェクトは外部委託しているものだったためツールをコントロールできなかった なぜ移行を行ったのか 次になぜツールの移行を行ったのかを紹介します。以下のような理由から開発支援サーバーのツールの移行を決意しました。 全社的に利用するツールの統一が行われ始めた プロジェクトをまるごと内製化するため自分たちでツールをコントロールできるようになった 同じような役割のツールが複数存在し、情報が分散されてしまっていた EC2に開発支援サーバーを立てているためデータのバックアップなどすべて自分たちでやる必要があった サーバーがダウンすると3つすべてのツールにアクセスできなくなる 最も大きな理由としては、全社的に利用するツールが統一され始めたこと。外部委託をしていたプロジェクトをまるごと内製化し自分たちでツールをコントロールできるようになったことが主な動機となりました。 移行先の選定 それぞれのツールの移行先として、様々なツールを検討しました。最終的にどのような理由から移行先のツールを決定したのかを紹介します。 GitHub GitBucketの移行先をGitHubにした理由は以下になります。 全社的にリモートリポジトリがGitHubに統一された 開発者全員が利用したことのあるツールである 権限管理などが柔軟 SaaSのため自分たちで管理の必要がない 基本的には社内でリモートリポジトリがGitHubに統一されたことが大きな要因になりました。 CircleCI Jenkinsの移行先をCircleCIにした理由は以下になります。 全社的にCircleCIが利用されている チームメンバーがCircleCIに慣れている 設定をコードで管理できる Jenkinsでやっていたことがすべて実現できる SaaSのため自分たちで管理の必要がない チームメンバーがCircleCIに慣れており、かつやりたいことを柔軟に実現できたことが採用の大きな要因になりました。 GitHub issue Redmineの移行先をGitHub issueにした理由は以下になります。 進行中のタスクと完了したタスクを分けることができる 新たにツールの導入が必要ない 検索ができる SaaSのため自分たちで管理の必要がない 全社的にドキュメントはConfluenceにチケット管理はJiraに統一しているので、はじめはどちらかに移行することを検討しました。そしてRedmineの移行先の条件としては進行中のタスクと完了したタスクを分けることのできるツールが好ましいと判断しました。Jiraはこの条件に当てはまりますが、Redmineでは一部の作業履歴などドキュメントのように利用していました。また、Jiraでは工数管理も同時に行っているため移行先として好ましくないと判断しました。そこで今までの作業履歴が参照できれば良い程度の要件だったため、GitHub issueが適当であると判断しました。 ツールの移行 移行手順 ツールの移行は以下の順番で行いました。 GitBucket Jenkins Redmine まずはじめに、GitBucketの移行を行いました。Jenkinsの移行先であるCircleCIではGitHubとの連携を考えていました。そこで先にリポジトリをGitHubを移行する必要がありました。次にJenkinsを移行します。Redmineはどちらのツールにも依存をしていないため最後に移行をしました。以下ではそれぞれのツールの移行手順を紹介します。 GitBucket to GitHub 最初にGitBucketからGitHubへの移行手順を紹介します。 今回移行が必要だったリポジトリは10個ほどだったので無理にスクリプトを作成したりツールを利用せず手作業で移行を行いました。 移行作業では「 Gitリポジトリの中身を、ブランチとタグも含めて別リポジトリにコピーする 」の記事を参考にさせていただきました。 手順は以下の通りです(1〜4は上の記事通りに進めました) GitBucketのリポジトリを手元にcloneする すべてのブランチ・タグを取得 リモートリポジトリにGitHubを追加する GitHubにコンテンツをpush GitBucket上で作成されていたPull RequestをGitHub上で作成 Jenkinsの向き先をGitHubへ変更する 1. GitBucketのリポジトリを手元にcloneする まず手元に移行が必要なGitBucket上のリポジトリをCloneします。 $ git clone GitBucketのリポジトリ 2. すべてのブランチ・タグを取得 次に全てのブランチ・タグを取得します。 $ git branch -r | grep -v " \- > " | grep -v master | while read remote; do git branch --track " ${remote # origin/ } " " $remote " ; done $ git fetch --all $ git pull --all 3. リモートリポジトリにGitHubを追加する リモートリポジトリにGitHubを追加します。 origin の部分はGitBucketと同じものにして上書きしても別にしても作業しやすいものを指定してください。 $ git remote set -url origin GitHubのリポジトリ 4. GitHubにコンテンツをpush GitHubにコンテンツをpushします。 $ git push --all origin $ git push --tags 5. GitBucket上で作成されていたPull RequestをGitHub上で作成 GitBucket上で作成されていたPullリクエストをGitHubで作成します。 こちらも、数が多くなかったので手動で行いました。 6. Jenkinsの向き先をGitHubへ変更する 利用しているJenkinsではGitBucketからコードをPullしてビルドを行っていました。 次の章でJenkinsをCircleCIに移行しますが、移行が完了するまでJenkinsのリポジトリの参照先をGitHubへ変更する必要があります。 以下がその手順です。 GitHub用のデプロイキーを作成 github用のデプロイキーをリポジトリの数だけ発行します。以下では github-repository1 、 github-repository2 リポジトリ例でのデプロイキー作成の例になります。 $ ssh-keygen -l -f github-repository1.pem $ ssh-keygen -l -f github-repository2.pem .ssh/configにHostを追加 ~/.ssh/configの設定を発行したデプロイキーの数だけ追加します。 Host github-repository1 User git Port 22 HostName github.com IdentityFile ~/.ssh/github-repository1.pem TCPKeepAlive yes IdentitiesOnly yes Host github-repository2 User git Port 22 HostName github.com IdentityFile ~/.ssh/github-repository2.pem TCPKeepAlive yes IdentitiesOnly yes GitHubへデプロイキーを配置 GitHubの各リポジトリのSettingsでデプロイキーを登録します。 Jenkinsの設定を変更 最後にJenkinsのプロジェクトの設定から、ソースコード管理の欄を変更します。 Repository URLには先ほど。 .ssh/config で設定したHost名を利用します。またCredentialsにはデプロイキーを発行したユーザーと紐付いたものを利用します。 それぞれの項目の例は以下のようになります。 RepositoryURL: git@github-repository1:user_name/repository_name Credentials: Jenkins Jenkins to CircleCI 次にJenkinsをCircleCIへ移行します。今回Jenkinsで扱っているプロジェクトはすべてMavenで管理されたJavaアプリケーションでした。 そこで、以下の手順で移行を行いました。 Jenkinsでやっていることを確認 JenkinsでやっていることをCicrcleCIで再現 CircleCIによってビルドされたファイルの動作確認 1. Jenkinsでやっていることを確認 まずはじめにJenkinsでやっていることを確認する必要があります。 Jenkinsの各Mavenプロジェクトの「設定」で何をやっているのか把握を行いました。 1. Mavenでやっていることを確認 Jenkinsで扱っていたプロジェクトはすべてMavenプロジェクトのためビルド項目の「ルートPOM」と、「ゴールとオプション」を調べます。これによりJenkinsで実行されるMavenのゴール・オプションがわかります。以下の例では、単純に mvn clean package を行っていることがわかります。 2. JDKのバージョン確認 JavaのバージョンをCircleCI移行時に合わせる必要があるので、ビルド時のJDKのバージョンを調べておく必要があります。以下の例では、「OpenJDK 1.8」が利用されていることがわかりました。 3. ビルド・トリガの確認 どのようなタイミングでビルドが行われるかを確認します。私達のプロジェクトでは以下のように、定期的にビルドを実行するような設定になっていました。 4. 成果物の確認 最後にJenkinsの実行によって生成される成果物を確認します。CircleCI上でも同じように成果物を出力する必要があるため調べておく必要があります。 2. JenkinsでやっていることをCicrcleCIで再現 次にJenkinsでやっている内容をCircleCIで再現します。以下がその例になります。やっている内容はコメントをご参照ください。 version : 2.1 jobs : build-job : # Jenkinsサーバーがcentos7だったため同じOSを利用する docker : - image : centos:7 user : root working_directory : ~/repo # Jenkinsサーバーと同じ環境変数をセット environment : LANG : ja_JP.UTF-8 LANGUAGE : ja_JP:ja TZ : Asia/Tokyo steps : - checkout - run : name : 'Setup' command : | cp /usr/share/zoneinfo/Japan /etc/localtime # OpenJDKとMavenをインストール # localtimeの設定 - run : name : 'Install Dependencies' command : | yum -y install java-1.8.0-openjdk yum -y install maven # 依存ライブラリをローカルにダウンロード # NOTE : mvn dependency:go-offline command has a bug. Refer to https://issues.apache.org/jira/browse/MDEP-516 # - run: # name: 'Install dependency maven' # command: mvn dependency:go-offline # テストを実行 - run : name : 'Test' command : mvn clean test # パッケージング - run : name : 'Package' command : mvn package -Dmaven.test.skip=true # 成果物をCircleCI上の画面からダウンロードできるようにする - run : name : 'Set Artifacts' command : | mkdir /tmp/artifacts cp artifact.jar /tmp/artifacts - store_artifacts : path : /tmp/artifacts workflows : version : 2 build-deploy : jobs : - build-job Jenkinsでは mvn clean package のみを行っていました。しかし、CircleCIでは失敗した場合にどのタイミングで失敗したのかがわかりやすいようにテストとパッケージングを別々にしました。また、依存ライブラリのインストールも別にしたかったのですが問題が生じて断念しました。1つのプロジェクトで複数のMavenプロジェクトに分けている場合 mvn dependency:go-offline で失敗してしまいます。詳細は こちらのissue をご参照ください。 3. CIによってビルドされたファイルの動作確認 JenkinsでビルドしたjarとCircleCIでビルドしたjarを100%同じものにすることはできませんでした。そこで動作確認を開発環境やステージング環境できちんと検証する必要があります。検証ができたものから順次本番のjarをCircleCIでビルドされたものに切り替えて行きます。 Redmine to GitHub issue 最後にRedmineをGitHub issueへ移行する方法を紹介します。手順は以下のようになります。 移行のためのツールを探す RedmineとGithubの設定をする 移行ツールをfetchして自分好みにカスタマイズ redmine2githubを実行 1. 移行のためのツールを探す Redmineのチケットは300ほどあったため、流石にこれを手作業で移行すると日がくれてしまいます。そこで、これに関しては自動化することを検討しました。 今どきRedmineからどこかに移行するというニッチなツールが無く、最初は自分で作ろうと思いました。しかし、redmine2githubという素晴らしいツールのおかげで最小限の力で移行を実現できました。 以下がredmineからGitHubへ移行するためのツールで、Ahmy YulrizkaさんがGitHubで公開しています。利用するタイミングではライセンスが書かれていなかったのですが、事情を説明したところ丁寧にライセンスの付与をしていただくことができました。ちなみにライセンスはMITライセンスです。 https://github.com/yulrizka/redmine2github このツールでできないこと 以下のことはredmine2githubでは実現できません。しかし、今回の要件では作業履歴等が参照できれば良いという程度の要件だったためこのツールを利用することにしました。 ファイルの移行 Redmine上の他のチケットへのリンクの参照をGitHub issueに合わせて変換を行う 2. RedmineとGithubの設定をする redmine2githubを利用するために、Redmine及びGitHubの設定を行う必要があります。 Redmine 管理 > 認証のページから「RESTによるWebサービスを有効にする」 管理画面の承認ページから「RESTによるWebサービスを有効にする」を有効にします。 個人設定のAPIアクセスキーをメモ 個人設定の画面からAPIアクセスキーをメモします。これはredmine2github実行時に指定する必要があります。 csvをダウンロード チケット一覧のページで必要なチケットだけ絞り込んだら、以下の画面右下のCSVを選択しチケットの一覧をダウンロードします。 github GitHub側の設定はissueを作成できる権限のあるユーザーの作成が必要です。 各個人のユーザーでもかいまいませんが、そのユーザーとしてissueが作成されます。 3. 移行ツールをcloneして自分好みにカスタマイズ Redmineを日本語で利用している場合は、以下のようにカスタマイズしないとカテゴリや題名などを取得できません。 https://github.com/katsuyan-stt/redmine2github/pull/2/files#diff-222c979e694bb42a7f8468f67bbaddf0R111 redmine2githubは1ファイルのRubyスクリプトでできており、簡単にカスタマイズが可能です。自分のRedmine環境に合わせてカスタマイズして利用してください。 4. 実行 最後にredmine2github.rbを実行します。 オプションはredmine2githubの README を参照してください。 ruby redmine2github.rb リポジトリのユーザ/組織 リポジトリ名 ダウンロードしたCSVのパス -e 上の3でメモしたAPIキー,RedmineのURL -m -s -v -c 完了 移行したことによる意外なメリット ツールの移行によってEC2の料金や管理コストが減ることなどは考えていましたが、他にも以下のようなメリットがありました。 コードや問題の把握が加速した コードレビューがより活発になった CIツールの移行によってプロジェクトの理解が深まった コード管理やチケット管理を普段使い慣れているGitHubに移行したことにより、コードや問題の把握が加速しました。また、GitHubにコード管理が統一されたことによってコードレビューがより活発になりました。そして一番のメリットは、CIツールの移行によりプロジェクトの理解が深まったことです。CIツールの移行にはプロジェクトのビルドだけでなく、アプリケーションにもある程度詳しくなる必要があります。これにより、プロジェクトの理解を深めることができました。 今後の展望 以上のようにツール移行によりさまざまなメリットがありました。しかし、まだまだ改善の余地があり将来的には以下の実現を考えています。 CircleCIを利用した継続的デプロイメント(CD)の実現 今回CIをJenkinsからCircleCIに移行しましたが、Jenkinsでやっていたことを移行したのみで継続的デプロイメントの実現はできませんでした。もともとCDをしようという動きがないプロジェクトだったため簡単には実現できません。しかし、よりリリースのサイクルを早めユーザーに価値を提供できるようCDの実現を試みています。 まとめ 本記事ではEC2で管理していたツールをSaaSへ移行したことについて紹介しました。RedmineからGitHub issueへの移行など少しニッチな内容も含まれていましたが、少しでもお役に立てれば幸いです。 ツールの改善は直接ユーザーに価値を提供することはできません。しかし、チームや会社全体の作業効率やサービスの品質を高めるなど間接的にユーザーへの価値提供を加速させることができます。弊社では、チームや会社全体の業務の仕組み化を自ら進んで考えることができるエンジニアを募集しています。興味がある方は以下のリンクからぜひご応募ください。 www.wantedly.com
こんにちは。 最近愛猫にトイレの出待ちをされるようになった、品質管理部エンジニアリングチームの高橋です。 品質管理部ではアプリの自動テストを主に担当しております。 本記事はAI(Artificial Intelligence, 人工知能)を活用したテスト自動化の奮闘記となっております。 内容的にはお世辞にも先進的と言えるものではありませんが、是非あたたかい目で見て頂けると幸いです。 AI時代におけるソフトウェアテスティング 言うまでもなく、今やAIは身近な存在となっています。 ソフトウェアテスティング業界も例外ではなく、既にAIを用いたテストツールやサービスが公開されている状況です。そしてその流れは今後更に大きくなり、AIによる自動テストが当たり前の時代がやってくると予想されます。 私達もその波に乗り遅れないよう、自動テストにAIを導入することを目標に掲げ、AIの調査・検討を開始しました。 現在、品質管理部では、モバイルアプリの回帰テストを全て自動で行なっています。 iOS/Androidそれぞれのテストフレームワークを用いて、対象アプリ毎にテストスクリプトを実装し、日々メンテナンスを行なっております。 スクリプト形式の自動化の為、コーディング通りにテストが行われる単純なものではありますが、リリース前の速やかな動作確認&リグレッションテストに一役買っています。 そしてここにAIの力が加われば、伝統的なスクリプト形式のテスト自動化の枠を超えることができるのではと考えました。 画像認識AIの活用 現在のスクリプト形式自動テストの悩ましい点の1つに、「 人間の認知・判断 」が必要な項目は自動化を行うことができないということが挙げられます。 例えば、「キッズ」で絞り込んだ際に、「キッズの画像」が表示されていることを確認するテストです。このような人間にしか判断できない項目はテスターが手動で確認する必要がありました。しかし既にディープラーニングによる画像認識の精度は十分に人間に追いついています。よってまずはAIの得意分野である、画像認識を利用した自動テストを実装することに決定しました。 また現在のスクリプト形式のコードはアプリのUIヒエラルキーを1階層ずつ辿っていくような書き方であり、自然とコードが長くなってしまいがちです。 WEARのUIヒエラルキー 画像認識を用いて「この画像を探してタップする」という書き方で統一できれば、1つ1つのテストコードがよりシンプルになり、メンテナンスがしやすくなる可能性もあります。 WEAR-画像認識 WEAR画面上の投稿コーディネートを認識させる AIのライブラリはTensorFlowを利用してみることにしました。 オープンソース/ライブラリが豊富/知識さえつけば自由度が高いという魅力があるらしく、公式チュートリアルも充実している為、こちらを採用することにしました。 https://www.tensorflow.org/ 本来AIが物体認識を行うには機械学習でモデルを学習させるプロセスが必要になりますが、TensorFlowは学習済みのライブラリを提供しています。WEARアプリは"人間"が写っているコーディネート画像が表示されている為、今回は物体認識の学習済モデルを利用します。 1.TensorFlow実行環境の構築 まずTensorFlow Object Detection APIのインストールが必要ですが、ここでは詳細手順は省略します。下記の手順通りに実行すれば、問題なくインストールできると思います。 https://github.com/tensorflow/models/blob/master/research/object_detection/g3doc/installation.md 次に、学習済のTensorFlow Object Detectionモデルをダウンロードしておく必要があります。今回は下記の物体検出モデルを使用しています。 http://download.tensorflow.org/models/object_detection/ssd_inception_v2_coco_2017_11_17.tar.gz その他の物体検出モデルは、次のURLからダウンロードできます。 https://github.com/tensorflow/models 2.モデルダウンロード、メモリへロード ここからは、実際のテストコードとなります。 WebCameraに写っている物を認識するサンプルコードを参考に致しました。 https://github.com/tensorflow/models/tree/master/research/object_detection # ダウンロード&保存済みのモデルとラベル・マップのパス設定 PATH_TO_CKPT = 'ssd_inception_v2_coco_2017_11_17' + '/frozen_inference_graph.pb' PATH_TO_LABELS = os.path.join( 'data' , 'mscoco_label_map.pbtxt' ) # モデルをメモリへロード / 下記、TensorFlowチュートリアルより detection_graph = tf.Graph() with detection_graph.as_default(): od_graph_def = tf.GraphDef() with tf.gfile.GFile(PATH_TO_CKPT, 'rb' ) as fid: serialized_graph = fid.read() od_graph_def.ParseFromString(serialized_graph) tf.import_graph_def(od_graph_def, name= '' ) 3.ラベル、マップをロード # 下記、TensorFlowチュートリアルより NUM_CLASSES = 90 label_map = label_map_util.load_labelmap(PATH_TO_LABELS) categories = label_map_util.convert_label_map_to_categories( label_map, max_num_classes=NUM_CLASSES, use_display_name= True ) category_index = label_map_util.create_category_index(categories) 4.テスト対象のAndroid WEARアプリを起動 # am startコマンドでMainActivity起動 subprocess.Popen( "adb shell am start -n com.starttoday.android.wear/.main.MainActivity" , shell= True , stdin=subprocess.PIPE, stdout=subprocess.PIPE).wait() 5.画像認識画面取得 # adbコマンドでscreenshot実行 TEST_IMAGE_PATH = <ホストPC保存先+ファイル名> subprocess.Popen( "adb shell screencap -p /sdcard/image.jpg" , stdout=subprocess.PIPE).wait() subprocess.Popen( "adb pull /sdcard/image.jpg TEST_IMAGE_PATH" , stdout=subprocess.PIPE).wait() subprocess.Popen( "adb shell rm /sdcard/image.jpg" , stdout=subprocess.PIPE).wait() 6.物体検出処理開始 # 分析対象画像の配列化 image = Image. open (TEST_IMAGE_PATH) image_np = load_image_into_numpy_array(image) image_np_expanded = np.expand_dims(image_np, axis= 0 ) # 1つの画像に対する物体検出 # run_inference_for_single_image()は、Tensorflow公式Tutorial提供関数 # 画像より検知した物体データがoutput_dictに保存 output_dict = run_inference_for_single_image(image_np, detection_graph) # run_inference_for_single_imageの処理内容は下記の通りである def run_inference_for_single_image (image, graph): with graph.as_default(): with tf.Session() as sess: # Get handles to input and output tensors ops = tf.get_default_graph().get_operations() all_tensor_names = {output.name for op in ops for output in op.outputs} tensor_dict = {} for key in [ 'num_detections' , 'detection_boxes' , 'detection_scores' , 'detection_classes' , 'detection_masks' ]: tensor_name = key + ':0' if tensor_name in all_tensor_names: tensor_dict[key] = tf.get_default_graph().get_tensor_by_name( tensor_name) if 'detection_masks' in tensor_dict: # The following processing is only for single image detection_boxes = tf.squeeze(tensor_dict[ 'detection_boxes' ], [ 0 ]) detection_masks = tf.squeeze(tensor_dict[ 'detection_masks' ], [ 0 ]) # Reframe is required to translate mask from box coordinates to image coordinates and fit the image size. real_num_detection = tf.cast(tensor_dict[ 'num_detections' ][ 0 ], tf.int32) detection_boxes = tf. slice (detection_boxes, [ 0 , 0 ], [real_num_detection, - 1 ]) detection_masks = tf. slice (detection_masks, [ 0 , 0 , 0 ], [real_num_detection, - 1 , - 1 ]) detection_masks_reframed = utils_ops.reframe_box_masks_to_image_masks( detection_masks, detection_boxes, image.shape[ 0 ], image.shape[ 1 ]) detection_masks_reframed = tf.cast( tf.greater(detection_masks_reframed, 0.5 ), tf.uint8) # Follow the convention by adding back the batch dimension tensor_dict[ 'detection_masks' ] = tf.expand_dims( detection_masks_reframed, 0 ) image_tensor = tf.get_default_graph().get_tensor_by_name( 'image_tensor:0' ) # Run inference output_dict = sess.run(tensor_dict, feed_dict={image_tensor: np.expand_dims(image, 0 )}) # all outputs are float32 numpy arrays, so convert types as appropriate output_dict[ 'num_detections' ] = int (output_dict[ 'num_detections' ][ 0 ]) output_dict[ 'detection_classes' ] = output_dict[ 'detection_classes' ][ 0 ].astype(np.uint8) output_dict[ 'detection_boxes' ] = output_dict[ 'detection_boxes' ][ 0 ] output_dict[ 'detection_scores' ] = output_dict[ 'detection_scores' ][ 0 ] if 'detection_masks' in output_dict: output_dict[ 'detection_masks' ] = output_dict[ 'detection_masks' ][ 0 ] return output_dict 7.物体検出結果の解釈 output_dictの出力結果の項目は次のようになっており、検出物体の詳細情報が保存されています。 - class 認識結果 - prediction 検出精度 - boundingbox 検出された座標位置 8.検出結果の判定と画面操作コード実行 # output_dictに保存している検出結果の判定処理を開始 for i in range ( len (output_dict[ 'detection_boxes' ])): # class_name = 検出物体名(クラス名、例:person, car, bag, cellphone,,,) # accuracy = 検出物体の精度(例:"person"クラスである確率がXX%) class_name = category_index[output_dict[ 'detection_classes' ][i]][ 'name' ] accuracy = output_dict[ 'detection_scores' ][i] # 物体検出の精度が70%未満であれば何もしない if accuracy < 0.7 : break # 検出物体クラスが'人(person)'の場合のみ処理実行 if class_name != 'person' : break else : # 分析画像の横縦長さ:adbコマンドで検出物体のxy座標計算時に利用 width = image_np.shape[ 1 ] # Number of columns height = image_np.shape[ 0 ] # number of rows # 該当投稿の座標を指定しタップ実行 # コマンドの例:adb shell input touchscreen tap x座標 y座標 print ( "詳細画面へ移動(クリック)" ) xPosition = int (width * output_dict[ 'detection_boxes' ][i][ 1 ])+(( int (width * output_dict[ 'detection_boxes' ][i][ 3 ])- int (width * output_dict[ 'detection_boxes' ][i][ 1 ]))/ 2 ) yPosition = int (height * output_dict[ 'detection_boxes' ][i][ 0 ])+(( int (height * output_dict[ 'detection_boxes' ][i][ 2 ])- int (height * output_dict[ 'detection_boxes' ][i][ 0 ]))/ 2 ) subprocess.Popen([ 'adb' , 'shell' , 'input' , 'touchscreen' , 'tap' , str (xPosition), str (yPosition)], stdout=subprocess.PIPE).wait() # 投稿をクリック後、正常に画面遷移が行われ、コーデ詳細画面に移動しているかの確認 # WEARアプリのコーディネート詳細画面Activity名:DetailSnapActivity out = check_output([ 'adb' , 'shell' , 'dumpsys' , 'activity' , '|' , 'grep' , 'mResumedActivity' ]) if b 'DetailSnapActivity' in out: print ( "OK! 詳細画面への正常遷移" ) print ( "前の画面へ移動(キーイベントで移動、KEYCODE_BACK=4)" ) subprocess.Popen( "adb shell input keyevent 4" , shell= True , stdin=subprocess.PIPE, stdout=subprocess.PIPE).wait() time.sleep( 2 ) else : print ( "NG! 詳細画面への遷移失敗" ) 実行すると…しっかりコーディネート画像をタップしてくれました! 画像認識 コーディネート画像タップ 今後の展望 今回は学習済モデルを使用した為、予想よりも短い時間で実装までたどり着くことができました(もちろん四苦八苦しましたが)。 このチャレンジによって少しずつTestingにおける画像認識AIの活用性が見えてきた気がします。 今後は以下のような人依存のテストパターンもAIを利用して行ってみたいと考えています。 ・投稿コーディネートが性別と一致しているか ・投稿コーディネートが本当にファッション関連投稿なのか ・投稿コーディネートが該当タグ(帽子、ブランド名等)に一致しているか また将来的には、弊社アプリ向けにモデルをトレーニングしてテストを実行してみたいと思います。 以上が、AI-assistedの第一歩としてTensorFlowの学習済モデルを使用してみた体験談です。 AI門外漢/文系出身の自分としては、機械学習に関する知識が全く足りていないことを痛感しました。理想が実現するのは近い将来ではなさそうですが、日々精進していきたいと思います。 ZOZOテクノロジーズでは、一緒にサービスを作り上げてくれる方を募集中です。 ご興味のある方は、以下のリンクからぜひご応募ください。 www.wantedly.com
こんにちは。iOS担当の荒井です。 今回はiOSチームで構築しているCI/CDのWorkflowについて紹介します。 CI/CD環境 はじめに、ZOZOテクノロジーズのiOSチームがどのようなCI/CD環境を整えているかを簡単に説明します。ZOZOテクノロジーズではZOZOTOWNやWEAR、プライベートブランドZOZOなど様々なサービスを運営しています。プロダクトによりCI/CD環境は違うのですが、多くのプロダクトでBitrise + DeployGateという構成をとっています。今回お話するWorkflowもBitriseを例に紹介していきます。 Bitrise 多くのCIサービスが存在していますが、以下の理由でBitriseを選定しています。 導入時、利用目的に対して価格面が見合っていた Xcodeバージョンアップへの対応速度 日本市場に注力 日本市場に注力していくという話もあり、今後サポートに期待が持てます。 最近では 日本語の記事 も掲載されています。 DeployGate TestFlightやFabricを使っている方も多いと思いますが、弊社のiOSチームはDeployGateを選択しています。元々CI/CDの環境が整っていないプロダクトが多かったため、環境構築を進めるにあたり、会社としてAndroidチームで運用実績があるDeployGateにしました。プロダクトをまたいでテストを行うチームが存在するため、ツールは統一しようといった理由があります。 Workflow それではWorkflowの話に移ります。 WorkflowはStepと呼ばれる定義済みスクリプトをまとめたものです。例えば「Carthageコマンドを実行する」には「Carthage」というStepを設定します。BitriseではGUIで簡単にSetpを追加し目的に応じたWorkflowを組むことができます。今回は以下のようなフローを例に、自動化する際どのようなWorkflowを組んでいるかを紹介します。 エンジニアがGitHubにコードをPush GitHubへのPushをWebhook Workflowの実行 DeployGateへのアップロード Slackへ完了通知 よくある「エンジニアがコードを書いてGitHubにPushしたら、テストが実行され、アーカイブしてQAに配布する。終わったらSlackに通知する」というものです。実運用ではPushのたびに配布をしていたら頻度が高いため、PullRequestの生成タイミングなど、目的に応じてトリガーを設定しています。 上記のようなフローを構築する場合、Bitrise上で以下のようなWorkflowが考えられます。 SSH keyの登録 リポジトリのクローン ビルドキャッシュのダウンロード 証明書のインストール Carthageコマンドの実行 キャッシュの更新 スクリプトの実行 テストの実行 アーカイブ DeployGateへのアップロード Slack通知 しかし弊社ではこれらのStepのほとんどをfastlaneに任せています。 fastlane fastlaneはアプリの様々な作業を簡単に解決してくれるツールです。冒頭のフローを実装するのに必要となる「ビルドに必要な証明書類の管理」「DeployGateへのアップロード」などはすべてfastlaneで行なっています。fastlaneを使用すると以下のようなWorkflowになります。 SSH keyの登録 リポジトリのクローン ビルドキャッシュのダウンロード Carthageコマンドの実行 キャッシュの更新 スクリプトの実行 fastlaneの実行 fastlane以外のStepスクリプトを使用しない理由は、CIサービスの移行を容易にするためです。他のCIサービスに魅力的な機能が出たり、何かしらの理由でCIサービスを変更しなくてはいけない場合でもfastlaneに処理を寄せておけば移行がスムーズになります。Bitrise上ではCode Signingの設定もせず、出来ることは可能な限りfastlaneにしています。サンプルのフローを例にSlackへ通知するまでのfastlaneの実装を紹介します。 証明書のインストール iOSに関わる証明書などのファイルは暗号化されGitHubで管理しています。fastlaneではアクションという形で様々な機能が提供されており、証明書の管理はfastlaneの match というアクションを使って実装しています。今回は詳しく触れませんが、証明書の管理はGitHubにしておくと何かと便利です。まだすべてのプロダクトで使用しているわけではないので現在対応を進めています。 テスト テストには scan というアクションを使用します。 fastlaneの処理はFastfileという設定ファイルに記述して実装していきます。 lane :test do scan( scheme : " SCHEME-NAME " , clean : true , skip_slack : false ) end FastfileはRubyで記述していくことになりますが、ドキュメントも充実しており、Rubyに馴染みのないiOSエンジニアでも記述に困らないと思います。 Swiftでの記述 もサポートがあるため、こちらを試しても良いかもしれません。 アーカイブ アプリケーションのビルドサンプルです。ビルドには gym というアクションを使用しています。 private_lane :build do match # 証明書のダウンロード gym( scheme : " SCHEME-NAME " , configuration : " InHouse " , clean : true , export_method : " enterprise " ) end DeployGateへのアップロード DeployGateへのアップロードもfastlaneです。私たちはDeployGateを使用していますが、TestflightやFabricなどのアクションも揃っています。 private_lane :deploy_gate do | options | target = options[ :release ] ? " release " : " development " deploygate( api_token : ENV [ " DEPLOYGATE_API_TOKEN " ], user : " DEPLOYGATE-USER " , ipa : " ./ #{ target } .ipa " , message : last_git_commit[ :message ] ) end Slack通知 全員が業務でSlackを使用しているため、何かの処理が終わった場合にはSlackで通知するように組んでいます。プロダクトに影響しない処理はサービス間で同じ設定を使うこともあります。 手元のテストでSlack通知が行われないように、CI上のみで通知をすることが多いです。 before_all do ENV [ " SLACK_URL " ] = " WEBHOOKURL " end after_all do | lane | if is_ci? slack( message : " Test Successfully " ) end end error do | lane, exception | if is_ci? slack( message : exception.message, success : false ) end end Bitriseに設定するlane Bitriseに設定するlaneを作成します。 lane :deliver_qa do build deploy_gate( release : false ) end 今回はビルドとデプロイのみにしていますが、 他にもビルドバージョンやGitタグなどもfastlaneで自動化できます。 fastlane運用をしてみての利点 すでにCircleCIからBitriseに移行したサービスがありますが、基本的にfastlaneで書いてあったため移行はとてもスムーズでした。CI上の細かな設定を確認せずとも、fastlaneの設定ファイルであるFastfileを確認すれば処理の流れが把握出来るのも利点だと思います。また、fastlaneによるコードでの記述を行なっていれば、プロダクトをまたいで設定ファイルを簡単に共有できるメリットもあります。CI上でGUIによるWorkflow構築は簡単ですが、コードで記述していくメリットも十分あるように感じています。 まとめ 今回はiOSチームで構築しているWorkflowについて紹介しました。CI/CD環境は随時改善を進めており、どのチームも自動化を目指しています。まだまだ改善の余地があり今後も注力していきたいと思います。CI/CDに興味があり、自動化が好きな方下記よりご応募お待ちしております。 www.wantedly.com
ZOZO研究所の後藤です。本記事ではGoogle Cloud TPUを使った計量学習の高速化の事例を紹介します。 はじめに 深層学習を用いたプロダクトを開発・運用する上で、モデルの学習にかかる膨大な時間はボトルネックの1つです。 ファッションにおける深層学習を用いた画像認識技術にも同じことが言えます。 今回はファッションの分野において定番のタスクであるStreet2shopの課題設定に対し、Google Cloud TPUを用いて計量学習の高速化を試みます。 Street2shopは、スナップ画像から商品部分を切り出す物体検出のパートと、切り出した画像と類似した商品を検索するクロスドメイン画像検索のパートに分けられます。 今回の取り組みでは、後者のパートで利用する画像間の距離を測るためのモデルの学習の高速化を行います。 目次 はじめに 目次 Google Cloud TPUとは タスク Street2shop 計量学習 学習データ 学習 プロファイリング 各オペレーションとメモリ使用量の関係 各オペレーションとTPUリソースの関係 学習結果 計算にかかる料金 モデルの活用 Shop2shop トップス アウター ドレス ボトムス スカート シューズ Street2shop スナップ1 スナップ2 まとめ 最後に Google Cloud TPUとは Tensor Processing Unit(以下、TPU)は、機械学習で頻繁に行われる行列演算を高速に処理することを目的にGoogleが開発したハードウェアです。 CPUやGPUは様々なソフトウェアやアプリケーションに対応するための汎用プロセッサとして設計されている一方で、TPUは汎用性を犠牲にして行列演算に特化した設計がなされています。 そのため、行列演算を多用する機械学習の計算にTPUを用いることによって、モデルの学習や推論にかかる時間を大幅に短縮することが可能になります。 TPUがなぜ高速に演算できるのかを理解するには、公式のブログ記事が詳しいです。 cloudplatform-jp.googleblog.com 速いことは良いことです。 学習速度が速いと、学習が収束したモデルをより短時間で獲得できるようになります。 また、限られた時間の中での試行錯誤の回数を増やすことができるため、ハイパーパラメータチューニングが捗り、最終的に得られるモデルがより良いものになる可能性が高くなります。Cloud TPUはTPUをクラウド上のインスタンスとして利用できるようにしたサービスです。TPUの強力な演算能力を時間単位で誰でも利用できます。 注意点として、TPUは設計上、得意な演算が限られるため、あらゆるモデルを高速化できるわけではありません。 適切な計算環境の選び方に関しては、以下の記事が参考になります。 cloud.google.com 例えば、以下の4つの項目に当てはまる場合、TPUによる高速化が達成可能かもしれません。 ほとんどの計算が行列計算である 学習ループにカスタムTensorFlow operationsが含まれない 学習に数週間から数ヶ月かかる 大きなバッチサイズで学習する 一方で、TPUを使うのに向いていない場合もあります。 頻繁なブランチングや要素ごとの積が多いプログラム スパースなメモリアクセス 高精度演算 カスタムTensorFlow operationを含むネットワーク タスク Street2shop Street2shopはストリートで撮影されたスナップ写真から、商品部分を検出して、ショップの商品と対応づけるタスクです。 ECサイトの商品検索の文脈においてこのタスクを応用すると、言葉では表現しにくい直感的な検索を画像を使って行うことができます。 弊社では過去に、この問題設定を解決するシステムの解説をブログに書いています。 techblog.zozo.com 計量学習 写り方の異なる同一商品の画像や、見た目が似ている商品を検索するには、画像同士の距離や類似度が適切に測れる空間が必要です。画像をそのような空間に埋め込むモデルを獲得するタスクが計量学習です。今回は損失関数にN-pair loss、ベースとなるネットワークにResNet-50、最適化の手法にAdam、空間の次元数は256を採用しています。問題設定の詳細な解説は過去のテックブログを参考にしてください。 techblog.zozo.com 実装のフレームワークにはTensorFlowを用います。 TensorFlowのTPUEstimator APIを利用して、CPU、GPU、TPU全てのチップで動作する汎用的なコードが得られます。 Googleが用意しているリファレンス実装を元に、自前のタスクに関する部分を書き換えることでTPUを使って動作するコードを得られます。 cloud.google.com 学習データ 今回の計量学習のために、商品画像と着用時の画像のペアを120万組ほど用意しました。 ストリートで取られたスナップ画像に対して商品検出器をかけ、商品部分を切り出しておきます。今回対象としたカテゴリはトップス、アウター、ドレス、パンツ、スカート、シューズの6種類で、女性ファッションのみに絞りました。 学習 プロファイリング Cloud TPUによる学習を高速化するためには、速度のボトルネックとなっている部分を特定して、適宜チューニングしていくという作業が必要になります。その際、cloud_tpu_profilerというツールを利用すると、ボトルネックの特定が容易になります。この辺りの作業は、一度Brennan Saetaさんによる実演を見ておくと理解がスムーズかと思います。 www.youtube.com cloud.google.com cloud_tpu_profilerを実行すると、TensorBoardでプロファイリングの結果が閲覧できるようになります。 サマリー画面は以下のようなものです。 この画面の3段目「Recommendation for Next Steps」に、学習のパフォーマンスを上げるためのアドバイスが書かれています。 summary TPUの学習速度に対してデータのインプットが追いついていない場合は、TPUに待ち時間が発生しますし、その逆も考えられます。プロファイラの各項目を深掘りすることで、どこが計算のボトルネックになっているのかを特定できます。 各オペレーションとメモリ使用量の関係 各オペレーションとTPUリソースの関係 今回の学習の場合、リファレンス実装をそのまま利用することにより高いパフォーマンスでCloud TPU動作させることができたため、特別なチューニングは行なっていません。 TPUはフィードフォワードとバックプロパゲーションが高速であり、データの読み込みがボトルネックになることがあります。 例えば、データの読み込み速度と学習速度のバランスをみてバッチサイズを変えるというチューニングが考えられます。 今回は、1バッチあたり1024枚と大きなミニバッチサイズを設定します。 学習結果 TPUにはコアが8つあるため、シングルGPUとの単純な比較は不公平ですが、現状の計算環境とTPUを使った場合の差分をはっきりさせるために敢えて比較します。 以下の表は、同じタスクを5日間ほど回した際の各評価値です。 学習の速度の観点では、P100で学習した場合、1秒あたり20組の画像を学習するのに対し、TPU v2では1100枚の画像を学習できました。 Cloud TPUを使うことにより、これまでに比べて55倍の学習の高速化が達成できたと言えるでしょう。 精度の観点では、同じ期間内にイテレーションをより多く回せたため、TPUで学習したモデルのAccuracyがGPUで学習したモデルのものより高い値になっています。 Unit Batch Size Top1 Accuracy (batch size 64) Top5 Accuracy (batch size 64) Train Loss Val Loss Elapsed Time / 1000 step N image / sec Nvidia P100 64 0.6511 0.9085 1.784 1.355 ~ 54 min ~ 19.75 image Cloud TPU v2 1024 0.8070 0.9573 1.155 0.7812 ~ 15.5 min ~ 1100 image 計算にかかる料金 2019年1月31日の時点でCloud TPU v2の単位時間あたりの料金は$4.50 USDです。 cloud.google.com 一方で、NVIDIA P100のGPUの料金は単位時間あたり1GPUあたり$1.46 USDです。 cloud.google.com 具体的な料金はCPUの数やメモリ、ディスクの容量にもよりますが、料金はせいぜい数倍の差です。その一方で、55倍の速さで学習ができたと考えると、Cloud TPUを使った方が最終的にかかる料金が低く抑えられるかもしれません。 モデルの活用 今回学習したモデルはインプットとして、「スナップ画像から商品部分を切り出した画像」と「商品画像」の両方を受け付け、256次元の数値をアウトプットします。 Shop2shopという単なる類似画像検索と、Street2shopという問題設定の両方を定性的に評価してみます。 評価時の入力と検索対象には、学習には用いていないバリデーション用データセットを利用しています。 Shop2shop 左側の赤枠で囲んだ画像を空間に埋め込み、ユークリッド距離が近い順に商品を選び出すという検索を行なっています。商品画像同士の類似度を直接学習したわけではありませんが、多くの場合で、形、色、柄、素材の観点で似ている商品が検索できています。以下、カテゴリ別の検索結果になります。 トップス 色、形はもちろんのこと、中央に写っているキャラクターも一致している ネック部分や袖の形の一致、花の刺繍部分の特徴も捉えることができている shop2shop_tops アウター 丈、色、カテゴリーが近いものを検索できている スカジャンの入力画像のポーズが独特だが、正面を向いたものが検索できている shop2shop_outer ドレス 丈、色、形、ネックの一致が見られる シースルーの素材感や柄の特徴も捉えられている shop2shop_dress ボトムス 色、形、柄の一致が見られる 例には載せていないが、ダメージジーンズのダメージ具合やダメージ位置の特徴の一致も検索できる shop2shop_bottoms スカート 丈、素材の一致が見られる デニムスカートのボタンの位置の特徴を押さえることが出来ている shop2shop_skirt シューズ 色、形、丈、素材感の一致が見られる 検索結果に着用画像と商品単体画像が混じり、その両方をある程度公平に評価できていることがわかる shop2shop_shoes Street2shop ストリートの環境で撮影された画像の商品部分を切り出して入力に使うタスクです。 自然光のもとで撮られたり、影が発生したりと複雑なシチュエーションが多くShop2shopよりも難しいタスクです。 スナップ1 トップスの文字部分の特徴を捉えている フォントの形も似ているものが選ばれている パンツは形が異なる商品も含まれる一方で、色、幅、丈が一致する商品も含まれる street2shop_1 スナップ2 ロゴが含まれたTシャツを検索することができる スカートのボタンのポイントは押さえているが、丈や形の面で間違えるパターンがある シューズはつま先とヒール部分を押さえられている street2shop_2 まとめ Cloud TPUを使うことで今回の計量学習に関しては、単体のGPU(Tesla P100)を使って学習した場合に比べて、55倍の学習速度の改善ができました。 55倍の速度改善は、1週間かかっていたモデルの学習が3時間で終わることを意味します。 Cloud TPUによる高速化が可能かどうかの見極めは必要になりますが、時間短縮のためにTensorFlow + Cloud TPUを使うことは一考に値するでしょう。 最後に 弊社では次々に登場する新しい技術を使いこなし、プロダクトの品質を改善できる機械学習エンジニアを募集しています。 ご興味のある方は、以下のリンクからぜひご応募ください。 www.wantedly.com
こんにちは! ZOZOテクノロジーズ開発部の塩崎です。 この記事ではCloudFormationにDBのマスタパスワードなどの秘密情報を渡す3つの方法を説明いたします。 前提 我々のチームではAWSインフラリソースのプロビジョニングにCloudFormationを使用しています。 CloudFormationのテンプレートファイルはGitHubでバージョン管理されており、スタックに対するチェンジセットの作成をCircleCIから行っています。 このあたりの詳細は以下の記事に書かれているため、詳細はそちらをごらんください。 techblog.zozo.com 課題 このような方法でCloudFormationテンプレートを管理していましたが、それに伴う課題が生まれました。 DBのマスタパスワードなどの情報をどのようにして渡すかということです。 テンプレート内で使用するためのパラメーターは以下のような形式でテンプレートの中に埋め込み・参照できます。 Parameters : VPCCidrBlock : # パラメーターの定義 Type : String Default : '10.0.0.0/16' Resources : EC2VPC : # VPCを作成 Type : 'AWS::EC2::VPC' Properties : CidrBlock : !Ref VPCCidrBlock # こんな感じで参照。他の箇所からも!Refで参照できる。 しかし、この方法をそのまま使うと秘密情報をテンプレートにそのまま埋め込む必要が出てしまいます。 ちょうど以下のテンプレートのような形になります。 Parameters : RDSMasterUserPassword Type : String Default : 'Very_$ecret_Data' Resources : RDSDBCluster : Type : 'AWS::RDS::DBCluster' Properties : MasterUserPassword : !Ref RDSMasterUserPassword これではGitHubにアクセスできる人から秘密情報が丸見えです。 可能ならばこれらの情報はGitHubにコミットしたくありません。 さらにCircleCIなどの外部SaaSなどに秘密情報(もしくはどこかから秘密情報を取り出すことができる権限情報)を渡すことも避けたいです。 一方でCloudFormation側の事情を考えると、CloudFormationは何らかの方法で平文の秘密情報を知っておく必要があります。 CloudFormationが内部的にAWSのAPIを呼び出してリソースを作成するときにはこの情報が必要なためです。 これらの要件をまとめると、以下の図で示すようなCloudFormationに秘密情報を渡す「何らかの仕組み」が必要です。 解決策 上の図の「何らかの方法」に対応する解決策を3つ紹介いたします。 UsePreviousValue 最初に紹介するのは、CloudFormationにパラメーターを渡す時にUsePreviousValueをTrueにする方法です。 この方法で以前テンプレートに渡した値を引き継げます。 ですので、そのパラメーターを2回目以降使う時はテンプレートの中に値を埋め込む必要がなくなります。 Parameters : RDSMasterUserPassword Type : String Default : '' # ここは空でOK Resources : RDSDBCluster : Type : 'AWS::RDS::DBCluster' Properties : MasterUserPassword : !Ref RDSMasterUserPassword チェンジセットを作成する際には以下のようなJSONを作成し、awsコマンドを叩くことで以前の値を再利用することが出来ます。 [ { " ParameterKey ": " RDSMasterUserPassword ", " UsePreviousValue ": true } ] aws cloudformation create-change-set --stack-name =< stack name > --parameters = parameters.json 参考: https://docs.aws.amazon.com/cli/latest/reference/cloudformation/create-change-set.html カンの良い方ならば既にお気づきかもしれませんが、この方法には最初にパラメーターをセットするときにはどうするのかという問題があります。 最初にパラメーターをセットするためにはCloudFormationのテンプレートをAWSマネジメントコンソールもしくは手元のターミナルから反映する必要があります。 せっかくCircleCIを使ったCI/CDを構築しているのに、この部分だけが手動反映なのは残念な気持ちになります。 また、parameters.jsonを作成する必要があるのも少々面倒です。 Systems ManagerのSecureStringを使用する 次に紹介する方法はSystems ManagerのSecureStringを使用する方法です。 Systems ManagerはEC2インスタンスやオンプレのインスタンスを管理するためのものです。 その中に文字列を暗号化して保存するためのストレージがあるので、それを活用します。 Systems Managerに保存した文字列はDynamic Referenceという方法でCloudFormationから参照できます。 Resources : RDSDBCluster : Type : 'AWS::RDS::DBCluster' Properties : MasterUserPassword : '{{resolve:ssm-secure:rds-master-user-password:1}}' 最終行がDynamic Referenceでパラメーターを参照している部分です。 Dynamic Referenceの書式は以下に示すように : で区切られた4つのセクションに分かれています。 {{resolve:ssm-secure:parameter-name:version}} 前半の2つはSystems ManagerのSecureStringを使うことを指定しています。 後半の2つで保存されたパラメーターのキーとバージョンを指定しています。 最新のバージョンを指定するということはできず、必ず数字でバージョンを指定する必要があります。 参考: https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/dynamic-references.html#dynamic-references-ssm-secure-strings パラメーターをセットするためには以下のようにawsコマンドを叩くか、もしくはAWSマネジメントコンソールで行います。 aws ssm put-parameter --name rds-master-user-password --value ' Very-$ecret-Data ' --type SecureString Systems Managerがこの値を保存するときにはKey Management System(KMS)を用いた暗号化がなされます。 暗号化に使う鍵はデフォルトキーだけでなく、 --key-id パラメーターを使ってユーザーキーを指定することも可能です。 この方法を使うことでCloudFormationにパラメーターを渡すことが出来ますが、注意点もあります。 適用できるリソースの種類に制限があるということです。 現時点ではIAMユーザーのパスワードやRDSのマスタパスワードなどの11種類のリソースに対してのみこの方法を適用できます。 そのため、任意の箇所へ秘密情報を埋め込むということは出来ません。 たとえば、ECSで動いているアプリケーションに対して環境変数を通して秘密情報を渡すことを考えると以下のようなテンプレートになるかと思います。 AWS::ECS::TaskDefinitionリソースはSecureStringに対応していないため、SSM SecureStringを使った方法は適用できません。 Resources : ECSTaskDefinitionApplication : Type : 'AWS::ECS::TaskDefinition' Properties : ContainerDefinitions : Environment : - Name : 'DB_PASSWORD' Value : 'Very-$ecret-Data' # ここには{{resolve:ssm-secure:rds-master-user-password:1}}と書けない Secrets Manager 最後に紹介する方法はSecrets Managerを使って秘密情報を渡す方法です。 この方法はSystem ManagerのSecureStringを使用する方法に似ていますが、少々異なる面もあります。 Secrets Managerを使った方法もDynamic Referenceを使用してパラメーターの参照を行うため、書式がかなり似ています。 Resources : RDSDBCluster : Type : 'AWS::RDS::DBCluster' Properties : MasterUserPassword : '{{resolve:secretsmanager:rds:SecretString:password}}' こちらの方法のDynamic Referenceの書式は以下に示すように : で区切られた5つのセクションに分かれています。 secret-id と json-key の2つを指定して秘密情報の取り出しを行います。 Secrets Managerのそれは連想配列型であるため、これら2つの情報を使って秘密情報を指定します。 {{resolve:secretsmanager:secret-id:SecretString:json-key}} 秘密情報をセットするためには以下のコマンドを使用します。 この例ではRDSのパスワードだけを設定していますが、RDSのユーザーやホスト名などの関連する情報を設定することも出来ます。 また、Secrets ManagerでもKMSのユーザーキーを用いた暗号化を行うことが出来ます。 aws secretsmanager put-secret-value --secret-id rds --secret-string ' {"password":"Very-$seret-Data"} ' Systems Managetとは異なり、Secrets Managerを使ったDynamic Referenceはどのリソースに対しても使用することが出来ます。 このような利便性がある一方で、うっかりと秘密情報を公開してしまう危険性も同時に持っているため注意が必要です。 Resources : ECSTaskDefinitionApplication : Type : 'AWS::ECS::TaskDefinition' Properties : ContainerDefinitions : Environment : - Name : 'DB_PASSWORD' Value : '{{resolve:secretsmanager:rds:SecretString:password}}' 参考: https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/dynamic-references.html#dynamic-references-secretsmanager 3つの手法の比較 上で紹介した3つの方法の比較です。 UsePreviousValue SSM Secure String Secrets Manager CloudFormationのチェンジセット作成を自動で行えるか X O O パラメーターが暗号化されて保存されるか X O O KMSのユーザーキーを利用できるか X O O 任意のCloudFormationリソースに対して使用できるか O X O 他のAWSサービスとのインテグレート X O O この表を見てみると、Secrets Managerを使うのが現状のベストだと思います。 他のサービスとのインテグレートについて 今回紹介した方法はCloudFormationとSSM、CloudFormationとSecrets Managerという組み合わせでの使い勝手を見るという観点でした。 一方でSSMやSecrets ManagerはCloudFormation以外のサービスと組み合わせて使うことも出来ます。 その観点から見ると、現時点ではSSMのほうが他のサービスとのインテグレートへの対応が早いかと思います。 例えばElastic Container Service(ECS)とのインテグレートを考えます。 ECSでタスクを起動する時に秘密情報を環境変数にセットしてから起動したいとします。 2019年2月現在ではこのようなことが出来るのはSSM SecureStringのみです。 Secrets Managerの方が後発のサービスであることを考えると致し方ない気もしますが、SecretsManagerのこれからに期待したいです。 2/5追記 いつの間にかSecrets ManagerからECSに秘密情報が渡せるようになっていました。 気づいたらどんどん便利になっていくaws!! Secrets Managerの方が後発のサービスなので対応時期に少々の差(2018年11月と2019年1月)が出てしまっていますが、将来的にはこの差が縮まっていくことを期待しています。 よいまとめありがとうございます! 実は1月末の時点で ECS から Secrets Manager の値取れるようになってるので、ブログ記事にも反映して欲しいです〜 — ポジティブな Tori (@toricls) 2019年2月5日 参考: https://aws.amazon.com/jp/about-aws/whats-new/2018/11/aws-launches-secrets-support-for-amazon-elastic-container-servic/ https://docs.aws.amazon.com/AmazonECS/latest/developerguide/specifying-sensitive-data.html#secrets-create-secret まとめ CloudFormationに秘密情報を渡す3つの方法をお伝えしました。 現時点ではSecrets Managerを使うのがベストだと思います。 我々Marketing AutomationチームではAWSのインフラ構成をCloudFormationで管理することによって、Infrastructure as Codeを実現しています。 最近ではGCPのインフラも増えてきたことからTerraformの使用も視野に入れて日々の開発運用を行っております。 ZOZOテクノロジーズでは、一緒にサービスを作り上げてくれる方を募集中です。 ご興味のある方は、以下のリンクからぜひご応募ください。 www.wantedly.com
前書き こんにちは、スマートファクトリー向け制御ソフトウェア開発チームの高石( @ksk_taka )です。 本記事では、アパレル業界や製造業界など、CADを取り扱う業界で広く使われているdxfファイルを 一括で画像ファイルに変換する 手法について記載します。 dxfファイルとは そもそも dxfファイルとは何ぞや? という方のために簡単に説明をします。 dxfファイルは CAD間を仲介する中間ファイル として使うことを目的としたファイルです。 例えば機械設計をする際に3D-CADが利用されますが、よく使われる3D-CADソフトとして、以下のものがあります。 CATIA SolidWorks Creo Parametric これらのそれぞれのソフトで作られる図面データは 別々のファイル形式(拡張子) を持っており、基本的に互換性がありません。 アパレル業界で用いられるCADソフトも同様で、以下の様な異なるCADソフトにて生成される図面データには互換性がありません。 クレアコンポ AGMS このままでは一方のCADソフトで生成した図面データを、別のCADソフトを持った人が開こうとすると「開かない!」という状況になってしまいます。 そんなの困る! ということで、必要となるのが 中間ファイル という存在です。 dxfファイルは恐らく 現在最も広く使われている であろう中間ファイルで、 使用するCADソフトに関わらず開くことができる図面データ になります。 dxfファイルを用いることで、異なるCADソフトを利用している人同士でも、図形データのやり取りが可能になります。 ※但し、「各社独自のCADオブジェクト」などはdxfでは再現できません。やり取りの際には注意が必要です。 dxfファイルを画像化する目的 上記のように便利なdxfファイルですが、万能なわけではありません。 例えば以下のような状況では、もっと汎用的なデータ形式の方が望ましいと言えます。 CADソフトを持たない人に図面データを渡したい場合 Web上や社内のドキュメントファイルなどに図面(絵柄)を貼付したい場合 そのような状況に対応するため、dxfファイルに含まれる図面データを一括で画像化するソフトをGo言語で実装しました。 dxfファイルの構造 dxfファイルはテキスト形式のファイルです。その為、テキストエディタで簡単に内容を確認できます。 ファイル内のテキストは2行で1組となっており、1行目は グループコード 、2行目はグループコードに応じて 文字列 、 数値 などが入ります。 dxfファイルの例を以下に記載します。 0 SECTION //SECTION開始 2 BLOCKS //BLOCKS SECTION開始 0 BLOCK //1個目のBLOCKの開始 8 1 //1個目のBLOCKの階層 2 1_BlockName //1個目のBLOCKの名前 10 0 //1個目のBLOCKのX座標 20 0 //1個目のBLOCKのY座標 0 POLYLINE //1個目のBLOCK内の初めのENTITYデータ(POLYLINE:連続した頂点で描画される図形) ・ ・ ・ 0 LINE //1個目のBLOCK内の2個目のENTITYデータ(LINE:2点を結ぶ線分で描画される図形) ・ ・ ・ 0 ENDBLK //1個目のBLOCK項目の終了 0 BLOCK //2個目のBLOCK項目の開始 ・ ・ ・ 0 ENDBLK //n個目のBLOCK項目の終了 0 ENDSEC //BLOCKS SECTION終了 0 EOF //FILEの終了 今回はひとまず、使用頻度の高い POLYLINE と LINE の2つの図形を描画する機能を実装します。 実装方法 ここからは具体的な実装について記載していきます。 今回、ソフトウェアを実装する上で肝となるのは以下の機能です。 dxfファイルを1行ずつ読み取る機能 読み取ったデータを構造体に格納する機能 構造体の格納データに応じて画像を描画する機能 1つずつ見ていきましょう。 事前準備 まず、事前準備から。 目標の機能を実装するにあたり、Go言語のパッケージは以下のものを使用します。 import ( "bufio" "image" "io/ioutil" "os" "path/filepath" "regexp" "runtime" "strconv" "sync" "time" "github.com/llgcode/draw2d/draw2dimg" "image/color" "fmt" "golang.org/x/text/encoding/japanese" "golang.org/x/text/transform" ) 更に、読み取ったdxfファイルから各要素を格納していくための構造体を準備しておきましょう。 // Section is a top level group. type Section struct { Blocks []Block Entities []Entity Headers [][] string Tables [][] string } // Block is a second level group in Section. type Block struct { Name string LayerName string BlockType string X float64 Y float64 Entity []Entity } // Entity is a third level group in Section or Entities. type Entity struct { TYPE string Name string FULL [][] string } dxfファイルを1行ずつ読み取る まずはdxfファイルを1行ずつ読み取り、Sliceデータとして返す関数を実装します。 func getFileStream(inputpath string , fileName string ) (data [][][] string ) { var row [][] string var scangroup [][][] string input, err := os.Open(filepath.Join(inputpath, fileName)) if err != nil { // Openエラー処理 panic (err) } defer input.Close() scangroup = nil //dxfファイル ロード開始 sc := bufio.NewScanner(transform.NewReader(input, japanese.ShiftJIS.NewDecoder())) for i := 0 ; sc.Scan(); i++ { if err := sc.Err(); err != nil { // エラー処理 break } if i != 0 && sc.Text() == " 0" { scangroup = append (scangroup, row) //区切り文字で塊を作る row = [][] string {} //塊を作ったら初期化 } //2行で1要素分なので判別しやすいようにまとめてSlice化 gkey := sc.Text() sc.Scan() gvalue := sc.Text() row = append (row, [] string {gkey, gvalue}) } scangroup = append (scangroup, row) row = [][] string {} return scangroup } ファイルパス と ファイル名 の文字列が与えられると、該当ファイルを1行ずつ読み込む関数です。 上でお伝えした通り、dxfファイルは2行で1組となっているため、 1組分をまとめてSlice にしています。 データを構造体に格納する 続いて、読み取ったデータを各種構造体に格納する処理を実装していきましょう。 func makeSection(data [][][] string ) (sec Section) { var isBlocks bool var isEntities bool var blk Block var ent Entity var copyData [][][] string var copyGroup [][] string var copyRows [] string for _, g := range data { for _, r := range g { copyRows = make ([] string , 2 ) copy (copyRows, r) copyGroup = append (copyGroup, copyRows) } copyData = append (copyData, copyGroup) copyGroup = [][] string {} } sec = Section{} for _, group := range copyData { if group[ 0 ][ 0 ] == " 0" { switch group[ 0 ][ 1 ] { case "SECTION" : //SECTIONの処理 for _, rows := range group { if rows[ 0 ] == " 2" { switch rows[ 1 ] { case "BLOCKS" : isBlocks = true isEntities = false case "ENTITIES" : isBlocks = false isEntities = true case "HEADER" : isBlocks = false isEntities = false sec.Headers = group case "TABLES" : isBlocks = false isEntities = false sec.Tables = group default : } } } case "VERTEX" , "LINE" , "POLYLINE" , "SEQEND" , "TEXT" : //図形データ ent = Entity{} ent.TYPE = group[ 0 ][ 1 ] ent.FULL = group if isBlocks { blk.Entity = append (blk.Entity, ent) } else if isEntities { sec.Entities = append (sec.Entities, ent) } case "BLOCK" : //画像は1BLOCKにつき1枚 blk = Block{} //BLOCKの処理 for _, rows := range group { switch rows[ 0 ] { case " 8" : blk.LayerName = rows[ 1 ] case " 2" : blk.Name = rows[ 1 ] //file名に使用 case " 70" : blk.BlockType = rows[ 1 ] case " 10" : blk.X, _ = strconv.ParseFloat(rows[ 1 ], 64 ) case " 20" : blk.Y, _ = strconv.ParseFloat(rows[ 1 ], 64 ) default : } } case "ENDBLK" : //BLOCKの終わりを示す sec.Blocks = append (sec.Blocks, blk) case "EOF" : //fileの終わりを示す default : } } } return sec } 先ほど作成したデータを入力として、各構造体にデータを格納していくコードです。 最終的に全てのSECTIONをまとめたものがSliceで返されます。 画像化する 続いて、構造体に格納されたデータを用いて画像を描画する機能を実装します。 func exportPNGperBlk(sec Section, dir string ) { const PrefixSEQEND = - 1 var vertexX, vertexY float64 var lstartX, lendX, lstartY, lendY float64 var maxX, maxY, minX, minY float64 var exportFlag bool var vertexs [][] float64 var lines [][] float64 var name string var i int exportFlag = false for _, bl := range sec.Blocks { //file名にはディレクトリ名とブロック名を使用する name = dir + "-" + bl.Name exportFlag = true for _, en := range bl.Entity { if en.TYPE == "SEQEND" { //線の切れ目 vertexs = append (vertexs, [] float64 {PrefixSEQEND, PrefixSEQEND}) continue } if en.TYPE == "VERTEX" { //POLYLINEで頂点を繋ぐ線分を描画する際に使用 for _, rows := range en.FULL { switch rows[ 0 ] { case " 10" : //頂点のX座標 vertexX, _ = strconv.ParseFloat(rows[ 1 ], 64 ) if vertexX > maxX { maxX = vertexX } if vertexX < minX { maxX = vertexX } case " 20" : //頂点のY座標 vertexY, _ = strconv.ParseFloat(rows[ 1 ], 64 ) if vertexY > maxY { maxY = vertexY } if vertexY < minY { minY = vertexY } } } //"vertexs"に、頂点のXY座標を格納 vertexs = append (vertexs, [] float64 {vertexX, vertexY}) } if en.TYPE == "LINE" { //LINEで線分描画する際に使用 for _, rows := range en.FULL { switch rows[ 0 ] { case " 10" : //開始地点のX座標 lstartX, _ = strconv.ParseFloat(rows[ 1 ], 64 ) if lstartX > maxX { maxX = lstartX } if lstartX < minX { minX = lstartX } case " 11" : //開始地点のY座標 lendX, _ = strconv.ParseFloat(rows[ 1 ], 64 ) if lendX > maxX { maxX = lendX } if lendX < minX { minX = lendX } case " 20" : //終了地点のX座標 lstartY, _ = strconv.ParseFloat(rows[ 1 ], 64 ) if lstartY > maxY { maxY = lstartY } if lstartY < minY { minY = lstartY } case " 21" : //終了地点のY座標 lendY, _ = strconv.ParseFloat(rows[ 1 ], 64 ) if lendY > maxY { maxY = lendY } if lendY < minY { minY = lendY } } } //"lines"に、開始-終了地点のXY座標を格納 lines = append (lines, [] float64 {lstartX, lstartY, lendX, lendY}) } } if exportFlag { //画像サイズがPartsごとに異なるので毎回準備する img := image.NewRGBA(image.Rect( int (minX), int (minY), int (maxX+ 1 ), int (maxY+ 1 ))) gc := draw2dimg.NewGraphicContext(img) gc.SetFillColor(color.White) gc.SetStrokeColor(color.RGBA{ 0 , 0 , 255 , 255 }) gc.Fill() //POLYLINE描画 for _, vertex := range vertexs {} i++ fmt.Println( len (vertexs), vertex, i) if vertex[ 0 ] == PrefixSEQEND && vertex[ 1 ] == PrefixSEQEND { if i < len (vertexs) { //線の切れ目では描画をせず座標移動だけ実施 gc.MoveTo(vertexs[i][ 0 ], maxY-vertexs[i][ 1 ]) } continue } //線を描画。画像とdxfでY座標の方向が反転する点に注意 gc.LineTo(vertex[ 0 ], maxY-vertex[ 1 ]) } //LINE描画 for _, line := range lines { gc.MoveTo(line[ 0 ], maxY-line[ 1 ]) gc.LineTo(line[ 2 ], maxY-line[ 3 ]) } gc.Stroke() gc.Close() //出力フォルダを作成する outputpath, err := filepath.Abs(filepath.Join( "." , "file" , "output" , dir)) if err != nil { panic (err) } os.Mkdir(outputpath, 0777 ) //出力フォルダを作成する outputpath, err = filepath.Abs(filepath.Join( "." , "file" , "output" , dir, "png" )) if err != nil { panic (err) } os.Mkdir(outputpath, 0777 ) //png画像として保存 draw2dimg.SaveToPngFile(filepath.Join(outputpath, name+ ".png" ), img) //各変数を初期化 exportFlag = false vertexs = [][] float64 {} lines = [][] float64 {} maxX = 0 maxY = 0 minX = 0 minY = 0 i = 0 } else { fmt.Println( "画像化対象のパーツが見つかりません。" , "パーツ名[" , name, "]" ) } } } 最後に、main関数を実装します。 func main() { // 正規表現を使って対象ファイルを設定 rep := regexp.MustCompile( "[A-Z]*[a-z]*[0-9]*.dxf" ) // Inputディレクトリがあるかどうか確認 inputparentpath, err := filepath.Abs( "./file/input/" ) if err != nil { panic (err) } dirs, err := ioutil.ReadDir(inputparentpath) if err != nil { panic (err) } var wg sync.WaitGroup cpus := runtime.NumCPU() // CPUの数 limit := make ( chan struct {}, cpus) // Inputディレクトリ配下の全ファイルを読み込み for _, dir := range dirs { dirName := dir.Name() if dir.IsDir() != true { continue } inputpath, err := filepath.Abs(filepath.Join(inputparentpath, dirName)) if err != nil { panic (err) } files, err := ioutil.ReadDir(inputpath) if err != nil { panic (err) } // 各ディレクトリ配下の全ファイルを読み込み for i, file := range files { fileName := file.Name() // dxfファイルが見つからない場合 if !rep.MatchString(fileName) { continue } if file.IsDir() { // ディレクトリはスキップ continue } // ファイル読み込み〜出力までは並列処理で実行 wg.Add( 1 ) go func (i int ) { defer wg.Done() limit <- struct {}{} fmt.Println( "処理開始" , "[" , i, "]" , dirName, fileName) //dxfファイル読み取り scangroup := getFileStream(inputpath, fileName) //データを構造体に格納 sec := makeSection(scangroup) //画像ファイル生成 exportPNGperBlk(sec, dirName) fmt.Println( "処理済" , "[" , i, "]" , dirName, fileName) <-limit }(i) wg.Wait() } } // 全ての処理が完了するまで待機 wg.Wait() fmt.Println( "全ての処理が完了しました。キーを押すと終了します。" ) fmt.Scanln() } 実行結果 実際に、冒頭で紹介したdxfファイル(デニムパターン)に対して本機能を適用してみました。 実行した結果出力された画像は以下の通り。 画像1: デニム左前身頃 画像2: デニム右前身頃 画像3: デニム左後身頃 画像4: デニム右後身頃 画像5: デニム後ポケット(左) その他、デニムパーツ多数出力(数が多いので省略) 想定通り、複数の図形データを持つdxfファイルを、画像に一括で変換できていることが確認できます。 最後に 今回は「POLYLINE」「LINE」という2つの図形に絞って画像化する機能を実装しました。 恐らく他にもdxfファイルに利用されている図形はあると思いますので、気になる方は是非続きを実装してみて下さい。 ZOZOテクノロジーズでは、一緒にサービスを作り上げてくれる方を募集中です。 ご興味のある方は、以下のリンクからぜひご応募ください。一緒に楽しく仕事しましょう! www.wantedly.com
こんにちは!ZOZOテクノロジーズ開発部の池田( @yuji_ikepon )です。 本記事では、 ケアラベルデザイン発行を自動化した際に使用したパッケージの紹介と、プロトタイプ開発までのプロセスを紹介したいと思います。 ケアラベルとは ケアラベルとは上記の様な、繊維製品になくてはならない品質表示のことを指します。ケアラベルは、品質表示法の下に適切で明確な表示が義務づけられています。 プロトタイプ製作までの経緯 ケアラベルのデザインを行なっていた担当者から、「ケアラベルを自動的に発行できないだろうか」という相談が来ました。 担当者から聞いた仕様は以下の通りです。 エクセルのデータを元にアウトプット出来る事。 指定したフォントが使用できる事。 プログラミング未経験でも使える事。 Macだけでなく、Windowsでも動く事。 開発環境によらず実行できる事。 これらの条件を踏まえ、PDFでのデザイン出力を軸にプロトタイプを作成することにしました。 また、実行環境の事を考え、言語はGo言語を使用します。 使ったパッケージ、ライブラリ プロトタイプを実装した中で使った、Go言語のライブラリとソースコードの一例を以下に示します。 xlsx : https://github.com/tealeg/xlsx エクセルの任意のシートを読み込む // xlsx読み込み excel, err1 := xlsx.OpenFile(file) // xlsxが読み込めなかった場合 if err1 != nil { log.Printf("error: not open xlsx file.") return false } else { log.Printf("debug: success open xlsx file.") } var sheet *xlsx.Sheet = nil // シート名で該当のシートを検索 for k, _ := range excel.Sheet { searchSheet := excel.Sheet[k] if searchSheet.Name == "検索したいシート名" { log.Printf("debug: success open xlsx sheet.") sheet = searchSheet break } else { // 何もしない } } // 該当シートが見つからない場合 if sheet == nil { log.Printf("error: failur open sheet.") return false } // エクセルの値を参照 for rowKey, rowValue := range sheet.Rows { // このループではエクセルの1行ごとに処理ができる // 左から0番目のセルを参照できる cell := rowValue.Cells[0].Value } gopdf : https://github.com/signintech/gopdf pdfの作成 pdf := gopdf.GoPdf{} // rect作成 rect := gopdf.Rect{} rect.W = "pdfの幅" rect.H = "pdfの長さ" // gopdf作成 config := gopdf.Config{} config.PageSize = rect pdf.Start(config) fontPath := string("data/Font/" + data.FontPath + ".ttf") pdf.AddTTFFont(data.FontPath, fontPath) pdf.SetFont(data.FontPath, "", 2*13) pdf.AddPage() // 画像書き込み // 画像の場所 inputPath = "画像のパス" // 出力 size := gopdf.Rect{ H:"画像の高さ", W:"画像の幅", } pdf.Image(inputPath, "pdfのX座標", "pdfのY座標", &size) // テキスト書き出し // 座標を設定 pdf.SetX("pdfのX座標") pdf.SetY("pdfのY座標") // 出力 pdf.Cell(nil, "テキスト") 出来たデザイン 下記の様なデザインで出力する事ができました。 ※ 表示されている情報はあくまでテストの為の仮の表記です。 海外向けケアラベル 日本向けケアラベル(表面) 日本向けケアラベル(裏面) プロトタイプのデザイン自体はそれっぽく実装する事ができました。 しかし、使うフォントの種類によっては、表示される位置がずれてしまったり、改行の位置が揃わないといった問題がありました。 次回プロトタイプ改修の際にはこれらの問題点と合わせて、デザイン的な観点からも実装を考えられたらなと思います。 Go言語で書く上でつまずいた点 今回Go言語でプロトタイプを作成していく中で、以下の様な点で苦労しました。 パッケージ間の共有データをどう持つか エクセルからデータをインプット→PDF or PNGでアウトプットを行う際に、パッケージ間でデータの受け渡しが必要でした。 その為、データ共有のパッケージを作成し、そこに構造体を保持する事でデータを参照していました。 しかし、保持したいデータの中に構造体の配列を入れた際に、初期化のタイミングの違いにより、参照が上手くいかない場合があり苦労しました。 ライブラリの情報不足 Go言語のライブラリの中には、基本的な使い方しか表記されていないものが多く、自分でライブラリ内のコードを見ながら探っていくことが多かったです。 アウトプットするまで実装が正解か分からないこともあり、デバックに苦戦を強いられました。 最後に 今回はプロトタイプの作成のフローを紹介させて頂きましたが、この他にも色々な業務を行なっています。 制御系エンジニアに関しての詳しい情報は以下のリンクからご覧になれます。 https://tech.zozo.com/recruit/mid-career/detail29/ ZOZOテクノロジーズでは、一緒にサービスを作り上げてくれる方を募集中です。 ご興味のある方は、以下のリンクからぜひご応募ください。 www.wantedly.com
こんにちは、ZOZOテクノロジーズでプロジェクトマネージャーをしている 新井(@masamasaking) です。 最先端の小売業界のトレンドを体験できる全米最大のリテール向けのイベント「 NRF Big Show 」が今年も1月13日から開催されました。現地視察を通じて感じたことをレポートします。 この記事では気になった出展企業や全体を通して感じたことをご紹介します。 NRF Big Showとは 世界99カ国から機器やソリューションが800以上の出展。37,000人が参加するNRF(全米小売業協会)が行っている小売業界向けのイベントです。展示と並行して6つのステージで3日間セミナーやキーノートスピーチも行われ小売業界の今がここに集まっていきます。 気になった展示 AIがもはや当たり前かのように AIやMLを使ったオムニチャネル化、商品トレンドの分析、需要予測、ダイナミックプライシング、価格算出など顧客体験の向上や業務オペレーション改善だけでなく小売のいたるところにこれらの技術を使われるのは必須でした。オフライン上でのデータを収集およびユーザーごとにデータを蓄積し、足元の店舗の最適化からその先のダイナミックプライシングやディスプレイの見せ方の個別最適化の進んだ世界が近い将来くることが伺える。消費者が何を求めていて、どのタイミングで商品を並べるのか、どういった価格で・どういった情報を訴求するのか・・・・といった課題に対してAIやMLがこれでもかというくらい絡んでくる。これまでバズワードであった「AI」がいよいよ小売の現場に本格化したようです。 ディスプレイのリッチ化・インタラクティブ化 *1 これまで店頭は紙などのPOPが主流であったがデジタル化が進んでいる。情報をただ見せるだけであればオンラインのストアでもできるが、その先をいくインタラクティブな見せ方のソリューションが散見されました。 実際に商品を手に取るとすぐさまディスプレイに連動して、動画が流れ商品を訴求。またスマートミラーの出展もあり、消費者が試着をするたび鏡に商品情報が表示される。それだけにとどまらず近くにある商品で次はこれが似合うと思うのでいかがですか?とレコメンドまでしてくれます。 ディスプレイの表示内容の最適化と分析 *2 インテルが展示していたディスプレイが目を引きました。ただの大きいディスプレイだと思ったがよくみるとカメラが取り付けられている。そのカメラから見てるユーザーのデモグラを推測し、また視線の動きを追尾してどの商品・モデルに興味をもっているかが把握できる。また天候や曜日なども加味しそれらの情報を逐一分析し、商品ごとのスコアリングがされている。店舗側は推すべき商品が把握出来るのに加えて、ユーザーはついつい見てしまうデジタルサイネージができるかもしれない。また画像認識とオンラインの・オフライン合わせたユーザーデータの突合が進めばユーザーごとに見せる商品の出し分けができる未来がくるかもしれないです。 ドローンによる商品確認 店舗でドローンを飛ばして陳列棚を周遊しながら画像解析をして、商品の在庫確認ができる。商品が従来の位置になかったり・正面を向いていなくても認識ができます。それも瞬時に。例えば、高さ2m・長さ5mの商品棚であれば1分も掛からずに集計ができます。小さな店舗であれば不要かもしれないがショッピングモールやスーパーといった大型店舗になればなるほどこれまで人が随時手作業で在庫のチェックしていたものがドローンを活用することであっという間にかつ正確にできる未来がすぐそこまで来ています。 イノベーションラボ/スタートアップゾーン またスタートアップ系の企業・サービスが集まる2018年から新設されたエリアからもいくつか気になったものをご紹介します。 Texel 身体計測をする技術を持っているロシアの企業。3D化だけでなく、手足の長さなどの計測も可能。アプリ版(未リリース)もあり被写体を中心にアプリ(カメラ)を周囲360度を撮影することで計測が可能です。今後実用レベルまで開発が進みより多くの人が自分の正確な身体データをもてたらオンラインでもより洋服やアクセサリーが買いやすくなるので今後の開発・普及が楽しみです。 americhip プロペラを使ったホログラムを表示する技術をもった会社。多少音が気になるが投影物がVRゴーグルを使わなくても目の前に立体的に見えます。複数のプロペラ使えば大きな表示も可能です。 caper Amazon GOのように天井のいたるところにカメラやセンサーを設置し無人レジを実現するのに対して、こちらはスマートカートと呼ばれるカメラつきのカートを用いることでショッピング自動化を模索しています。導入コストが前述のものより低いため差別化できる。またショッピングカートに収集されたショッピングデータを陳列方式の改善や品揃えに活かす。カートに商品を入れずに売り場から立ち去った場合にどうなるか気になるものの違うアプローチでショッピング自動化を目指していて、要チェックです。 まとめ エクスポで目立ったのが「AI/ML」をベースにしたテクノロジーの活用でした。オフラインにある情報を店頭に設置したカメラやセンサーで画像解析等を通じてデータ化。これまでの店舗全体での商品のトレンドや売上の増減ではなく、各顧客の把握にシフトしています。またオムニチャネル化を進めてオンライン・オフラインに関係なくユーザーの情報を溜めて解析をしている。アルゴリズムをもとに算出された提案をもとに商品の価格設定を柔軟に変更したり、店頭での商品説明もPOPからサイネージに切り替わることでよりインタラクティブに、そしてゆくゆくはユーザーごとに最適化した魅せ方に変わっていく。 また商品の在庫確認や物流のロボ化なども進み人の手を介さずに小売ができるようになる「無人化」関連のテクノロジーが多くみられました。これらはオペレーションの効率化といった店舗コストの削減はもちろんのこと、ここでもこれまでオフラインにしかなかったユーザーの購買情報を収集・蓄積するために進化が進んでいるといったほうがただしく流れを読み取れそうです。無人レジにすることで人件費を削減よりかは店舗でのユーザーの行動を追尾ができるようになることでデータを収集・解析しその次に商品需要の予測やレイアウトの検証をしている。小売がテクノロジーを活用して、デジタル化を進めているだけでなく複数のデータソースから予測分析をAIを用いて収益最大化を図っています。 小売業界のデジタル化やテクノロジー化を「オペレーションコスト改善」とだけ見るか、その先にある「AI活用による個別最適化の推進」と見るかで近未来の予測を読み誤ったり、戦略策定を誤ったりするので意識の転換が必要です。 もはや小売業界にもAI活用の流れは不可逆であると感じます。 世界中を飛び回ってテクノロジーを使ったファッション革命を一緒に目指すPM・エンジニアを大募集中です。ご興味のある方は、以下のリンクからぜひご応募ください! www.wantedly.com *1 : シューズを手に取るとそのプロモーションビデオがすぐさま流れる *2 : 黒い柱の中央にある黒い塊がカメラである
どうも品質管理部エンジニアチームの木村です。 最近の話ではないんですがWeb UIテスト自動化をしようとなった時の事を書きます。 まずは初期段階の実行環境についてです、自動テストスクリプトの構築や処理そのものはまた次回。 Seleniumでテストを自動化したい! ZOZOTOWN に限らず最近のサービスはなんでもリリース頻度が高いです。 そして何故なのか、いついかなる時も、開発スケジュールは押し気味になります。 これは業界七不思議の1つです。たぶん。 品質管理部としてのテストは開発スケジュールの一番最後に置かれます。 つまり…短期決戦必須となります…。 そんなよくある話からSeleniumを使ってWeb UIテストを自動にしたいという流れになりました。 リリース頻度が高ければ高いほど、リグレッションテストはおろそかになると思うので、そこを自動テストで改善できると素敵です。 じゃあ自動テストスクリプトは書くけれど、その前にどこで実行するの? という事で、正直なところSeleniumもよくわかっていないところから、どうしたら良いのかを調べていました。 実行環境への要望としては下記の5点です。 ・ 並列処理でテスト実行時間を最大限短くしていきたい ・ 作業中PCで自動テストを実行した時、ブラウザがピョコピョコ動いてるのは目障り ・ 実行するPCによって環境がかわるのは避けたい ・ でも実行環境は気軽に変えられるようにしたい ・ 実行する人数やテスト数が増えた時に環境を増強できるようにしたい 調べていると、Selenium Gridを使えば上記問題は全て解決できそうだとわかりました。 Selenium Grid / docker-selenium Selenium ブラウザ上でのwebページ操作をスクリプトから行うためのツールです。 https://www.seleniumhq.org/ http://www.selenium.jp/ Selenium Grid Seleniumで実行される動作を管理するツールです。 https://www.seleniumhq.org/docs/07_selenium_grid.jsp node内に起動するブラウザでテスト処理を実行し、hubがnodeを管理してテストスクリプトからの命令を仲介します。 nodeや、node内のブラウザを複数起動してもhubが接続を管理してくれるので並列処理なども簡単に行えます。 hubとnodeは分けて起動できるので、nodeの追加や変更時にhubやその他のnodeを止める事なく簡単に行えます。 またdocker-seleniumというDocker版SeleniumGridもありました。 Dockerのコンテナを使用するので、場所を選ばず、起動が手軽で、管理もしやすく、クジラも可愛いです。 Dockerよくわからないけど素敵です。 https://github.com/SeleniumHQ/docker-selenium とりあえず作ってみた環境 内容に合わせて3種類に分けました。 ・ 自動テストスクリプトを実行するPC ・ hubを起動するPC ・ nodeを起動するPC 自動テストスクリプトはhubの情報さえ持っておけば、nodeの起動場所を一切気にする必要なくテスト処理を行えるので、PCを分けても問題なく動いてくれます。 hubはhub専用、もしくは各テストスクリプトの設定データや結果等、自動テストスクリプトで使用される共有データを保管しておくのも良いかもしれません。簡易的な形でdbを入れてみたりとか。 nodeはnode専用にして、社内の片隅に放置されてる可哀想なPCを集めて使うのも優しい気持ちになれて良いかもしれません。モッタイナイ精神。 画像内に表示してるPCアイコンは適当に選んだだけなので、これらを使用しているわけではないです。 いざ環境作成 とりあえず1台のPC内で環境を作ってみます。 以降の環境作成を実際に試す時はDockerとPython 3.6をインストールしてください。今回使用していたPCは全てmacOS High Sierraです。 尚、以下の作成手順にはIPを書き込む部分がいくつかありますが、1台で動かしている間は全て同じIPです。 必要なファイルを全部まとめて作っておきます。 TestAutomation |_hub | |_docker-compose.yml | |_node | |_docker-compose.yml | |_selenium_grid_setup.py | |_test_automation.py hubの準備 dockerでhubを起動する為に、設定ファイルを作ります。 ./hub/docker-selenium.yml version : "3" services : selenium-hub : image : selenium/hub:3.14.0-helium container_name : selenium-hub ports : - "4444:4444" environment : - GRID_BROWSER_TIMEOUT=120 - GRID_TIMEOUT=150 - GRID_MAX_SESSION=30 使用するselenium imageのバージョンは全て統一しないと動かなくなる時があるので、管理の事も考えてとりあえず書いておきます。 environmentのところで設定を変更できます。 GRID_BROWSER_TIMEOUTがブラウザのタイムアウト、クリック等で失敗を判断するまでの時間です。 GRID_TIMEOUTはhubがnodeを未使用の状態だ判断するまでの時間です。例えば自動テストスクリプトがエラーで終了した時、 hubに終了した事を伝えられないままになるのでnodeを掴んだまま解放されなくなります。なので、命令の無い状態が一定時間続いた時に解放するよ、という設定です。たぶん。 詳しくは公式で https://github.com/SeleniumHQ/docker-selenium/blob/master/Hub/Dockerfile.txt ymlファイルを作ったら起動します。 mac:TestAutomation user$ docker-compose -f hub/docker-compose.yml up -d URLでアクセスできたら成功です。 http://<hub用PCのIP>:4444/grid/console nodeの準備 hubと同様にnode用の設定ファイルを作ります。 ./node/docker-selenium version : "3" services : browser_0 : image : selenium/node-chrome-debug:3.14.0-helium container_name : chrome0 ports : - "55550:5900" - "5555:5555" environment : - NODE_MAX_INSTANCES=5 - NODE_MAX_SESSION=5 - HUB_PORT_4444_TCP_ADDR=<hub用PCのIP> - HUB_PORT_4444_TCP_PORT=4444 - REMOTE_HOST="http://<node用PCのIP>:5555" browser_1 : image : selenium/node-chrome-debug:3.14.0-helium container_name : chrome1 ports : - "55560:5900" - "5556:5555" environment : - NODE_MAX_INSTANCES=5 - NODE_MAX_SESSION=5 - HUB_PORT_4444_TCP_ADDR=<hub用PCのIP> - HUB_PORT_4444_TCP_PORT=4444 - REMOTE_HOST="http://<node用PCのIP>:5556" この設定ファイルでchromeを5つまで動かせるnodeを2つ起動できます。 browser_0とbrowser_1です。 portsを設定しておかないとホスト側のポートがランダムに選ばれるのでちゃんと書く必要があります。 左側がホストのポート、右側がコンテナのポートです。ホスト側のポートは空いているポートを指定してください。 portsの"5555:5555"がhubからnodeへの命令時に使用されるポートで、"55550:5900"がvnc接続に使用するポートです。 自動テストスクリプトの作成中はvncで確認したい事があると思うので、ポート番号がわかりにくいと面倒です。 各nodeのホスト側ポート「5555」「5556」は気軽に確認できるので、そこから連想しやすい番号だと楽です。上に書いたファイルの場合は末尾に0をつける形です。 nodeのenvironmentでもいろいろと設定できます。 NODE_MAXは1つのnode内で起動しておくブラウザの個数や、同時に動かせるブラウザの個数です。 タイムアウト等様々な設定がありますが必要ないものはもちろん、自動テストスクリプトの中で切り替える事ができるものもあります。 特殊な使い方をしないのであればほどほどで良いのかなと思います。 困ったら足すくらいの流れで。 https://github.com/SeleniumHQ/docker-selenium/blob/master/NodeBase/Dockerfile.txt ymlファイルをnodeフォルダに保存したら、起動します。 mac:TestAutomation user$ docker-compose -f node/docker-compose.yml up -d もう一度アクセスしてみて表示内容が変わっていれば成功です。 http://<hub用PCのIP>:4444/grid/console 自動テストスクリプトを実行した時にちゃんと動作しているかがわかりにくいので、vnc接続もしておきます。 Finder>移動>サーバーへ接続から下のアドレスを入力して接続です。vncのパスワードはsecretです。 vnc://<node用PCのIP>:55550 vnc://<node用PCのIP>:55560 Windowsの場合はUltraVNCとか入れると良いと思います。使い方は、わかりません! 必須ではないけれどnodeの起動方法 node起動時はnode数・使用ブラウザ・使用port等の設定内容を変更しながら試していたので、その都度ファイルを書き換えるのは面倒でした。 なので「ymlファイルを作ってからnodeを起動する」というスクリプトを書く形にしました。 node毎に設定するportはSTART_PORTで決めています。1つ目のnodeは5555、2つ目は5556といった形になっています。 ./selenium_grid_setup.py import os NODE_MAX = 5 START_PORT = 5555 def get_yml_text (node_count, hubip, myip, image, browser_name): text = 'version: "3" \n services: \n ' for i in range (node_count): text += ' browser_{num}: \n ' \ ' image: selenium/{image} \n ' \ ' container_name: {browser_name}{num} \n ' \ ' ports: \n ' \ ' - "{port}0:5900" \n ' \ ' - "{port}:5555" \n ' \ ' volumes: \n ' \ ' - /dev/shm:/dev/shm \n ' \ ' environment: \n ' \ ' - NODE_MAX_INSTANCES={NODE_MAX} \n ' \ ' - NODE_MAX_SESSION={NODE_MAX} \n ' \ ' - HUB_PORT_4444_TCP_ADDR={hubip} \n ' \ ' - HUB_PORT_4444_TCP_PORT=4444 \n ' \ ' - REMOTE_HOST="http://{myip}:{port}" \n ' . format ( NODE_MAX=NODE_MAX, image=image, browser_name=browser_name, hubip=hubip, myip=myip, num= str (i), port= str (START_PORT + i) ) return text def create_file (name, text): f = open (name, 'w' ) f.write(text) f.close() def launch_node (): hubip = '<hub用PCのIP>' myip = '<自身のIP>' node_count = 2 image = 'node-chrome-debug:3.14.0-helium' browser_name = 'chrome' yml_text = get_yml_text(node_count=node_count, hubip=hubip, myip=myip, image=image, browser_name=browser_name) yml_file_name = 'node/docker-compose.yml' create_file(yml_file_name, yml_text) os.system( 'docker-compose -f node/docker-compose.yml up -d' ) # 作る前に停止 os.system( 'docker-compose -f node/docker-compose.yml down' ) launch_node() ちょっと適当な感じが滲み出ちゃってますが、こんな形で作りたい内容に合わせてymlファイルを作って保存します。 yml設定ファイルを上書きする前に、古いyml設定ファイルで起動されているコンテナを停止するのだけ忘れないように。忘れるとコンテナで溢れかえります。 どうせ作るならhubの設定ファイルもスクリプトで作るようにするのが良いですね。imageのバージョンを1箇所で管理したいですし。 あと、流石に直接Pythonで文字列を作るより、PyYAMLを使うのが綺麗かもしれないですね。何も考えずに書いても長さは特に変わっていませんが。 import yaml from collections import OrderedDict # dict だと並び順が変わってしまったのでOrderedDictを使いました # OrderedDict だと出力の形式が違ったのですがyaml.add_representerで設定を変更できるみたいです def represent_odict (dumper, instance): return dumper.represent_mapping( 'tag:yaml.org,2002:map' , instance.items()) yaml.add_representer(OrderedDict, represent_odict) # get_yml_text を改変します def get_yml_od (node_count, hubip, myip, image, browser_name): services_dict = OrderedDict() for i in range (node_count): num = str (i) port = str ( 5555 + i) services_dict[f 'browser_{num}' ] = OrderedDict([ ( 'image' , f 'selenium/{image}' ), ( 'container_name' , f '{browser_name}{num}' ), ( 'ports' , [ f '{port}0:5900' , f '{port}:5555' ]), ( 'volumes' , [ f '/dev/shm:/dev/shm' ]), ( 'environment' , [ f 'NODE_MAX_INSTANCES=5' , f 'NODE_MAX_SESSION=5' , f 'HUB_PORT_4444_TCP_ADDR={hubip}' , f 'HUB_PORT_4444_TCP_PORT=4444' , f 'REMOTE_HOST="http://{myip}:{port}"' ]) ]) return OrderedDict([( 'version' , '3' ), ( 'services' , services_dict)]) # yaml の為にちょっと設定を追加します def create_file (name, yml_dct): with open (name, 'w' ) as file : yaml.dump(yml_dct, file , default_flow_style= False ) # 引数は適当です od = get_yml_od(node_count= 2 , hubip= '0.0.0.0' , myip= '1.1.1.1' , image= 'node-chrome-debug:3.14.0-helium' , browser_name= 'chrome' ) create_file( 'node/docker-compose.yml' , od) 実行してみる テスト処理を行うスクリプトを作って、実行します。 JavaとPythonでは書いてましたが、RubyやJavaScriptでは書いた事ないです。いろんな言語でかけるみたいです。 ./test_automation.py from selenium import webdriver from selenium.webdriver.common.desired_capabilities import DesiredCapabilities def get_web_driver (): return webdriver.Remote( command_executor=f 'http://<hub用PCのIP>:4444/wd/hub' , desired_capabilities=DesiredCapabilities.CHROME ) driver = get_web_driver() driver.get( 'https://www.google.com' ) driver.quit() ↓せっかくなのでJavaの場合 import org.openqa.selenium.WebDriver; import org.openqa.selenium.remote.DesiredCapabilities; import org.openqa.selenium.remote.RemoteWebDriver; import java.net.URL; public class Main { public static WebDriver getWebDriver(){ try { return new RemoteWebDriver( new URL( "http://<hub用PCのIP>:4444/wd/hub" ), DesiredCapabilities.chrome()); } catch (Exception e){ System.out.println(e); return null } } public static void main(String args[]){ WebDriver driver = getWebDriver(); driver.get( "https://www.google.com" ); driver.quit(); } googleを開いて、ブラウザを閉じるだけです。 スクリプトが動いてくれたら成功です。 ここまで1台のPCで動かしていたので、次はPCを3台にわけてみます。 分ける前に、今現在立ち上がってるdockerのコンテナを全て停止しておきます。 mac:TestAutomation user$ docker-compose -f node/docker-compose.yml down mac:TestAutomation user$ docker-compose -f hub/docker-compose.yml down 「実行用PC」「hub用PC」「node用PC」の3台にわける 追加するPCのdockerインストールやPythonのバージョンにご注意ください。 ここまで書いたファイルの<hub用PCのIP><node用PCのIP>の部分を書き直します。 IPを書き直したら全てのファイルを3台のPCにコピー&ペーストします。コピペなんてやってられるか!Gitだ!Git! と、おわかりの方はGitよろしくです。 あと、実際の運用を考えると、別途設定ファイルを用意して置いてそこにIPを書いて置いた方が切り替えやすいので良いですね。 各PCにファイルを揃えたらこれまでの手順通りに進めます。 1.hub用PCでhubを起動 2.node用PCでnodeを起動 3.起動したnodeにvnc接続 4.最後に実行用PCで自動テストスクリプトを実行 動いてくれたら完成です。もしも動かないという時は各IPを確認してください。 あとはhub用PCにも追加でnodeを起動してみたり、さらに4台目のPCを用意してnodeを追加してみたり、hubを複数にしてみたり。 使用中に起こった困った事 selenium使用中に発生した環境に関する問題で思い出せた事例を書いておきます。 <困ったこと1> 自動テストスクリプトの中でWebDriverに対して何らかの命令を送った時に、実行されなくなった事がありました。 各箇所で使用しているseleniumのバージョンが異なる時に、問題が発生しやすいみたいです。 絶対問題が出るというわけではないので余計に見落とします。 docker-compose.ymlで指定してるhubやnode、自動テストスクリプト内で使用してるselenium、ローカル上のブラウザで動かす時はそのpcに入っているブラウザ等、それぞれのバージョンに気をつける必要がありそうです。 docker-compose.yml version : "3" services : selenium-hub : image : selenium/hub:3.14.0-helium java - pom.xml <dependency> <groupId> org.seleniumhq.selenium </groupId> <artifactId> selenium-java </artifactId> <version> 3.14.0 </version> </dependency> Python - pip ターミナルから「pip list」でインストールしているバージョンを確認できるので確認。 <困ったこと2> 連続したテスト実行中に、開始時は問題なかったのに途中からWebDriverを掴めなくなる事がありました。エラーを見るとhubへの接続が失敗しているような雰囲気でした。 自動テストスクリプトの中でWebDriverに対して何らかの操作を行うと、 作業pc → hub → nodeという流れで命令を送信します。hubは中間地点として大量の命令をさばいてます。 ウィルス/スパイウェアで聞いた事があるような「自動で大量のリクエストを送信する」という行為と同じようなことをやってます。 社内環境だとセキュリティがしっかりかかってる事も多いと思いますから、そのウィルスのような挙動が原因となってセキュリティソフトに接続をブロックされる場合がありました。 リクエストが多いと言っても抑えられる範囲なので、同時実行数を抑えたり、hubを分けて分散させたりで解決できると思います。もしくはセキュリティチームに相談したり。 おわり ツールの使い方はいろんなところに書いてあるのですが、どんな環境・流れで動かしているか、どんなトラブルがあったかという記事は見当たらないことが多かったので書いてみました。 と言っても、まずは初期段階のSeleniumGrid環境についてでしたので基本的な事ばかりではありますが。 ZOZOテクノロジーズでは、一緒にサービスを作り上げてくれる方を募集中です。 ご興味のある方は、以下のリンクからぜひご応募ください。 www.starttoday-tech.com
こんにちは! ZOZOテクノロジーズ フロントエンドエンジニアの高橋(ニックネームはQ)です( @anaheim0894 ) 昨年12/26、毎年年末に行われる大忘年会(ZOZOCAMP2018)で、 グループ会社 も含めた1,000人規模でのリアルタイムアンケートを、FirebaseとVue.jsを使って制作しました。 当日会場にて弊社の昨年の事業紹介や、「楽しく働く」というコンセプトの動画を流し、動画の合間で質問をし動画と一体となるような演出を行いました。 その質問に対して全社員それぞれのスマートフォンで回答できるシステムを作ったので、その制作の裏側や、当日の様子などご紹介させていただきます。 まずは当日の様子の紹介 これを実現するまでの様子をご紹介いたします。 CAMP運営してくれている社員にもらった要件 CAMPの2週間前、運営の社員の方からこのような依頼をもらいました。 会場で、 リアルタイムで アンケートをWebでとり、その集計を即時に、目の前のプロジェクタに映し出す 選択肢はYES / NOの2つ 質問は全部で7〜8問 QRコードを読み取って、 スマホで回答できる いい感じのデザイン でプロジェクタに出したい 質問と回答は動画と動画の間に行いたい なかなか厳しい要件でした。 どうやったら実現できるか考える 依頼に答えるべく実現方法を検討しました。 Slackの Polly を使って全社員が投票、Slack APIを使ってプロジェクタに表示 → グループ会社含めるとSlackには全社員のアカウントがないため、NGに。 外部のサービスを使う →リアルタイムアンケートのサービス、 respon があったものの、カスタマイズができるか不明なこと、2週間で要件を完璧に実現できないと感じたためNGに。 PBグローバル を担当しているフロントエンドチームで自作 → Firebaseならリアルタイムデータ集計の可能性があったため、 こちらで進めていくことに決定しました 。 自作にあたって整理した要件 CAMP運営からの要件を元に、自作するための要件を整理しました。 質問は7〜8問 動画演出の間に質問を差し込み 動画と質問の切り替えはプロジェクタのスイッチングのみ 選択肢 Yes / No の二択 QRコードで読み取ってサイトを表示 回答結果がリアルタイムで集計され、すぐに会場のプロジェクタで表示 次の質問に移るときはリロードなし 質問の切り替えは管理画面を用意 スマホでの回答は1回のみで解除は不可 回答時間は15秒(質問画面10秒その後5秒のカウントダウン演出) 結果はYES/NOのパーセンテージ割合で表示 当日その場で質問の追加はなし 準備 / 事前に確認したこと CAMPは1,000人規模のイベントです。 そのため、 当日起きることをどれだけ予測できるか 予測外の事態にどれだけすばやく対応できるか が重要になると考え、様々なケースを想定して情報を集めました。 参加人数 約1,000人 1,000人に耐えられるネットワークの準備 イベント自体はイベント会社が入っていたため、イベント会社にWi-Fiの用意を依頼(NTT様にご協力頂きました) 会場スクリーンスペック 解像度:1920×1080 コンテナ:MOV コーデック:映像H.264 / 音声PCM 48kHz フレームレート:59.94または29.9 会場スクリーンの台数 4画面 スクリーンに繋ぐマシンのネットワーク環境 有線で用意 当日のリハーサルの可否 15時から30分〜40分リハーサルができた 1,000人規模のテスト 人数を用意できないため、ぶっつけ本番 (擬似的にコードレベルでの負荷テストは実施) 当日不具合があった場合の対応 質問だけのスライドを用意する。最悪挙手なども考えられる デザイン 社内のデザイナーに依頼 さまざまなスマホでの表示確認 社内のテスト検証を専門に行なっているチームに依頼 選定した技術 上記の情報から自作するために、以下の技術を選定し、作成しました。 データ管理: Firebase フロントエンド: Vue.js 結果のアニメーション: TweenMax 作成した画面 以下の3画面を作成しました。 回答画面 各ユーザーがスマートフォン上で回答を送信する画面 質問の開始・終了時に画面の活性を制御 質問/結果画面 会場のスクリーンに表示される質問や結果の画面 質問の開始・終了時に質問/結果画面を切り替え、結果画面には各ユーザーの回答を集計して表示 管理画面 進行管理者が質問を切り替えるための画面 質問を切り替えると同時に、質問の終了時刻も設定 Firebaseセットアップ 質問の表示切り替えと投票結果の集計をリアルタイムで行うため、リアルタイムリスナー搭載のFirestoreがあるFirebaseを選定しました。 開発に携わったのが全員フロントエンドのメンバーだということもあって、サーバーサイドの開発はせずクライアントJSからデータベースに直接アクセスする構成を取りました。 ここでは、FirebaseをWebアプリケーションで使用するためのセットアップ方法をご紹介します。 プロジェクトのセットアップ Googleアカウントが無ければ作成します。 Googleにログインして、 Firebaseコンソール からプロジェクトを追加します。 プロジェクト名を入力します。 アナリティクスやテクニカルサポートについては必要無さそうなのでチェックを入れずに進んでいきます。 自動でプロビジョニングが行われ、プロジェクトページに飛びます。 デフォルトでは無料のSparkプランが設定されています。ちょっとしたアプリケーションなら無料枠で問題なく動くと思います。 今回は1,000人規模のユーザーがリアルタイムで操作するということだったので、念のため従量課金のBlazeプランに設定しました。 Firestoreセットアップ Firestoreを設定していきます。 左側のナビゲーションから 開発 > Database を選択し、データベースを作成します。 セキュリティルール:今回は要件上セキュリティ面をさほど気にする必要がないので、スピード重視でテストモードを選択します。 コレクション(RDBでいうテーブルのようなもの)が無い空のデータベースが作成されます。 FirestoreはNoSQLなのでスキーマの作成も必要ありません。 Webアプリケーションとの連携を行います。コンソールトップのHTMLタグ風のアイコンをクリックします。 ポップアップでスニペットが出てくるのでこれを開発中のHTMLコードに貼り付けます。 (アプリケーション側のコードをまだ何も作成していなかったので、ここでVue.jsのプロジェクトをinitializeします) Vue.jsのセットアップは こちら を参考にしてください。 ポップアップのメッセージにある HTMLの一番下、他のスクリプトタグの前 ではなく、 main.js に貼り付けます。 .vue ファイル内で import firebase from 'firebase' と記述すれば、 firebase 変数でFirebaseのAPIが利用できます。 GUIコンソール上でガイドにしたがって画面操作するだけでセットアップが完了しました。 Firebase Hostingセットアップ WebアプリケーションのホスティングにもFirebaseを利用することにしました。 こちらはFirestoreと比較しても、とても簡単に設定できます。 左側のナビゲーションから 開発 > Hosting を選択し、 使ってみる ボタンから開始します。 ポップアップのガイドにしたがってfirebaseのコマンドラインツールをインストールします。 $ firebase login でGoogleアカウントにログインします。 開発中のvueプロジェクトのディレクトリに移動し、 $ firebase init でデプロイ設定ファイルを作成します。 生成された firebase.json の hosting.public にデプロイ対象ディレクトリを設定します。 今回はvueプロジェクトをwebpackでビルドしたコードが dist ディレクトリに出力される構成なので、 dist と入力します。 $ firebase deploy で、デプロイ完了です。 firebaseコンソールのHosting画面にデプロイ先のサーバーのドメインが表示されているので、このURLでブラウザからアクセスできます。 以上でFirebase側の設定は完了です。 実際20分くらいでインフラセットアップ作業が完了し、すぐさまアプリケーション側の開発に入れる状況まで持ってこられました。 Firestoreと画面の連携 アプリケーション側のFirestore連携部分の実装を紹介します。 3画面の状態を各端末で同期的に制御するために、Firestoreで以下を管理することにしました。 現在の質問id 回答の締切時間 ユーザーの回答 実際には以下のようなデータ構造でこれらを管理しました。 (実際のデータはJSONではありませんが、便宜上JSON形式で書いています) { " questions ": { " current ": { " id ": 1 , // 現在の質問id " endTime ": 1545906141 // 回答の締切時間 } , } , " votes ": { // 各ユーザーの回答 " 001EjYAwtSMXlrWWTP5r ": { // (Firestoreが自動付与するid) " answerId ": 0 , // 回答id " questionId ": 2 // 質問id } , " 004gO0YzXUJ2bNFSbc5y ": { " answerId ": 1 , " questionId ": 3 } , . . . } } 回答・質問/結果画面でこれらのデータの変更を購読します。 現在の質問idと回答の締切時間(回答画面・質問/結果画面) import firebase from 'firebase' export default { . . . created() { // this.db = firebase.firestore() this .unsubscribe = this .db.collection( 'questions' ).doc( 'current' ) .onSnapshot((doc) => { const currentQuestion = doc.data() // => // { // "id": 1, // "endTime": 1545906141 // } } ) } . . . } 各ユーザーの回答(質問/結果画面) import firebase from 'firebase' const db = firebase.firestore() export default { . . . created() { // this.db = firebase.firestore() this .db.collection( 'votes' ).onSnapshot((collection) => { this .votes = collection.docChanges().reduce((votes, c) => { const vote = c.doc.data() // => // { // "answerId": 0, // "questionId": 2 // } return { ...votes, [ vote.questionId ] : [ ...(votes [ vote.questionId ] || [] ), vote ] } } , this .votes) // => // { // . // . // . // "2": [ // 質問id毎に集計 // { // "answerId": 0, // "questionId": 2 // }, // . // . // . // ], // "3": [ // { // "answerId": 1, // "questionId": 3 // }, // . // . // . // ], // . // . // . // } } ) } } 結果画面では質問毎に集計を行うので、質問id毎に回答のデータをまとめています。 また、回答のデータに関しては課金を最小限にするため docChanges() を用いて差分だけ取得するようにしています。 一方、管理画面では以下のようなコードでFirestoreに書き込みを行います。 import firebase from 'firebase' import moment from 'moment' export default { . . . methods: { setQuestion() { db.collection( 'questions' ).doc( 'current' ).set( { id: questionId, endTime: moment().add(questionTime, 'second' ).unix() } ) } } . . . } 管理画面で質問を開始するときには、Firestoreに質問idと締切時間を書き込みます。 書き込みが行われた時点でFirestoreから会場の全端末に通知され、一斉に質問が開始されます。 そして、各端末が締切時間に応じて質問の回答を締め切ります。 以上でFirestore連携は完了です。 ここまで来たら、後は画面にアニメーションや装飾を施すのみです。 アニメーションについて 質問/結果画面のアニメーションに関しては TweenMax で実装しました。 TweenMaxについて GSAP(グリーン・ソック・アニメーション・プラットフォーム)モジュールの1つです。 使用可能ライブラリとプラグイン以下のとおりです(要は全部入り) TweenLite TimelineLite TimelineMax CSSPlugin AttrPlugin RoundPropsPlugin BezierPlugin EasePack (後々考えると今回の実装内容だとTweenLiteのみでよかったかも…) 採用した理由 Vue.jsのデータ駆動設計と相性が良さそうだった 複雑なアニメーション作成に適している 構文が直感的でわかりやすい 使用方法 import import { TweenMax } from 'gsap' Methods 今回は主に下記Methodsを使用しました。 値(初期値)を設定する TweenMax.set( target:Object, vars:Object ) 初期値から設定した値にアニメーションさせる TweenMax.to( target:Object, duration:Number, vars:Object ) 設定した初期値から設定した値にアニメーションさせる TweenMax.fromTo( target:Object, duration:Number, fromVars:Object, toVars:Object ) 配列化されたtargetをindex順に設定した初期値から設定した値にアニメーションさせる TweenMax.staggerFromTo( targets:Array, duration:Number, fromVars:Object, toVars:Object, stagger:Number ) 実装サンプル See the Pen countdown by masahito.ando ( @masahito_ando ) on CodePen . 質問画面にて、 questions.current.endTime (Firestoreに書き込まれた締切時間)をフロント側でも setInterval にて監視し、締切時間が残り5秒になったタイミングで上記アニメーションを開始しています。 See the Pen graph by masahito.ando ( @masahito_ando ) on CodePen . 締切時間に到達した時点で結果画面を表示します。 そのタイミングで votes.[Firestoreが自動付与するid].questionId を questions.current.id でフィルタリングして現在の質問の回答データを取得し、YESとNOのパーセンテージとグラフの高さを計算します。計算完了後、上記アニメーションを開始しています。 当日起きたこと 動画と動画の合間に質問を入れるタイミングを画面のスイッチャーと息を合わせる必要があったため、リハーサルで入念にタイミングを確認しました。 実際にリハーサルしてみると、動画と質問の間に違和感があり、スムーズに見えませんでした。 そのため、動画と一体になっているような演出に見えるよう質問の前に真っ白な画面を用意しました。また、動画が真っ白にフェードアウトするようにしてもらいました。 1,000人同時アクセスされた場合ちゃんと動くかテストできずぶっつけ本番だったため、1問目が終わった時にちゃんとデータが入った(動いた)時に 全員でガッツポーズ をしました。 裏方の様子 イベント会社の方が、 こんなリアルタイムでアンケートなんて、これまでやったことないです! どうやってるんですか? と興味津々に食いついていただけて、嬉しかったです。 料金について Firebaseのプランですが、今回1,000人規模かつ初の試みということもあり、従量課金Blazeプランにしました。 イベント終了後、請求金額を見てみると…。 2円 なんと2円でした! 笑 データの作り方にもよりますが、2円の予算で実現できます。 今後の課題 全体を通してうまく作ることできたのですが、当日、質問と質問の間の動画の尺が長く、開いていたiPhoneなどの端末がスリープしておりました。 質問の回答時間15秒だったため、スリープ解除している間に回答時間が終了してしまうという事態が発生してしまいました。 アンケートとしては十分な回答時間でしたが、動画と合わせたときの状況を考え、回答時間が適切かを考えるべきだったなと思いました。 最後に制作メンバーの紹介 今回の制作は弊社 PBグローバル のフロントエンドチームで制作をしましたので、ご紹介させていただきます。 進行管理:高橋( @anaheim0894 ) フロントエンド、アニメーション:安藤 Firebaseなどバックエンド担当:松井( @nahokomatsui )、茨木( @niba1122 )、権守( @AmatsukiKu ) 進行補助:松浦( @mtmn07384 ) こんな突発的かつ刺激的なことや、ZOZOを作ってみたい! という方。 弊社では一緒に作ってくれる方を大募集しています。 ご興味がある方は以下のリンクから是非ご応募ください。 www.wantedly.com
こんにちは、ZOZO研究所の岩本です。 2018年12月5日から12月8日にかけて富山で開催された学会 SCIS & ISIS 2018 に、同じく研究所の岩崎と参加してきました。 SCIS & ISISについて SCIS & ISISはソフトコンピューティングと知能システムに関する国際学会です。 SCISとISISはそれぞれ別の学会ですが、今年は共同で開催されました。 また、ISWSというワークショップも同時に同じ会場で開催されていました。 今回はこの学会に参加して他の方の発表を聞いたり、研究成果をポスターで発表したりしてきました。 学会のプログラムは主に、招待講演、トークセッション、ポスターセッションに分かれています。 招待講演では国内外の大学教授の方などがホールでいくつかのテーマについてお話しされていました。 トークセッションでは、いくつかの部屋で20分程度の発表が並列して行われます。 ポスターセッションでは会場に20枚ほどのポスターが掲示され、参加者は興味のあるポスターの前に行って発表者から説明を受けたり、内容に関するディスカッションを行ったりします。 ポスターセッションでは自分たちも発表していたため他の発表を見ることが出来ませんでしたが、招待講演とトークセッションでは色々な興味深い話を聞くことが出来ました。 また、現在共同研究を行っている九州工業大学の古川研究室からも、4人の学生さんがトークセッションとポスターセッションで発表をされていました。 このレポートでは、今回私たちが行った発表の紹介と、その他の発表の中で特に興味深かったものを紹介したいと思います。 ZOZO研究所のポスター発表について ZOZO研究所からは「Visualization of User-item Rating Matrix by Hierarchical Tensor SOM Network(階層型Tensor SOMネットワークによるユーザー・アイテム評価行列の可視化)」というタイトルでポスター発表を行いました。著者は岩本海童、岩崎亘、古川徹生(九州工業大学)です。ここでは、簡単にその概要をご紹介します。 現在、岩崎と岩本(私)のチームは 九州工業大学との共同研究 で、大規模な関係データの可視化を研究しています。 関係データの可視化と言ってもイメージが難しいと思うので、例を使って説明してみます。 何人かの人にいくつかのアイテム(例えば食べ物など)の好みについて質問したアンケートデータがあるとします。 このようなデータを解析すると、「どんな人がどんなアイテムを好んでいるのか」や「ある人と好みが似ている人たち」「あるアイテムと好む人の傾向が似ているアイテム」を直感的に可視化することが出来ます。 しかし、ユーザーやアイテムの数が多い場合には、個別のユーザーやアイテムだけではなくもう少し俯瞰的な視点でデータを見たいことがあります。 例えば、「甘い食べ物が好きなのはどのユーザーなのか」「30代のユーザーはどんな食べ物が好きなのか」などです。 そこで、私たちは関係データと一緒にユーザー属性情報とアイテム属性情報を解析してみました。 属性情報とは、例えばユーザーの場合は性別や年齢層、アイテムの場合はカテゴリーや価格帯などです。 その結果、「あるアイテムはどんなユーザー属性の人に好まれているのか」や「あるユーザー属性の人は、どんな属性の商品が好きなのか」といった情報を視覚的に確認できるようになりました。特に後者が重要です。先行研究ではユーザー属性だけを利用していたのですが、今回はユーザーとアイテムの両方を一緒に解析し、それらの関係を可視化しました。 そうすることにより、 ユーザーとアイテムそれぞれ について、 個別のデータレベルの視点と属性レベルの2つの視点から関係データを視覚的に見る ことができるようになりました。 文章だけだとよく分からないと思うので、可視化の様子の一例を簡単に図にしてみました。 実際はこのように静的なものではなく、フォーカスするものを切り替えながら、インタラクティブに可視化された結果を確認できます。 (それを表現するにはポスターでは限界があったので、発表ではiPadで可視化のデモを行いました) 今回の発表では寿司の好みに関するアンケートデータを利用しましたが、最終的にはこの手法を使ってZOZOの購買データやWearのコーディネートのデータを可視化したいと考えています。 膨大なデータを可視化することで、今まで知ることのできなかったデータの一面が発見できるかもしれません。 また、今回発表したポスターで、 ポスターセッションアワードを頂きました 。 全く予想外の出来事で、大変嬉しく思います。 ポスター発表の様子 こちらはポスターの前で参加者に研究を説明しているところです。インタラクティブな可視化を見ていただくためにiPadでのデモンストレーションも行いました。 ポスターの前に立つ私です。 その他の発表の紹介 ここでは、学会で視聴した発表の中から特に印象的だったものをいくつかを紹介します。 なお、見出しの下の斜体の文は、発表者名と私が訳したタイトルです。 Cognitive Mechanism of Humor in Riddles: Examination of Relationship between Humor and Semantic Structure of Riddles Asuka Terai, 「謎かけのユーモアの認知メカニズム:ユーモアと謎かけの意味構造の関係の調査」 謎かけを自動生成して、その面白さと使われている単語との関係を調べる研究です。 ここでは、「AとかけてBと解く。その心はC(C')」(AとBはそれぞれ名詞で、CとC'は同音異義語でそれぞれAとBに関係がある)というような限定したスタイルの謎かけを扱います。 AとBの単語をランダムに選び、それぞれに意味が近い単語CとC'をWikipedia日本語版のコーパスとword2vecを使って選ぶことで、謎かけを生成します。 あらかじめ用意したいくつかの謎かけについて、ユーモアがあるかどうかのアンケートを行い、またA-C, B-C', C-C'間の単語の類似度を調べました。 その結果、A-CとB-C'の類似度が共に高い謎かけは高評価が得られやすかったそうです。しかし、生成した謎かけはA-CとB-C'が共に高くなることはあまりなかったそうです。 感想 ビジネス寄りの実用的な研究が多い中、こういうある種の「遊び」に関する研究はすごく面白いなと思います。 もっと複雑な文章の謎かけを生成したり、あるいはユーモアに関してもっと多角的に研究(ユーモアの種類、地域や世代などによる感性の違いなど)してみたりすると面白そうだと思いました。 Non-parametric Continuous Self-Organizing Map Ryuji Watanabe, 「ノンパラメトリックで連続な自己組織化写像」 九州工業大学の渡辺さんの発表です。 (現在行っている共同研究にも関連する内容です) 自己組織化写像(self-organizing map, SOM)は高次元のデータの低次元表現を離散的に求める手法です。 この研究では連続した空間での低次元表現を求められるようにSOMを改良しました。 また、求められる低次元表現はデータ点だけに依存する(ノンパラメトリック)ようになっています。 その結果、ユニット数を増やした高解像度なSOMと同じような表現を得られることが実験から確認できたそうです。 感想 離散化した写像を求めるSOMを、連続かつノンパラメトリックに拡張する流れがとても自然で美しいです。 データ数が増えた場合にいかにメモリと計算時間を節約するかが実用に向けた課題だと思います。 Visualized Onomatopoeia Thesaurus Maps based on Deep Autoencoder Daiki Urata, 「深層オートエンコーダーによる類似オノマトペのマップ」 オノマトペの類似度を可視化する研究です。 オノマトペを音声に変換したものをオートエンコーダーにかけて2次元の表現を得ます。 それを2次元上に表示すると、似た音のものや、同じカテゴリー(汁をすする音、飲み物を飲む音など)のものが近くに配置されるそうです。 未知のオノマトペに対しても、同様に2次元の表現を得て、そこからもっとも近くにあるオノマトペのカテゴリーをそのオノマトペのカテゴリーとします。 その結果、高い精度で未知のオノマトペのカテゴリーを推定できたそうです。 感想 オノマトペの研究というのを初めて知ったので、とても興味深かったです。 ファッションを表現する際にもオノマトペを使うことがあるので、雰囲気や質感を表す言葉から服やコーディネートを提案できたら面白いなと思いました。 An Extreme Learning Machine Based Pretraining Method for Multi-Layer Neural Networks Pavit Noinongyao, 「エクストリームラーニングマシンに基づく多層ニューラルネットワークの事前学習の手法」 エクストリームラーニングマシン(ELM)はある種のニューラルネットワークですが、勾配法を使わず、1層目の重みをランダムに初期化し2層目の重みを逆行列を使って決定的に求めます。 このELMをベースにしたオートエンコーダーを用いると、多層ニューラルネットワークの事前学習を行うことができます。 この研究では、通常のELMとは逆に2層目をランダムに初期化し1層目を計算するbackward-ELMという手法を提案し、事前学習の手法として有用であることを実験により示しました。 感想 ELMという手法について詳細を知らなかったのですが、勾配法を使わずに一気に重みを計算するというのはとても大胆だなと思いました。 時間があるときに詳しく調べてみたいです。 参加した感想 実は、今回は私にとって初めてのポスター発表でした。 私は人の前で話すのがあまり得意ではないのですが、視聴者の方々が優しく聴いてくださり、説明が不十分なところを適宜質問していただいたのでなんとか研究の内容をお伝えできたように思います。 また、今回は可視化の研究であり、iPadでのデモンストレーションにより視覚的に相手に訴えることができたというのも良かった点かなと思っています。 ポスター発表は1人(または少数の人)を前にして相互にコミュニケーションしながら発表を行うことができます。 これは多くの人の前で行う口頭発表にはない良さですが、一方で事前に頭の中で発表する内容をよく整理しておく必要があることに気づきました。 次にポスターで発表する機会があれば、もっとスムーズに説明できると良いなと思っています。 招待講演やトークセッションでは、色々な分野の発表を聞くことができて、大変勉強になりました。 また、今回の学会では自分の英語の能力の足りなさを実感しました。 今後、もっと英語の発表を聴いて良く理解したり、もっと英語で情報を伝えられるようになりたいと思いました。 おまけ 上に書いていない学会や富山の様子を写真で一部ご紹介します。 九州工業大学の学生さんたちによる発表 古川研究室から4人の学生さんがトークセッションとポスターセッションで発表されていました。 晩餐会 学会の最終日の前日の夜に、晩餐会が行われました。 運営の方の挨拶や学会の歴史についての紹介が行われた他、杖道のパフォーマンスも行われました。 富山の食べ物 富山名物の、富山ブラックと呼ばれるラーメンです。 元々、肉体労働者に食べてもらうために作られたこともあり、かなり醤油の塩味が効いています。 味が濃すぎるあまり途中でライスを注文しましたが、1杯では全然足りませんでした。 そして、富山といえば何と言っても海の幸です。 私たちも学会の期間中、何回かお刺身や焼魚などの海の幸を味わいました。 また、魚と一緒に飲む富山の日本酒も美味しかったです。 最後に、岩崎と私で会場前でパシャリ。 お疲れ様でした。 おわりに ZOZO研究所では、ファッションに関する研究を行なっています。 私と岩崎のようにデータの可視化を研究しているチームもあれば、コーディネートの研究をしているチームもあり、日々さまざまな課題に取り組んでいます。 あなたも一緒にZOZOのデータ資産を活用して「似合う」を研究しませんか? ご興味のある方は下のリンクからご連絡いただき、ぜひ一度オフィスに来てください。 研究所のオフィスは青山と福岡にあります。 www.wantedly.com www.wantedly.com
こんにちは。開発部の廣瀬です。 本記事では、昨年障害が発生してしまったZOZOTOWNの福袋発売イベントについて負荷対策を実施し、今年の福袋イベント期間を無傷で乗り切った話をご紹介したいと思います。 大規模サイトの障害に関する生々しい話はあまり公開されていないように思いますので、長くなってしまいましたが詳細に書いてみました。尚、今回のお話は弊社のサービスで使用しているDBMSの1つである、SQL Serverに関する話題がメインです。 福袋イベント「ZOZO福袋2019」とは 年に1度、多数のブランドの福袋が一斉に発売される、ZOZOTOWNの年末の風物詩的イベントです。今年は450以上のブランド様にご参加いただきました。お客様からも毎年大変ご好評いただいており、年間を通して最も多くのトラフィックを記録するイベントの1つです。 アクセスが殺到するが故に、昨年は福袋の発売直後からエラーが多発し、一時的に買い物し辛い状態を発生させてしまいました。昨年も負荷対策を実施していたのですが、それでもエラーが多発する状況となってしまい、エンジニア一同、お客様にご不便をおかけして申し訳ない気持ちで一杯でした。 今年こそは絶対に何事もなく福袋イベントをお客様に楽しんでいただこうと、負荷対策に力をいれましたので、順を追って紹介します。 昨年の福袋イベントで発生した障害について 昨年の福袋イベントにおける障害を時系列でまとめると以下のようになります。 12:00 福袋発売開始。 12:01 開始直後のみエラー無く好調だったものの、1分後から大量のエラーが発生。 主にDBサーバーへの接続エラーであった。 必ずエラーが発生する状態ではないものの、購入フローの途中でエラーとなるケースが多発。 注文し辛い状態が続く。 13:40 エラー多発状況が自然解消され、スムーズに注文ができるようになる。 注文数の推移からも、障害の影響が確認できます。 最初の1分間のみ多くの注文が入っています。しかし、その後急激に注文が入らなくなっています。13時40分ごろ急激に注文数が増大したことから、潜在的にはより多くのご注文をいただける余地があったと考えられます。購入フローにおいて、注文確定までの数回の遷移のいずれかでDBの接続エラー発生によりなかなか注文完了できなかったと思われます。 障害の原因調査 適切な負荷対策を実施するためには、昨年発生した障害の原因を突き止める必要があります。そのために調査を実施しました。原因調査には、障害発生時にリアルタイムで収集しておいた 動的管理ビュー の情報と、弊社で導入している監視製品の Spotlight を使用しました。 Spotlightについて簡単に説明しますと、サーバーの様々な情報を定期的に収集しておいて、後から任意の時間帯のサーバー状況を確認できるソフトウェアです。CPUやメモリなどのリソース使用状況や、秒間バッチ実行数、コネクション数などの各種メトリクスから実行中のクエリテキストまで様々な情報が保存されます。そのため、事後調査の際に重宝します。 サーバーの状況をSpotlightで追っていきます。秒間のバッチ実行数をSpotlight上で確認します。 12時の開始直後は普段の約5倍のクエリ実行要求があったようです。その後急速にバッチ実行数が低下しています。この波形が自然でないことは、エラーが大量に発生していた事実からも、注文数の推移からもいえると思います。一度は大量のバッチ実行数を記録したDBサーバーが、何らかの原因で実行数を抑えつけられているようです。DBの内部で起きていたことをさらに調査していきます。 コネクション数の推移です。SQL Serverは仕様で同時接続数が最大32,767と決められており、上限を超えるとクライントにはエラーが返されます。平常時より高いコネクション数で推移しているものの、上限値まで達するということは無く余裕があり、コネクションボトルネックではありませんでした。 次に、障害発生中のDBサーバーの待ち状態の傾向を確認します。 SQL Serverでは、クエリ実行要求を受け付けてから実行完了するまでの間に生じた待ち時間を、ロックなどの待ち事象の項目ごとに確認できます。この情報は動的管理ビューの1つである sys.dm_os_wait_stats から取り出すことができます。SpotlightからもこのViewと同等の情報を確認できます。 秒間の累積待ち時間が多い順に並び替えると、平常時は以下のような傾向を示します。 一方で、障害発生時は以下のような状況でした。平常時と比べて、まったく違う傾向を示していました。また、各項目の待ち時間も非常に高い数値を示していました。 ここでは、上位5つのWaitTypeについて紹介します。 THREADPOOL : ワーカースレッドの確保待ち LCK_M_S : 共有ロックの獲得待ち PAGELATCH_SH : 共有ページラッチの獲得待ち LCK_M_U : 更新ロックの獲得待ち PAGELATCH_EX : 排他ページラッチの獲得待ち 「ワーカースレッド」と「ページラッチ」については、聞いたことのない方もいらっしゃるかもしれませんので、それぞれの用語について少し補足をしておきたいと思います。 ワーカースレッドとは、SQL Serverのクエリ実行時にスレッドで使用されるリソース、ページラッチは、SQL Serverのデータ格納領域である「ページ」の一貫性を保つための排他制御の仕組みとイメージしていただければと思います。 (SQL Serverでは、1ページあたり8KBで管理され、ページの中には同一テーブルのレコードが複数入っています。1ページあたり何レコード格納されるかは、1レコードあたりのデータサイズに依存します。) 先ほどの、障害発生時の待ち事象の1位はTHREADPOOLのwaitが圧倒的に多く、次いでページラッチ系とロック系のwaitが2位から5位までを占めていました。平常時は、これらのwaitが多く発生することはありません。 CPUリソース不足時に高い数値を示すSOS_SCHEDULER_YIELDの値はTHREADPOOL待ち時間の100分の1程度でした。そのため、CPU負荷もボトルネックでは無いと判断しました。 THREADPOOL waitが圧倒的に多かったため、多発していたエラーとの関連性を調べるために、ローカル環境のDBでワーカースレッドを意図的に枯渇させてクエリを実行してみました。その結果、本番環境で多発していたものと同様のエラーが発生することを確認できました。 このことから、多発していたDB接続に関するエラーは、ワーカースレッドが枯渇したことが、原因の一つであると判断しました。ということで、昨年の福袋の障害の原因は、DBの状態としては「ワーカースレッドの枯渇により大量のTHREADPOOL waitが発生した」ということが確認できました。 ここで、「ではワーカースレッドの最大数を増やせば解決するのでは?」という疑問がわきます。 ただし、現状のワーカースレッドでも平常時の5倍の要求を一時的に受け付けていることから、単純にワーカースレッド数を増やせば解決する問題でもなさそうです。 次に、ワーカースレッドが枯渇してしまった原因について調べました。Spotlightを使って、エラー発生時に実行中だったクエリリストを確認します。 「Last Wait Type」「Last Wait Resource」から、同一のリソースに対して、大量のページラッチ獲得待ち(PAGELATCH ****)と、キーロック獲得待ち(LCK_M **)がそれぞれ発生していることが確認できます。「LCK_M_U」の情報から、獲得しようとしているロックの粒度がKEYであるため、特定のレコードに対する読み取り要求、更新要求が大量に実行されていることが分かります。 「PAGELATCH_SH」「LCK_M_U」の「Last Wait Resource」に表示されているリソースの値だけでは「どのデータが対象になっていたのか」までは、判断することができません。そこで、次の方法を使用して、データの取得を行いました。 PAGELATCH_SH で出力されていた、Last Wait Resourceの値を使って、具体的にどのテーブルのどのようなレコードが含まれているページへのアクセスであるかを調査しました。 ページラッチ待ちが多発しているLast Wait Resourceの値と「DBCC PAGE」を使ってページの中身をダンプさせます。 DBCC TRACEON( 3604 ) DBCC PAGE (N 'DB名' , 1 , 4xxxxx8, 3 ) DBCC TRACEOFF( 3604 ) 次に、ロック待ち(LCK_M_U)が多発しているLast Wait Resourceについて対応するテーブルとレコードを特定します。KEY:DBID:hobt_id(%%lockres%%)という構成になっているため、下記のクエリを使います。 --1.hobt_idを指定してテーブル名を取得 select sc.name as schema_name ,so.name as object_name ,si.name as index_name from sys.partitions as p join sys.objects as so on p.object_id = so.object_id join sys.indexes as si on p.index_id = si.index_id and p.object_id = si.object_id join sys.schemas as sc on so.schema_id = sc.schema_id where hobt_id = 72xxxxxxxxxxxxx28; --2.取得したテーブルのレコードを、%%lockres%%を使って絞り込んで特定 SELECT * FROM テーブル名 WHERE %%lockres%% = '(4bxxxxxxxx5d)' ; 以上のクエリを実行した結果、ページラッチとロックは同じテーブルの特定レコードに関連する待ちであることが分かりました。購入フローの中でDB上の在庫に関するデータを更新する処理があるのですが、人気福袋の購入希望者が殺到したことで読み取り要求、更新要求の競合が多発してボトルネックとなっていたようです。 図示すると以下のようになります。 ロックはレコード単位で競合するのに対し、ページラッチはページ単位で競合します。そのため、ページラッチについては、人気福袋と同一ページにある他の福袋商品へのアクセスとも競合してしまっていました。 障害発生中、ページラッチ待ちとロック待ちが発生しているクエリは、実行中の全クエリの約半分を占めていました。そのため通常よりワーカースレッド解放に時間がかり、大量のDBアクセスを捌ききれなくなり、ワーカースレッド確保待ちが大量に発生してエラーの多発につながったと考えられます。 この状況を図示します。まず、SQL ServerのCPUのアーキテクチャの概要図です。 SQL ServerはCPU1コアにつき1つのスケジューラが割り当てられます。各スケジューラに対して複数のワーカースレッドが割り当てられます。クエリ実行時にワーカースレッドが1つ以上利用され、クエリの実行が行われます。SQL Server では、作成できるワーカースレッドの数に上限があります。(作成できるワーカースレッド数の上限は、「max worker thread」という設定で変更できます。) 実行されていたクエリのうち、ページラッチとロック競合が発生していなかったクエリは全体の半分を占めていました。これらのクエリではワーカースレッドの利用と解放が高速に行われていました。一方で、解放および競合が発生していたもう半数のクエリでは、ページラッチやロックといったリソース獲得待ち状態で、ワーカースレッドの解放が遅くなっていました。結果として利用可能なワーカースレッドの低下による全体のスループットが低下し、にもかかわらずクエリの要求数は増え続け、慢性的なワーカースレッド枯渇状態に陥って大量のエラー多発、というシナリオだと思います。 人気商品以外の商品を閲覧、購入されていたお客様は、ワーカースレッドが確保できてしまえば、高速に要求を処理できていたと考えられます。一方で人気商品へのアクセスは、ワーカースレッドを運よく確保できたとしても、実行時にロック獲得待ちなどの理由で実行時間がかなり伸びてしまいます。そのためタイムアウトも発生しやすい状況であったはずです。 ただし、12時からの1分間は大量に注文が入っていることから、その後のエラー多発状態へと移行したタイミングで何が起きていたかの説明はできていません。 12時1分を過ぎたタイミングから、ロックおよびページラッチ獲得待ちが大量に発生していることから、ブロッキング(ロック競合)が発生していたと考えられるため、Spotlightでブロッキングのグラフを確認しました。 ブロッキングが大量に発生しています。ブロックされているプロセス(オレンジ色)が多数あるのに対して、ブロックを引き起こしているプロセス(青色)の数は多くありません。少数のプロセスがその他大勢のプロセスをブロックしていることが分かります。ブロッキングの情報をさらに追っていきます。 ↑13時15分時点のブロッキング情報の一部です。Head Blocker(黄色線の情報、ブロッキングを発生させる原因となっているプロセス)のStatusが「Seeping, blocking」となっています。つまり、スリープしているプロセスが、大量のプロセスをブロックしていたことになります。「スリープしてるのにブロックしてる」とはどういう状況なのだろうと調べてみたところ、同じような症状に関する記事を見つけました。 Sleeping SPID blocking other transactions この記事によると、明示的なトランザクションを張ったまま途中でタイムアウトしてしまうと、クエリの実行方法によっては、ロールバックされずトランザクションが継続された状態になります。したがってロックを獲得している場合はロックも保持し続けることになります。ローカル環境にて実験したところ、確かに再現しました。 ただし、ローカル環境で試す中でタイムアウト後に即座にロールバックされる場合もありました。調べたところ、明示的にトランザクションを開始し、ロックを保持したままタイムアウトした場合、そのロックの解放タイミングは「コネクションプールに戻ったコネクションが別のクエリによって再利用されるとき」でした。それは0.1秒後かもしれないし、数分後かもしれません。 ※コネクション/コネクションプールの仕組みについては この記事 で非常に詳しく書かれていました。 昨年の福袋イベントのケースにあてはめて考えると、大量にアクセスされていた状況ならすぐにコネクションが再利用されることでロックが解放されてもおかしくないのでは?という疑問がわきます。この疑問を検証するために実験を行ったところ、ロールバックされるためには、コネクションの再利用時にまずワーカースレッドを確保する必要があるとの結論に至りました。 これらの調査を踏まえて、福袋発売後に以下のシナリオが発生してバッチ実行数が著しく低下したと考えました。 1.購入フローにおいて、人気商品に関するデータへの読み取り要求、更新要求が大量に発生。 それに伴い特定のレコードに対してのロック、ページラッチの獲得待ちが発生。 2.人気商品データのロックを獲得したプロセスAが処理の途中でタイムアウト。 ロールバックされず、取得したロックが解放されないままコネクションプールに戻る。 3.別プロセスが該当コネクションの再利用を試みるも、 ワーカースレッド枯渇のためTHRADPOOL waitが長時間発生してエラー発生。 そのためロールバックとロックの解放が長時間おこなわれなかった。 4.プロセスAが保持したロックによって、他のプロセスが大量にブロックされる。 (複数の人気商品において2~4の事象が発生) 5.ブロッキングによって各プロセスの実行速度が急速に低下。 ワーカースレッドの解放が遅くなり全体としてスループットが低下した。 昨年の障害発生原因を踏まえた対応を実施 ここまでのまとめ ・昨年の福袋イベント中にエラーが多発したのは、DB内部でワーカースレッド確保待ちが大量発生したため ・ワーカースレッド確保待ちが大量発生したのは、人気福袋データに対するクエリ実行要求が集中し、かつタイムアウトしたプロセスがロックを解放しないことでブロッキングが急激に増大したため ・昨年の障害発生時には、以下の五項目の待ち時間が圧倒的に多かった THREADPOOL LCK_M_U LCK_M_S PAGELATCH_SH PAGELATCH_EX 以降では、昨年の障害調査を踏まえて実施した負荷対策についてまとめます。 対策1. プロセスをSleepingかつBlockingにさせない タイムアウトしたプロセスがそのままロックを掴みっぱなしになっていたことが、ブロッキングの状況を加速度的に悪化させた原因のため、タイムアウト時のトランザクションを適切に処理する必要があります。 対応方法としては、 SET XACT_ABORT ON をトランザクション開始前に設定しました。このオプションは、トランザクション内でエラーが発生すると即座にロールバック+ロックの解放を行うように指示できるオプションです。 この対策を実施することで、特定のクエリがタイムアウトした途端にブロッキングが加速するというリスクを無くすことができます。 対策2. 昨年多く発生していた待ち事象を減らす ■ THREADPOOL この待ち事象は、ブロッキングが大量に発生したためにワーカースレッドの解放が遅れたことが原因と考えられるため、THREADPOOL Waitを減らすためには、ブロッキングを減らす必要があります。ただし、万一ブロッキングが大量発生する等の理由でワーカースレッドが枯渇するときのことを考慮し、ワーカースレッドの最大数を2倍に増やしました。合わせて、maxサーバーメモリの値を減少させました。これは、ワーカースレッドの確保にはメモリが必要となり、このメモリはバッファプール用に確保しているメモリ(maxサーバーメモリ)とは別の領域から確保する必要があるためです。 そのため、maxサーバーメモリを減少させ、ワーカースレッドを作成する際に必要なメモリを確保できるように、メモリサイズの調整を行いました。 この対応により、THREADPOOL Waitを削減できるわけではありませんが、大量にワーカースレッドを使用する環境下においては同時実行性能の向上が期待できます。 ■ LCK_M_U 購入フローの一部にDB上の在庫に関するデータを更新する箇所があり、人気商品の購入希望者が殺到すると、人気商品の在庫に関するデータレコードの更新処理で大量のブロッキングが発生していました。これを軽減させるために、人気商品に関しては、サイト上の見え方およびユーザーには影響ない形で在庫に関するデータを分割して販売しました。 ■ LCK_M_S 一部のクエリにwith(nolock)というロックヒントを付与することで、Sロックよりも競合が少ないSch-Sロックを取得するに留めさせました。もちろん、ダーティリードを許可できる箇所である、というのが前提となります。 ■ PAGELATCH_SH / PAGELATCH_EX 獲得できるページラッチは1ページあたり1つだけのため、同一ページ内の異なるレコードへの更新要求同士であってもページラッチの競合は発生します。そのため、1ページの中に福袋商品の在庫に関するデータレコードが1つだけ存在する状況を意図的に作り出しました。具体的には、福袋商品以外のレコードは、サイト上からは見えないダミーの在庫に関するデータレコードで埋めました。これにより、ページラッチ競合の改善が期待できます。 これらをすべて対応した際のイメージを以下の図にまとめました。 昨年の状況と比較すると、以下のように読み取り要求、更新要求に伴う排他制御の分散が期待できます。 当日のモニタリング環境の整備 昨年の障害時はワーカースレッドが枯渇していたため、クライアントソフトから情報収集のためのクエリを実行することすらできない時間帯がありました。そこで今回は、ワーカースレッドの枯渇に備えて、 DAC で事前に接続しておき、いかなる状況でもDBから情報を採取できる体制を整えておきました。DACで接続すると、専用のワーカースレッドをつかえるので、ワーカースレッド枯渇時でもクエリが実行できます。今年はワーカースレッドが枯渇することはありませんでしたが、準備しておいたことでいつでも情報取得できるという安心感につながりました。 今年の福袋イベントについて 本記事で紹介した負荷対策を実施したことで、今年は福袋イベントを障害無しで乗り切ることができました。 以下のグラフは、去年と今年の福袋イベントにおける、分間注文数の推移をグラフにしたものです。 今年は、開始と同時に瞬間的に注文数が跳ね上がり、その後ゆるやかに減少しています。今年はほぼエラーが起きていないため、あるべき注文数の推移であり、お目当ての商品をスムーズに購入していただけたようです。昨年と今年の波形と比較すると、昨年は障害発生によって注文数の波形が不自然であることがよくわかります。 また、DB起因のエラーも、昨対比で99.99%以上削減でき、平常時とほぼ同じようなサーバー負荷状況でした。 まとめ 本記事では、昨年障害を発生させてしまった福袋イベントについて、障害原因に関する調査内容をご紹介しました。また、調査結果を踏まえて今年実施した負荷対策についても紹介しました。 人気商品にアクセスが集中するという特定の性質をもったワークロードにおいて、同時実行性能をできる限り向上させることに注力したことが功を奏しました。性質が異なれば実施すべき対応策も変わってくるため、ワークロードの性質を適切に把握することと、障害発生時はできる限り根本的な原因を特定することが、何よりも大切だなと感じました。 今年はお客様にストレスなく福袋イベントを楽しんでいただけたと思いますので、エンジニア一同がんばってよかったです! ZOZOテクノロジーズでは、一緒にサービスを作り上げてくれるエンジニアを大募集中です。ご興味のある方は、以下のリンクからぜひご応募ください! https://www.wantedly.com/companies/zozo-tech/projects
こんにちは、ZOZOテクノロジーズ VPoEの今村( @kyuns )です。 この記事は ZOZOテクノロジーズ Advent Calendar の25日目の記事になります。 今年の4月にスタートトゥデイテクノロジーズ(現ZOZOテクノロジーズ)が発足してから約8ヶ月が経ちました。新型ZOZOSUITやプライベートブランド「ZOZO」の発表など今年は色々と新しいチャレンジをしていた弊社ですが、外から見たときにエンジニア観点だとまだまだ謎めいている部分がたくさんあると思います。 ちょうど先日代表の前澤がツイートしたことでTwitter採用が話題になりましたが、反響も非常に大きく、多くの方にご応募いただき実際に何名かのエンジニアを採用することができました。 news.yahoo.co.jp この祭りでも 非常に多くの質問 をいただきましたが、このエントリではZOZOテクノロジーズが行っている事業やプロダクトの話を交えつつ、ZOZOテクノロジーズが求めているエンジニア職を改めて紹介したいと思います。 この記事が少しでも弊社を知ってもらう機会となれば幸いです。 ZOZOテクノロジーズとは? 株式会社ZOZOテクノロジーズは株式会社ZOZOの100%子会社です。 ZOZOとZOZOテクノロジーズ、それぞれ主な役割が違っており、ZOZO本体にはZOZOTOWNを運営するために必要なスタッフ、例えばEC営業・マーケティング・カスタマーサポート・ZOZO BASEなどの物流まわりのスタッフやプライベートブランドの服作りをするスタッフなどが在籍しています。 一方ZOZOテクノロジーズにはWebサイトやアプリ、システム開発を行う為に必要なスタッフ、エンジニア・デザイナー・アナリスト・リサーチャーなどが在籍しています。 2018年12月25日時点ではZOZOテクノロジーズには約250名の従業員が在籍し、そのうちエンジニアは約150名ほどになります。 企業理念、事業理念、経営理念 我々の企業理念はZOZO本体と同じく 「世界中をかっこよく、世界中に笑顔を。」 テクノロジーズの事業理念は 「70億人のファッションを技術の力で変えていく」 です。 事業理念が示すとおり、我々の対象は世界70億人、そして領域はファッション、その手段として技術を重要視しています。 経営理念は 「いい人をつくる」 です。 ここでいういい人とは以下の3つを満たした人物です。 想像力と創造力に富んだ人 GIVE&TAKEのバランスがとれた人 いい人を作れる人 また、 前澤代表のメッセージ にもあるように 我々は 「楽しく働く」 ことを徹底するような行動を心がけています。 どんな事業が存在するのか? 現在ZOZOテクノロジーズが関わる主な事業は以下の4つになります。 日本最大級のファッションECサイト「ZOZOTOWN」 自分サイズのベーシックアイテムブランド「ZOZO」 日本最大級のファッションコーディネートアプリ「WEAR」 ファッションに関する研究開発を行うZOZO研究所 それぞれの事業が行っているプロダクト開発の内容を、どのようなエンジニアが向いているのかということも含めて紹介していきたいと思います。 ZOZOTOWN ZOZOのメイン事業である日本最大級のファッションショッピングサイトです。 年間購入者数は730万人以上と非常に多くのユーザーに利用されています。 ZOZOTOWNの特徴としてはシステム開発・デザイン・物流など、ECに関わる機能を全て自前で開発しています。そのため色々な箇所でエンジニアの力が必要になってきます。 募集職種 ZOZOTOWN SRE/インフラエンジニア(オンプレミス) ZOZOTOWNおよびWEARのオンプレのインフラを支えるエンジニアになります。 現在ZOZOTOWNのインフラのほとんどはオンプレに存在しています。画像サーバーは一部AWSに置くなど、クラウドへの移行も進めていますが、まだまだオンプレの役割も重要です。そのため、オンプレに詳しいインフラエンジニアが必要となります。オンプレでの運用経験があってハードウェアやネットワークに詳しいけど、クラウドにも興味があるというようなインフラエンジニアが非常に向いています。 利用技術: Windows Server(IIS) / VBScript / SQL Server / 仮想化基盤 / 各種NW機器 ZOZOTOWN フロントエンドエンジニア(マークアップ) ZOZOTOWNのサイト改修やコーディングを行うエンジニアです。フロントエンドには現在ZOZOTOWNのUI改善や運用を行うチームと、特集などの企画ページの開発を行うチームがあります。ZOZOTOWNのトラフィック規模になると少しのUI改善が売上に直結します。たとえ0.1%の改善でも売上への影響は非常に大きく、様々なUI改善の施策をやりがいをもって開発することができます。また後者のチームでは、非常に多くの企画や施策を行います。しかもその企画を通した売上規模は何十億円となることもあります。どちらのチームともUI/UXにこだわりをもって開発していけるようなエンジニアが向いています。 また、モダン化も並行して進めているので、今のZOZOTOWNのフロントエンドをもっと新しくパフォーマンスよくしたい!という方、大歓迎です。 利用技術: HTML / CSS / SASS / JavaScript(ES5) / jQuery ZOZOTOWN フロントエンドエンジニア(iOS/Android) ZOZOTOWNのiOS/Androidアプリの開発を担当するエンジニアです。 利用ユーザーも非常に多く、どの画面をとってもUI/UXの改善が使い勝手や売上に直結します。 また、QAエンジニアと連携してテストの自動化などを行ったりもしており、より効率的な開発を行えるような基盤づくりなどもしています。とにかくこだわりを持って大規模ECサイトのアプリを開発したいアプリエンジニアが向いています。 利用技術: iOS: Xcode / Objective-C / Swift / Carthage / Bitrise Android: Android Studio / Java / Kotlin / CircleCI ZOZOTOWN バックエンドエンジニア(Web/API) ZOZOTOWNのサーバーサイドの開発を行うエンジニアです。サーバーサイドといっても非常に多くの機能があり、既存機能の改修や新機能の開発、iOS/Androidアプリに必要なAPIの開発なども行います。 商品検索やカート、決済処理部分など、ZOZOTOWNの非常に重要な部分を担う開発を担当しています。 商品検索を改善していきたい、様々なミドルウェアを扱いたい、APIの設計にこだわりをもってやりたい、と思えるようなエンジニアが向いています。 利用技術: Windows Server(IIS) / VBScript / SQL Server / ElasticSearch ZOZOTOWN バックエンドエンジニア(リプレイスプロジェクト) ZOZOTOWNの既存システムをクラウド移行するための開発を行うエンジニアです。 凄まじい勢いで成長してきたZOZOTOWNは、VBScriptを用いた開発を始めとして、システム的にもレガシーな部分が存在しているのが事実です。 そのため、このような技術的負債に対応するために将来的な拡張性や柔軟性の確保を見据えて、既存のオンプレ環境をパブリッククラウドへと移行するプロジェクトを進めています。単純にクラウドへ移行するといってもその規模は巨大ですし、システムの停止は極力避けなければなりません。 大規模プロジェクトになるため、いくつかのフェーズに分けて現在取り組んでいます。 アーキテクチャとしてはDocker/Kubernetesを中心としたクラスタ構成になっており、セールなどの負荷にも柔軟に耐えれるようなAPIの開発を行っています。 モダン技術を用いて巨大なAPIやインフラ基盤の開発を行いたいエンジニアが向いています。 利用技術: SQL Server / Java / Spring / Azure / Docker / Kubernetes / NewRelic / Wercker / SonarQube / Datadog / PagerDuty / Prometheus / Sentry リプレイスまでの道程や詳細については、 今月号のWEB+DB PRESS Vol.108 で特集されていますので、気になった方はぜひご覧ください。 ZOZOTOWN バックエンドエンジニア (マーケティングオートメーション) 何百万人ものZOZOTOWNのユーザーとの接点を受け持つ部分の開発を支えるエンジニアです。 プッシュ通知やメルマガ配信、LINE@のメッセージ配信など、ユーザーとのコミュニケーションを技術の力でより良くしていく部門であり、A/Bテストやデータを用いたレコメンデーションなどを行います。 ユーザーの行動に応じて、様々なチャネルで、最適な情報を届けるためのリアルタイムマーケティングシステムと呼ばれるシステムの開発も行います。 また、社内に存在するあらゆるデータを集約させるためのデータ基盤の開発も行っています。 データ基盤にはBigQueryを用いており、fluentd・Embulk・Digdagなども活用しています。 ここに集められたデータはサービスの改善や研究開発などに利用されます。 データの規模も非常に大きく、何百億レコード、数百テーブル、リアルタイムで飛んでくるイベントログなどをうまくさばく技術が必要になります。 データをつかってユーザーに最適な情報を届けたい、ユーザーとの接点をより良くしたい、大量のデータをうまく扱えるような巨大データ基盤を開発したい、と思うようなエンジニアが向いています。 利用技術: AWS / GCP / JBoss Data Grid / Aurora / Java / Ruby / fluentd / Embulk / Digdag ZOZOTOWN バックエンドエンジニア(基幹システム) ZOZOTOWNのバックオフィスと呼ばれる裏側を支えるシステムの開発を行うエンジニアです。 主に、社内や取引先に開放している販売管理システムや物流業務システムの企画・開発・改修やブランド様などからの問い合わせ対応などを行います。また、弊社は物流ロジスティクスも自前で保有しているため、エンジニアリング視点でどのオペレーションや業務を効率化できるか、という視点がとても重要になります。他社とのデータ連携などを受け持つデータ連携基盤の開発なども行っています。 ZOZOTOWNを支える裏側の仕組みに興味がある人、倉庫や物流などのオペレーションを技術で改善していくことに興味のあるエンジニアが向いています。 利用技術: Windows Server(IIS) / VBScript / SQL Server ZOZOTOWN ロボットエンジニア(BASE/倉庫) ZOZOTOWNの物流倉庫で利用予定のロボットの開発を行うエンジニアです。既にある程度自動化されているZOZOBASEですが、さらなる自動化のために各種ロボットの導入を検討しています。様々な最適化のためにシュミレーションを行ったり、実際にロボットの試作機の開発などを行います。ハードウェアやロボット開発に興味のあるエンジニアが向いています。 利用技術: C / C++ / Python / CAD / Solidworks プライベートブランド「ZOZO」 プライベートブランド「ZOZO」(以下PB)に関する事業です。 PBはZOZOSUITを用いて計測した、ユーザーのサイズにジャストフィットなベーシックアイテムを販売する事業になります。現時点ではTシャツやデニム、シャツやビジネススーツ、ニットなど様々なアイテムを販売しています。PBは日本以外の国でも販売を開始しており、海外向けのグローバルECサイトzozo.comをはじめとしてZOZOSUIT、スマートファクトリーなど最新の技術が活用されるプロダクトになります。 PB事業には以下のプロダクトが存在します。 海外向けグローバルECサイト zozo.com ZOZOSUIT 生産管理/ERP 生産管理/生産マネジメント パターン自動生成 スマートファクトリー PB事業ではウェブサイトだけでなく、ZOZOSUITやスマートファクトリーなど、ソフトウェアだけでなくハードウェアの技術なども必要とされているのが特徴です。 海外向けグローバルECサイト zozo.com プライベートブランド「ZOZO」を海外に向けて販売していくためのサイト「 zozo.com 」の開発を行います。海外の拠点とやり取りする機会も多く、業務内容によっては海外出張に行くこともあります。 募集職種 グローバルECサイト フロントエンドエンジニア(Web) グローバルECサイトのフロントエンドの開発を行います。グローバルECサイトのフロントエンドは現在TypeScriptとVue.jsを用いたSPAで作られており、多言語対応や決済機能まであるSPAの規模としても非常に大きいものになります。また、現在Nuxt.jsを用いたリニューアルを行うなど、フロントエンド周りの新しい技術や海外事業に興味のある人が向いています。 詳しくは こちらのスライド をご覧ください。 利用技術: HTML / CSS / Vue.js / Vuex / webpack / TypeScript / Nuxt.js グローバルECサイト フロントエンドエンジニア(iOS/Android) プライベートブランド「ZOZO」のiOS/Androidアプリの開発を行います。世界中で使われるアプリのため、多言語対応などももちろんのこと、使いやすいUI/UXを考慮しながら開発する必要があります。世界で使われるアプリを開発してみたいというアプリエンジニアが向いています。 利用技術 - iOS: Xcode / Objective-C / Swift / Carthage - Android: Android Studio / Java / Kotlin グローバルECサイト バックエンドエンジニア(API) グローバルECサイトのサーバーサイドの開発を行います。フロントエンドがSPAなのでAPIサーバーを主に作ることになりますが、グローバルECサイトなので国際化対応に紐づくような決済、通貨、サイズ、配送など様々な事象を考慮したサイトの開発を行う必要があります。サーバーサイド側のAPIは Scala / Playで開発されており、Scalaを用いたマイクロサービスの開発に興味があるエンジニアが向いています。 利用技術: AWS / Aurora / Postgres / DynamoDB / Scala / Play Framework / nginx / Sentry / CircleCI グローバルECサイト インフラエンジニア/SRE(クラウド) グローバルECサイトやZOZOSUITの計測サーバーのインフラの開発および運用を行います。世界展開を考慮した上でのインフラ設計やマイクロサービス実現のためのインフラの構築を行います。現在はすべてAWSで動いています。グローバル展開しているサイトのインフラに興味がある人が向いています。 利用技術: AWS / Aurora / Postgres / DynamoDB / Scala / Play Framework / nginx / Sentry / CircleCI ZOZOSUIT ZOZOSUITの開発や計測精度向上などを行います。 募集職種 ZOZOSUIT バックエンドエンジニア(ZOZOSUIT) ZOZOSUITの計測データを受け取るサーバーの開発を行います。世界中から集まるZOZOSUITのデータを安全に管理し、様々なチームと連携しながら安全にデータが利用できるようなAPIの開発を行います。ZOZOSUITに興味のあるエンジニアがとても向いています。 利用技術: AWS / Aurora / Python / Flask ZOZOSUIT フロントエンドエンジニア(iOS/Android) ZOZOSUITの計測SDKの開発を行います。様々なチームと連携しながらZOZOSUITの計測精度の向上なども行っています。ZOZOSUITの計測精度向上に興味があるアプリエンジニアがとても向いています。 利用技術 - iOS: Xcode / Objective-C / Swift / Carthage / Bitrise - Android: Android Studio / Java / Kotlin / CircleCI 生産管理 バックエンドエンジニア(ERP) プライベートブランド「ZOZO」の生産管理や在庫管理、会計管理などを行う独自のERPの開発を行います。サイトとの連携APIだけでなく工場とのデータ連携など、非常に多岐にわたるシステムの開発を行います。生産の裏側を技術で支えていきたいエンジニアが非常に向いていると思います。また、このチームではGoをメイン言語として利用しています。 利用技術: AWS / Postgres / Go / Docker 生産管理 インフラエンジニア/SRE (ERP) プライベートブランド「ZOZO」の生産管理システムのインフラの開発、運用を行います。日本国内だけでなく海外の工場との連携などもあるため、世界各国からの利用を意識したインフラの構築を行う必要があります。世界を股にかけるインフラ基盤を構築してみたいインフラエンジニアが向いていると思います。 利用技術: AWS / Postgres / Go / CloudFormation / Docker 生産管理 バックエンドエンジニア(生産マネジメント) プライベートブランド「ZOZO」の生産工場へのシステム導入支援や、プロジェクトマネジメントを行います。海外にある生産工場に出向いて、工場へのシステムの導入支援や運用の改善、管理システムの開発などを行います。基本的に海外出張が多いので海外に興味があり、工場の効率化や自動化に興味のあるエンジニアがとても向いています。 利用技術: AWS / Postgres / Go / CloudFormation / Docker パターン自動生成 エンジニア(パターン自動生成) ZOZOSUITで計測したデータを用いて、ユーザー一人一人に応じた最適なサイズの型紙(パターン)を自動生成する完全オーダーメイド技術の開発を行います。エンジニアとして服作りを勉強してみたいエンジニアがとても向いています。 利用技術: 言語非公開 / 服作りの知識 / CAD スマートファクトリー 自動縫製工場におけるファクトリーオートメーション化の機械制御アプリケーションの開発や自動裁断機、ミシン、ハンガーラインなどの制御プログラムの開発、社外のハードウェアと連携したアプリケーションやIoTプログラムの開発を行います。 募集職種 スマートファクトリー バックエンドエンジニア(Web/アプリケーション) PB「ZOZO」の縫製工場におけるファクトリーオートメーション化にむけたアプリケーションの開発/運営を行うエンジニアです。システム設計、インターフェース設計、インフラ設計、データベース設計、コード設計、実装、レビューなど、スマートファクトリーの実現に関する非常に幅広い業務を担当していただきます。 WebやIoTなどに興味のあるエンジニアが向いています。 利用技術: AWS / Go スマートファクトリー バックエンドエンジニア(制御系) ファクトリーオートメーション化における機器を操作するためのアプリケーション(ミドルウェア)を開発するエンジニアです。モーターやセンサーなどを駆使して生産ラインの自動化を洋服のプロたちと主に目指します。Webシステムから自動裁断機、ミシン、ハンガーラインをコントロールするためのシステム全般業務をしていただきます。ロボットアームや、センサー制御など、ハードウェアの知識も必要になります。色々なメーカー出身の方々が集まってスマートファクトリーの実現を目指しています。回路設計やロボットアームの制御など、ハードウェア経験があって新しいことを実現したいエンジニアが向いています。 利用技術: C / C++ / Go / ROS WEAR 日本最大級のファッションコーディネートアプリです。世界1200万ダウンロードを超え、さらなる成長を目指しています。投稿されているコーディネートの投稿数も800万点以上、MAUが1100万と巨大なファッションメディアになります。また、10万人以上のフォロワーを持つWEARISTAと呼ばれるユーザーがいたり、高橋愛さんや吉岡里帆さんなどをはじめとした 多くの芸能人 にも利用されています。 募集職種 WEAR フロントエンドエンジニア(マークアップ/Web) WEARのPC/SPサイトのフロントエンドの開発を行うエンジニアです。WEARは多くの面白い特集や企画なども頻繁に行っています。またチームメンバーにはWEARがとにかく大好きなエンジニアが多く、ファッションメディアサービスを開発したいエンジニアが非常に向いています。 利用技術: HTML / CSS / JavaScript / jQuery / Windows Server(IIS) / VBScript / SQL Server WEAR フロントエンドエンジニア(iOS/Android) WEARのiOS/Androidアプリの開発を行うエンジニアです。ベストアプリも受賞したことのあるアプリになりますので、UI/UXを非常に大事にしています。メディア系サービスのアプリ開発を行いたいエンジニアが向いています。 利用技術: iOS: Xcode / Objective-C / Swift / Carthage Android: Android Studio / Java / Kotlin WEAR バックエンドエンジニア(Web/API) WEARのサーバーサイド側の開発を行うエンジニアです。います。コーディネートの検索機能の改善を行ったり、アプリのAPIの開発などもやコーディネートの検索をより使いやすくしたり、ユーザーファーストな開発を行っています。 利用技術: Windows Server(IIS) / VBScript / SQL Server WEAR バックエンドエンジニア(リプレイスプロジェクト) こちらも同じくサーバーサイドですが、現在のWEARプロジェクトを新しいアーキテクチャでリプレイスする開発を行っています。現在ZOZOTOWNやWEARはVBScriptで書かれていますが、ZOZOTOWN側にもリプレイスプロジェクトがあるように、こちらWEAR側でもRubyを用いたリプレイスプロジェクトを進めています。大規模メディアサイトをRailsで開発したいエンジニアが向いています。 利用技術: AWS / Ruby / Ruby on Rails / Aurora ZOZO研究所 ( ZOZO RESEARCH ) ZOZOテクノロジーズの研究開発部門として今年の1月に「ファッションを数値化する」をミッションとしてZOZO研究所は設立されました。 現在は青山および福岡に拠点が存在し、約20名程度のメンバーが在籍しています。 ZOZOのビッグデータを用いた研究開発や、機械学習、深層学習を用いたアルゴリズムの開発、新しい技術を用いたプロトタイプの開発など様々な活動を行っています。 現在ZOZO研究所では主に3つのテーマを軸に研究開発を行っています。 最近は コーデ生成に関する論文を投稿 したり、 海外カンファレンスでも発表 したりする機会が増えてきました。世界一のファッション研究所にすべく、新しいチャレンジを沢山行っているチームになります。 募集職種 リサーチャー ファッションに関する技術の研究開発を行うエンジニアです。研究領域の対象はECサイトの利便性向上だけでなく、物流最適化、ZOZOSUITに代表されるような計測デバイスなど、非常に多岐にわたります。リサーチサイエンティストは様々な課題に対し、各自が最も興味のある対象と得意とする手法を選択して研究に取り組むことができます。 有効性が確認された研究成果はジャーナルや国内外のカンファレンス等での発表以外に、エンジニアやデザイナーと協力しながら実際のサービスに導入します。ZOZOにしか出来ない研究を行い、世界をリードするような研究成果を出していきたい人が向いています。 利用技術: 機械学習(MachineLearning) / 深層学習(DeepLearning) / Python / Chainer / TensorFlow / PyTorch / AWS / SageMaker / GCP / GPU / TPU MLエンジニア(機械学習/深層学習) リサーチサイエンティストとともに、ZOZOTOWNやWEARから得られるビッグデータを使い、ユーザークラスタリング、画像認識、画像生成タスクなどの機械学習・深層学習を用いた実装を行うエンジニアです。 アルゴリズムの開発、実装だけでなく、サーバー側への実装なども行います。自らの技術を用いて、ZOZOTOWNや事業に貢献したいエンジニアが向いています。 利用技術: 機械学習(MachineLearning) / 深層学習(DeepLearning) / Python / Chainer / TensorFlow / PyTorch / AWS / SageMaker / GCP / GPU / TPU リサーチエンジニア(最新技術) 今後流行するであろう技術を用いたPoCの開発などを行うエンジニアです。現在だとAR/VRなどを用いた3D試着やGoogle HomeやAmazon Echoなどのスマートスピーカーを用いたPoCの開発など様々な開発を行っています。未来のサービスのベースとなるようなコンセプトを、新しい技術やZOZOの資産を活用しつつ実現するエンジニアになります。Webやアプリ、サーバーサイドなど、幅広い技術や最新技術に興味関心のあるエンジニアが向いています。 利用技術: Web / iOS / Android / GCP / AWS / Google Assistant / Unity その他 上記で紹介した4つの事業以外にもエンジニアを募集しています。 募集職種 コーポレートエンジニア(社内情報システム) ZOZOグループの社内インフラや社内システムの開発、設定、運用を担当していただきます。オフィス各拠点だけでなく、各倉庫の拠点のための社内ネットワークやセキュリティ、デバイス管理やグループウェアなどその業務領域は非常に多岐に渡ります。自らの技術や働きによって、社員が業務を効率的に行えるために何ができるか、を自ら考え提案および改革を推進できる必要があります。現在はG Suiteへの移行やリモートワーク環境の整備なども行なっています。技術で社員を裏側から支えたいエンジニアが非常に向いていると思います。 利用技術: Azure Active Directory / Office 365 / G Suite / Firewall / 拠点間NW構築 / Cisco / リモートワーク関連技術 / ISMS / GDPR 20年度新卒エンジニア ZOZOテクノロジーズでは20年度卒の新卒エンジニアを募集しています。 フロントエンド、サーバーサイド、インフラ、iOS/Android、データサイエンティストなど幅広いエンジニアやデザイナーを募集しています。 労働環境について さて、ここまで30職種近いエンジニア職を紹介してきました。働く上で仕事内容も重要ですが、労働環境も重要です。ここからはZOZOの労働環境について紹介していきたいと思います。 勤務場所 ZOZOテクノロジーズのオフィスは現時点で青山、幕張、福岡の3拠点が存在します。 担当するプロダクトによって勤務場所が変わりますが、ざっくり分けると現時点では以下のような形です。 青山 PB(グローバルEC) / ZOZOTOWN(アプリ) / 新規事業 ZOZO研究所(青山) 幕張 ZOZOTOWN(アプリ以外) / WEAR / PB(生産) 福岡 ZOZO研究所(福岡) よく幕張勤務ですか?と尋ねられますが、担当するプロダクトによって変わるため必ずしも幕張というわけではありません。幕張と青山の人数比率はおおよそ6:4です。 勤務体系や働き方など ほとんどの社員はフレックスタイム制(コアタイム10:00〜17:00)になっており、 一部の専門スキルを持っているスペシャリストおよび研究所メンバーは裁量労働制となっています。 子供がいるママさんなどは時短勤務の制度がありますので、そちらを利用して働いています。ちなみに弊社は出産された方の産休復帰率が100%となっており、子供を持つ母親としても非常に働きやすい環境となっています。 また、リモートワークに関しては現時点では正式に制度としては存在していませんが、現在環境を整えている最中です。世の中の働き方が多様化する中、しっかりと弊社でもリモートワークができる体制や制度を整える必要があると感じており、着々と準備を進めています。 給与 一番良く質問されるのが「給与って全員一律なんでしょう?」という質問です。 この問いに対する答えは半分正解、半分間違いです。 現在、給与は 基本給+職能給+賞与+各種手当 という構成になっており、基本給および賞与は全員一律同じですが、職能給は各々によって違います。 職能給はいわゆる給与テーブルによって定義されており、年2回の人事考課面談によって決定されます。給与の上限は、天才枠採用の場合最大1億円となります。 あくまでもその人のスキルや実績に合った適切な報酬を支払う、というスタンスです。 環境、福利厚生、制度・手当等 ZOZOでは社員がパフォーマンスを最大限発揮できるための環境を最大限用意できるようにしています。特徴的なものをいくつか紹介いたします。 支給マシン エンジニアは入社の際に希望のPCスペックを選ぶことができます。MacやWindows、iMacやMacBookProなど、モニターやキーボードなど必要に応じたマシンやスペックを選択可能です。 公休 会社としての休みは土日・祝日、育休、慶弔などがあります。有給は試用期間終了を待たずとも、入社日に10日間付与されます。(付与日数は入社月によって変動) 夏休み、冬休みがともに3日間存在します。有給とあわせてまとめて1週間ほど休んで旅行に行く人などが多い印象です。 住宅通勤手当 住宅通勤手当として全員に月5万円が支給されます。会社の近くに住めば実質5万円がまるまる住宅補助となります。 家族手当 家族がいる人には家族手当として扶養家族一人あたりにつき月5,000円が支給されます。 社員割引 自社サービスを積極的に使ってもらう為にも、ZOZOTOWNの商品を社員特別価格で購入することができます。 退職金制度 3年以上在籍している社員には退職金が在籍年数に応じて支払われます。社員にはZOZOで長く働いてほしいという思いから、このような制度があります。 書籍購入補助 書籍購入の補助が全額出ます。事前の申請などは必要なく、上限も今の所存在していません。ルールとしてあるのは、本のレビューをslackに投稿するだけです。 カンファレンス参加費用補助 国内外のカンファレンスへの参加費用を全額負担します。例えば今年はGoogle I/O、WWDC、AWS Re:invent、Google Cloud Nextなど、海外の主要なカンファレンスに参加しています。航空券やホテル代などは全て会社が負担しますし、さらに出張手当として1日あたり一定の金額が支給されます。 社会人ドクター支援制度 ZOZO研究所の研究に関連する分野において、博士号の取得を目指す方を対象とした制度です。共同研究先との研究開発及び修学を優先的な業務とし、給与の支給のほか修学費用を支援します。これにより働きながら学位取得を目指すことができるため、経済的な不安解消と研究時間の確保によるスキル向上が可能となります。 副業OK 本業に支障をきたさないという前提で、会社として副業を許可しています。個人サイト運営、アプリ開発、友達の手伝い、書籍執筆など自らの可能性を広げるための活動を行っている人達もいます。 この他にも現在様々な制度を整備している最中です。どうすれば今よりももっと生産性が高く、より効率的に楽しく働ける環境にできるかどうか、日々模索しています。 テックカンパニーとしてのZOZO ZOZOテクノロジーズが誕生してまだ8ヶ月、僕自身がZOZOのエンジニアリング組織を任されるようになってからまだ日が浅いですが、着実にテックカンパニーへと生まれ変わろうとしています。 ZOZOにJOINしてから感じていることとして、この会社はZOZOSUITのように理想や夢を現実のものにできる、そんな素晴らしい力を持ったエンジニア達がたくさんいます。 テックカンパニーとしてのビジョンを描き、仕組みや制度を整えることによって、確実にもっと強いエンジニアリング組織にできるという確信があります。どういう取り組みを行っているかなど、ZOZOのエンジニアリング組織についてのお話はまた後日詳しく紹介できればと思います。 まとめ ここまで紹介してきたとおり、ZOZOでは非常に多様なエンジニアを募集しています。 言語もインフラも、ソフトウェアだけでなくハードウェアまで様々な技術スタックを活用してサービスの開発を行うことができますし、皆さんの技術を活かせる仕事が必ずあると思います。 ファッションの世界を技術で変えていくことに興味のある方はぜひ応募してみてください。 この記事を読んで、ZOZOに応募してみたいと思ったエンジニアの方は下記の採用ページからぜひご応募ください。その際に「テックブログみました」と書いていただけると幸いです。 tech.zozo.com 2019年も世間をあっと驚かせるようなプロダクトを出していきたいと思います。 私達と一緒に楽しく働いてみませんか?
こんにちは。そろそろ生後7か月になる息子が可愛くて仕方がないうっちー(@k4ri474)です。 12/10〜12/13に開催された KubeCon+CloudNativeCon へ参加してきました。 大型カンファレンスらしい演出のKeyNoteやハンズオンセッション、プレゼンテーションなど盛り沢山なイベントでした。 今回、僕はKubernetes運用経験やDockerを用いた設計・開発の経験がほとんどない状態で参加しました。 現状はチーム・個人として業務の中心にはなっていないものの、今後の技術選定に関わるタイミングで選択肢としてKubernetesを持てるようにキャッチアップしておきたかったというのが理由です。 知っていて選定しないのと、知らずに選定できないのとでは大きな違いがあると思っています。 カンファレンスはKubernetesの盛り上がりを肌で感じて手を動かして身につけるキッカケにしようと意気込んで申し込みました。 本記事では、ビギナー目線で印象に残ったセッションをメインにしていくつかご紹介します。 KeyNoteは後ほど動画がアップロードされるかと思うので取り上げません。また、セッションのスライドURLを貼りましたが、埋め込みできる形式でアップロードされておりませんでしたので、リンクを開いてご覧いただければと思います。 KubeCon+CloudNativeConとは コンテナオーケストレーションツールであるKubernetesと、CNCFがホストするPrometheusやEnvoyなどのOSSについて、最新技術やユースケースなどが紹介されるカンファレンスです。 概ねKeyNoteから始まってプレゼンテーション・ハンズオンを挟み、KeyNoteで終わるといった構成になっています。 KeyNoteの時間以外は同時刻に15ほどの別々のテーマの発表が行われていて、参加するセッションを自由に選択する形式です。 また、常時スポンサーブースが開設されていて、営業・広報担当者だけでなく時には開発者と直接会話できます。 印象に残ったセッション You can't have a cluster[bleep] without a cluster You can't have a cluster[bleep] without a cluster heptioのSenior Developer Advocateによる、Kubernetes自体の複雑さ・リスクを認識した上でどのような取り組みをして乗りこなしていけばよいか、というセッションでした。 Kubernetesの望ましい特徴として回復性・効率性・反復性が挙げられるが、複雑性やリスクも内包している、ということが最初に強調されました。 Kubernetesはアプリケーション・OS・ハードウェアなど多くのコンポーネントに対して干渉できる故、把握すべき範囲が広がっていることが一因とのことでした。 また、先日発覚した CVE-2018-1002105 も触れられ、適切な設定で構築しないとKubernetesクラスタが攻撃者の手に落ちるというような大惨事も発生しうるということを改めて振り返りました。 これらの複雑性・リスクと向き合うためには、以下の4つが重要だそうです。 Learning to mitigate hazards and observe our systems Learning to measure our systems Learning to build tools to gain confidence Learning from our mistakes 僕はこれまでKubernetesの優れている話を聞く機会が多かったです。実際には便利なだけではなく複雑なシステムになりうることを認識し、うまく扱っていくことが重要なのだと認識できた良いセッションでした。 Monolith to Microservice Monolith to Microservice GitLabがシステムのマイクロサービス化に取り組んでいる中で過去に取ったアプローチや、今後の挑戦について語られたセッションでした。 モノリスとマイクロサービス の長所短所の紹介から始まり、Gitホスティングサービスの特性・直面する課題について触れつつ、モノリスが持つ制限から解放されるためにマイクロサービス化を推進し始めた話に入っていきます。 GitLabではディスクをスケール・高速化するために導入したNFSによって、ディスクI/Oとネットワークスループットという新たな課題に直面した過去があるそうです。 これを解決するために、 Gitaly というRPCを利用したOSSを開発し、プロダクション環境へ導入しています。 GitalyのREADME によると、GitLab 11.5の時点でほぼ全てのアプリケーションコードが、ディスクへの直接アクセスではなくGitalyを通じたアクセスに置き換わっているようです。 GitLabはGitalyによっていくつかの問題を解決していますが、ロードバランシングやスケーリングなどまだ課題があるようで、引き続き推進していきたいとのことでした。 システム全体を俯瞰で見た時にどのような課題があって、それをどうやって解決していくかを見据えて開発を行なっているようで、この視点・進め方は見習いたいと感じました。 MySQL on Kubernetes MySQL on Kubernetes Kubernetesの機能を利用することで、RDBも自然な形でコンテナとして管理することが可能だ、ということをデモを交えて紹介するセッションでした。 Cattle vs. Pets という例えがありますが、一般的にRDBはペットのように扱われるため、イミュータブルなコンテナでRDBを動かすことは難しいと僕は思っていました。 RDBのコンテナ運用は以下の機能・ソフトウェアを利用することで実現するようです。 Affinity and anti-affinity :DB用の特定のホストにMySQLのPodを配置する StatefulSets :ノード障害時の台数維持を行う Headless Service :MySQLのPodをディスカバリーする StorageClass, VolumeClaimTemplate :永続化ボリュームのプロビジョニングとその定義 MySQL Operators :Kubernetes上でCRDを使ってMySQLクラスターを作成・設定・管理するためのソフトウェア 上記のリストを見てわかる通り、MySQL Operatorsの存在がキーのようでした。 OSSとしては MySQL Operator や Presslabs MySQL Operator など複数あるため、要件に合わせて選択することになります。 僕は業務でAmazon RDSのAuroraクラスターを運用することがあるのですが、まさしくペットのように扱っています。このセッションを通してRDBもコンテナ運用できるという選択肢を持てたので、1つ自分の中の手札が増えてためになりました。 帰国次第、サンプル等を使って自分で手を動かして構築してみようと思っています。 まとめ Intro系セッションへの参加がメインでしたが、様々な切り口のプレゼンが行われていため毎日毎セッションが興味深く、意欲的に参加できました。 このブログには書きませんでしたが、心に残ったものが他にいくつもあります。 大変勉強になったと同時に技術・人の両方の盛り上がりを肌で感じられて非常に良い経験でした。 実際の業務へどのようにして取り入れられるか考えながら参加したため、自分ごととして学べたことが成果だと感じています。 それと同時にCNCFのプロダクトの魅力に触れ、自分も開発に携わりたいと感じました! ZOZOテクノロジーズでは、最新技術をキャッチアップしつつ地に足のついた技術選定ができるエンジニアを大募集中です。ご興味のある方は、以下のリンクからぜひご応募ください! www.wantedly.com
初めまして。ZOZO Technologies 分析部部長の牧野( @makino_yohei )です。 今回はZOZOのビッグデータを収集・加工してビジネスに活用する私の部門、分析部について紹介させてください。 「分析部」のミッション ミッションは2つです。データを活用して・・・ 1.大きな売上を作る 2.業務の効率や精度を上げる としているのですが、まあ、それはそうだろうという感じでしょうか。 1に関しては、部門の中期目標は 部門発信の施策で年間取扱高1,000億円の純増を作る というもので、 一人称で売上を作るぞと言っているのが少しユニークなところかなと思っています。 現在の進捗は10数%くらい。頑張ります。 「分析部」の役割 仕事の中身は、およそ以下のように分類できます。 1. ビジネスプランニングのサポート 施策や事業自体をデータから事前・事後に評価して、次のアクションを決めるのを支援するという仕事です。 また、依頼のみではなく提案もよくやります。SQL→XLS→PPTという流れが多いです。 2. ビジネスロジックのモデリング 主に機械学習を使って業務の裏で使う予測モデルを作るという仕事です。 予測するのは人単位の購入意欲や、SKU単位の需要など。DataRobotも活用しています。 3. ビジネスインテリジェンス MicrosoftのPowerBIを使ってダッシュボードや分析ツールを作る仕事です。 BIの裏のデータマートも設計・構築します。 4. データエンジニアリング DWH(BigQuery)とDMPを中心としたデータ分析基盤・マーケティング基盤を設計・構築する仕事です。 また、2で作ったモデルをシステム上に実装するのもこのチームの仕事です。 5. ハンズオンサポート 仕事内容は上記の1,3ですが、特定の事業部門にはりついて作業をするのが特徴です。 現時点ではプライベートブランドZOZOの担当部門に数名のメンバーが常駐して、データで事業開発を支援しています。 メンバーの紹介 上記のように、分析部門としての一通りの業務機能を備えているのですが、実は昨年2017年の夏に設立した1歳半くらいの組織です。 1年半前に会社やグループを横断してアナリティクスサービスを提供する組織として、 外から入社した私と数人のメンバーで立ち上げ、現在は約15人、90%以上が2年以内に入社したメンバーです。 面接などでどんなメンバーがいるのかよく聞かれますので、いくつか数字でまとめてみます。 年齢の平均値 = 31歳、最頻値 = 37歳、最小値 = 26歳、最大値 = 37歳 20代後半から30代前半がボリュームゾーンです。世代が近いのでみんな仲が良いです。 女性比率 = 20%弱 男女問わず募集中です。 B2C企業出身メンバーの割合 = 約50% インターネット系の企業出身者が多いです。残りのB2Bはコンサル、SIer、ツールベンダーなど 千葉県在住率 = 約70%、徒歩・チャリ通勤率 = 約40% 自分は東京在住なので徒歩通勤、羨ましいです。。 1年半を経て続々と優秀なメンバーが入社してくれています。 ディレクター・マネージャー陣を何名か軽く紹介させてください。 私、牧野(37) 部門の責任者で、総合コンサルティング会社で、ビジネスコンサルティングを10年強経験してZOZOに入りました。 前職ではビッグデータを使ったビジネスサービスの分析・設計をしてきました。 KOさん(36) ZOZO社員歴13年。CRMの専門家・エンジニアで、CRMチームのリーダーから部門立ち上げ時に異動してくれました。 ゼロから一緒に体制を作ってくれました。 全体のマネジメントと、5のハンズオンサポートをリードしています。 MYさん(33) 7月にインターネット企業から移籍。前職では数100人のエンジニア組織のデータ責任者をやっていたメンバーです。 検索基盤・分析基盤・DMPなどあらゆるプラットフォームを作ってきたスーパーエンジニアで、 4のデータエンジニアリングをリードしてくれています。 MNさん(34) つい先日、11月にJOIN。DMP業界では名の知れたデジタルマーケティングのスペシャリストです。 彼は、1のビジネスプランニングを中心に、1から4まで暴れまわってもらいます。 YKさん(38) 3のビジネスインテリジェンスのところも年度内にスペシャリストがJOINしてくれる予定です。 2のビジネスロジックのモデリングは、特に強化していきたいので、ご興味をお持ちの方は是非連絡ください!  若手メンバーも皆、優秀で向上心も高く、そしていいやつです。 一様に「あれっ、入ったのそんなに最近だっけ?」という状況で、 気が付くと馴染んでいて、気が付くとメンバーが集まって幕張界隈で飲んでいます。 最後に、ZOZOの分析の仕事が面白い5つの理由 ブログは慣れないのですが、最後に私の所感をよくある感じで。。 1. みんな本当にいいやつ 経営理念が「いい人をつくる」で、実際に部内のメンバーも社内クライアントである各部門やグループ会社のメンバーも一緒に仕事をしていて気持ちの良い人ばかり。 縦割りの概念がなく、いい仕事を一緒にして成果が出て一緒に喜ぶというヘルシーな組織です。 当然、分析の仕事なのでトンネルをなかなか抜けられないという苦しいフェーズもありますが、 その先に一緒に喜べる仲間がいるというのは、分析の仕事をするうえで結構重要なんじゃないかと思います。 2. 大きな仕事ができる 良くも悪くも、データを使って改善すべき案件がゴロゴロしています。その分、大きな仕事ができます。 例えば、pythonでクーポンの売上を事前に予測するツールを作って数10億(後半)売上増を作ったり、新しい事業の根幹となるロジックを作ったり。 コンサル会社から来た若手メンバーも、こんなに大きくこんなに面白い案件をいきなり任せてもらえるなんて…と、士気高く頑張っています。 3. 次から次に新しいことができる 私も入社時はデータを使ってZOZOTOWNを改善していく業務が中心というイメージをもっていました。 入社して間もなくZOZOSUITが登場し、プライベートブランドZOZOが出て、海外展開が始まり…、 1年後どうなっているか全く分かりません! 4. 扱えるデータが多様 というのが面接中に魅力としてよく言われます。確かにZOZOTOWNの実績・マスタデータだけでなく、 ZOZOSUITの計測データ、WEARやおまかせ定期便の洋服同士の組み合わせデータ、一つ一つのアイテムにつけられたタグデータやサイズデータなど他社にはない種類のデータがたくさんあってそれらを使って分析やモデルを作ることも多いです。 ただ、個人的にすごいと思うのは横ではなく縦に大量のデータを持っているということで、普通の量なら傾向あるね、 統計的に有意だねとしか言えないことが滑らかなグラフとして出せたりします。 それを初めて体験したときは鳥肌が立ちました。 5. 部門自体が発展途上 まだまだ1歳半の部門でケイパビリティも全然足りていません。 ここから優秀なメンバーを集めたり、社内・社外のプレゼンスをあげたり、成果を積み重ねていくフェーズです。 ないものを嘆くのではなく自分で作れる人にとっては、自分自身も成長し、部門の成長を感じられる場所です。 メンバー一同で助け合いながら、部門を育てています。 というわけで、いいメンバー・面白い仕事が待っているので、ご興味を持たれた方は是非↓からご応募を!  よろしくお願いします。 データサイエンティスト tech.zozo.com ビジネスアナリスト tech.zozo.com データエンジニア tech.zozo.com