ユニファ開発者ブログ

ユニファ株式会社プロダクトデベロップメント本部メンバーによるブログです。

テストコード(RSpec)の高速化事例

はじめまして、ユニファ株式会社でWebエンジニアとして働いている本間と申します。 普段は、Ruby on Railsを使ったサービスのサーバーサイドの機能追加や改善を担当しています。

弊社の主要サービスである写真サービス るくみーは2013年5月に開発がスタートし、あと少しで5年目を迎えようとしています。 開発がこれだけの期間続けられているというのはとても喜ばしいことだと思うのですが、一方で開発初期にはなかった問題も発生してきています。 そのような問題の1つに「テストコードの実行完了までに時間がかかる」というものがあり、弊社でも発生しておりました。

今回、この問題に対して改善施策を実施し、それなりの効果を得ることができましたのでご紹介したいと思います。

※注意 : RSpecは2系、かつDatabaseCleanerを使用していることが前提となる箇所があります。

結果比較

はじめに改善施策の実施前後の結果の比較です。 以下は、私のローカル環境(MacBook Pro (Retina 13-inch、Early 2015))で全てのテストコードを走らせた時の実行時間の比較です。

実施前(17:02)

f:id:ryu39:20170123113700p:plain

実施後(3:16)

f:id:ryu39:20170124111411p:plain

改善対象の洗い出し

性能改善を始めるにあたって、最初に攻めどころを明確化を行いました。

RSpecはデフォルトで時間がかかるexampleやspecファイルを表示してくれますが、今回の用途に少しマッチしなかったため「合計実行時間」が大きい順にspecファイルを表示してくれるFormatterを自作して対応しました。作成したFormatterは扱いやすいようにGemにしてあります(※注意 RSpec 2系でしか動きません)。

このFomatterを使ってRSpecのテストを実行すると、下記のように合計実行時間の上位10specファイルが表示されます(クラス名とパスは隠しています)。

f:id:ryu39:20170124112446p:plain

改善施策の実施

合計実行時間が長い順にspecファイルを調べ、原因と思われる箇所に対して対策を実施していきました。

不要な機能のテストコードを削除

実行時間がかかる上位のspecの中に、今後使う予定がないrakeタスクのテストコードがいくつかありました。 このコードは不要でしたので本体コードごと削除しました。

権限チェックテストの簡略化

弊社サービスには何種類かのロールがあり、ロールによって操作可能なアクションが異なります。

時間のかかるControllerのテストでは、各アクションに対して全てのロールで成功 or エラーのテストを実施していました。具体的には、以下のようなコードになります。

describe UsersController do
  describe '#index' do
    let!(:users) { create_list(:user, 10) } # テストデータ作成

    subject(:response) { get :index }

    # 全てのロールで成功 or エラーをテスト
    UsersController::PERMITTED_ROLE_IDS.each do |role_id|
      before do
        session[:role_id] = role_id
      end

      it { 
        expect(response.status).to eq(200)
        expect(assigns(:users)).to eq(users)
      }
    end
    (Role::ALL_ROLE_IDS - UsersController::PERMITTED_ROLE_IDS).each do |role_id|
      before do
        session[:role_id] = role_id
      end

      it { expect(response.status).to eq(403) }
    end
  end
end

ロールによる権限チェックのテストは重要なのですが、「テストデータ作成」と「アクション処理」が複数回実施されていて、ここに改善の余地があると考えました。

解決策としては、機能的なテストは1つの代表ロールでのみチェックし、ロールの権限チェックテストではAnonymous controllerを使用することで「テストデータの作成」と「成功時のアクション処理」をスキップするようにしました。

Anonymous controller はRSpecが提供する機能の一つで、テスト対象のControllerを継承したクラスをテストコード内で定義することができます。Anonymous contollerでアクションの中身をオーバーライドして空のレスポンスを返すことで、before_action で実施している権限チェックのコードをテストしつつ、アクション処理の実行時間を短縮できます。

修正後は以下のようなコードになりました。

