Quantcast
Channel: クックパッド開発者ブログ
Viewing all 726 articles
Browse latest View live

ログ収集ライブラリ Puree の iOS 版をリリースしました

$
0
0

モバイルファースト室の @slightairです。 先日、モバイルアプリのログ収集ライブラリ「Puree」をリリースしましたという記事で Puree というログ収集ライブラリを紹介しました。 Android 版につづき iOS 版もリリースしたので紹介したいと思います。

puree-ios : https://github.com/cookpad/puree-ios

モバイルアプリのログ記録の難しさや、それを解決するための Puree の思想についての説明は前回の記事におまかせします。 iOS 版の Puree も Android 版と同じようにフィルタリング、バッファリング、バッチ、リトライの機能を備えています。 ログのフィルタリングや出力の振る舞いをプラグインとして定義し、それらを組み合わせることで効率的なログ収集を実現します。

Puree iOS の使い方

Puree の導入方法

Puree-iOS は CocoaPods でプロジェクトに導入することができます。 Podfile に以下のように書いてください。

pod "Puree"

Puree の初期化

PURLoggerクラスのインタンスを生成し、このロガーにログをポストしていくのが基本的な Puree の使い方です。 PURLoggerConfigurationクラスのインスタンスでフィルタやアウトプットなどのプラグインの設定を行い、ロガーの作成時に渡します。 プラグインの設定にはプラグインのクラスとその設定、反応するログのタグと一致するパターンを指定します。

// Swiftlet configuration = PURLoggerConfiguration.defaultConfiguration()
configuration.filterSettings = [
    PURFilterSetting(filter: ActivityFilter.self, tagPattern: "activity.**"),
    // filter settings ...
]
configuration.outputSettings = [
    PUROutputSetting(output: ConsoleOutput.self,   tagPattern: "activity.**"),
    PUROutputSetting(output: ConsoleOutput.self,   tagPattern: "pv.**"),
    PUROutputSetting(output: LogServerOutput.self, tagPattern: "pv.**", settings:[PURBufferedOutputSettingsFlushIntervalKey: 10]),
    // output settings ...
]

let logger = PURLogger(configuration: configuration)
// Objective-C

PURLoggerConfiguration *configuration = [PURLoggerConfiguration defaultConfiguration];
configuration.filterSettings = @[
    [[PURFilterSetting alloc] initWithFilter:[ActivityFilter class]
                                  tagPattern:@"activity.**"],
    // filter settings ...
];
configuration.outputSettings = @[
    [[PUROutputSetting alloc] initWithOutput:[ConsoleOutput class]
                                  tagPattern:@"activity.**"],
    [[PUROutputSetting alloc] initWithOutput:[ConsoleOutput class]
                                  tagPattern:@"pv.**"],
    [[PUROutputSetting alloc] initWithOutput:[LogServerOutput class]
                                  tagPattern:@"pv.**"
                                  settings:@{PURBufferedOutputSettingsFlushIntervalKey: @10}],
    // output settings ...
];

PURLogger *logger = [[PURLogger alloc] initWithConfiguration:configuration];

ログを送る

任意のタイミングでログを送ります。 ログにはNSDictionaryなど任意のオブジェクトを指定できます。 フィルタプラグインが受け取ったオブジェクトを加工します。 ログを送るときにはタグを指定します。Pureeは、このタグをもとにどのフィルタまたはアウトプットプラグインを適用するか決定します。

// Swift

logger.postLog(["recipe_id": "123"], tag: "pv.recipe_detail")
// Objective-C

[logger postLog:@{@"recipe_id": "123"} tag: @"pv.recipe_detail"]

タグ

タグには .で区切られたひとつ以上の項で構成される任意の文字列が使えます。例えば、activity.recipe.view, pv.recipe_detailなどの文字列が使えます。

プラグインの設定時に渡すタグのパターンには、完全一致のほかにワイルドカード*, **が使えます。

ワイルドカード *はひとつの項に一致します。例えば、aaa.*というパターンは aaa.bbb, aaa.cccというタグに一致します。aaaaaa.bbb.cccには一致しません。

ワイルドカード **は0以上の項に一致します。例えば、aaa.**というパターンは aaaaaa.bbbaaa.bbb.cccなどのタグに一致します。xxx.yyy.zzzには一致しません。

これらのタグのルールは fluentd を参考にしています。ただし {aaa, bbb, ccc}のような項の一部が指定したどれかに一致したらというようなパターンは実装していません(必要だとは思っています)。

フィルタプラグインの定義

フィルタプラグインの役割はロガーが受け取った任意のオブジェクトから Puree のログの内部表現である PURLogインスタンスを生成することです。 PURLogはタグとログの日付、任意の情報を詰められる userInfo プロパティを持ちます。NSCoding プロトコルに準拠していればカスタムクラスのオブジェクトも PURLog に含められます。

フィルタプラグインは PURFilterクラスを継承して実装します。logsWithObject:tag:captured:メソッドを実装して、ひとつまたは複数の PURLog インスタンスを返すようにします。

以下は Recipe や BargainItem というカスタムクラスのオブジェクトから必要な情報をとりだして PURLog のインスタンスを作るフィルタです。

// Swiftclass ActivityFilter: PURFilter {
    overridefunc configure(settings: [NSObject : AnyObject]!) {
        super.configure(settings)

        // configure filter plugin
    }

    overridefunc logsWithObject(object: AnyObject!, tag: String!, captured: String!) -> [AnyObject]! {
        let currentDate =self.logger.currentDate()

        iflet recipe = object as? Recipe {
            return [PURLog(tag: tag, date: currentDate, userInfo: ["recipe_id": recipe.identifier, "recipe_title": recipe.title])]
        } elseiflet bargainItem = object as? BargainItem {
            return [PURLog(tag: tag, date: currentDate, userInfo: ["item_id": bargainItem.identifier, "item_name": bargainItem.name])]
        }

        returnnil;
    }
}
// Objective-C#import <Puree.h>@interface ActivityFilter : PURFilter

@end@implementation ActivityFilter

- (void)configure:(NSDictionary *)settings
{
    [super configure:settings];

    // configure filter plugin
}

- (NSArray *)logsWithObject:(id)object tag:(NSString *)tag captured:(NSString *)captured
{
    NSDate *currentDate = [self.logger currentDate];

    if ([object isKindOfClass:[Recipe class]]) {
        Recipe *recipe = object;
        return @[[[PURLog alloc] initWithTag:tag date:currentDate userInfo:@{@"recipe_id": recipe.identifier, @"recipe_title": recipe.title}]];
    } elseif ([object isKindOfClass:[BargainItem class]]) {
        BargainItem *bargainItem = object;
        return @[[[PURLog alloc] initWithTag:tag date:currentDate userInfo:@{@"item_id": bargainItem.identifier, @"item_name": bargainItem.title}]];
    }

    returnnil;
}

アウトプットプラグインの定義

アウトプットプラグインには OutputBufferedOutputの2種類のプラグインがあります。前者はすぐにログの出力を行い、後者はバッファリングと失敗時のリトライを行います。

Output プラグイン

f:id:Slightair:20141223165621p:plain

Output プラグインは、コンソールへの出力やバッファリングなどが考慮された他のログライブラリへ記録するときに向いています。 Output プラグインは PUROutputクラスを継承して定義します。emitLog:メソッドをオーバーライドして出力処理を実装します。

// Swiftclass ConsoleOutput: PUROutput {
    overridefunc configure(settings: [NSObject : AnyObject]!) {
        super.configure(settings)

        // configure output plugin
    }

    overridefunc emitLog(log: PURLog!) {
        println("tag: \(log.tag), date: \(log.date), \(log.userInfo)")
    }
}
// Objective-C#import <Puree.h>@interface ConsoleOutput : PUROutput

@end@implementation ConsoleOutput

- (void)configure:(NSDictionary *)settings
{
    [super configure:settings];

    // configure output plugin
}

- (void)emitLog:(PURLog *)log
{
    NSLog(@"tag: %@, date: %@, %@", log.tag, log.date, log.userInfo);
}

Output プラグインには特別なメソッドがあり、特定のイベント時に呼ばれます。

  • start - プラグインが最初に設定されたときに呼ばれる
  • suspend - アプリケーションがバックグランドに入った時に呼ばれる
  • resume - アプリケーションがフォアグラウンドに戻った時に呼ばれる

BufferedOutput プラグイン

f:id:Slightair:20141223165640p:plain

BufferedOutput プラグインはログを外部のサーバに送信する時など一定数のログをまとめたり(バッファリング)、失敗時のリトライが必要になるログ出力に向いています。

BufferedOutput プラグインは PURBufferedOutputクラスを継承して定義します。writeChunk:completion:メソッドをオーバーライドして処理を実装します。Chunk(PURBufferedOutputChunk)は複数のログのコンテナであり、ここからバッファリングされたログを取得できます。completion はその Chunk のログ出力が成功したかどうかを返すハンドラです。ログの出力処理結果に応じてこれを呼ぶ必要があります。もし失敗(fales または NO)とした場合は、一定時間をあけてリトライします。デフォルトでは3回までリトライします。

以下は、ログサーバにログをJSONにシリアライズして送信するプラグインの実装例です。

// Swiftclass LogServerOutput: PURBufferedOutput {
    overridefunc configure(settings: [NSObject : AnyObject]!) {
        super.configure(settings)

        // configure buffered output plugin
    }

    overridefunc writeChunk(chunk: PURBufferedOutputChunk!, completion: ((Bool) -> Void)!) {
        let logs = chunk.logs.map { (object: AnyObject) -> NSDictionary in
            let log = object as PURLog
            var logDict = log.userInfo
            logDict["date"] = log.date
            return logDict
        };

        let logData = NSJSONSerialization.dataWithJSONObject(logs, options: nil, error: nil)
        let request = NSURLRequest(URL: NSURL(string:"https://logserver")!)
        let task = NSURLSession.sharedSession().uploadTaskWithRequest(request, fromData: logData, completionHandler:{
            (data:NSData!, response:NSURLResponse!, error:NSError!) -> Void in
            let httpResponse = response as NSHTTPURLResponse
            if error !=nil|| httpResponse.statusCode !=201 {
                completion(false)
                return
            }
            completion(true)
        })
        task.resume()
    }
}
// Objective-C#import <Puree.h>@interface LogServerOutput : PURBufferedOutput

@end@implementation LogServerOutput

- (void)configure:(NSDictionary *)settings
{
    [super configure:settings];

    // configure buffered output plugin
}

- (void)writeChunk:(PURBufferedOutputChunk *)chunk completion:(void (^)(BOOL))completion
{
    NSMutableArray *logs = [NSMutableArray new];
    for (PURLog *log in chunk.logs) {
        NSMutableDictionary *logDict = [log.userInfo mutableCopy];
        logDict[@"date"] = log.date;

        [logs addObject:logDict];
    }

    NSData *logData = [NSJSONSerialization dataWithJSONObject:logs options:0 error:NULL];
    NSURLRequest *request = [NSURLRequest requestWithURL:[NSURL URLWithString:@"https://logserver"]];
    NSURLSessionUploadTask *task = [[NSURLSession sharedSession] uploadTaskWithRequest:request
                                                                              fromData:logData
                                                                     completionHandler:^(NSData *data, NSURLResponse *response, NSError *error){
                                                                         NSHTTPURLResponse *httpResponse = (NSHTTPURLResponse *)response;
                                                                         if (error || httpResponse.statusCode != 201) {
                                                                             completion(NO);
                                                                             return;
                                                                         }
                                                                         completion(YES);
                                                                     }];
    [task resume];
}

@end

PURBufferedOutput にはいくつかの設定項目があらかじめ用意されおり、ロガーの初期化時に指定することで動きを変えることができます。

  • PURBufferedOutputSettingsLogLimitKey - バッファリングするログ数(default: 5)
  • PURBufferedOutputSettingsFlushIntervalKey - ログを出力する時間の間隔 (default: 10秒)
  • PURBufferedOutputSettingsMaxRetryCountKey - リトライ回数。この数を超えるとアプリケーションが再起動したりリジュームされるまでリトライしません(default: 3)

まとめ

puree-androidに続き、 puree-iosをリリースしました。Puree を使うことでモバイルアプリのログ収集がやりやすくなるとうれしいです。ログを上手に取得することでサービスの改善につなげ、ユーザに快適なアプリを届けられるようになるとうれしいですね。


RESTful Web API 開発をささえる Garage (client 編)

$
0
0

料理動画事業室の @yoshioriです。前に「RESTful Web API 開発をささえる Garage」で紹介した RESTful Web API を開発する Garage のクライアント側のライブラリを公開しました。この記事ではその使い方を紹介したいと思います。Garage の設計思想やサーバ側の実装については上記記事を御覧ください。

今回は簡単にクライアント側の挙動を知っていただくために pry を使って説明したいと思います。 アクセスするサーバは先程の記事で作成したアプリケーションを使用してみます。

サーバの準備

https://github.com/taiki45/garage-exampleの README にも書いてありますので簡単に進めたいと思います。

まずは下準備としてコードを github から clone してきて、ライブラリのインストールと DB のマイグレーションを行います

> git clone git@github.com:taiki45/garage-example.git
> cd  garage-example
> bundle install
> bundle exec rake db:create db:migrate

ここまででサーバの準備が整いました。次にユーザーを一人作り、アプリケーションを起動してみます

> bundle exec rails runner 'User.create(name: "alice", email: "alice@example.com")'
> bundle exec rails s

実際にブラウザで http://localhost:3000/oauth/applicationsにアクセスするとアプリケーションの登録画面が開きますのでサンプルアプリケーションひとつ作ってみます。

f:id:Yoshiori:20141226185629p:plain

ここで表示された Application IdSecretを使い下記コマンドを実行します。 (下記の $APPLICTION_ID$SECRETをそれぞれ Application IdSecretに書き換えて実行して下さい)

> curl -u "$APPLICTION_ID:$SECRET" -XPOST http://localhost:3000/oauth/token -d 'grant_type=password&username=alice@example.com' 

そうすると下記のように access_tokenが手に入ると思います。 今回はこれを利用します。

{"access_token":"975430489343f3d468d69f68763b3cb11d7cf98ee1f504aa319ab031a47462eb","token_type":"bearer","expires_in":7200,"scope":"public"}

GarageClient の基本的使い方

gem として公開していますので、gem コマンドでインストール出来ます

> gem install garage_client

インストールが終わったら pry を起動しライブラリをロードします。

[1] pry(main)> require"garage_client"
=> true

次にクライアントの初期設定を行います。 今回は endpoint のみを指定します。その他のオプションに関しては READMEに纏めてありますのでそちらを御覧ください。

[2] pry(main)> GarageClient.configure do |c|
[2] pry(main)*   c.endpoint = "http://localhost:3000"  
[2] pry(main)* end  
=> "http://localhost:3000"

次に先ほど取得した access_token を利用して client を作成します

[3] pry(main)> client = GarageClient::Client.new(access_token: "975430489343f3d468d69f68763b3cb11d7cf98ee1f504aa319ab031a47462eb")
=> #<GarageClient::Client:0x007f8e7ba20cb8 @options={:access_token=>"975430489343f3d468d69f68763b3cb11d7cf98ee1f504aa319ab031a47462eb"}>

では、Garage の記事で行っていたユーザーリソースを取得するリクエストを GarageClient で行ってみます。

[4] pry(main)> users = client.get("/users")
//省略
[5] pry(main)> users.first.name
=> "alice"
[6] pry(main)> users.first.email
=> "alice@example.com"

取得出来ましたね。 上記のように GarageClient で取得した値はプロパティアクセスの様に(実際にはメソッド呼び出しですが)呼び出せます。

get 以外のメソッド

次に get 以外の動作を見ていきます。garage-example は記事を投稿できるようになっているので、それを使ってみます。

[7] pry(main)> client.post("/posts", title: "sushi", body: "naoya")

投稿できました。実際に見てみましょう。

[8] pry(main)> post = client.get("/posts").first
//省略
[9] pry(main)> post.title
=> "sushi"

ちゃんと作られていますね。

では次に更新をしてみましょう。

[10] pry(main)> client.put("/posts/#{post.id}", title: "niku")
//省略

実際に更新されたかの確認を行いますが、今度は garage の link機能を使って見てみます。 GarageClient はリソースに link が指定してあると link 先のリソースを取得するときにリクエストを投げて取得してくれます。

[11] pry(main)> user = client.get("/users").first
//省略
[12] pry(main)> posts = user.posts
//ここで posts 取得のためにリクエストを投げています
[13] pry(main)> posts.first.title
=> "niku"

ちゃんと更新されているのが確認できました。 では、削除をしてみましょう。

[14] pry(main)> client.delete("/posts/#{post.id}")
//省略
[15] pry(main)> client.get("/posts").size
=> 0

ちゃんと消えましたね。 では次にもうちょっと応用的な使い方を見てみようと思います。

取得する値を絞る

Garage にはリソース取得時に必要な値だけ指定して所得する機能があります。 これは例えばレシピのタイトルだけ取得したい時などに材料や手順まで取得してしまうと効率が悪い時に値を絞ったり、逆にレシピに付いている動画情報を取得するなど標準では付加されない情報を取得したりする時に使います。

[16] pry(main)> users = client.get("/users", fields: "id,name")
//省略
[17] pry(main)> users.first.name
=> "alice"
[18] pry(main)> users.first.email //取得していないのでエラーになる
NoMethodError: undefined method `email' for #<GarageClient::Resource:0x007f8e7e03bbc8>from /usr/local/opt/rbenv/versions/2.1.5/lib/ruby/gems/2.2.0/gems/garage_client-2.1.1/lib/garage_client/resource.rb:51:in `method_missing'

キャッシュを利用する

取得する値を絞るだけでなく、取得した値をキャッシュさせることも出来ます。 動作確認様に下記に簡単な Cacher クラスを書いてみました。(実際には READMEにあるように Rails.cache を使うなどの形になると思います)

classMyCacher< GarageClient::Cachers::Baseprivatedefread_from_cache?trueenddefwritten_to_cache?
    read_from_cache?
  enddefkey"test"enddefstore@@store ||= MyStore.new
  endclassMyStoredefread(key, options)
      store[key]
    enddefwrite(key, response, options)
      store[key] = response
    enddefstore@store ||= {}
    endendend

上記をコピーし、pry コンソール上にペーストして下さい。 次に、クライアントを作るときに Cacher を指定して作ります。

[20] pry(main)> client = GarageClient::Client.new(access_token: "e32234b3d113b97d1fd38f3233c663cad67ceb4cb57e8e0a66bec00d6742b1ec", cacher: MyCacher)

実際にアクセスするときにサーバ側のログを一緒に見て下さい。

[21] pry(main)> client.get("/users")
//省略
[22] pry(main)> client.get("/users") //こっちはキャッシュから取得するのでサーバにリクエストが飛ばない
//省略

このように簡単にキャッシュ機構を組み込むことが出来ます。


簡単ですが GarageClient をつかって Garage と連携する方法を説明してみました。

また、今回は認証周りは本筋と外れるため省略しています。実際に Garage の Doorkeeper と連携する時は OAuth2 のライブラリを使用したりすると思います。 クックパッドでは Garage の記事にも出てきましたが、認証認可サーバーを別途立てていたり、内部 API として連携するときは認証を省いて使ったりしています。

社内でも実際に運用しているライブラリなので先ほど紹介した取得する値を絞る機能やキャッシュ機構など実用的なライブラリになっていると思います。 また、 今後 iOS, Android の client ライブラリも準備が整い次第公開していこうと思っています。 ご期待ください。

OSSEC ではじめるセキュリティログ監視

$
0
0

インフラストラクチャー部の星 (@kani_b) です。

Heartbleed, ShellShock, XSA-108 (a.k.a. EC2 インスタンス再起動祭), POODLEなど、今年は話題となるような脆弱性が各地を襲う一年でした。 脆弱性への対応に加え、いわゆるセキュリティ対策に日頃頭を悩ませている方も多いのではないかと思います。 一言にセキュリティ対策と言っても、実際やるべきことは多岐にわたります。今回はそのうちの一つとして、OSSEC という IDS (侵入検知システム) を使ったセキュリティログ監視についてご紹介します。

OSSEC とは

OSSECは、いわゆるホスト型の IDS (HIDS) です。以下のような機能を持っています。

  • ログ解析、監視
  • ファイルの変更監視
  • rootkit の検知
  • それらをトリガにしたプログラムの自動実行 (Active Response)

もともと OSS として開発がスタートし、米トレンドマイクロ社に買収された後もトレンドマイクロ社をスポンサーとして継続して開発が行われています。 GPLv2 ライセンスで配布されています。GitHub にレポジトリがあります。(こちら)

Linux はもちろんのこと、OSX, Windows, Solaris など様々な OS をサポートしていることも特徴の一つです。混在環境を一つのソフトウェアでモニタリングすることができます。

インストール

ダウンロードページから最新版をダウンロードしてインストールします。(本記事執筆時点で 2.8.1)
tarball にあるソースコードから自分でビルドすることもできますし、ビルドしたバイナリをサーバに配信することも可能ですが、 CentOS, Fedora, Debian にはパッケージが用意されていますのでそちらを使うと楽です。
詳細なインストール手順については割愛します。

構成

OSSEC の Web サイトにも記載がありますが、基本的には1台マネージャを用意しエージェントを各サーバにインストールする形をとります。
エージェントはマネージャにサーバのログやファイルハッシュなどを送るだけの存在で、ログの分析や変更検知、アラートの発報などは全てマネージャが行います。 そのため、特にマネージャの性能は、監視対象となるサーバの数やログの流量によって調整する必要があります。
クックパッドでは数百台のインスタンスが常に稼働していますが、現在のところそれらのログを一台のマネージャで処理することができています。

マネージャへのエージェント追加

マネージャは、各エージェントの状態も (変更検知など) 含めて監視を行うため、まずマネージャに対してエージェントを登録する必要があります。
このため、マネージャのセットアップとエージェントのインストールが終わったら、マネージャ側で manage_agentsコマンドを実行し、発行された鍵をエージェントにコピー、という手順を踏む必要があります。
この手順は ossec-authdを使うことで自動化できます (マネージャで ossec-authd を実行した状態でエージェントから agent-auth コマンドを利用)。
ですが、現在はまだ ossec-authd に認証機構が存在しないことにご留意ください。

設定

OSSEC の設定は、大きく以下のようなものに分かれています。

  • 基本設定
    • サーバ名、マネージャのホスト名、監視対象ファイルなどの設定
  • デコーダ (マネージャのみ)
    • 送信されてきたログファイルのパーサ
  • ルール (マネージャのみ)
    • ログファイルの監視ルール

これらの設定は XML で記述します。 今回は OSSEC でどんなことができるのかを中心にご紹介するため、基本設定などについては割愛します。

ルールについて

まず、デフォルトで用意されているルールからシンプルなものを用いて説明します。 以下の XML は こちらからの引用です。

<rule id="31110"level="6"><if_sid>31100</if_sid><url>?-d|?-s|?-a|?-b|?-w</url><description>PHP CGI-bin vulnerability attempt.</description><group>attack,</group></rule>

このルールは、Web サーバのログを対象とし、URL に ?- という文字列が存在すると発報します。 description に書いてあるように、数年前話題になった PHP (CGI モード) の脆弱性ですね。

各ルールは一意の ID を持ちます。level はアラートを発報する閾値として使われます。 group は Active Response やサマリ作成時などのグルーピングに利用できます。

if_sid というパラメータは、それ以前にマッチしたルール ID を指します。 rule id 31100 を見てみると、以下のように記述されています。

<rule id="31100"level="0"><category>web-log</category><description>Access log messages grouped.</description></rule>

このルールでは、新たに category: web-log が参照されています。 category とは、ログがデコーダによって解読される際に付けられるグループ名です。 syslog や web-log (アクセスログ) などが種類として用意されています。
例えば Common Log Format のデコーダを見てみます。

<decoder name="web-accesslog"><type>web-log</type><prematch>^\d+.\d+.\d+.\d+ |^::ffff:\d+.\d+.\d+.\d+ </prematch><regex>^(\d+.\d+.\d+.\d+) \S+ \S+ [\S+ \S\d+] </regex><regex>"\w+ (\S+) HTTP\S+ (\d+) </regex><order>srcip, url, id</order></decoder>

送られてきたログは上記のような正規表現にかけられ、ソース IP, URL, ID (HTTP レスポンス) を取り出します。 ここで取り出している URL などが実際にルールに渡されることになります。
紹介が逆順になってしまいましたが、OSSEC ではこのように、送られてきたログはまずデコーダで処理され、 広いルールから細かいルールへと判定されていきます。

デコーダとルールの改良

例えば、ログインページへの攻撃を監視するために、以下のようなルールを作ることを考えてみます。

  • リクエスト先は /login
  • リクエストの種類は POST
  • 同じ IP アドレスから2分以内に20回以上リクエストがあったらアラート

実際のログは以下のようなものになります。

198.51.100.10 - - [24/Dec/2014:23:43:56 +0900] "POST /login HTTP/1.1" 200 -

デコーダやルールは、OSSEC インストールディレクトリの bin/ossec-logtest でテストすることができます。 起動して、ログを貼り付けてみます。

$ bin/ossec-logtest
2014/12/26 10:44:53 ossec-testrule: INFO: Reading local decoder file.
2014/12/26 10:44:53 ossec-testrule: INFO: Started (pid: 27470).
ossec-testrule: Type one log per line.

198.51.100.10 - - [24/Dec/2014:23:43:56 +0900] "POST /login HTTP/1.1" 200 -


**Phase 1: Completed pre-decoding.
       full event: '198.51.100.10 - - [24/Dec/2014:23:43:56 +0900] "POST /login HTTP/1.1" 200 -'
       hostname: 'ossec-test'
       program_name: '(null)'
       log: '198.51.100.10 - - [24/Dec/2014:23:43:56 +0900] "POST /login HTTP/1.1" 200 -'

**Phase 2: Completed decoding.
       decoder: 'web-accesslog'
       srcip: '198.51.100.10'
       url: '/login'
       id: '200'

**Phase 3: Completed filtering (rules).
       Rule id: '31108'
       Level: '0'
       Description: 'Ignored URLs (simple queries).'

Phase 1, 2 でデコードされたログが Phase 3 でルールと照合されます。 ルールで使われるのは Phase 2 でデコードされた srcip, url, id ですので、このままだと HTTP メソッドを判断材料にできません。 そのため、デコーダをちょっと変更します。

<decoder name="web-accesslog"><type>web-log</type><prematch>^\d+.\d+.\d+.\d+ |^::ffff:\d+.\d+.\d+.\d+ </prematch><regex>^(\d+.\d+.\d+.\d+) \S+ \S+ [\S+ \S\d+] </regex><regex>"(\w+) (\S+) HTTP\S+ (\d+) </regex><order>srcip, extra_data, url, id</order></decoder>

デコーダの syntaxにある通り、HTTP メソッドを格納する項目はないため、
今回は自由に使える extra_data という項目に格納します。
内で () でグルーピングされた文字列が 内で指定した項目の順に格納されます。 この変更をした状態で、同じログをテストすると以下のようになります。

**Phase 1: Completed pre-decoding.
       full event: '198.51.100.10 - - [24/Dec/2014:23:43:56 +0900] "POST /login HTTP/1.1" 200 -'
       hostname: 'ossec-test'
       program_name: '(null)'
       log: '198.51.100.10 - - [24/Dec/2014:23:43:56 +0900] "POST /login HTTP/1.1" 200 -'

**Phase 2: Completed decoding.
       decoder: 'web-accesslog'
       srcip: '198.51.100.10'
       extra_data: 'POST'
       url: '/login'
       id: '200'

**Phase 3: Completed filtering (rules).
       Rule id: '31108'
       Level: '0'
       Description: 'Ignored URLs (simple queries).'

Phase 2 で extra_data にメソッドが格納されていることがわかります。 これでデコーダの準備はできたので、ルールを作成していきます。

<rule id="91000"level="10"frequency="20"timeframe="120"><if_sid>31108</if_sid><url>/login</url><extra_data>POST</extra_data><same_source_ip /><description>Multiple login challenge from same source ip.</description><group>attack,</group></rule>

ルールのパラメータとして frequency と timeframe があります。 今回の設定では、120秒以内に20回このルールに合致するログがあった場合にアラートが発報されます。

これでルールも完成したのでテストをしてみます。 bin/ossec-logtest を実行して、ログを20行流してみると、以下のようにアラートが上がることが確認できます。

**Phase 1: Completed pre-decoding.
       full event: '198.51.100.10 - - [24/Dec/2014:23:43:56 +0900] "POST /login HTTP/1.1" 200 -'
       hostname: 'ossec-test'
       program_name: '(null)'
       log: '198.51.100.10 - - [24/Dec/2014:23:43:56 +0900] "POST /login HTTP/1.1" 200 -'

**Phase 2: Completed decoding.
       decoder: 'web-accesslog'
       srcip: '198.51.100.10'
       extra_data: 'POST'
       url: '/login'
       id: '200'

**Phase 3: Completed filtering (rules).
       Rule id: '91000'
       Level: '10'
       Description: 'Multiple login challenge from same source ip.'

実運用ではさらに細かく、ログインページへの POST を攻撃ではないイベントとしてルール化し、 その繰り返しをアラートとするなど、ルールの作り方を工夫すればより見通しが良くなると思います。

デフォルトルール

ここまで、カスタマイズしたルールを作ることを前提に説明してきましたが、 それ以外にも OSSEC は色々なルールをデフォルトで持っています。

  • SSH ログインの連続失敗
  • XSS, SQL インジェクション
  • 長すぎるパラメータ
  • SEGV

これら、直接何かの異変に繋がるようなものの他にも、 通常のログインや root 権限の取得、ユーザの追加/削除 などをイベントとするルールも用意されています。

可視化

公式に Web UI は存在しますが、FAQ に書かれているようにあまりコミットされておらず、 また実装もアラートのテキストファイルを直接読むような実装ですので、ある程度以上の環境になった途端に破綻します。
アラートの出力は MySQL や PostgreSQL にも可能で、 Analogiという Web UI も作られてはいますが、 最近ではみんな大好き ELK (Elasticsearch + Logstash + Kibana) を使うのが良さそうです。
Logstash 側で OSSEC のログを拾えるようにする設定などが公開されています。

OSSEC に (そのままでは) できないこと

これまでご紹介してきた通り、OSSEC のログ監視機能はあくまでシステムのログを用いるものであり、 実際の通信を監視するようなことはできません。
例えば HTTP POST パラメータに含まれる攻撃を監視したい場合などは、別途 mod_security や Snort などを導入する必要があるでしょう。
ただし、mod_security が出す BAN ログなどを監視することはもちろん可能です。ある程度対応するルールは標準で用意されています。

まとめ

OSSEC という HIDS で、ログを主にセキュリティの視点から監視する方法について紹介しました。 実際にクックパッドでは、2011年頃から OSSEC をサーバ監視の一部として利用しています。

AWS EC2 では、例えば全トラフィックが通るミラーポートなどを作成する、などの方法が取れないため セキュリティ面での監視も基本的に全ホストにノードを導入して行うような構成になることが多いです。 商用ソフトウェアだと DeepSecurity や AlertLogic, Sourcefire などがネットワーク監視も含めた形で AWS 界隈では有名ですね。
OSSEC は、それ単体でセキュリティ機能を完結させるのではなく、他のセキュリティソフトウェアも含めてログを流し、 全サーバのイベントを監視してみるとなかなか有用なのではと思います。

変更監視や rootkit 検知などの機能、Active Response など今回紹介できなかったこともありますが、 興味のある方は是非試してみてください。