describe UsersController do
  before do
    session[:role_id] = 9999 # 代表ロールID
  end

  # 機能的なテストは1つの代表ロールでのみ確認
  describe '#index' do
    let!(:users) { create_list(:user, 10) } # データ準備

    subject(:response) { get :index }

    it { 
      expect(response.status).to eq(200)
      expect(assigns(:users)).to eq(users)
    }
  end

  describe '権限チェック' do
    controller do
      # 空レスポンスを返すだけ、成功時のアクション処理時間を短縮
      def index
        head :ok
      end
    end
    before do
      routes.draw do
        resources :users, controller: :anonymous, only: [:index]
      end
    end

    describe '#index' do
      # 権限チェックではテストデータを作成しないことで時間短縮

      subject(:response) { get :index }

      UsersController::PERMITTED_ROLE_IDS.each do |role_id|
        before do
          session[:role_id] = role_id
        end

        it { expect(response.status).to eq(200) }
      end
      (Role::ALL_ROLE_IDS - UsersController::PERMITTED_ROLE_IDS).each do |role_id|
        before do
          session[:role_id] = role_id
        end

        it { expect(response.status).to eq(403) }
      end
    end
  end
end

共通データの事前作成

時間のかかるspecの多くでは、DBへのデータ書き込み(特にどのテストでも使う共通のオブジェクトの作成)で時間がかかっていました。

この問題を解決する方法として、specファイル内で共有可能なデータは before(:all) で事前に作成し、これを各exampleで参照するようにしました。

しかし、この方法はテスト間の独立性を壊してしまう可能性があるため利用には注意を払う必要があります。今回、以下のルールに沿って実施しました。

  • 共有するのは参照専用のオブジェクトのみ。
  • 共有する範囲は1つのspecファイル内まで。
  • 対象は合計実行時間の上位20specファイルまで。
  • 必ず after(:all) で作成したオブジェクトを全て削除する。

テストコードは以下のようになります。

describe SomeController do
  before(:all) do
    @user = create(:user)
  end
  after(:all) do
    DatabaseCleaner.clean_with(:truncation)
  end

  # 以降、@userでユーザーオブジェクトを参照
end

rspecの並列実行

上記施策を実施して実行時間を10分前後に減らすことはできたのですが、大きな改善を見込める箇所が見当たらなくなってしまいました。 そこで、最終手段としていたテストの並列実行を導入しました。

RSpecを並列実行するGemとしてparallel_teststest-queueが有名なようです。 今回は「parallel_tests」を使用しました。導入にあたって実施した内容は以下になります。

  1. Gemfileに parallel_tests を追加し、 bundle install
  2. database.ymlを開き、テスト用のデータベース名の末尾に <%= ENV['TEST_ENV_NUMBER'] %> をつけるように修正。
  3. bundle exec rake parallel:setup を実行し、テスト用のデータベースを準備する。
  4. parallel_rspec spec でテストを実行する。

並列化の効果は非常に大きく、これだけでテストの実行時間が3分の1程度になりました。

parallel_testsを使う場合、以下の設定をしておくとオススメです。

  • 各specの実行時間を記録したログを出力しておく。公式リポジトリに説明があります。この設定を行うと前回の実行時間に基づいてspecファイルが分配されるため、プロセスごとの実行時間のばらつきが小さくなり、早く全てのテストが終わる。
  • parallel_rspecを実行する際に --nice を指定すると、テスト実行中に他の作業をするとき動作がもっさりしにくくなる。

実施しなかった対策

下記の施策は検討には上がっていたのですが、実施はしませんでした。 本体への影響が大きかったり、実施に時間がかかるというのが主な理由です。

  • インメモリDBを使う
  • RubyやRailsのバージョンアップ
  • テストの精査・サンプリング

Werckerでも並列実行

弊社ではCIサービスのWerckerを利用しており、リポジトリへのpushを検知して自動でテストを流すようにしています。 こちらも並列化により実行時間を大幅に短縮できたので、合わせてご紹介いたします。

設定は非常に簡単で wercker.yml の記載を以下のように変更するだけでした。

build:
    steps:
        - bundle-install

        # rails-database-ymlが生成するdababase.ymlにはDB名に環境変数「TEST_ENV_NUMBER」が組み込まれている。
        - rails-database-yml

        # db:schema:load の代わりに parallel:setup を、 rspecコマンドの代わりにparallel_rspecコマンドを使うだけ。
        - script:
            name: Set up db
            code: bundle exec rake parallel:setup
        - script:
            name: rspec
            code: parallel_rspec spec

まとめ

今回、弊社で実施したRSpecのテストコードの高速化事例をご紹介させていただきました。