AWS 対応した GitHub Enterprise v2 へスムーズに移行した話

$
0
0

技術部 id:sora_hです。今回は v2 より AWS 上での運用に対応した GitHub Enterpriseを、オフィスにある既存の環境から AWS へスムーズに移行した話について説明します。

GitHub Enterprise v2

GitHub Enterprise (以下 GHE) とは、github.com を自前の環境で運用できるアプライアンスです。クックパッドでは主に GHE 上で開発を行っています。

従来まで GHE の実行環境として VMware vSphere, VirtualBox 上の実行しかサポートされていませんでしたが、11 月にリリースされたアップデート v2 より、AWS での実行がサポートされました。

クックパッドでは全面的に AWS を利用していますが、いままで GHE を AWS 上で動作させる事はできませんでした。そのため、オフィス内に ESXi 用物理マシンを用意・メンテナンスしていましたが、 GHE の物理ホストのメンテナンスコストが高い事が問題でした。

2014 年 10 月頃に GitHub 社から AWS サポートが追加された GHE v2 のベータテストに招待されました。それを機会に、AWS への移行を前提に検証・バグ報告・移行を行いました。

ESXi 時代の構成

まずは移行前の環境について説明します。

f:id:sora_h:20150113101440p:plain

前述の通り、GHE はオフィス内に設置された VMware ESXi マシンの上で稼動させていました。

しかし、このままだと AWS 側からの SSH や HTTPS でのアクセス、および GHE からも AWS 側のリソースへアクセスする事ができません。そのため、ESXi 上に Linux の VM を別途立て、そこから AWS 側サーバーへ SSH トンネルを掘る事で対応していました。

具体的には、ssh_config(5) の RemoteForwardを利用して AWS 側サーバーの特定ポートを GHE の 443, 22/tcp ポートへ転送するようにしました。同様に、GHE がアクセスする必要のある LDAP サーバー、SMTP サーバー等は LocalForwardで転送し、それぞれ GHE に設定しました。

AWS 側サーバーからの git clone で利用する ssh ホストおよびポートの指定は、鍵と一緒に配布される ssh_config で Host ...および HostNameを利用して、RemoteForward しているポートを利用するように設定していました。尚、配布には puppet を利用しています。

また、開発者がオフィス外のネットワークから SSH access を利用する際には、AWS 内の remote forwarded port へ接続するように、 ssh_config の ProxyCommandを利用する設定ファイルを提供していました。

外部からの HTTPS でのアクセスは、GHE の 443 に対応するポートに reverse proxy しつつ、外部ネットワークの場合は HTTP 認証をかける Apache を稼動させたサーバーを用意し、外部から GHE の FQDN を DNS で引いた時はこのサーバーの IP アドレスが返るようになっていました (この https reverse proxy は以降 github-proxy と呼称します)

以上が移行元の環境でした。

一つ、GHE 導入当時になく、現在利用できる設備として存在するのが、AWS VPC との拠点間 VPNがオフィスとの間で確立された事です。移行に際して、SSH トンネルから拠点間 VPN ベースへの切り替えも行う事になりました。

移行の要件

まずは業務に支障を出さずに移行を行う事が求められました。

また、このタイミングで GHE 導入以来利用してきた FQDN を変更する事になりました。FQDN がそのままであれば、前述の RemoteForwardの ssh_config だけ対応して DNS を切り替えるだけで移行が完了します。 ですが、FQDN を変更するため、旧 URL そのままでの git clone, http access もリダイレクト等で対応し、スムーズに移行できる事が追加の要件になりました。

移行の手順

移行は下記の手順で行う事にしました。

  1. メンテナンスウィンドウを決め、事前に社内へアナウンスを流して周知する
  2. 当日、出社して時間になったら旧 GHE をメンテナンスモードに入れる
  3. 旧 GHE の ghe-backup を行う
  4. 完了次第、新 GHE に ghe-restore する
  5. proxy で旧 GHE の FQDN を新 GHE の FQDNにリダイレクトさせる設定に切り替える
  6. 旧 GHE の DNS を旧 GHE 自体から github-proxy に切り替えて、TTL を待つ
  7. 新 GHE のメンテモードを外す

下準備

バックアップ

バックアップでは従来より公式ツールである github/backup-utilsを利用していました。 元々バックアップは ESXi 上の別仮想マシンで実行していましたが、バックアップ先をまず AWS へ移行し、インターネット経由でのバックアップに切り替えました。この時の経路は VPC との拠点間 VPN を利用しています。

移行までの間、自動バックアップ完了後に AWS 上に立ててある GHE のインスタンスへリストアするように設定し、なるべく最新のデータがつねに新 GHE へ同期されるようにしました。

github-proxy の新 FQDN 対応

github-proxy は今迄、ただリクエストを remote port forward された HTTPS ポートへリバースプロキシし、必要に応じて BASIC 認証を要求するというサーバーでした。

移行に際し、まず最初はリクエストに合わせて旧ドメインは旧 GHE、新ドメインは新 GHE へリバースプロキシするように設定しました。 移行後に利用する旧ドメインのリクエストを新ドメインへリダイレクトする設定の準備も行いました。

移行後は社内からもリダイレクト用に、旧ドメインのレコードを github-proxy へ向ける計画のため、SSH (port 22) へのトラフィックを新 GHE へ転送する stoneも起動しました。stone についてはアクセスを社内 (VPN 経由) に security group で制限しました。

既存のサーバーや一部の開発者向け ssh_config への対応

前述のように既存のサーバーと一部の開発者に配布している ssh_config は、GHE を特定サーバー上の remote port forward された port を参照するように設定されています。また、ssh_config を持ったサーバーは大量に存在するため、移行後、git remote の url を変更するか新設定を反映させるまでアクセスするたびにエラーというのは避けたいという要件がありました。

そのため、新 GHE + 新 FQDN の ssh_config は移行前に追加しておき、いままでの ssh_config もそのまま利用できるよう、移行後は remote port forward していたポートを新 GHE の port 22 へトラフィックを転送する stone に置き換えました。その後、旧ホストの設定を削除し、新 GHE 設定の Hostに旧 GHE の FQDN も追加しました。

このような手順をあらかじめ踏む事により、FQDN 変更によるエラーを最小限に留める事ができました。API を利用していた箇所についてはリダイレクトに対応できず、手動で URL を変更するまで動作しないといったケースがありましたが、開発者からの HTTPS アクセスやサーバ上の git clone/pull についてはエラーなく移行と FQDN 変更を達成しました。

移行後

git remote URL の切り替えを促す

FQDN を変更したため、git remote の設定を新ドメインに変更してもらう必要がありました。以下の ssh_config を利用すると楽ですよ、と社内ブログで周知したりしました。

Host 旧ドメイン
  Hostname 新ドメイン
  PermitLocalCommand yes
  LocalCommand bash -c 'for remote in $(git remote -v|grep "@旧ドメイン"|cut -f 1|sort|uniq); do url=$(git config remote.${remote}.url | sed -e "s/@旧ドメイン/@新ドメイン/"); echo "warn: set-url ${remote} ${url}" >/dev/stderr; git remote set-url ${remote} ${url}; done'

ssh_config の LocalCommand を利用して、旧ドメインの名前で接続を試みた時に git remote set-url を勝手に呼んでくれます。

まとめ

f:id:sora_h:20150113091602j:plain

本記事では、どのようにクックパッドが GitHub Enterprise v2 + AWS に移行しつつ、GHE の動作 FQDN を最小限のエラーで変更したかを解説しました。

(なお、写真は GitHub からベータテスト協力の gift として頂いた Octocat Figurine です (かわいい)。Thank you <3)

ES6時代のJavaScript

$
0
0

こんにちは会員事業部の丸山@です。

最近のWebフロントエンドの変化は非常に激しく、ちょっと目を離した間にどんどん新しいものが出てきますよね。そんな激しい変化の一つとしてES6という次期JavaScriptの仕様があります。このES6は現在策定中で、執筆時点ではDraft Rev31が公開されています。

JavaScriptはECMAScript(ECMA262)という仕様をもとに実装されています。
現在のモダンなWebブラウザはECMAScript 5.1th EditionをもとにしたJavaScript実行エンジンを搭載しています。
そして次のバージョンであるECMAScript 6th Editionが現在策定中で、略称としてES6という名前がよく使われます。

今回は、他の言語にはあってJavaScriptにも欲しいなと思っていた機能や、JavaScriptでよく頻出するパターンを統一的に書ける機能を中心に紹介していきます。

Class

JavaScriptは「プロトタイプベースのOOP」と呼ばれている通り、JavaやRubyなどの「クラスベースのOOP」とは少し毛色が違います。しかしプロトタイプベースの機能を効果的に使うということはこれまで少なかったように思います。むしろ擬似的なクラス機能を実装したり、クラスを実現するためのライブラリを使ってプログラムを書くことが多いはずです。これはnpmでclassで検索するとたくさんのパッケージがヒットすることからもわかると思います。そこでES6ではクラス機能が導入され、クラスを簡単に扱えるようになりました。

// ES5'use strict';

function User(name){this._name = name;
}

User.prototype = Object.create(null, {
  constructor: {
    value: User
  },

  say: {
    value: function() {return'My name is ' + this._name;
    }}});

function Admin(name) {
  User.apply(this, arguments);
}

Admin.prototype = Object.create(User.prototype, {
  constructor: {
    value: Admin
  },

  say: {
    value: function() {var superClassPrototype =  Object.getPrototypeOf(this.constructor.prototype);
      return'[Administrator] ' + superClassPrototype.say.call(this);
    }}});

var user = new User('Alice');
console.log(user.say()); // My name is Alicevar admin = new Admin('Bob');
console.log(admin.say()); // [Administrator] My name is Bob
// ES6'use strict';

class User {
  constructor(name) {this._name = name;
  }

  say() {return'My name is ' + this._name;
  }}class Admin extends User {
  say() {return'[Administrator] ' + super.say();
  }}var user = new User('Alice');
console.log(user.say()); // My name is Alicevar admin = new Admin('Bob');
console.log(admin.say()); // [Administrator] My name is Bob

Function Arguments

JavaScriptで関数のデフォルト引数や可変長引数を使いたいと思っても言語には直接的な方法がなかったため、||を使ったおまじない的な方法やargumentsを使ったメタプログラミング的な方法を取ってきました。そこでES6では関数の仮引数の宣言方法が強化され、自然に書くことができるようになりました。これは後でプログラムを読むときに、シグネチャだけを見ればその関数が期待する引数をある程度わかるようになるという効果もあります。

// ES5'use strict';

function loop(func, count) {
  count = count || 3;
  for (var i = 0; i < count; i++) {
    func();
  }}function sum() {var result = 0;
  for (var i = 0; i < arguments.length; i++) {
    result += arguments[i];
  }return result;
}

loop(function(){ console.log('hello')}); // hello hello hello
console.log(sum(1, 2, 3, 4)); // 10
// ES6'use strict';

function loop(func, count = 3) {for (var i = 0; i < count; i++) {
    func();
  }}function sum(...numbers) {return numbers.reduce(function(a, b) {return a + b; });
}

loop(function(){ console.log('hello')}); // hello hello hello
console.log(sum(1, 2, 3, 4)); // 10

実はこのデフォルト引数や可変長引数は関数の仮引数部だけで使えるというわけではなく、変数の代入処理全般が強化されたうちの一部分になります。ES6での変数の代入処理についてはDestructuring and parameter handling in ECMAScript 6にてサンプル付きで様々なパターンが紹介されています。

Arrow Function

JavaScriptではイベント駆動の処理をよく書きます。例えばDOMがクリックされたら何か処理する、XHRのリクエストが完了したら何か処理をする場合などです。このような処理をJavaScriptで実装するには、コールバック関数やイベントリスナと呼ばれるものを対象のオブジェクト(DOMやXHR)に設定します。このコールバック関数を登録する時点でのthisにコールバック関数内からアクセスしたくなる場面がよくありますが、これまではクロージャを使ってthisを保存しておいたり、Function.prototype.bindを使ってthisを束縛したりしていました。ES6ではArrow Functionと呼ばれる新たな関数定義|式が導入され、このthisに対する煩わしさを解消しています。

// ES5'use strict';

var ClickCounter = {
  _count: 0,

  start: function(selector) {var node = document.querySelector(selector);
    node.addEventListener('click', function(evt){this._count++;
    }.bind(this));
  }};

ClickCounter.start('body');
// ES6'use strict';

var ClickCounter = {
  _count: 0,

  start: function(selector) {var node = document.querySelector(selector);
    node.addEventListener('click', (evt)=>{this._count++;
    });
  }};

ClickCounter.start('body');

Promise

これまでXHRなどの非同期処理は開始時にコールバック関数を設定して、非同期処理が終わったらそのコールバック関数が呼び出されるというのが一般的ですが、様々なコールバック関数の設定方法がありました。例えば非同期処理の関数にコールバック関数を引数として渡したり(setTimeoutsetInterval)、非同期処理を行うオブジェクトにコールバック関数を登録したり(XHRWebWorker)、非同期処理の戻り値にコールバック関数を登録したり(IndexedDB)などがあります。

このように様々な方法があるため、使う側としてはそれぞれの方法を使い分ける必要があります。そこでES6ではPromiseという非同期処理を統一的に扱う方法が言語として提供されるようになりました。使い方は、非同期処理を行う関数はPromiseを戻り値として返し、呼び出し側はPromiseにコールバック関数を登録するというものです。

// ES5'use strict';

function sleep(callback, msec) {
  setTimeout(callback, msec);
}

sleep(function(){
  console.log('wake!')
}, 1000);
// ES6'use strict';

function sleep(msec) {returnnew Promise(function(resolve, reject){
    setTimeout(resolve, msec);
  });
}

sleep(1000).then(function(){
  console.log('wake!');
});

また非同期処理では例外処理が問題になります。単純にtry-catchで囲っても非同期で例外が起きると補足できません。そこでPromiseでは非同期処理の例外処理を統一的に行える方法も提供しています。このPromiseについてはWeb上で無料で読めるJavaScript Promiseの本が大変参考になります。

Generator

最後にGeneratorについて紹介します。ここまではすでにJavaScriptに存在するけど使いにくかったり、統一されていなかったものを改善したという機能でしたが、このGeneratorというのは全く新しい概念としてES6に取り込まれています*1

Generatorというのは関数処理内の任意の場所で処理を中断/再開できる仕組みを提供するものです。この仕組は一般的にコルーチン(co-rutine)と呼ばれています。コルーチンを使うと無限リストやイテレータなどを実装することができます。

このGeneratorとPromiseを組み合わせることで非同期処理を同期処理のように直列に書くことができるようになります。基本的な考え方は「非同期処理が開始されたら処理を中断し、非同期処理が完了したら処理を再開し後続の処理を実行してく」というものです。先ほどのPromiseの項で紹介したサンプルコードをGeneratorを使って直列に書いてみます。以下のサンプルコードではGeneratorとPromiseを使って非同期処理を直列に書くことができるcoというライブラリを使っています。

// ES6'use strict';

co(function*(){
  console.log('sleep...');
  yield sleep(1000);
  console.log('wake!');
});

今回はcoを使って解説しましたが、coを使わずに非同期処理を直列に書く仕組みとしてasync/awaitという機能がES7に提案されています*2

Generatorについては私のブログでES6 Generatorを使ってasync/awaitを実装するメモとして解説しているので興味のある方は御覧ください。

まとめ

今回はJavaScriptで歯痒い思いをしていた所が、ES6でどのように変わるのかを中心に紹介しました。ここで紹介した内容はES6の一部であり、他にもModules, Symbol, Data Structures, Proxy, Template Stringなどの様々な機能が追加されています。現時点ではES6で書いたコードをそのままブラウザやnodeで実行するのは難しい状況ですが、ES6をES5にトランスパイルするツールとしてtraceur-compiler6to5があるのでお手軽に試してみることができます。また各ブラウザやツールがES6のどの機能に対応しているかはECMAScript compatibility tableが参考になります。

ES6時代のJavaScriptに備えて今から少しずつ触ってみるのはいかがでしょうか?

*1:Firefoxでは2006年ごろにはすでに実装されていました

*2:C#のasync/awaitと同様のもの

初めての新規サービス開発を通して学んでいること

$
0
0

こんにちは。投稿推進部の清水(@pachirel)です。 2009年にクックパッドに入社してから、インフラ周り、クックパッドの人事周り(採用・評価)や広告周りのシステム開発を担当していました。

2014年4月頃から、2〜3名の小さなチームで新規サービスのプロトタイピングをいくつか行っています。

企画の詳細は省きますが、私がこの10ヶ月ほどで学んだことをまとめました。アジャイル開発やLean startupの考えに共感しているので、そこから得た内容に私の体験を付け加えたものになっています。

今回はプログラミングに関する技術的な内容は含まれていません。

なぜ作るか

スタートアップが失敗する原因で一番多いのは「人が必要としていないものを作ってしまった」というものです。 The Top 20 Reasons Startups Fail

社内の新規サービス開発でも同じ傾向があるのではないでしょうか。

一方で「自分たちが作りたいものを作る」のが大切だと思っています。どうすれば「需要のあるものを自分たちが作りたいと思う」状態になれるか。それは、自分たちがそのサービスのユーザーになることです。

エンジニアがエンジニア向けのサービスやツールを作るというのは分かりやすい例ですが、仮に自分とは遠い対象に向けて作る場合、例えば料理をしない人が毎日料理をしている人に向けて何かサービスを作るとしても同じことです。

誰かのために料理を作る。家で他にやりたいことがある中でも作る。子どものために料理を作るユーザーになりきって、子ども向けのレシピを実際に作ってみる。 実際に子ども向けに料理を作っている主婦に話を聞きに行き、実際に料理をしている現場に立ち会い、子どもがそこに居る状態で料理を作るというリアルを理解する。そういう現場を目の当たりにして自分の身を持って理解することではじめて、同じ目線に立つことができます。

私はお芝居をしたことがありませんが、俳優の役作りにも近いかもしれません。 同じ目線に立てれば、その目線で自分が感じる「困りごと」や「それを解決できそうなもの」はかなり正解に近づいているはずです。その手法に自分たちのアイデアが組み込まれていれば、ユニークで競争力のあるサービスになると思いませんか。 この状態でペルソナやユーザーストーリーを書けば、ずっと信じられるものになるでしょう。私はチームメンバー全員でこのプロセスを踏む必要性を感じていて、今のチームで実践しています。

なにを作るか

ここまでで、サービスの輪郭は見えてきているので、プロトタイピングでそれを具体化していきます。私の居たチームは少数で、かつコードが書けるメンバーが私1人だったので、プロトタイプを実装しようとすると私がボトルネックになってしまい、思ったスピードでサービスの検証が進められませんでした。

そこで、モックサービスを利用しました。flintoprottを試しましたが、デザイナーとプロジェクトを共有しやすいprottを今は使っています。最初はこれで本当に検証できるのかという不安もありましたが、実際にやってみると、かなり有用なフィードバックを得ることができ、時間の節約に繋がりました。特にデザイン(色、アイコン、配置、文言等)部分は、動くソフトウェアでなくても十分検証できます。

実際にインタビューをしてみて気づいたのは「理想のユーザーは居ない」ということです。確かに目の前でインタビューに答えてくれている方は将来ユーザーになってくれる可能性が高いのですが、人によって使い方は異なり、感想は食い違うこともあります。多数決で決めてしまうと、全体としてちぐはぐしたサービスになってしまいます。 人はひとりひとり違います。でも、実際に使ってくださるのはそういう方々です。様々なフィードバックのうち、どれを取り入れるべきかという判断に困りました。

そこで、私たちは想定している問題をきちんと解決できているか優先することにしました。想定している問題をインタビュー対象の方が同じように問題だと認識しているか確認できていることが前提にはなりますが、その上であれば細かい部分はリリース後に数字を見ながら改善していけばよいと割り切りました。どうしても、ユーザーインタビューでは数が確保できないし、インタビューよりも実際に使ってもらったほうがより正しいフィードバックを得ることができます。

この方法を取るためには、自分たちのサービスが何の課題に向き合っていて、解決できているかをどういう指標で見るかということが大事です。

Lean analyticsではそれをOMTM(One Metric That Matters: 最重要指標)と呼んでいます。例えば、クックパッドを例に取ると、ページビューが多いことは必ずしも良いことではありません。レシピを素早く決めたいと思っている人にとっては、ページビューは少ない方がいいのです。ページビューの増加がユーザー数の増加によるものなのか、ユーザーの迷いの結果なのかがこの指標だけでは判断できません。この指標はレシピ検索サービスの満足度の指標としては悪い指標です。サービスの質に直結する指標を設定するのが重要です。

きちんと指標を決め、計測をし、それを統計的に意味のある形で理解する必要があります。最近、日本語訳が出たLean Analyticsは去年、社内で読書会をして英語で読みましたがとても参考になりました。統計学は社内で何冊か読書会をしたのですが、その中では入門統計学が数学の難しい部分をなるべく省いて、統計学の雰囲気をつかめるように書かれていて、一番読みやすかったです。

インタビューの手法については、 Running LeanLean customer developmentを参考にしています。

Lean customer developmentについては翻訳が出ておらず、私も読んでいる最中ですが、日本語ではこちらのスライドが参考になりました。 人間と話す: Lean Customer Development (Lean Startup Update 2015)

だれと作るか

先ほど、「チームメンバー全員でユーザーになりきる」という話をしましたが、その前提として、それを実践できる人だけでチームを作る必要があります。なりきれる度合いには個人差がありますが、実際にそれにチャレンジできる、してもいいと思えるテーマかどうかはそのプロジェクトが成功するかどうかのテストの1つになると思います。 ここで躓くならば、最初から始めない方がよかったなんてことがあるかもしれません。自分にその熱意がないなと思ったらそっと手を引くのも長期的にはチームのためになると思っています。

異なる専門性を持つメンバーでチームを組むというのも重要です。 サービス開発に必要なあらゆる要素に専門性を持つメンバーが居れば完璧ですが、世の中そんなにうまくはいきません。 スタートの時点できちんとメンバーを揃える努力をするのはマネージャの仕事とはいえ、自分たちのチームはどの分野が強く、どの分野が弱いのかということを全員が把握しておく必要があります。

そして、チームに足りないものを補ったり、良いところを伸ばしたりすることで戦える状態を作るのです。

サッカーなどのチームスポーツに例えると、選手の特性を把握して適切なポジションを決めたり、素質のあるメンバーを足りないところにコンバートしたりすることが必要です。その上で、メンバー間の理解を深めていきます。

このあたりは「ジャイアントキリング」という漫画に最近ハマっていて、おすすめです。引退したエースストライカーが元居たサッカーチームの監督になって低迷するチームを再生していくストーリーで、漫画みたいにうまくいくことはそうそうないですが、こういう物語に感化されて思い切った行動ができて、良い方向に向かえばいいよねと思っています。

ファシリテーションやスクラムのケーススタディとしては、ザ・ファシリテーターSCRUM BOOT CAMP THE BOOKが参考になりました。

と偉そうに書いたものの、実際には私は管理職ではないので、用意されたチームと期待された役割の中でどうにかする必要がありました。 同じチームのメンバーとの会話を増やして何を考えているかを知り、自分の考えを共有して少しずつチームとしての役割分担を決めていくようにしました。 穏便に進めようとするとどうしても遅くなってしまうので、スピード重視の新規プロジェクトでは考え方の相違による摩擦を恐れず、コミュニケーションを取ることが重要なのだと学びました。 チャットやメールで済まさず対面で話す、笑顔を忘れない、相手の立場を尊重することを大事にしています。

まとめ

私は普段エンジニアとしてRubyやSwiftを書いているのですが、今回は新規サービス開発を通して学んだことについて書きました。

エンジニアとして、自分が心をこめて作ったものがたいして世の中の役に立たずに消えていくというのはくやしく、寂しいものです。そういう経験を少しでも減らすために、コードを書き始める前段階から主体的に関われるチームに身を置きたいし、自分の居るチームがそういうチームになるような影響を与えられる存在でありたいと考えています。

チームメンバーとの信頼関係を築く:定期個人面談の薦め

$
0
0

こんにちは。新規広告開発部所属エンジニアのレオ(@lchin)です。

ここ2年ほどは、大きな事業部のなかの小規模なエンジニアチームのリーダーを務めてきました。エンジニアリーダーとしては、1人のエンジニアとしてソフトウェア開発をしつつ、チームのメンバーの力をまとめて、事業部のゴールを推進しました。事業部のマネージャほど、マネジメント業務が中心になるわけではありませんが、多くのエンジニアが苦手な人間関係スキルはエンジニアリーダーにも必要です。

メンバーは何か大きな不安を抱えていないのか?ポテンシャルを発揮できていないメンバーにどうフィードバックするのか?メンバー間に何かトラブルはないのか?見えないところで仕事の妨げはないか?チームでソフトウェア開発を行う上のよくある悩みだと思いますが、皆さんはどう解決していますか?私は、個人面談はこういった悩みを解消するための大変有効な手段だと思います。

なぜ個人面談

毎日一緒に仕事をしてる人とは、なぜわざわざ個人面談をする必要などないと感じるかもしれません。毎日のスタンドアップミーティング、毎イテレーションのふりかえりなど、チームメンバーと話す場面は沢山あります。しかし、いくら普段からコミュニケーションを取っていても、実は1対1ではないと話せないことが沢山ありますし、忙しさに埋もれて話題に上がらないことも沢山あります。そこで問題を発見できれば、早い段階の解決に繋がることもありますし、何よりお互いの理解を深めることができます。

何を話すか

メンバーが話したいことであれば、何でもよいです。仕事の話題でも、プライベートの話題でも、技術ネタでもよいです。ただし、個人面談はチームメンバーの抱えてる課題を発見して解決に導く上、お互いの理解を深めて、結果として強いチームを作ることを目的としますので、特に注目したい話題がいくつかあります。

仕事のブロッカー

仕事が順調に進んでるように見えるかもしれないけど、直接聞かないと気づかないような仕事のブロッカーはよくあることです。それがメンバーの仕事に対する不満かもしれないし、チーム内の齟齬や他部署とのやりとりかもしれないです。経験豊富なリーダーとして、大した問題ではないと感じるかもしれないけれど、本人にとっては大変な問題だと感じます。しっかり耳を傾けて、会話によって本当の問題を発見して解決に導くことは個人面談の主目的になります。

相手を配慮したネガティブ・フィードバック

定期的な面談のもう一つのメリットは、実はネガティブ・フィードバックの機会であることです。チームの前でネガティブなフィードバックをすると相手を傷つけかねません。いきなり呼び出しをすることも同様です。定期的な個人面談であれば、相手の気持ちに気を配りつつ、ネガティブなフィードバックを提供できる絶好の機会です。もちろん、そんなフィードバックが必要になる前にこの場を使って未然に防ぐことができたらもっとよいでしょう。

逆に相談する

メンバーの話を聞くチャンスだけではなく、逆に自分自身の不安や課題について相談するチャンスとしても活用できます。自分の相談ごとをその場で解決する必要はないのですが、メンバーもきっと優秀ですので、新たな視点や気付きを貰えるかもしれません。お互いに何か得るものはあるはずです。

評価の話をしない

一方、今回取り上げてる個人面談はチームの運用をより上手く行うためのもので,会社の人事制度で社員を評価をするための面談ではありません。評価の話になってしまうと、本当に大事な話ができません!

個人面談のやり方

1. 定期的に実施する

決まったスケジュールで実施しましょう。たとえば、「毎月の第一火曜日」のように、決まった頻度と日程に設定します。そうすると、面談の習慣が定着し、更に信頼につながります。そして、相手を尊重してきちんと約束の日程を守りましょう。継続は力なり。 どうしても約束が守れないときは、飛ばさずにリスケしたほうがよいです。

2. 十分に時間を確保する

個人面談に多くの時間を割くのは無駄だと感じるかもしれませんが、そもそも十分な時間を確保しなければ、浅い話しかできなくなります。最低でも30分くらいに設定するとよいでしょう。

3. 自由に話せる場所を選ぶ

自由に色々な話題が話せるように、なるべく相手が安心して何でも話せるところを選ぶとよい思います。普通に会議室で行うのがベストだと思いますが、リラックスして気持よく話せるところならどこでもよいです。外で散歩しながらでもよいし、ランチミーティングでも居酒屋ミーティングでもよいです。

4. 相手の話したいことを引っ張り出して、聴く

とりあえず「最近、どう?」など、簡単でオープンな問いかけから始めましょう。相手を誘導しないような質問から面談を開始すると、本当に何を考え、何を感じてるかを話してくれる確率が高くなります。

勝手にいろいろと喋ってくれるような人であれば、その後は耳を傾けて聞けばよいのですが、そう上手くいかないことも多いです。話が長く続かなければ、もっと話を誘導するような質問に切り替えましょう。

  • 仕事のブロッカーありますか?
  • 今のチームのやり方、どう思いますか?
  • 最近、試してみたい技術ありますか?
  • なんか不安ありますか?
  • 最近ひどいと思ったコードありますか?
  • 今の仕事、楽しいですか?
  • など

もちろん、普段からの気付き、聞きたいことを事前に用意しておくことがベストです。

注意点として、面談が進捗報告会にならないようにしてください。「進捗ダメです!」をただ知るではなく、なぜ進捗がダメなのかを発見する場として活用しましょう。

5. 話し合う

話題を発見できたら、積極的に意見交換しましょう!

まとめ

皆さんもぜひチームメンバーと定期的に面談してみてください。上手に個人面談を実施すると、チームのメンバーの思いを汲み取り、チームの色々な課題を発見し、解決に導くヒントに出会えると思います。そして、メンバーと強い信頼関係を作り、チームにさらなる活気をもたらし、より良い仕事が出来るようになるはずです。

【学生限定】日経電子版さんと共同でデータハッカソン開催します!

$
0
0

こんにちは!クックパッド編集室動画グループの @yoshioriです。

今回、日経電子版さんと共同で学生さん向けにデータハッカソンを 3 月 7 日 (土) に開催することになりました!!

f:id:Yoshiori:20150209150938p:plain

データハッカソンというあまり聞き慣れない (しかも凄そう!な) 名前ですが簡単に言ってしまうと、普段あまり触れない実際に企業が使っているデータを使って機械学習や可視化など自分の興味のあることをやってみるイベントです。

両社ともこの日限定で公開されるデータもありますし、両社のエンジニアも皆さんの質問などにすぐに答えられるように多数参加します!!

クックパッドから参加するエンジニアを何人か紹介します。


原島 純

京都大学で自然言語処理を専攻。 2013 年 3 月、博士 (情報学) を取得。 同年同月、言語処理学会論文賞を受賞。 2015 年 1 月から NLP 若手の会プログラム委員に就任。

クックパッドには 2013 年 4 月にジョイン。現在は検索・編成部に所属。 自然言語処理 (レシピの解析) や機械学習 (レシピの分類・コンテンツの推薦) に関する業務に従事。


@chezou有賀 康顕

名古屋大学で音声感情認識を専攻、東芝で自然言語処理、音声対話、大規模データを用いた機械学習の研究を行う。2013 年まで人工知能学会広報委員を務める。 kawasaki.rb, Machine Learning Casual Talks の主催の他、技術計算のための言語 Julia の啓蒙にコミットしている。

現在は会員事業部で、副菜提案などのレコメンデーションを中心とした新規機能の開発をしている。


兼山 元太