この活動はまだまだ終わりではなく、継続して改善していく必要があります(テストコードはどんどん増えていくため...)。 今後、大きな改善をすることができたら、またこのブログで紹介したいと考えています。

それでは最後まで読んでいただきありがとうございました。

td-agent(Fluentd) を利用したログ収集

はじめまして! 仕事内容がわかりにくいインフラエンジニアのすずきです。

先日このブログにて弊社田渕が書いていましたが、 昨年Ruby biz Grand prix 2016で審査員特別賞をいただくことができとてもありがたく思います。

Ruby biz Grand prix 2016 - ユニファ開発者ブログ

この賞を頂いたからというわけではないですが、
いろんな方に弊社サービスを安心して快適に利用していただけるように日々頑張っていかねばと改めて思った次第です。

そういえば、Ruby biz Grand prix といえば前回大賞となったトレジャーデータ株式会社様、 弊社でも一部で恩恵をうけているので軽く紹介できればと思います。

ただサービスではなく、公開していただいているFluentdのパッケージ版td-agentを利用させていただいているといった感じです。

Fluentd とは

Fluentd はログの収集、集約やデータの収集、集約、プラグインを利用した集計などなど ログやデータの収集にまつわるものならなんでも出来ちゃいそうなツールです。
プラグインによる拡張ができるので必要な機能が必要であれば自分で作ることも可能です。

今回Fluentdの事を書いていますが、検索すれば設定例などは沢山出てくるとおもうので一つの例として見ていただけたらなと思います。

ログの収集の方法

Fluentdの利用はまだログの収集がメインになっています。
弊社エンジニアもログの集約サーバでログを見てもらっているかなと思いますが、
裏でFluentdが動いてる事を知ってる方は少なそうなのでこの場を借りて共有しておこおうかなと思います。 ※詳細は別途勉強会で

構成としては単純で以下の2種類になります。

  • サービス用のサーバ群
    • ログ収集の設定がされたFluentd
  • ログ集約サーバ
    • ログ集約用の設定 がされたFluentd

ログ収集の設定

ログ収集ならトレジャーデータ株式会社の田籠さんの作られたfluent-agent-liteや、株式会社カヤックのfujiwaraさんの作られたfluent-agent-hydraなどもあるのですが、 これらは軽量なのですがツール停止時点からログ収集を再開することができなさそうだったので収集もFluentdを利用しています。

サンプルとしてWebサーバのログ収集設定を用意してみました。(fluentdハイライト何が良いんだろ、とりあえずshが見やすそうでした)
なお利用しているFluentdのバージョンはちょっと古くて申し訳ないのですが0.12系です。
設定の説明はコメントとして追記しています。
また、これから出てくるpathの値などは適当な値を羅列しているだけなので、 試される方はご自分の環境にあわせて変更してください。

<source>
  @type tail
  ## apacheのアクセスログはltsvにしているのでltsvフォーマットで収集するようにしています。
  format ltsv
  path /var/log/httpd/access_log
  tag httpd.access_log
  pos_file /var/log/td-agent/httpd-access.log.pos

  ## Fluentdのデータのやりとりにはtimeフィールドがつくいているのですが、time_formatをつけることでFluentdが処理した時間ではなく、
  ## ログのtimeフィールドの時刻を利用してFluentdを処理できます。
  time_format %d/%b/%Y:%H:%M:%S %z
  read_from_head true
  rotate_wait 60

  ## TAILラベルをつける
  @label @TAIL
</source>