静岡大学で情報科学を専攻。楽天株式会社に新卒で入社し、楽天市場の開発運用に従事。趣味で twitter のツイートを検索できるサービスを開発。数十億件を収集検索できるようにしているうちに検索が楽しくなり、 2010 年よりクックパッドでレシピ検索の開発に従事、2014 年に大谷さんらと一緒に Elasticsearch の書籍を翻訳。

検索・編成部所属。


他にも僕含め手伝いで何人か参加します!

また、最後には発表をしてもらい、良かったものには豪華賞品も用意しています!

たった一日のハッカソンで実用的な結果が出てくるとは私達も思っていません (もちろん、そんな結果が出たら私達の想像を超えているので大変素晴らしいですが!)

どんなデータを元に、どんな世界を築こうとしたかのアプローチが大事です!失敗を恐れず参加、挑戦してください。

イベント詳細

タイムテーブル

09:15~ 開場・受付開始

09:30~ オープニング&オリエンテーション

10:40~ 作業開始

12:30~ 昼食 (お弁当をご用意します)

18:00~ 発表

19:00~ 審査・結果発表・講評

19:30 終了

※ 時間・内容は一部変更になることがあります

日時

3 月 7 日 (土) 9:30~19:30 (受付開始 9:15~)

場所

クックパッド株式会社東京本社オフィス

アクセス: https://info.cookpad.com/location/

参加対象者

大学生または大学院生。学年・学科等は問いません。

持ち物

ご自身の PC (無線インターネット環境は用意します)

参加方法

こちらよりご応募ください (外部サイトに移動します)

締め切り

2 月 27 日 (金) 18:00 まで

※ 応募者多数の場合抽選とさせていただきます。

※ 当選結果については 3 月 3 日 (火) 18:00 までにメールでご連絡いたします。

最後に

私達も当日皆さんにお会いできるのを楽しみにしています!

学生の皆さんのご応募、お待ちしています!!


大量の印刷用画像をウェブ用に変換する方法

$
0
0

こんにちは。広告事業部の上田です。
今はおもに新広告商品の開発をしていますが、少し前までプロのレシピを開発していました。 そのときの話を少し書きます。

プロのレシピは雑誌や料理本、料理研究家のレシピが見放題、横断検索もできるサービスです。
2014年9月にリリースしました。
インターネットで公開されているレシピだけではなく、雑誌や本にしかないレシピもたくさん含んでいます。
開発中、これらの大量のレシピをどうプロのレシピにインポートするかが問題の一つとなっていました。

データは出版社から、基本的にInDesignの形式で受け取りました。
テキストはPDFに変換してコピー&ペーストして手で修正という力技で対処しましたが、画像はEPSファイルをウェブ用に変換しなければいけません。 印刷用の画像なのでカラーモデルはCMYKです。 普通にJPEGに変換しただけだと『IE8以下では見られない』『ChromeやFirefoxで色がかなり変わってしまう』などの問題が起きます。

たとえばCMYKのサンプル画像を、『Photoshopで適切なカラープロファイルを用いて変換したもの』と『一般的なウェブブラウザで表示したもの』を並べてみると一目瞭然です。

Safariが一番マシですが、それでも淡い色合いになっているのが分かりますね。

試行錯誤の結果、ある程度の質を保って変換する方法が確立できたので書いておきます。

ImageMagickの準備

ここから先はMacを想定していますが、WindowsやLinuxでも可能です。

HomebrewでImageMagickを入れている場合は先にアンインストールしてください。

brew uninstall imagemagick

つぎにLittle CMSと、Little CMS対応版のImageMagickをインストールしましょう。

brew install little-cms2
brew install imagemagick --with-little-cms2

最後にAdobeからICCプロファイルをダウンロードして展開します。 使うのはその中のJapanColor2001Coated.iccだけです。

sRGBのプロファイルはMac付属のものを使いますが、無い場合はInternational Color Consortiumからダウンロードしましょう。

画像の枚数が少ないとき

CMYKなJPEGの場合:

convert -profile '/path/to/JapanColor2001Coated.icc' -colorspace cmyk -profile '/System/Library/ColorSync/Profiles/sRGB Profile.icc' -colorspace srgb cmyk.jpg srgb.jpg

EPSの場合: (EPSファイルを300dpiとみなし縦横1000px以下にする)

convert -profile '/path/to/JapanColor2001Coated.icc' -colorspace cmyk -profile '/System/Library/ColorSync/Profiles/sRGB Profile.icc' -colorspace srgb -density 300 -resize '1000x1000^>' -flatten -quality 90 src.eps dest.jpg

画像の枚数が多いとき

複数のファイルを一気に変換する場合はmogrifyコマンドを使いましょう。
CPUのコア数分、並列で変換してくれます。
リソースを食いつぶすようなら-limitで制限できます。

EPSの場合:

mogrify -profile '/path/to/JapanColor2001Coated.icc' -colorspace cmyk -profile '/System/Library/ColorSync/Profiles/sRGB Profile.icc' -colorspace srgb -density 300 -resize '1000x1000^>' -flatten -format jpg -quality 90 *.eps

カラープロファイルは決めうちでいいのか

本来、カラープロファイルは画像やInDesignのファイルに結びついたものを使うべきなのですが、往々にして指定されていません。
そういう場合は"Japan Color 2001 Coated"を指定することで大抵うまくいきます。
"Japan Color 2011 Coated"も出ていますが、"Japan Color 2001 Coated"はAdobeのデフォルトですし、デファクトスタンダードはまだしばらく続くでしょう。

変換による色の劣化


(EIZO×MdN特別セミナーより)

上の色度図のJapan Color 2001 CoatedとsRGBを見ると分かるように、お互いカバー出来ない色域が存在します。 このはみ出た部分の色を変換する場合、変換の手法によって圧縮されたり切り捨てられたりします。
(Photoshopでは『編集→プロファイル変換→マッチング方法』で切り替え可能)
つまり、sRGBからCMYKに変換したあとsRGBに再変換しても、元の色になるとは限らないということですね。

まとめ

印刷物を扱う機会はそれほど頻繁にはないでしょうが、それだけにノウハウが少なくハマりがちです。 CMYKで痛いめにあったらこの記事を思い出してください。

  • カラープロファイル無しでCYMKからRGBに変換するとたいていおかしくなる
  • カラープロファイルが謎の場合は Japan Color 2001 Coated を使おう
  • 安寧のために、なるべく元のRGB画像をもらおう

【学生限定】エンジニア志望の方が抱いている素朴な疑問を解決する「Cookpad TechBar vol.2」開催します!

$
0
0

こんにちは、投稿推進部の勝間(@ryo_katsuma)です。

2015/1/23(金)に、学生の皆さんがクックパッドに対して抱くような質問に対して、料理とお酒を楽しみながら弊社エンジニアがお答えするイベント「Cookpad TechBar」を開催し、盛況を博しました。当日の様子はStaff Blogでもレポートさせていただきました。

参加者の方のアンケート結果や、あっという間になくなってしまった料理を目の当たりにして「これはもう一度行うべき!」と私たちは判断して、来る2015/3/6(金)にCookpad TechBar vol.2を開催することにしました!

f:id:ryokatsuma:20150212144846j:plain

今回もvol.1と同じように「クックパッドってレシピだけじゃないの?」の質問に答えることを中心に、学生の皆さんと弊社エンジニアがじっくり話せる場を設けようと思います。下記に1つでも当てはまるエンジニア志望の学生の方はぜひご参加ください。

  • 「クックパッドってレシピだけじゃないの?」と思っている
  • クックパッドのエンジニアとして働くことに興味がある
  • Web業界でエンジニアとして働くことに興味がある
  • クックパッドの開発者ブログを読んでいる
  • クックパッドのエンジニア勉強会に興味を持っている
  • でも、いきなり勉強会に参加するにはハードルを感じている...
  • 前回のCookpad TechBarに行きたかった!

イベント詳細

参加社員

  • 勝間 亮(投稿推進部)
  • 関口 隆介(ヘルスケア事業部)
  • 多田 圭佑(ホリデー事業室)
  • 出口 貴也(ユーザーファースト推進室)
  • etc

プログラム

  • オープニング
  • 会社概要LT/質疑応答
  • 各エンジニア社員のLT/質疑応答
  • 自由交流会(お酒とお料理の用意も予定しております。)

※ ノンアルコールの飲み物もご用意いたします。

日時

2015/3/6(金)18:30〜20:30(18:15〜受付開始)

場所

クックパッド株式会社東京本社オフィス アクセス:https://info.cookpad.com/location/

参加対象者

エンジニア志望の学生の方

歓迎

2016年度卒業予定の方

参加費

無料

持ち物

筆記用具(アンケートをご記入頂きます)

参加方法

こちらよりご応募ください (外部サイトに移動します)

締め切り

2015/3/4(水)21:00 ※応募者多数の場合抽選とさせていただきます。

最後に

学生の皆さんのご応募、お待ちしています!!

f:id:ryokatsuma:20150212144846j:plain

社内 apt リポジトリの運用と deb パッケージビルドの話

$
0
0

id:sora_hです。今回はクックパッドの社内 apt リポジトリ管理・deb パッケージビルド・リリースフローについて紹介します。

クックパッドではいままで CentOS 6 を利用していましたが、最近は Ubuntu への移行を進めています。現在は CentOS / Ubuntu 両方が共存したインフラになっています。

CentOS では社内に yum リポジトリを置き、本家リポジトリから消えてしまったパッケージや、独自でビルドしたパッケージのホストを行っていました。Ubuntu 導入以降も同様に、社内 apt リポジトリを設置し、必要があれば独自でパッケージをビルドすることにしました。具体的には、わたしは ruby2.1 パッケージや ruby2.2 パッケージをメンテナンスしています。

(なお、わたしは rpmspec および ebuild の方が慣れていて、未だ deb パッケージビルドでよくハマっているので間違いやより便利な方法があればフィードバックいただけると助かります...)

管理

deb パッケージは rpmspec や ebuild と違い、展開されたソースの上に debianディレクトリを作成、その下へパッケージの情報やビルド処理を記述していきます。

CentOS 向けの rpmspec は単一の git リポジトリへ全てチェックインしていましたが、deb パッケージ作成において 1 リポジトリに全部入れるのはファイル数・サイズ的にも難しいという話になりました。また、git-buildpackageのワークフローを採用するためには git リポジトリを分割しておく必要があります。 そのため、deb パッケージにおいては、パッケージごとに 1 つ git リポジトリを持つポリシーになりました。

クックパッドでは deb パッケージのリポジトリをまとめるための organization を 1 つ GitHub Enterprise (GHE) へ作成し、そこへ deb パッケージのソースリポジトリをまとめています。

ビルド

パッケージビルド用 vagrant 環境の用意

クックパッドでは開発環境として全員 Mac を利用しているので、ビルドに必要な物をインストールした Ubuntu の環境を作成する Vagrantfile を用意してあります。 なお、vagrant の box は実際に AWS 上で動作しているインスタンスに近い、共通のものがあるのでそれを利用しています。

https://github.com/sorah/sandbox/blob/master/misc/deb-build/Vagrantfile

git-buildpackage (gbp)

git リポジトリで管理する事にしたため、git-buildpackage のフローに則っています。

  1. 新規パッケージの場合は gbp import-dscgbp import-orig, git clone + dh_makeでパッケージの体裁を整える
  2. gbp buildpackageでビルド
  3. 更新分を git push --all && git push --tagsで push
  4. 後述する方法でリポジトリへアップロード

また、ビルド回りのコマンドに関しては社内でチートシートのようなものを置いてあります。 (抜粋したものがこちら: https://github.com/sorah/sandbox/blob/master/misc/deb-build/README.md )

パッケージのリリース

aptly

apt リポジトリの管理には aptlyを利用しています。既存リポジトリのミラーや、Amazon S3 へのアップロードが簡単にできるのが特長です。

リリースコマンド

パッケージがビルドできた後は aptly へ aptly repo add, aptly publish updateを実行してリポジトリへ公開します。 また、GHE の Releases機能を利用して、gbp が作成した git tag から release を作成、成果ファイルを添付する、という事もしています。

つい最近まで、これは手動でやっていたのですが、アップロードする成果ファイル数は 1 つではありません (.dsc, .deb, .changes, .orig.tar.{gz,xz}ファイル)。 ファイル名規則も物によって異なる (orig ファイルの命名規則は deb などとは別) ので、バージョンを重ねる事に増えていくファイルから、どのファイルをアップロードするかを調べて人間が選択するのは億劫でした。

そのため、ビルド用 vagrant 環境に GHE, aptly へのアップロードを自動化するコマンドを追加して心の平穏を保ちました。それを公開可能な状態に改変したものがこちらです。参考になると幸いです。

https://github.com/sorah/sandbox/blob/master/misc/deb-build/bin/deb-release

まとめ

本記事ではクックパッドでの deb パッケージビルドや apt リポジトリの管理について解説しました。

参考文献

2014年でもっとも効果の大きかったプレミアムサービス訴求施策の話

$
0
0

自己紹介

会員事業部*1森田です。昨年はプレミアムサービス(以下PS)*2の訴求改善を担当しました。その中で抜きん出て効果の高かった施策を紹介します。具体的な効果の数値を書くことは出来ないものの、この施策一つで前期に私がおこなった他の施策効果の合計を上回ります。

施策内容

紹介する施策は検索結果に関係するものです。今までは検索結果の下に控えめに表示していた人気順検索*3以外のPSコンテンツの訴求を検索結果の間に追加しました。

  • 殿堂入りレシピ*4 (人気順検索以外のPSコンテンツ)の訴求 f:id:idwtstwof:20150216133201j:plain

予期せぬ効果

実はこの施策は会員事業部によるものではなく、別部署による検索ページへの大きなデザイン変更施策*5の一部として行われました。そのため検索結果の間でPSコンテンツを紹介することも私が考えたわけではなく、効果についてさほど深く考えていませんでした。当時私が気にしていた事は今までも存在していた人気順検索の訴求効果の変化です。訴求枠の位置を下げた事と殿堂入りレシピをはじめとした他のPSコンテンツの訴求が増える事で人気順検索の訴求が低下する事を心配しました。

  • 人気順検索の訴求 f:id:idwtstwof:20150216133448j:plain

しかし結果をみれば人気順検索の訴求効果はわずかに改善し、殿堂入りレシピの訴求効果は人気順検索の訴求の半分程度の効果がありました。表示位置を考えると非常に高いものです。

教訓

手法の教訓 - 複数コンテンツの紹介

これまでは新着順検索の利用者には人気順検索のみを強く訴求していました。たしかに人気順検索はクックパッドのPSで一番の人気コンテンツです。そのため紹介枠が排他的であれば人気順検索を訴求したほうが良い結果になります。しかし今回のように利用者の目的に合致するコンテンツを複数提示できる場合はそのまま複数提示したほうが良いかもしれません。*6

なおこの結果を踏まえ、排他的にしかコンテンツを訴求できない場合も、利用者が複数回目にするような場所では表示割合を考慮しながら複数のコンテンツを切り替えて表示する事を試しています。利用者の心に響かなかったコンテンツの訴求を繰り返すよりは別のコンテンツを訴求しようという考えです。これは外部の広告では意識せずに行っている話にもかかわらず自社コンテンツの訴求となると行えていませんでした。少ないデータを見る限りは良い手応えを感じているため今後も検証を続けていきたいとおもいます。

姿勢の教訓 - 手札を整理して利用者の行動と照らし合わせる

この施策を行うまで私は人気順検索の訴求改善に殆どの時間を費やしていました。クックパッドでは「人気順」のキーワードは強力です。新しいコンテンツを作った場合も人気順検索の訴求枠を簡単に利用する事はできません。その結果ほかのコンテンツを紹介する事をあきらめつつありました。しかし今回の施策により利用者の行動にあわせて適切に提示することで大きな成果を出せることを知りました。初心に返り、自分たちの持つ手札と利用者の目的を考えたうえで適切なPSの価値を提示したいと思います。

さいごに

ここまでの記事を同僚に読んでいただいたところ「ちなみにこの話の中で一番大事だと思うことは何ですか」と聞かれました。そういう話であれば、私としては今回の改善を事前に気づくことが出来なかった事こそ一番に考える必要のある事だと思います。つまりは課題を解決する材料は一通り有る状態で肝心の問題と結びつけるにはどうすれば良いかです。しかし門外漢なうえ多くの経験があるわけでも、ましてや答えがあるわけでもありません。一般的な手法としてマインドマップやブレストなど様々なものが既にありますので、今回はそういった事を考えるための一事例として記事を書かせていただきました。

とはいえ個人的な経験を振り返るならば同僚との雑談*7が問題解決のきっかけになることは非常に多いです*8。過去、私は豊富なRoR*9の知識を持つ方たちとRoRのバージョンを上げる仕事をしました。そういう方達も時にはちょっとした事で詰まります。しかし他のメンバーと気軽に話をすることで大体は解決策を得ることができました。解けてみれば「なんだそんなことか」という場合も少なくありません。さらに遡れば今回の施策の効果測定で使用した「Chanko*10」も、こういう物が欲しいよね。必要だよね。という雑談によるものだったと思います。そしてまた現在検討している新しい施策も雑談なしでは辿りつけませんでした。

今回の件をみても同僚達の頭のなかにアイデアの種はあったのですから、多く雑談をしていればよりはやく行動する事ができたかもしれません。そして今現在も材料はすべて揃っているにも関わらず問題と結び付けられていない事はたくさんあるはずです。そこに気づくためにも今以上に積極的に雑談することで触発しあっていきたいです。

*1:私の所属する部署です。クックパッド会員になっていただくために努力しています。

*2:月額税抜き280円で提供しているサービス。

*3:クックパッドのレシピを人気順で検索出来ます。PSの人気コンテンツ。

*4:レシピを作ったレポートをレシピ投稿者に送る「つくれぽ」を1000人以上から得たレシピ。PSのコンテンツ。

*5:レシピ写真が大きくなり、検索結果から直接マイフォルダへ登録出来るようになりました。

*6:とはいえ訴求もつまりは広告なので利用者の体験を著しく低下させないように気をつけたいと思います。

*7:会話ではなく雑談としたのはアイデアをだそう!と構えた場合よりも気軽な会話のほうがきっかけになることが経験的に多いからです。

*8:他には本を読むこともきっかけになることが多いです。2014年度の施策のいくつかも本の内容がきっかけでした。

*9:Ruby on Rails。

*10:限定したユーザに機能を公開するためのクックパッド製のRoR拡張 https://github.com/cookpad/chanko

脱ビギナー!Androidのnullな話

$
0
0

新規広告開発部の松本です。 クックパッドiOS/Androidアプリの広告の開発に携わっています。

Androidアプリ開発の際、皆さんはnullをどのように扱っていますか?また、nullチェックを行うのであれば、どのような基準で行っていますか?私自身まだまだAndroid開発歴が浅いため、特に何か基準がある訳でもなく至る所でif (foo != null)といったnullチェックを行おうとしていました。

これに対し、先日の社内コードレビューでとてもためになるアドバイスをもらいました。私のようなAndroid初心者にとってnullに対する考え方の基礎を作ってくれるレビューだったので、本稿で共有したいと思います。

また、AndroidやJava開発に慣れた方にとっては「今更そんな話か」といった内容かと思いますが、クックパッドでのレビューの一例としてご覧いただければ幸いです。

やりがちなnullチェック

あるとき、「Androidではクラッシュしないことが大事であり、そのためにnullチェックは基本!」という考えのもと、以下のようなコードを至るところに書きました。

if (foo != null) {
    foo.bar();
}

するとコードレビューでこんなコメントをもらいました。

一般に、過剰なエラーチェックは混乱を招くのでよくないし、内部状態が壊れているのになんとなく動いてしまうというのは結果が予測できず、クラッシュより深刻なバグになる可能性があるのでよくないです。

今まで考えていた事とは逆ですが、確かにレビュアーの言うとおりです。歪な修正は根本的な原因を隠す*1ことに繋がるのですね。

では、どんな基準でnullチェックを行えば良いのでしょうか? そんな疑問が浮かんだ頃、こんなアドバイスをもらいました。

  • nullはふつうにあるし、無視でよい
    • ->現状この振る舞いでOK
  • nullは想定内の例外なのでハンドルしたい
    • -> error listenerを登録できるようにする
  • nullは完全に想定外なので、APIのバグでしか起こりえない
    • -> IllegalStateExceptionでクラッシュさせる。またその結果Crashlyticsにログが記録される

nullチェックだけでなく、try catch文やCrashlyticsを使う基準までまとめてもらいました。「敢えてクラッシュする可能性を残す」ことも時には必要なんですね。

@Nullableと@NonNull

またあるとき、こんなコードを書きました。

publicclass Car {
    private Engine engine;

    public Car(Engine engine) {
        this.engine = engine;
    }
}

これに対し、こんなレビューをもらいました。

engineについて @Nullable or @NonNull annotationがほしいすね。nullがくるのが仕様なのかそうでないか知りたい。

そもそも仕様としてnullはこない(くるべきでない)なら、引数を @NonNull で注釈しつつnullがきたらIllegalArgumentException投げるみたいなのでもいいんですよ。

そんなアノテーションも使えるんですね!どうやらAndroid Support library version 19.1からサポートしているようです*2

更にこんなアドバイスも。

引数にも @NonNull Engine engine と指定することができて、そうしているとこのメソッドを呼び出すときに @Nullable な値を渡そうとするとIDEが警告出してくれたりするんですよ!まあ、警告はかならず出してくれるわけではないので、documentの意味合いのほうが強いですけど。

@Nullable などはどの変数やフィールドにつけてもそれなりに効果はありますが、一番効果がある(ゆえに優先度が高い)のは公開インターフェイス部分です。つまり引数と戻り値すね。

引数に指定してあると「あーnull渡すとだめなんだ」ってすぐわかるし、戻り値がnullableだと someMethod().chainedMethod() みたいにすると警告が出るのでわかりやすい。とはいえとりあえずは引数だけつけるのがおすすめです。

Android Studioはversion 0.5.5から@Nullable/@NonNull対応、すなわち警告を出してくれるようですね*3。 引数に@NonNullを指定するのはとても便利ですね。ライブラリやSDKを作る際に、「この引数はnull禁止!」ということを明示的に示すことで想定外の使われ方が減りそうです。

正しくnullを扱う例

以上のアドバイスを元に以下のコードを修正してみましょう。

publicclass CarMaker {

    public Car make(Engine engine) {
        Car car = new Car(engine);
        car.checkEngine();
        return car;
    }

    privateclass Car {

        private Engine engine;

        public Car(Engine engine) {
            this.engine = engine;
        }

        publicvoid checkEngine() {
            if (engine != null) {
                engine.check();
            }
        }
    }
}

コード中のengine変数はnullを許容できないので、CarMakerクラスの利用者にその旨を通知したいとします。

そこで、engine変数は@NonNullであることをメソッドの引数で宣言します。

publicclass CarMaker {

    public Car make(@NonNull Engine engine)  {
        Car car = new Car(engine);
        car.checkEngine();
        return car;
    }

    privateclass Car {

        private Engine engine;

        public Car(@NonNull Engine engine) {
            this.engine = engine;
        }

        publicvoid checkEngine() {
            engine.check();
        }
    }
}

checkEngine()メソッド内のnullチェックがなくなりスッキリし、可読性が上がりました。

また、engineはnullを受け入れないことが明示的に宣言されたので、アノテーション対応のIDE*4を使用した際に警告が表示されるようになりました。

余談

実はこのnullの扱い、そう単純に割り切れないようです。 いくつか考慮すべき点があります。

どこまで@NonNullを付けるか

上の例では各メソッドの引数にのみ@NonNullを付けました。 しかし、Carクラスのengineメンバなどにも@NonNullを付けることは可能です。 どこまで@NonNullを付けて、どこからは付けなくてよいのでしょうか?

厳格に考える場合

厳格に考えればCarのメンバであるengineにも@NonNullを付加すべきでしょう。 理由は以下のとおりです。

  • public methodの引数はドキュメントとしても、IDEに対する注釈としても有効なので付ける
  • さらに、@Nullable/@NonNullは付ければ付けるだけ恩恵が広がるのでなるべく付ける
publicclass CarMaker {

    public Car make(@NonNull Engine engine)  {
        Car car = new Car(engine);
        car.checkEngine();
        return car;
    }

    privateclass Car {

        @NonNullprivate Engine engine;

        public Car(@NonNull Engine engine) {
            this.engine = engine;
        }

        publicvoid checkEngine() {
            engine.check();
        }
    }
}

緩く考える場合

緩く考えれば、@NonNullを付加するのはmake()メソッドだけで十分でしょう。 理由は以下の通りです。

  • CarはCarMakerの内部クラスなので、CarMaker#make() にNonNullを付ければ十分
publicclass CarMaker {

    public Car make(@NonNull Engine engine)  {
        Car car = new Car(engine);
        car.checkEngine();
        return car;
    }

    privateclass Car {

        private Engine engine;

        public Car(Engine engine) {
            this.engine = engine;
        }

        publicvoid checkEngine() {
            engine.check();
        }
    }
}

何の例外を投げるか

これまでの例では明示的に例外を投げていないので、NullPointerExceptionが発生します。 しかし、「引数が不正なのだからIllegalArgumentExceptionを投げるべきだ」と考える事もできます。

その場合、コードは以下のようになるでしょう。 @NonNullは一切付加せず、engineに対してnullチェックをしています。

publicclass CarMaker {

    public Car make(Engine engine)  {
        if (engine == null) {
            thrownew IllegalArgumentException("Engine must not be null.");
        }
        Car car = new Car(engine);
        car.checkEngine();
        return car;
    }

    privateclass Car {

        private Engine engine;

        public Car(Engine engine) {
            this.engine = engine;
        }

        publicvoid checkEngine() {
            engine.check();
        }
    }
}

どこまで使用者を信頼するか

「どこまで@NonNullを付けるか」と「何の例外を投げるか」、以上の2つは「どこまでクラス使用者(今回はCarMaker使用者)を信頼するか」に依るところもあるようです。

例えばCarMakerを使うのは自分だけであれば、make()メソッドの引数に@NonNullを付加してnullを与えないよう気をつけるだけでよいでしょう。

一方、CarMakerをライブラリとして公開し不特定多数の人が使用するのであれば、make()メソッドの引数で@NonNullを付加し注意を促した上で、Carクラス内でIllegalArgumentExceptionを投げることもあるでしょう。

publicclass CarMaker {

    public Car make(@NonNull Engine engine)  {
        Car car = new Car(engine);
        car.checkEngine();
        return car;
    }

    privateclass Car {

        private Engine engine;

        public Car(Engine engine) {
            if (engine == null) {
                thrownew IllegalArgumentException("Engine must not be null.");
            }
            this.engine = engine;
        }

        publicvoid checkEngine() {
            engine.check();
        }
    }
}

まとめ

本稿では、Android開発において私自身が受けたレビューを元に、nullの扱いについてお話しました。 一見簡単ながら、知れば知るほど奥が深く、私も引き続き勉強したいと思います*5

私と同じく最近Android開発を始めた方のお役に少しでも立てば幸いです。

新規事業サービスのトーン&マナーを設計するときに考えたこと

$
0
0

こんにちは、ユーザーファースト推進室の坂本(@kanako29)です。

昨年12月に新規事業サービス「クックパッドおいしい健康」のトンマナをリニューアルしました。クックパッドに入社してから初めての大きなトンマナ変更を担当したので、その時に気をつけたことや考えたことなどをまとめてみました。

f:id:kanako-sakamoto:20150223121733p:plain

なぜ変えるのか?

今回のトンマナ変更の理由として、以下がありました。

  • ターゲットの拡大
  • メッセージの伝達の明確化

それぞれについて、詳しく説明していきます。

ターゲットの拡大について

これまでのターゲットは、「30代後半〜40代の病気の夫を持つ女性」だったのですが、新たに女性の悩みを解決するコンテンツの追加により、肌荒れや体の不調に悩んでいる20代〜30代前半の女性もターゲットになりました。

以前のトンマナだと新たにターゲットとなった女性たちをカバーできないので、新しいターゲットにも使ってもらえるよう変更する必要がありました。そのイメージとして、「可愛らしさ」、「親しみやすさ」が良いのではないかと考えました。それと同時に、サービスとして毎日使っても疲れないように「シンプルさ」も必要な要素としました。

「可愛らしさ」と「親しみやすさ」について

現在のサービスは病気の色が強く、普段の健康を意識する女性が訪問したときに違和感を覚えるのではないか、またそれにより使ってもらいづらくなるのではないか、という懸念がありました。それを少しでも和らげて、使ってみたい、という気持ちを持ってもらうために、「可愛らしさ」と「親しみやすさ」のイメージを選択しました。

これらのイメージはパッと見たときの印象になるので、20〜40代の社内メンバーにデザインを見てもらって感想をもらいました。概ね「可愛くて良い」、「やわらかくて良い」という良好な反応だったのですが、「グレーの色が渋くて固い」などの感想をもらうこともあり、その場合はグレーをベージュに近づけるなどの修正を都度行いました。

色、フォント、形状などで「可愛らしさ」、「親しみやすさ」を表現します。 (これらの要素は、ターゲットの方が普段読んでいると思われる雑誌や、使用している生活用品などからヒントを得ます)

f:id:kanako-sakamoto:20150223121756p:plain

「シンプルさ」について

「シンプルさ」は、過度な装飾が判断の邪魔をしていないか、何度も使っているうちに色がきついと感じたりしないか、などに留意しました。

これがきちんと達成できているかは実際に使ってみてもらわないとわからないので、公開前、社内のメンバーに1週間程使ってもらって判断してもらいました。サイトを開いてから、レシピ検索や献立作成などのコア機能のタスクを行ってもらいます。

そして1週間後にレビューのミーティングを開き、気づいた点などを言ってもらいます。例えばこの時出てきた内容として、「濃い色の帯が目立ちすぎる」、「色が少しきついのではないか」などの声がありました。これは紐解くと、他の必要な情報が見えづらくなる、目に痛い、ということなのだと思い、色の彩度を下げて白に近づけたり、濃い色の面積を少なくするようにしました。

修正したものは再度使って問題がないか確かめてもらい、都度改善を行いました。

f:id:kanako-sakamoto:20150223121815p:plain

メッセージの伝達について

今回サービスが伝えたいメッセージとして、「夫の病気や、自信の不調や肌の悩みなどでネガティブになりがちな気持ちの女性に、少しでも明るい気持ちで使って欲しい」というのがありました。トンマナを明るい、ポジティブなイメージにすることで、ユーザーのタスクである献立作成に取り組みやすくなってもらえるのではないかと考えました。

これまではそのメッセージの伝達がはっきりとしていない、また明るいイメージが足りなかったため、今回の変更でより伝わるようにしました。

具体的には、明るいイメージを表現するには色による影響が大きいと考え、白をベースとして差し色で明度と彩度の高めの色を複数使用することにしました。

f:id:kanako-sakamoto:20150223121828p:plain

変更後の数値の変化

このトンマナが成功だったかは、リニューアル前の1ヶ月と、リニューアル後の1ヶ月(年末〜お正月期間は数値の変動が激しくなるので、今回は、1月6日から2月6日までとしました)の、ユーザーのメインタスクを反映する献立確定数・献立帳へのレシピ追加数で判断しました。

結果は、献立確定数が約1.35倍の増加、献立帳へのレシピ追加数が約1.2倍の増加となりました。これにより、今回のトンマナ変更は概ね成功だったと言えるのではないでしょうか。

まとめ

クックパッドでは、Webサービスのトンマナ変更をこのような考え方、フローで行っています。公開前の社内メンバーのフィードバックという定性的な面と、公開後の実際の数値という定量的な面でみて、品質の向上に努め、本当にユーザーの役に立っているかを判断しています。

このように定性的・定量的な面でみれるのは、Webサービス、もしくはクックパッドならではだと思います。トンマナ設計に携わる方のお役に立てれば幸いです。

クックパッドのデータを研究者に公開します

$
0
0

こんにちは。検索・編成部の原島です。

大学の研究者にお会いすると、「クックパッドのデータを研究に使用したいんですが...」と相談されることがあります。料理に関する研究をしているけれど、実際のデータがないため、なかなか研究が進まないという相談です。

料理に関する研究が進まないのは、クックパッドにとっても残念なことです。これらの研究は、クックパッドのサービスを改善するための「芽」でもあります。データがないだけで芽が育たないのは、非常に悲しい話です。

このような現状を打破するため、本日から、クックパッドのデータを研究者に公開します。このエントリでは、我々が準備してきたデータ公開の仕様について QA 形式で解説します。

誰が利用できるの?

申請していただいた研究者です。ただし、公的機関(e.g. 大学、独立行政法人)の研究者に限ります。申請時には、クックパッドと国立情報学研究所(後述)による審査があります。

何が利用できるの?

レシピと献立に関するデータです。レシピについては、クックパッドで 2014 年 9 月 30 日までに公開されたレシピが利用可能です。各レシピに付随するデータは以下のとおりです。

  • タイトル
  • 材料
  • 手順
  • つくれぽ(「作りましたフォトレポート」の略)
  • etc

献立については、クックパッドで 2014 年 9 月 30 日までに公開された献立が利用可能です。各献立に付随するデータは以下のとおりです。

  • タイトル
  • 調理時間
  • ポイント
  • コツ
  • etc

レシピ・献立ともに、公開するのは、クックパッド上で誰でも閲覧できるデータ(クロール可能なデータ)だけです。

どうすれば利用できるの?

国立情報学研究所(以下、NII)が運営する情報学データリポジトリで利用申請を受け付けています。NII は、情報学分野の研究を促進させるため、様々なデータを集約しています。そして、それらを利用するための統一的な窓口を設置しています。クックパッドのデータも、NII の協力の下で公開しています。

http://www.nii.ac.jp/cscenter/idr/cookpad/cookpad.html

利用手順は下図のとおりです。申請の審査も含めて、申請してから 1 〜 2 週間でデータを利用できます。

f:id:jharashima:20150224014727j:plain

いつから利用できるの?

本日(2 月 24 日)から申請を受け付けます。

なぜ公開するの?

最後に公開の動機です。データを公開する一番の動機は、冒頭でも述べたとおり、料理に関する研究を促進させたいというものです。

料理に関する研究をしている人は沢山います。最近では、料理に関する研究会や国際会議なども開催されています。しかし、データがないことで、多くの研究が行き詰っています。これは非常に残念なことです。

今回のデータ公開は、これらの研究を支援するものです。データがあれば、既存の研究を発展させたり、新しい研究を創出させることができるかもしれません。そして、それらは、まわりまわって、クックパッドのサービスを改善するための芽となります。

また、別の動機として、悪質なクロールを減らしたいというものがあります。

残念ながら、研究者の中には、クックパッドのデータを使用するため、悪質なクロールを行う方がいます。短時間に膨大なリクエストが送られると、最悪の場合、クックパッドのサービスに影響が出る恐れがあります。

しかし、クックパッドがデータを公開すれば、わざわざクロールする必要はありません。公開されているデータを利用する方が、圧倒的に楽です。そして、悪質なクロールが減れば、サービスの健全性も向上します。

まとめ

本日からクックパッドのデータを研究者に公開します。公開するのはレシピや献立に関するデータです。利用申請は NII で受け付けています。本件を通して、沢山の研究が前進すれば幸いです。

データ公開は、私と会員事業部の有賀、同部の村田で運用しています。本件に関するお問い合わせなどありましたら、recipe-corpus [at] cookpad.com までご連絡ください。よろしくお願いします。


クックパッドモバイルアプリの開発体制とリリースフロー

$
0
0

こんにちは、技術部モバイル基盤グループの @slightairです。

今回は、クックパッドのモバイルアプリをどのような流れで開発しているか説明したいと思います。 この記事では技術的な話ではなく、どのようにして、どのようなことを考えて僕らがモバイルアプリを開発しているかに触れたいと思います。

開発体制

クックパッドにはモバイルアプリを専門で開発するようなチームはありません。 必要に応じて、誰でもモバイルアプリ開発に取り組みます。 機能追加・修正を行ったらリポジトリにプルリクエストを送ります。 プルリクエストが来たら、アプリ開発を行うエンジニア同士でレビューします。

様々な修正をひとつのバージョンにまとめるのは、僕が所属する技術部と後述するリリースマネージャーで行います。

リリースマネージャー

バージョンごとに、そのリリースの責任をもつリリースマネージャーをひとり選びます。 リリースマネージャーは、バージョンに含める機能追加・不具合修正の進捗確認、リリース作業、リリース後の監視を行います。 リリースマネージャーがリリースに関する作業を受け持つことで、他の開発者が自分の開発に集中できるようになりました。

リリースマネージャーがやるべき作業をまとめたリリース issue を作成しています。 チェックリストにすることで、何をすべきか、何がまだ終わっていないかがわかるようになり、開発フローをうまく回すことができるようになりました。

f:id:Slightair:20150224181304p:plain:w250:leftf:id:Slightair:20150224181315p:plain:w328

リリースマネージャーはアプリ開発者の中から持ち回りで選んでいますが、通常の業務に加えて担当するとなると負担が大きいので、作業の自動化を少しずつ進めようと考えているところです。 例えば、自動的にリリースビルドを作成したりアップロードしたりする仕組みやリリース後監視を楽にするダッシュボードを作ろうとしています。

開発イテレーション

開発では、以下の様な項目を1イテレーションとして回しています。

  • 開発期間(10営業日)
  • テスト期間(3営業日)
  • サブミット、リリース
    • iOSは審査で一週間くらい待った後リリース
    • Androidは 2日おきに 10%、50%、100% と段階的リリース

開発期間開始時に各事業部でタスクを整理して、GitHub Enterprise の issue にします。 issue でどのバージョンにどのような変更をいれるか管理するようにしています。

開発期間10営業日のうち8日目をプルリクエスト受付最終日としています。 これは、開発期間最終日に駆け込みでプルリクエストが大量にやってきてコードレビューが終わらないという事態になるのを防ぐためです。 機能追加が間に合わないとわかった時点で、修正を次のバージョンに回すなどの調整を行います。 開発期間が終わればコードフリーズとし、以降の修正はこのバージョンの修正によっておきた不具合修正など最低限にとどめます。

テスト期間の3営業日では、不具合の洗い出しとその改修を行います。 事業として優先度の高い内容を除くと、多くは前回リリースからの実装差分に焦点を充てて確認を行います。 基本的に、不具合の改修もこの期間内に行います。ここで行うのは主にリリース前の手動テストです。 この期間でなくとも、自動テストや手動テストを必要に応じて回し、不具合があれば修正します。

基本的には決まったペースでイテレーションを回していきたいのですが、必ず守っているわけでもありません。 特に iOS の場合は審査の都合で計画通りにリリースすることができない場合があります。 期日の決まった機能追加があっても、他の修正が原因でリジェクトされてしまったり不具合が起きたりしてリリースできない恐れがあります。 そのため、スケジュールだけでなくバージョンに含める機能の調整を行うことがあります。 全体に影響をあたえるような大きな変更がある場合は、他の修正を次にまわしてもらうなどの調整をしています。

リリース

テスト期間が無事終わったら、Android の場合は段階的リリース、iOSの場合はサブミットして審査が通り次第リリースします。

Android には段階的リリースをできる仕組みがあるので、10%、50%、100% と少しずつ対象ユーザを広げてリリースします。 10%リリースで問題が見つかれば修正して 再度 10%リリースを行います。 段階的リリースを行うのは、不具合によって迷惑をかけてしまうユーザをなるべく少なくするためです。 また、不具合による被害の拡大を防ぐためでもあります。 もちろん一番よいのは 10% リリースにあたったユーザにも不具合を出さないことなのでテスト期間を十分に設けていますが、起きるときは起きてしまいます。 ユーザが増えることで見えてくる不具合もあるので、慎重にリリースを進めています。

リリース可能な状態になったら翌日の午前中にリリース準備・公開作業を行うようにしています。 また休前日にはリリースを行いません。 休日中に不具合が見つかったとしても、きちんと対応することができないためです。

リリース後監視

アプリをリリースした後は以下の項目を確認しています。

  • アクティブユーザ数の伸び
  • APIのエラーレスポンス数
  • APIのエラー
  • クラッシュ数

アプリが利用しているAPIでエラーが増えていれば、なにか不具合につながるような予期せぬことが起きている可能性があります。 アプリのクラッシュ数が増えていたら最悪です。 リリースした日の夕方や翌日の朝など時間を決めて、アプリに異常がないか確認しています。

アプリエンジニア間の連携の取り方

あちこちの事業部にアプリエンジニアがいる状況は、事業部内での意思疎通が図りやすいメリットはありつつも、アプリ開発の足並みを揃えるのが難しいデメリットがあります。 そのため、毎日アプリエンジニアが集まる朝会を開いています。 朝会ではアプリに関する全体連絡やスケジュールの確認、各自の進捗報告などを行っています。 Android, iOS それぞれ 10分程度の軽いものです。 朝会によって、アプリに今何が起きているのかがみんなに伝わります。

また、開発期間の開始やリリース後には社内ブログで周知しています。 これによってエンジニア以外にもアプリのリリーススケジュールを意識してもらったり、アプリのどこが変わったのか伝わりやすくしています。

ふりかえり

少し前の記事「KPTで粘り強く品質改善に取り組んだ話」にもありますが、イテレーションが終わるごとに、そのバージョンの開発期間中に起きたことを少しでもコミットしたメンバーを集めて振り返っています。 よりよく開発を進めたり、良いアプリを作るために少しずつ改善を続けていきます。

おわりに

クックパッドでのモバイルアプリ開発の流れを説明しました。 モバイルアプリの品質を損なわずにどう開発を続けていくのがよいか、アプリエンジニア達で試行錯誤し、今はこの形に落ち着いています。 このやり方が必ずしも正解というわけでもないし、まだまだ困っていることもたくさんありますがより良い方法を考えていきたいと思っています。

データがどのように更新されてきたのか追跡する

$
0
0

こんにちは。技術部の吉川です。

みなさんは、異常なデータを見つけたが、どうしてそのような状態になったのか追跡できず困ったという経験はないでしょうか。 今回は、そんなときにクックパッドで利用されているAuditログについてご紹介します。

Auditログとは

クックパッドでのAuditログは特定のデータレコードに対して発生したイベントをコンテキストとともに記録するものです。 一般的に監査ログ、証跡ログといったものがありますが、それらとは多少異なっています。

ここでのイベントとは、あるデータレコードが

  • 作成された
  • 更新・変更された
  • 削除された

といったものです。またそれ以外にもログインした、ログアウトした、セキュアな情報が閲覧された、といったイベントも含まれています。 コンテキストは以下のようなものを記録します。

  • いつ
  • どこで
    • 処理が行われたホスト
  • 何が
    • イベント
  • 何を
    • 対象データの情報
      • スキーマやテーブル、変更されたカラム、レコードIDなど
  • 誰が
    • リクエスト元のIP、UA、ログイン中のユーザーID
  • どのように
    • HTTPリクエストであればアクセスエンドポイント、バッチ処理であれば呼び出し元のメソッド

Auditログのユースケース

開発者のデバッグ・調査

例えば異常なデータレコードがあった場合に、どうしてそのような状態になったのかを追跡することができます。 リクエストがあって正常に更新されたが、その後バッチ処理が意図しない挙動をして更新している、といったような調査ができるのです。

カスタマーサポートのアシスト

ユーザーの操作履歴を調べて、この設定変更をしているのが問題だ、といったようなアシストができます。 またその操作がユーザー自身が行ったのか、サポートスタッフが管理ツールから行ったのかといったことも確認できます。

Auditログをどのストレージに保存するか

Auditログを実装する上で悩ましいポイントの一つがストレージの選定です。

クックパッドではもともとTreasure Dataが多く利用されていましたが、例えばサポートスタッフがユーザーと電話対応中にも履歴を検索する場合、 クエリを考えてジョブキューになげてMapReduceを待つことはできません。というのも電話中に数分待つことができないためです。 当時はPresto Query Engineもなく、より即応性の高いものが必要でした。

また行動ログなどと比べると、個別のケースを追いかけるわけですから抽出条件が非常に多岐にわたります。 調査の際にはいろいろな切り口でトライアンドエラーで調査するケースが多いです。

これらのことを考えると、RDBMSのようなインターフェイスのほうが取り扱いはしやすそうです。 クックパッドで利用しているストレージではAmazon Redshiftがぴったりでした。

どうやってイベントのコンテキスト情報を記録するか

Auditログの実装する上でもう一つ悩ましいポイントが、どのようにコンテキスト情報を集めるかという点です。

データレコードを中心においているのですから、当然発火する場所はモデルの処理ということになります。 ところがモデルは一般的にHTTPリクエストの内容を知りません。頑張ってコントローラーからパラメータを引き回すこともできそうですが、実装が辛そうです。 そこでスレッドローカル変数としてコンテキストを保存する方法を採用しています。

このロギング処理については、各サービス担当者が簡単に利用できるように共通ログインターフェイスを用意しています。 社内ではFiglogと呼ばれています。例えばあるモデルのAuditログを取得したい場合、 Figlog::AuditObserverincludeするだけです。

classUser< ActiveRecord::BaseincludeFiglog::AuditObserver
    ...
end

あるいは、モデルにincludeせずにconfigとして設定もできます。

Figlog::AuditObserver.observes %i(user recipe)

これだけで、create/update/delete時に、スキーマ、テーブル、レコードIDとどのカラムが変更されたかが記録されます。 ActiveRecord以外にもデータソースラッパーを指定でき、Redisなどを使っていても同じインターフェイスで利用できます。

また先程のコンテキストを取得するメソッドが用意されており、通常はコントローラーの共通フィルタでセットするようにします。

classApplicationController< ActionController::Base
  before_action :set_figlog_user_context
    ...

  defcurrent_user@userendend

これでそのリクエストにまつわるコンテキストが保存され、以降使いまわせるようになります。 またこの処理が前回のコンテキストをリセットする処理も兼ねています。

ログインユーザーの情報は、コントローラのcurrent_userメソッドからFiglogが自動で取得しその情報を記録します。 そのため利用する場合はcurrent_userを実装する必要があります。

pros/cons

このAuditログのような機能を実装する場合、シンプルなものとしては変更履歴レコードを作る方法があります。 そういった手法と比べると、メリットとして

  • 抜け漏れが発生しづらい
    • モデルの変更操作で自動取得するため、あるケースだけログ処理が抜けていた、ということが発生しづらい
  • 導入コストが低い
    • 利用者がログフォーマットなどを意識する必要がなく、簡単に利用できる
    • サービスごとの独自仕様が発生しない
  • サービスを横断した検索がしやすい

といった点があげられます。

デメリットは、ログが記録されていることを保証しきれないという点です。 例えば同じRDBでログと対象データを管理していれば、トランザクションで一貫性を保つことも可能なはずです。 しかしモデルのコールバックによって処理され、実データは非同期に保存されるため、同じように一貫性を保つことができません。

[Appendix] 共通ログ基盤Figlog

ここまでAuditログの話をしてきましたが、Figlogは社内では共通ログ基盤という位置づけです。 Auditログ以外にもPVログや行動ログなど様々なログに対応しています。

例えば行動ログはTreasure Dataを利用しており、ストレージとしては全く別物です。 しかしインターフェイスを共通にすることで、開発者の学習・導入コストを減らし簡単に扱えるようにする狙いがあります。

Figlog::Activity.log(message: 'User acts something')

ここで先ほどのコンテキストが既に保存されていれば、自動でそれも記録されます。 そのためAuditログ同様にロジックの深い部分までログパラメータを引き回す必要がありません。

まとめ

クックパッドでのログの活用事例としてAuditログと共通ログ基盤をご紹介しました。

なお、Figlogは技術的に難しいことをやっている訳ではありません。むしろユースケースとログストレージの間をつなぐビジネスロジックに近い存在です。 そういった性質からOSS化は難しいものの、インターフェイスなどの参考になれば幸いです。

義理といえば?クックパッドのレシピをword2vecで料理してみた

$
0
0

会員事業部の有賀 (@chezou) です。

クックパッドは、先日学術機関向けにレシピと献立のデータを公開しました。 研究者の方々にクックパッドのレシピ・献立を使っていただくことで、料理に関する研究の発展に貢献できればと思いデータ公開に至りました。

今回は、その中でもクックパッドのレシピデータを使った分析事例として、word2vec を使ったテキスト分析を行ったのでご紹介します。

なお、 3 / 7 (土) に日経新聞電子版さんと共催で、このデータを含む各種データを使った学生向けデータハッカソンを開催します。締め切りは 2 / 27 (金) と間近ですが、興味がある方はぜひご参加ください。

word2vec とは

word2vec は単語を意味を含んだベクトルで表現できるようにするツールです。Tomas Mikolov らが提案し、その実装を公開しています。

CBOW (Continuous Bag-of-Words) や skip-gram といった Neural Network のモデルを用いて単語のベクトル表現を学習することで、異なる単語間の類似度を計算できるのはもちろん、意味的な足し算引き算ができるようになったという点で画期的だと言われています。

日本では、形態素解析器MeCabで有名な工藤拓さんの解説で広まり、あんちべさんが記事にしたことで一躍有名になりました。また、オライリーからもとてもわかりやすい解説書が出ています。薄い本ですのでこちらを読むと良いと思います。

義理といえば?

2月はバレンタインデーがありました。バレンタインデーは、クックパッドの1年の中で最もアクセスが集中する日です。

その主役の「チョコ」に類似する単語を見てみましょう。

順位単語類似度
1チョコレート 0.938
2ガナッシュ 0.757
3マシュマロ 0.746
4ビターチョコ 0.744
5クランチ 0.730

f:id:chezou:20150224110143p:plain

グラフは「チョコ」に類似する語のベクトルを主成分分析で可視化したものです。

マシュマロのように、バレンタインに送られるお菓子として出てくるものや、生チョコを作るときに使うガナッシュのように更に別のチョコを作るためのものも出てきています。

では、「義理チョコ」といえばなんでしょうか?

順位 フレーズ 類似度
1 友_チョコ 0.781
2 バレンタイン 0.718
3 バレンタインデー 0.688
4 本命_チョコ 0.682
5 本命 0.676

f:id:chezou:20150224110435p:plain

義理チョコといえば、どうやら友チョコが多いようです。 作る量としては友チョコの方が多いそうなので、やはり需要が多いのでしょうね。

義理チョコではバレンタインに関連したものばかりでしたが、試しに「義理」だけだとどういう関連性が見られるか調べてみましょう。

順位 フレーズ 類似度
1 0.670
2料理_上手_な 0.667
3義母 0.651
4お母さん 0.634
5お母様 0.623

f:id:chezou:20150224110503p:plain

そう、クックパッドの世界で「義理」だけだと、義理のお姉さんや義母といった方々がよく登場するようです。

バレンタインの要素はどこへいってしまったのでしょうか? word2vec の面白いところは、単語をベクトルで表現することで意味的に足したり引いたりできることでしたね。それでは、「義理」から「母」を引いてみましょう。

順位単語類似度
1デコペンレシピ 0.295
2マンディアン 0.291
3チョコカップケーキ 0.287
4小枝 0.286
5本命 0.281

このように、バレンタインっぽい単語が増えてきました。

擬態語の獲得の可能性

レシピの中には、「アツアツ」のように擬態語が良く出てきます。 このような擬態語は、人間なら似たイメージを持つ別の語を容易に思い浮かべることができます。しかし、機械がそれをするのは一苦労でした。

では、 word2vec で似た擬態語を捉えることはできるのでしょうか?「アツアツ」に類似した単語を見てみましょう。

順位単語類似度
1熱々 0.961
2あつあつ 0.946
3あったかい 0.745
4ホカホカ 0.712
5ハフハフ 0.685
6温かい 0.682
7アッツ 0.673
8あたたかい 0.647
9ふうふう 0.645
10フウフウ 0.631

f:id:chezou:20150224110527p:plain

word2vec を使うことで、「アツアツ」のような擬態語からも似たような文脈で用いられる擬態語が獲得できるようです。似た擬態語を持つレシピを集めることで、冬に食べたい体があたたまるレシピ、なんてものも見つけやすくできるかもしれませんね。

足し算、引き算

私は名古屋出身なのですが、名古屋といえば赤味噌です。一家に一本チューブ型味噌を持っているのは本当です。

まずは、赤味噌の類似フレーズを見てみます。

順位フレーズ類似度
1八丁_味噌 0.824
2味噌 0.809
3赤_みそ 0.805
4八_丁_味噌 0.767
5信州_味噌 0.741

f:id:chezou:20150224110548p:plain

なるほど、名古屋で赤味噌といえば八丁味噌ですね。納得です。

では、1世帯あたりの味噌の消費量日本一の長野はどうでしょうか? 赤味噌 - 名古屋 + 長野をみてみましょう

順位フレーズ類似度
1信州 0.509
2味噌 0.503
3麦_味噌 0.503
4信州_味噌 0.499
5赤味噌_白味噌 0.481

おお!確かに長野といえば信州味噌ですね。見事に引き算ができているのがわかります。

でも、これはたまたまなのではないでしょうか?それでは、赤味噌 - 名古屋 + 京都も見てみましょう。

順位フレーズ類似度
1白味噌 0.567
2麦_味噌 0.547
3顆粒_だし 0.507
4だし汁 0.496
5出し汁 0.488

確かに、京都では白味噌が使われています。 このように、クックパッドでは「名古屋」や「京都」というと「味噌」の意味合いが含まれているようです。

word2vec の可能性

word2vec は単語をベクトルとして表現するところが新しいと書きましたが、クックパッドで使うとしたら辞書の整備 (同義語・類義語の拡張) に使えるのではないか、と考えています。

ですが、現状の word2vec を単純に使う場合、それが本当に同義語なのか、辞書に追加して悪影響が出ないのか、という検証が必要になってくるため方法を模索しています。

また、来月開催される言語処理学会の年次大会でも、単語のベクトル表現を用いた同義語獲得の発表があるようですので、様々な研究成果を応用していきたいと思います。

なお、今回のグラフは、前述のオライリー本の付録の可視化ツールiPython notebook で使えるようにしたものを利用しました。(notebook)

iPython notebook は試行錯誤的にグラフを描画するのに最適ですし、 @ryot_a_raiさんに立てていただいた社内 nbviewerで、手軽に可視化したグラフを共有することができました。

参考文献

サービスをエコシステムで改善した(してる)はなし

$
0
0

投稿推進部の大前です。 普段はレシピを投稿するユーザーさんに向けたサービス開発・改善を担当しています。

一般に「サービス開発」というとどれくらい本質的なユーザーの課題解決をできているかや、価値提供ができているるか、ということが重要な観点として取り上げられますが(もちろんそういうった点が大事なのは大前提として)、今回はクックパッドという一つの大きなサービスの中で日々起こっているユーザーアクションを、クックパッドをひとつの大きなエコシステムとして捉えることでより多くの副次的な価値に還元できたらないいのになーというような話をしたいとおもいます。

クックパッドという大きなエコシステム

ご存知のとおりクックパッドは一般のユーザーの方が投稿してくださっているレシピを中心とした(すこし古い表現ですが)CGMサービスです。

すごく簡単に言ってしまうと、クックパッドに投稿されたレシピがそれを必要としている人のもとに届き、そこで「おいしかった!」を生み、その感謝の声がレシピを投稿した人の元に返ってきて、また次の新しいレシピを産む、というような循環の上に成り立っています。

日々多くのサービスに携わるスタッフが、クックパッドの中で新しい機能を開発したり改善をしたりしていますが、それらもゼロから価値を創造してユーザーさんに提供している、というよりは、前述したようなクックパッドの中で生まれているユーザーさんのアクションひとつひとつをきちんとその方や別のユーザーさんの価値にもなるように還元するような仕組みや形をつくることで、実現できているのではないかと思っています。

材料入力の難しさを改善

話がいっきに飛びますが、最近の事例でそういったユーザーのアクションを別のユーザーの価値に還元することで課題を解決しようとしている事例を紹介したいと思います。

最新のバージョンのクックパッドのアプリではレシピ投稿の材料入力時に、入力した材料に対して最適と思われる分量単位の候補を提案する機能が実装されています。(iOSのみ)

f:id:yuki-omae:20150302160120p:plain

これは、レシピの投稿のシーンにおいてかねてからあった材料入力の難しさという課題への取り組みの一つです。 単純に「g」や「本」など頻出する分量の入力を簡単にするだけでなく、例えば「 にんにく」や「しらす」などに意外とぱっと分量の表記の仕方が思いつかないものの入力を補助します。

クックパッドの約200万品のレシピをもとに提案する

提案される単位の候補は予め用意した辞書データなどを利用しているのではなく、これまでにクックパッドに投稿されたレシピのデータから各材料ごとの代表的な分量の単位名を返すAPIを作ることで実現しています。

例えば「醤油」であれば「大さじ」「小さじ」「cc」「g」「滴」などが候補として提案されますが、これはクックパッドのレシピで利用されている醤油という材料の分量表記は「大さじ」でされているものが一番多いということでもあります。

レシピチェックからの還元

多くのレシピで分量の単位として実際に利用されているからといって、一般の方が書いたレシピに使われているものが必ずしも単位として正しいの?という疑問があるかもしれません。

厳密な正確性ではそういう面があるかもしれませんが、クックパッドではすべての公開されたレシピをスタッフが確認する「レシピチェック」というフローが存在します。

せっかく書いていただいたレシピがより多くのひとに再現性高くつくっていただけるように、スタッフが表現やレシピの書き方などについてコメントをさせていただくことがあります。

APIが返すデータには言わばこういったものの還元も含まれています。コストをかけて完璧な辞書をもとに分量の単位を提案することもできますが、ユーザー発信の入力をベースにすることで新出の材料などにも比較的運用コストが低く対応できるのではないかと考えています。

まとめ

はじめに書かせていただいたとおりクックパッドはCGMのサービスです。閉じた個人の利用ではなく、多くはクックパッドを利用いただいているユーザーさんの発信する価値の相互の還元で成り立っています。 今後もクックパッドを利用していくことでご自身や他のユーザーさんがもっと便利にもっと料理を楽しめるようにクックパッド全体が還元できるサービスになっていけるように改善を続けていきたいと思います。

積極採用中です

アプリで利用する画像について

$
0
0

ユーザーファースト室のhidaka(@kaa)です。

クックパッドアプリ内では元々同じレシピの画像を画面、環境によって様々なサイズで表示しています。 レシピの検索結果でのサムネイルや、レシピ詳細画面、写真の拡大表示時などなど。

その際、端末の解像度にあわせ無駄のないよう、表示領域にあわせて画像をリクエストしていました。

*画像配信にはtofuという配信システムが稼働しています http://www.slideshare.net/mirakui/ss-8150494

これでそれぞれの端末にあわせた画像を配信していましたが、今年あたりからさらに最適化が必要になってきました。

問題1 画面密度の上昇

端末のスペックが上がることにより、1インチあたりのピクセル数が増加しました。

retinaと言われていたiPhone 5で326dpiだったのが去年あたりからの高解像度端末の幅1440pxの機種(au LGL24など)ですと534dpiなど。

縦×横のピクセル面積になると(534/326)*(534/326)≒2.68倍。 画像のファイルサイズはほぼ画像面積に比例するので通信量も2.68倍になります。

この計算はLGL24がiPhone 5と同じ液晶の物理面積だった場合です。

実際は高解像度端末は6インチなど液晶サイズも広いため、端末の画面幅にあわせた同じトリミングの写真だと5倍近いサイズの画像をリクエストすることもあります。

こうなるといくらLTEが普及しているとはいえ、サーバー負荷もかかりますし 携帯側のメモリや電池消費、速度が低下します。

問題2 パケット使い放題からデータ量限定定額に

一気にパケット使い放題からデータ制限ありの時代になりましたね。去年起こった一番のモバイル環境の変化ではないでしょうか。

使いすぎると速度制限というサービスが多いので、海外でよくあるプリペイドSIMのオーバーすると通信できなくなるものに比べるといくらかマシですが通信量は少ない方がいい。 サーバーコストの問題だけでなく、ユーザー的にもLTEの回線が速いからいい、という問題ではなくなりました。

問題3 MVNOの登場

回線速度は速かった日本国内ですが、MVNOにより遅い回線のユーザーが現れました。

MVNOの多くはドコモの回線を使っているため、回線品質が安定しているのに速度は遅い、というLTEの普及した国内では珍しいタイプ。 このためにもデータ量の軽量化が求められます。 MVMOについては変化が激しく、初期の常に低速というものから数Gまでは速い、といった現在の3キャリアのサービスに近くなっている傾向があります。

どの画質で表示するか

端末の画面密度には差があり、どれが必要十分によい、という基準は現状ありません。幅1440のandroid機が画質いいのは当たり前です。

大きな画像を使えば使うだけきれいだし、オリジナルの写真の画質にも左右されます。

そこである程度の人の共通認識として、国内ではiPhone 5のretinaディスプレイだと特に不満ないよね、というところからさらに高密度なディスプレイの端末でもiPhone 5の画質を採用することにしました。 (国内向けの場合です。グローバルチームではまた別の設定にすると思います)

PCのwebサイトですとADSL、光回線では通信量=サーバーコストとなりますが、モバイルでは通信量=サーバーコスト=表示待ち時間=ユーザーの携帯料金とまで繋がっていきます。 去年一気に変わったパケット代の仕組みのように、環境の変化は1,2年で起きるので常に実際の利用環境でどう見えるかを追っていく必要があります。

もっと速くできないか

画質、サイズ最適化したとはいえレシピ詳細画面ではそれなりに大きな画像を使っています。 クックパッドアプリに限らず、携帯のメモリ増加に伴い大きな画像を使ってみせていく画面構成は増えています。

レシピ詳細はアプリで一番大きな画像を使い、PVも多い画面ですので出来る限り最適化を行ないます。 幸いレシピ詳細画面への遷移元は、かなりの割合で検索結果だったり、レシピの一覧画面からです。 一覧画面ではサムネイル(もちろんこれも最適化します)、レシピ名、付属テキスト情報などを表示しています。

このデータを画面間で使いまわしてみましょう

before f:id:futura24pt:20150303151213p:plain

after f:id:futura24pt:20150303151212p:plain

ロード総時間、通信量は変わっていませんが、体感はかなり速くなりました。 遷移した瞬間にファーストビューがほぼ描画されるので、真っ白のまま待つ時間を軽減できます。

通信量、待ち時間は絶対的な数値ですが体感的な待ち時間にはこのような改善策もあります。

高速化結果

とりあえず手元にある幅1080pxのHTC oneで検証します。 クックパッドアプリのレシピ詳細画面での画像サイズは1080pxx810pxでしたが、iPhone 5の画面密度で取得するとするとサイズは750px*563px。

ファイルサイズは240kbから140kbに、4割ほど削減できました。

実際のアプリでの表示はこうなります。

画像リサイズなしの場合

縮小した画像の場合

また、webp対応を行うとさらに3割ほど削減できるので、元々のサイズから6割程度の通信量削減となります。

試したレシピはこちら。 こんにゃくと三つ葉 くるみとごま白和え by soyumina

Viewing all 726 articles
Browse latest View live