<source>
  @type tail
  ## errorログなどフォーマットが決まっていないものがあるのでnoneフォーマットで収集するようにしています。
  format none

  ## pathにワイルドカードを利用して全てのログを対象に収集します。
  ## 弊社ではログローテーションしたものはOLDフォルダに移動させているので、
  ## OLDフォルダと先頭で収集設定を入れたアクセスログを exclude_path に追加して2重で収集されないようにしています。
  path /var/log/httpd/*
  exclude_path ["/var/log/httpd/OLD", "/var/log/httpd/access_log"]

  ## 収集対象がワイルドカードするとpathの/を.に変えたものがタグとして付与されます。
  ## 今回の場合だと/var/log/httpd/error.logの場合 httpd.var.log.httpd.error.log というタグが付与されることになります。
  tag httpd.*
  pos_file /var/log/td-agent/httpd-other.log.pos
  read_from_head true
  rotate_wait 60

  ## TAILラベルをつける
  @label @TAIL
</source>

<source>
  @type tail
  ## こちらはRailsのログですがapacheのログ収集と同じく不要なものを除いて全て収集します。
  format none
  path /www/rails/web/shared/log/*
  exclude_path ["/www/rails/web/shared/log/OLD",  "/www/rails/web/shared/log/newrelic_agent.log"]
  tag web.*
  pos_file /var/log/td-agent/web.log.pos
  read_from_head true
  rotate_wait 60
   ## TAILラベルをつける。
  @label @TAIL
</source>


## TAILラベルのついたデータは全てログ集約サーバに転送するようにします。
## 古いFluentdでは httpd.**,web.**など書いてタグによる処理を書かないといけませんでしたが、
## 0.12から追加されたlabelでそのようなことをしなくてよくなりました。
<label @TAIL>
  <match **>
    @type forward
    flush_interval 1s
    <server>
      name log-server
      host 10.0.0.251
    </server>
  </match>
</label>

ログ収集ではこのような設定を利用しています。
path*を利用できるとはかいてあったのですが、 利用している記事などはあまりみかけなかったのでこんな感じかな?といった感じで設定を作っています。
path*を利用している理由ですが、サーバアプリのログが固定ではないからです。
固定というと語弊がありますが、必要に応じでてサーバアプリ側でログが追加されたり消されたりするためです。
その度にFluentdの設定変更対応をする為の連携を取るのもお互いに無駄かと思い手離れしやすいようにこの設定にしました。

ログ集約用の設定

先程収集されて、転送されてきたデータを受け取るログ集約サーバ側の設定サンプルも用意しました。

## サーバサーバに転送されてきたログをすべてにlogsラベルをつけています。
## 別のサービスからデータを受け取る場合はポートを変更して別のラベルをつけるなどの対応が必要です。
<source>
  @type forward
  @label @logs
</source>

<label @logs>
  ## apacheのアクセスログのみなので受け取ったログをファイルにおとすよくある設定にしています。
  <match  httpd.access_log>
    @type file

    time_slice_format %Y%m%d%H
    compress gz
    format ltsv
    include_time_key true
    buffer_chunk_limit 1g
    path /data/log/httpd/access_log
    symlink_path /data/log/httpd/httpd.access_log
  </match>

  ## apacheのアクセスログ以外すべて受け取っているのでfluent-forest-pluginを利用しています。
  ## このプラグインを利用することでタグに応じた設定がかけるので楽です。
  ## そうでない場合上記のapacheのアクセスログを同じような設定をログの数だけ用意する必要があって非常に面倒ですし、
  ## ログが増えるたびに設定を追加するなどの作業が発生します。
  <match {httpd,web}.**>
    @type forest
     subtype file
    <template>
       time_slice_format %Y%m%d%H
       compress gz
       format single_value
       add_newline true
       message_key message
       buffer_chunk_limit 1g
    </template>

    <case httpd.**>
       path  /data/log/httpd/__TAG_PARTS[0]__/__TAG_PARTS[-1]__
       symlink_path /data/log/httpd/__TAG_PARTS[0]__.__TAG_PARTS[-1]__     
    </case>

    <case web.**>
        path  /data/log/rails/__TAG_PARTS[0]__/__TAG_PARTS[0]__/__TAG_PARTS[-2]__.log
        symlink_path /data/log/rails/__TAG_PARTS[0]__/__TAG_PARTS[0]__.__TAG_PARTS[-2]__.log
    </case>
  </match>

__TAG_PARTS[0]__,__TAG_PARTS[1]__とありますが、 これらはタグを.で区切った配列とみなしてその情報を利用しています。

path  /data/log/rails/__TAG_PARTS[0]__/__TAG_PARTS[0]__/__TAG_PARTS[-2]__.log

コレはどういうことかというと web.app.error.logのタグが付いたデータは/data/log/rails/web/web/error.logに吐き出されるということです。

今回fluent-plugin-forestを利用したログ収集の設定を書きましたが、 v0.12からこのプラグインが無くてもいい感じに書けそうみたいなのを見た気がするのですがやり方が思いつかなかったので現在も利用しています。

ご存じの方居たらお知らせ頂けたらと思います。

さいごに

今回Fluentdの弊社の利用方法をすこし紹介させていただきました。
だれか1人の方にでも参考にしていただけたら幸いです。

他にもAWSサービスの連携や、文字列から条件分岐してSlackに通知するなどやっていたりもします。
またの機会があれば紹介させていただこうと思います。

弊社はZabbixでログ監視もしていますが可能ならいつかFluentdを利用したログ監視に移動したいなとはおもっています。
Zabbixはログ監視の追加が面倒なので………
そういった話も書けていけたらと思っています。

引続き弊社ブログよろしくお願いします。

すずき

Swiftを使ったAPIクライアントの実装方法

はじめましてiOSエンジニアのしだです。
おかげさまで、去年12月に るくみーnote という園で利用してもらう連絡帳アプリをリリースしました。(実際に園でご利用いただくのは4月からの予定です)

iOS版の るくみーnote では主に AlamofireObjectMapperRxSwiftSQLite.swift などのライブラリを利用しています。
今回は、そのときに実践した Alamofire + ObjectMapper + RxSwift を使ったAPIクライアント周りの実装方法を紹介したいと思います。

以下のサンプルの実装は Slack WebAPI を使って説明しています。

準備

APIの定義

今回は SlackのWeb APIのemoji.listchat.postMessageの実装例を示します。

  • emoji.list: 設定されている絵文字のリストが取得できます
  • chat.postMessage: 指定したchannelにメッセージを送信できます

まず Slack Web APIのmethodsを Enumeration を使って以下のように定義します。

enum SlackApi: API {

    /// 絵文字リストの取得
    case emojiList

    /// メッセージを送信
    case chatPostMessage(text: String, channel: String, options: [String: String])

    /// :
    /// ファイルのアップロード
    /// case fileUpload(filename: String, file: Data, options: [String: String])

    /// 任意のSlackのTest Tokenを利用してください
    static let token = “xoxp-xxx"

    var buildURL: URL {
        return URL(string: "\(baseURL)\(path)")!
    }

    var baseURL: String {
        return "https://slack.com/api"
    }

    var path: String {
        switch self {
        case .emojiList:                                        return "/emoji.list"
        case .chatPostMessage(text: _, channel: _, options: _): return "/chat.postMessage"
        }
    }

    var parameters: Parameters {
        var params = ["token": SlackApi.token]
        switch self {
        case .emojiList:
            return params
        case .chatPostMessage(text: let text, channel: let channel, options: let options):
            params["text"] = text
            params["channel"] = channel
            options.forEach { (k, v) in params[k] = v }
            return params
        }
    }
}

protocol API {
    /// リクエストURL(e.g. https://slack.com/api/emoji.list)
    var buildURL: URL { get }

    /// APIのベースURL(e.g. https://slack.com/api)
    var baseURL: String { get }

    /// APIのパス(e.g. /emoji.list)
    var path: String { get }

    /// リクエストのパラメーター(e.g. token=xoxp-xxx)
    var parameters: [String: Any] { get }
}

ポイントとしては、各APIを列挙型の要素で定義することと、 Associated Values を使ってリクエストに必要なパラメータがわかるようにしてあります。
欠点としては、APIの数が多くなれば、 enum SlackApi のコード量が多くなりますがAPIを一覧できるほうが利点かなと思います。

モデルの定義

続いて、レスポンスのJSON形式からオブジェクトに変換するために、 ObjectMapper を使ってモデルの定義をします。

/// 絵文字
struct Emoji: Mappable {
    var ok: Bool = false
    var list: [String: String] = [:]

    init?(map: Map) { }

    mutating func mapping(map: Map) {
        ok   <- map["ok"]
        list <- map["emoji"]
    }
}

/// メッセージ送信レスポンス
struct PostMessage: Mappable {
    var ok: Bool = false
    var ts: String = ""
    var channel: String = ""
    var message: AnyObject?

    init?(map: Map) { }

    mutating func mapping(map: Map) {
        ok      <- map["ok"]
        ts      <- map["ts"]
        channel <- map["channel"]
        message <- map["message"]
    }
}

HTTPクライアントの定義

最後に、 AlamofireRxSwfitObjectMapper を使って、HTTPのクライアントを定義します。

import Alamofire
import ObjectMapper
import RxSwift

struct Client {
    static let manager = Alamofire.SessionManager.default
    
    private static func json(method: HTTPMethod = .get, api: API, encoding: ParameterEncoding = URLEncoding.default, headers: [String: String]? = nil) -> Observable<Any> {
        return dataRequest(method: method, api: api, encoding: encoding, headers: headers)
            .flatMap { (request) -> Observable<Any> in
                return manager.rx.json(request: request)
        }
    }
    
    private static func dataRequest(method: HTTPMethod = .get, api: API, encoding: ParameterEncoding = URLEncoding.default, headers: [String: String]? = nil) -> Observable<DataRequest> {
        return Observable<DataRequest>
            .create { (observer) -> Disposable in
                let url = api.buildURL
                let request = manager.request(url, method: method, parameters: api.parameters, encoding: encoding, headers: headers)
                observer.onNext(request)
                observer.onCompleted()
                return Disposables.create()
        }
    }
    
    /// レスポンスのJSON形式がDictionary(JSONObject)の場合
    static func get<T: Mappable>(api: API, encoding: ParameterEncoding = URLEncoding.default, headers: [String: String]? = nil) -> Observable<T> {
        return Client.json(method: .get, api: api, encoding: encoding, headers: headers).map(mapping())
    }
    
    /// レスポンスのJSON形式がArray(JSONArray)の場合
    static func get<T: Mappable>(api: API, encoding: ParameterEncoding = URLEncoding.default, headers: [String: String]? = nil) -> Observable<[T]> {
        return Client.json(method: .get, api: api, encoding: encoding, headers: headers).map(mappingToArray())
    }
    
    static func post<T: Mappable>(api: API, encoding: ParameterEncoding = URLEncoding.default, headers: [String: String]? = nil) -> Observable<T> {
        return Client.json(method: .post, api: api, encoding: encoding, headers: headers).map(mapping())
    }
    
    /// JSONからオブジェクトへのマッピング
    static func mapping<T: Mappable>() -> ((Any) -> T) {
        return { (json) -> T in
            let item: T = Mapper<T>().map(JSONObject: json)!
            return item
        }
    }
    
    /// JSONからArrayタイプのオブジェクトへマッピング
    static func mappingToArray<T: Mappable>() -> ((Any) -> [T]) {
        return { (json) -> [T] in
            let item: [T] = Mapper<T>().mapArray(JSONObject: json) ?? []
            return item
        }
    }
}

Genericsを使って、 ObjectMapper でマッピングしたオブジェクトを返す getpost メソッドを用意しておくと、使うときにClient.get(api: SlackApi.emojiList)と書けるので可読性がいいかなと考えます。

また、レスポンスがJSON形式であれば、 Clientクラスはそのまま流用可能です。
iOS版 るくみーnoteの実装も、認証処理や putpatch が追加されてるなどの違いはありますがほとんど一緒です。

あと、ちょっと無駄のようにも思えますが、 Observable<DateRequest> を返すメソッドも用意しておくと、アクセストークンの有効期限切れなどリトライする場合に役に立ちます。

使い方

絵文字リストの取得: emoji.list

/// 絵文字のリストを取得する
let request: Observable<Emoji> = Client.get(api: SlackApi.emojiList)
request.subscribe(
    onNext: { (emoji) in
        print(emoji)
    },
    onError: { (error) in
        print(error)
    })
    .addDisposableTo(disposeBag)

メッセージの送信: chat.postMessage

/// #generalにメッセージを送信する
let api = SlackApi.chatPostMessage(
    text: "てすと",
    channel: "general",
    options: ["username": "Bot", "icon_emoji": ":robot_face:"]
)
let request: Observable<PostMessage> = Client.post(api: api)
request.subscribe(
    onNext: { (message) in
        print(message)
    },
    onError: { (error) in
        print(error)
    })
    .addDisposableTo(disposeBag)

まとめ

今回はSlack Web APIを例に、iOSのAPI周りの実装方法をこんな感じで書いてますという紹介でした。
(コールバックを何個も用意したり、NSURLSessionを自前で書いたり、JSONからオブジェクトへ変換するのにif letをたくさん書いたり、、なにもかもみな懐かしいです。)

僕個人もはじめて Alamofire + ObjectMapper + RxSwift をプロダクトで使ってみましたがとてもよかったです。
さらにSQLite.swiftも一緒に使いましたので今後そのあたりも共有できたらいいなと思います。

iOSアプリ開発の参考になれば幸いです。

Ruby biz Grand prix 2016

あけましておめでとうございます。ユニファの田渕です。

みなさま、年末年始は十分に楽しめたでしょうか?
今年は曜日の並び上、超大型休暇にはなかなかしづらく、弊社でも昨年の年末を思い出しため息を吐く人が多数……。
と言っても、色々と話を聞いていると、エンジニアは休暇中も自分の興味のある技術や言語を試した方が多いようです。

ユニファは昨年、大変有難いことに、色々な賞を頂きました。
その中でも、エンジニアとして印象深かったRuby biz Grand prix 2016について本日は書きたいと思います。

Ruby biz Grand prix 2016とは?

とても簡単に言うと

  • Rubyを使っている企業が各社のサービスや技術などをアピール

  • エントリー企業の中から、大賞や特別賞などを選考(うまくいけば賞が貰える!)

というものです。 詳細については、下記の公式ページを参照ください。

一つ前の記事でも話が出たように、弊社はRubyを利用しています。
せっかくだから応募してみようか?
(あわよくば、まつもと ゆきひろさんに会えるかも)
ということで、エントリー資料を書いたのが昨年の夏のことでした。

華々しい表彰式

Ruby biz Grand prixの受賞企業は、事務局より表彰式のご案内が届きます。
この度、ありがたくも、ご連絡を頂いた弊社。
この時点では何の賞を頂くのかはわかっておらず、当日発表される形です。
普段、華々しい席にはエンジニアはあまり赴かないのですが、(そういうの苦手な人エンジニアに多いですよね) 今回はRubyの賞であることもあり、エンジニア勢で出席することに。
そうして迎えた当日……。
f:id:unifa_tech:20170105144411j:plain
会場に着いた途端に我々を出迎えた、とっても立派な看板……。
もちろん、立派なのは、看板だけではありませんでした。
ライトに煌々と照らされた眩しいステージ、プレゼン用の巨大スクリーン……ちょっと色々気後れします。
若干緊張気味のCTOをステージ前の待機席に送りだし、我々は受賞企業用の席で落ち着かないまま待っておりました。

結果は??

弊社の名前が呼ばれたのは「審査員特別賞」の発表の時でした。

審査員特別賞は3点あり、弊社は三つ目に名前を呼ばれました。
実はここで呼ばれなければ残るは「大賞」だけだったので、お、もしや!?と思っていたのですが……。

とはいえ、「審査員特別賞」でも十分すぎる結果です!
賞状とトロフィー(るくみーロゴ入り!)、副賞の30万円を頂きました!

受賞と同時に、携帯からSlackで、オフィスに居る皆さんに実況。
Slack上では大いに盛り上がりました。

それだけでは終わらない

若干の緊張感のある表彰式、フォトセッション(!)の後、我々を待っていたのは、事務局が用意してくださった豪華なお昼でした!
立食形式で、他の受賞者の皆様と交流を深めつつ、お肉やデザートを頂きました。
こういう場に出て、他の会社の方々とお話をさせて頂くと、いろいろと発見があります。
業界によっての考え方や文化の違い、 かと思うと、まったく遠いと思っていた業界の事例が実は参考にできそうだな、とか 広い様で狭い、狭い様で広いこの世界を改めて実感します。

なんて思いながら、CTOと二人、話していたら、側をまつもと ゆきひろさんが通っていくではありませんか!
二人揃って、慌ててご挨拶に伺います。
こういう場ですので、長くお話は出来ませんでしたが、ありがたいお言葉を頂き、感無量。

エンジニアとしてそれなりの期間を過ごしていても、外部のこういった賞を頂く機会というのはなかなかありませんので、大変貴重な経験をさせて頂いたと思っております。

事務局の皆様、ご協力頂いた皆様、本当に有難う御座いました。

システム開発部の体制の紹介

f:id:akanuma-hiroaki:20161218150539p:plain

 こんにちは、ユニファ株式会社CTOの赤沼です。ちょっと前に2016年になったと思ったのに、もう今年も終わってしまいますね。秋葉原オフィス周辺も今はすっかりクリスマスムードですが、25日が過ぎると一気にお正月モードに変わるんでしょうね。

 さて、本日からユニファ開発者ブログをスタートします。システム開発部のメンバーが持ち回りで、弊社サービス開発に関係する技術的な内容や、それぞれ興味を持っている技術的なトピック、開発プロセス、開発体制、社内の文化などについてゆるく書いていきます。どんなメンバーがどんな風に開発しているのかを知ってもらう場になればと思っています。

 今回は初回ということで、ユニファの開発体制について簡単に紹介させていただこうと思います。

メンバー構成

 2016年12月現在、ユニファ全体での正社員数は(グループウェアの名簿を数えたところ)32名、パートタイムのメンバーも含めると40名をこえるぐらいという規模です。そのうちシステム開発部メンバーは正社員が15名で、それ以外にフリーランスエンジニアの方数名にパートタイムでお手伝いいただいています。

 開発部内でのメンバー構成は下記のようになっています。

  • Webエンジニア:6名
  • スマートフォンアプリエンジニア:3名
  • デザイナー:2名(1名育休中)
  • QA:2名
  • インフラエンジニア:1名
  • フリーランスのパートタイムメンバー:2〜3名

 この一年ぐらいで人数は倍ぐらいに増えた感じですね。個人的にはスタートアップの中でもQAがいる会社は少ないのではないかと思っています。年齢層としては30代前半が多く、以前は大企業で仕事をしていたり、受託開発中心に仕事をしていたり、様々な経験を持つメンバーが集まっています。ユニファに入るために上海から日本に来てくれた中国人エンジニアもいたりします。

拠点

 弊社は2013年に名古屋で創業したので、本社は名古屋なのですが、2015年2月に東京オフィスを設立し、現在は東京オフィスを中心に開発を行っています。開発部に限らず拠点間でミーティングをするときはSkypeを活用しています。また、開発部では自宅勤務も取り入れているので、その日オフィスにいないメンバーとミーティングをするときもSkypeを利用しています。リモート勤務については他のメンバーが詳しく書いてくれるのではないかと思うので、詳細はそちらに譲ります。

言語・フレームワーク・インフラ

 Webアプリでは基本的にはRuby/Railsで開発しています。最近開発したサービスでは最新のバージョンのRuby/Railsを使用していますが、初期の頃にリリースしたサービスでは結構古いバージョンを使っているので、バージョンアップしたいと思っていますがなかなか実施できていません。

 スマートフォンアプリは、 iOSアプリはSwift、AndroidアプリはJavaで開発していまして、それぞれ Xcode、 Android Studioを使って開発しています。

 インフラは全てAWS上に構成していて、主にインフラエンジニアが面倒を見ています。AWSはWebエンジニアでも扱いやすい部分もあるので、開発初期の段階ではWebエンジニアが直接AWS環境で色々と試してみたりもしています。

開発ツール

 開発プロセスの詳細を書いていると長くなってしまうので、今回は使っているツールだけひとまずご紹介しておきます。実際にどのように活用してどんな流れで開発しているかはまた別の機会に。

 社内のコミュニケーションは主にSlackを使用しています。タスク管理と情報共有にはJIRA/Confluenceを使用し、ソースコードのバージョン管理はGitで、Bitbucketのプライベートリポジトリで運用しています。

 CIツールとしてはWerckerを使用していて、Bitbucketにpushすると自動的にテストを実行し、結果をSlackに通知しています。

 開発環境の構築にはVagrantやDockerを使い、サービス開発初期のプロトタイピングにはProttを利用しています。

 また、簡易な脆弱性チェックツールとしてVAddyを利用していて、日次で開発環境で稼働しているサービスに対して脆弱性チェックをかけています。

ビジネスモデル・サービス領域

 弊社のサービスは主に保育園や幼稚園に導入していただき、その園に通われている園児の保護者の方に使っていただくサービスになりますので、ビジネスモデルとしてはB2B2Cになります。対象の業界としては主に保育業界ということになります。私もユニファに入ってから色々と知ったのですが、一般的なB2BやB2Cのサービスとはまた違う、保育業界ならではの気をつけないといけなことも多いので、業界をよく知るビジネスサイドのメンバーとサービス内容を検討したり、現場の保育士の方にヒアリングをさせていただいたりしています。

 ということで、開発部について簡単に紹介させていただきました。それぞれ詳細についてはまた改めてご紹介させていただこうと思いますので、ユニファ開発者ブログをよろしくお願いします。