アクトインディ開発者ブログ

子供とお出かけ情報「いこーよ」を運営する、アクトインディ株式会社の開発者ブログです

Rails Developers Beer BashでLT登壇してきました。

こんにちは。nakamuraです。

10月26日に行われたRails Developers Beer Bashで人生初のLTを行ってきました。

発表した際の資料はこちらです。

www.slideshare.net

登壇してみて

今回が人生初のLTだったのですが、とても貴重な経験になりました。 チケットサービスのリリース以降、Rails6の正式リリースにあわせて、出来るだけスムーズにアップデートを行うことができるようチームで頑張ってきた成果をこの機械に発表できたことはとても嬉しかったですし、久しぶりにRailsのTechイベントに参加できたのも改めて勉強になりました。

Beer Bash全体の感想

今回は、8月に正式リリースされたRails6をテーマにしたイベントでしたが、やはりアップグレードする際は、テストをしっかり書いておいたほうが安心できるよね。とか、サービスによってクリティカルなところはそれぞれ手動で動作検証を行っている。などの話を聞けたのは、共感できるし参考になりました。

Rails6の新しい機能の中では、Multi-DBを現場で導入したときの注意点など、とても興味深かったです。

あとは、パネルディスカッションで1つのトピックとしてあげられていた「技術的負債」に各社どのように取り組んでいるか?の回答で、毎週、時間を決めてエンジニア全員で取り組んでいる。など具体的な事例を聞けたのは、とても参考になりました。

さらに、dependabotはイベントをとうして、かなり話題にあがっていたし、未導入のところでも、導入を検討しているところがほとんどだったので、僕たちチームの選択もRailsのトレンドからずれてはいないんだなぁと実感できたこともよかったです。

最後に

人生初のLTでとても緊張しましたが、チームの頑張りをちょうどいいテーマのイベントで発表できたことは何より嬉しかったです。

アクトインディでは、ただいまエンジニアを絶賛募集中です! 興味のある方は是非!

actindi.net

いこーよにWebチケットサービスをリリースしました

こんにちは、nakamuraです。もはや2ヶ月前ほどになりますが、タイトルの通り、いこーよでWebチケットが購入できるようになりました。リリース当初はまだ未実装だった機能もどんどんリリースされ、これからもますます便利になっていくサービスですので、ぜひご利用ください!

さて、そんな待ちに待ったWebチケットの機能を、この記事で紹介したいと思います!

1. Webチケットを選択

まず、Webチケットが購入できる施設さんには、タイトルの下に緑色のバッヂが表示されます。

※以降の操作は全て、スマートフォンでアクセスされた場合を表示しています。

gyazo.com

そして、ページ中央部の「おトクな各種チケット」欄に購入対象のWebチケットが表示されるので、お好みのチケットを選択してください。

すると、購入できるチケット種別ごとに金額が表示されているチケット詳細ページに遷移するので、購入する枚数を選択します。

gyazo.com

枚数が1枚以上になると画面下部にあるボタンが反転するので、ここから購入フローに遷移します。

gyazo.com

ログインが済んでいない場合は、↓のページに遷移するので、ログインもしくは新規会員登録をお願いします。 ログイン後は、自動的に購入フローに遷移します。

gyazo.com

2. 購入フロー

初回はまず、購入者情報の入力ページに遷移しますので、必須項目を入力してください。2回目の購入からは、直接確認ページへ遷移します。

gyazo.com

購入者情報、枚数が正しいか確認してください。

gyazo.com

枚数の修正が必要な場合は、ページ下部の「枚数を選択しなおす」ボタンで修正が可能です。 修正の必要がない場合は、「お支払い情報を入力する」ボタンで支払い情報の入力ページへ遷移します。

gyazo.com

次に、クレジットカード情報を入力してください。ページ下部にある「次回以降もこのカード情報を使う」にチェックを入れると次回以降は入力の必要がなくなります。

gyazo.com

最後に、「チケットの購入を完了する」ボタンで購入が完了します。

gyazo.com

正常に購入が完了すると↓のページが表示されます。

gyazo.com

3. Webチケットを使う

チケット購入後はいつでもマイページの「チケットを見る」ボタンで購入したチケットを確認することが可能です。

gyazo.com

あとは、お出かけした際に「チケットを利用する」ボタンでWebチケットページに遷移し、表示されるメッセージに従い、チケットを使用してください。 (※現時点でチケットの購入、使用はスマートフォンを対象にしています)

gyazo.com

最後に

アクトインディのWebチケットサービスはこれからもどんどん内容を充実させて行く予定ですので、お出かけの際はぜひ、Webチケットが利用可能かどうかご確認ください!

また、アクトインディでは、Webチケットサービスをいっしょに改善していきたいというエンジニアも大募集中です!

VCRでWeb APIのモックを楽しよう!

morishitaです。

最近、Stripeを利用した決済処理の実装をしていました。

tech.actindi.net

tech.actindi.net

サーバーサイド(Rails)でStripe APIを利用しており、そのテストの実装にvcrを利用しました。
以前から使っていましたが、改めて便利だと思ったのでご紹介。

外部APIを叩くテストのツラミ

システム開発をしているとすでにある他のシステムを力を借りるということがあります。
他のシステムのAPIを利用するということですね。

あるいは、ひとつのシステムに必要なすべての機能を実装せず機能ごとにシステムを分割し、お互いにAPIで呼び合ってサービスを実現することもあります。
マイクロサービスアーキテクチャってやつですね。

システム間連携する手段としては古くはSOAP、最近ではgRPCなど様々な方法がありますが、現状最も使われているのはHTTPプロトコルのREST APIではないでしょうか。

そのようなシステムのテストを実装する際、いちいちAPIを叩くのは大変です。
なぜなら、テスト対象のシステムの他にAPIを提供するシステムの準備が必要になるからです。
それに他社サービスをテストのために大量にコールするようなことをしては迷惑になりますし、従量課金のAPIだったらコストも馬鹿になりません。
なのでモックすると思います。

Railsのテストでよく使われるライブラリにwebmockがあります。
単にAPIへのコールをモックし、その成否程度が必要なだけならいいのですが、レスポンスの内容により処理が変わるとなるとそれを実装しないといけません。

私はここ最近、決済サービスStripeを利用したRailsアプリケーションを開発していたと冒頭書きましたが、お客様からお金をいただくサービスなのでテストもしっかり実装しました。

StripeのAPIは物によっては約3KBのレスポンスを返してきます。
レスポンスボディだけそんなにデータを返してくる、加えて、レスポンスヘッダもあります。それらをテストケースで必要な分用意するのは大変です。

もちろんレスポンス全部を処理に利用するわけではないので使う部分だけをモック実装してもいいのですが、モックとはいえできるだけ本物に近づけたい。

そう考えると… webmock 辛い。

VCRという救い

vcrを使えば、そんな面倒なWeb APIのモック実装から解放されます。

vcrもwebmock同様Web APIをモックするためのライブラリですが、自分でモックを実装する必要がありません。
設定にもよりますが、モックデータが無ければ、実際にAPIをコールしそのリクエスト/レスポンスをYAMLファイルに保存してくれます。
2回目以降は実際にはAPIを叩かずに保存したYAMLファイルからレスポンスを作ってモックしてくれます。

レスポンスヘッダを含め、同じレスポンスを再現でき本物に限りなく近いモックを使ってテストできます。

VCRを使う

前置きが長くなりましたが、RSpecで使う前提でVCRの使い方を紹介します。

インストールは Gemfileのtestグループにでもgem 'vcr'を追加してbundle installすればOKです。

設定

spec_helpler.rbでVCR設定します。

当社のとあるプロダクトでは次のような設定で利用しています。

require 'vcr'
VCR.configure do |c|
  c.cassette_library_dir = 'spec/vcr' # カセットを保存するルートディレクトリ
  c.hook_into :webmock # 利用するモックライブラリ(内部ではwebmockを利用しています)
  c.configure_rspec_metadata! 
  c.allow_http_connections_when_no_cassette = true # VCRを使わない場所ではHTTP通信を許可する
  c.default_cassette_options = {
    record: :new_episodes, # カセットがなければAPIをコールしてそれを記録する
    match_requests_on: [:method, :path, :query, :body], # カセットを引き当てる条件
  }
  c.before_record do |interaction| # カセット保存前の処理の設定
    interaction.response.body.force_encoding 'UTF-8'
    interaction.response.body = JSON.pretty_generate(JSON.parse(interaction.response.body)) if interaction.response.body.present?
  end
end

VCRでは保存するモックデータをカセット(cassette) と呼びます1
どのような設定をしているかはコメントでだいたい記述しましたが、ポイントを説明します。

record: :new_episodes はカセットがなければ実際にAPIを叩いてそれを記録します。あればAPIを叩かずカセットを再生します。
match_requests_onはリクエストに対応するカセットを何によって判定するかを設定します。
上記の例では[:method, :path, :query, :body],を指定しています。これはリクエストのメソッドとパス、 クエリそしてリクエストボディが一致するカセットを再生するという意味です。

c.before_recordのブロックではカセットを保存する前の処理を実装しています。
上記の例ではレスポンスボディはUTF-8とみなし、しかもJSONとみなして見やすく出力するようにしています2

記録されるカセットの例を示します。
これはStripeの Refund(払い戻し)APIの例です。
ひとつのAPIのリクエスト/レスポンスのみを含んでいますが、一連の処理で複数回のAPIコールがあれば、すべてひとつのファイルに記録されます。

---
http_interactions:
- request:
    method: post
    uri: https://api.stripe.com/v1/refunds
    body:
      encoding: UTF-8
      string: charge=ch_XXXXXXXXXXXXXXXXXXXXXXXX
    headers:
      User-Agent:
      - Stripe/v1 RubyBindings/4.9.1
      Authorization:
      - Bearer rk_test_F7uEYj6rWVCmzjOl7tm0qo6n004oYskC75
      - Bearer rk_test_XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
      Content-Type:
      - application/x-www-form-urlencoded
      X-Stripe-Client-User-Agent:
      - '{"bindings_version":"4.9.1","lang":"ruby","lang_version":"2.6.2 p47 (2019-03-13)","platform":"x86_64-linux","engine":"ruby","publisher":"stripe","uname":"Linux
        version 4.9.125-linuxkit (root@659b6d51c354) (gcc version 6.4.0 (Alpine 6.4.0)
        ) #1 SMP Fri Sep 7 08:20:28 UTC 2018","hostname":"XXXXXXXXXX"}'
      Accept-Encoding:
      - gzip;q=1.0,deflate;q=0.6,identity;q=0.3
      Accept:
      - "*/*"
      Connection:
      - keep-alive
      Keep-Alive:
      - '30'
  response:
    status:
      code: 200
      message: OK
    headers:
      Server:
      - nginx
      Date:
      - Sat, 30 Mar 2019 16:15:08 GMT
      Content-Type:
      - application/json
      Content-Length:
      - '495'
      Connection:
      - keep-alive
      Access-Control-Allow-Credentials:
      - 'true'
      Access-Control-Allow-Methods:
      - GET, POST, HEAD, OPTIONS, DELETE
      Access-Control-Allow-Origin:
      - "*"
      Access-Control-Expose-Headers:
      - Request-Id, Stripe-Manage-Version, X-Stripe-External-Auth-Required, X-Stripe-Privileged-Session-Required
      Access-Control-Max-Age:
      - '300'
      Cache-Control:
      - no-cache, no-store
      Original-Request:
      - req_XXXXXXXXXXXXXX
      Request-Id:
      - req_XXXXXXXXXXXXXX
      Stripe-Version:
      - '2018-09-24'
      Strict-Transport-Security:
      - max-age=31556926; includeSubDomains; preload
    body:
      encoding: UTF-8
      string: |-
        {
          "id": "re_XXXXXXXXXXXXXXXXXXXXXXXX",
          "object": "refund",
          "amount": 1810,
          "balance_transaction": "txn_XXXXXXXXXXXXXXXXXXXXXXXX",
          "charge": "ch_XXXXXXXXXXXXXXXXXXXXXXXX",
          "created": 1553962508,
          "currency": "jpy",
          "reason": null,
          "receipt_number": null,
          "source_transfer_reversal": null,
          "status": "succeeded",
          "transfer_reversal": null
        }
    http_version: 
  recorded_at: Sat, 30 Mar 2019 16:15:08 GMT
recorded_with: VCR 4.0.0

テストケースでの利用

さて、上記の設定を行い、実際のテストケースでどのように使うかを見ていきます。

決済を取り消す処理を実装したコントローラのSpecです。

describe RefundController, type: :controller do
  describe 'POST #refund' do
    subject { post :refund, params: { id: 'XXXXXXXXXXXXXX' }, format: :json }

    context '正常に払い戻せる場合', vcr: { cassette_name: 'refund-success' } do
      it 'Refund (払い戻し)が作られる' do
        expect { subject }.to change { Refund.count }.by(1)
        expect(subject).to have_http_status(:success)
      end
    end
  end
end

vcr: { cassette_name: 'refund-success' }の部分以外はいたって普通のコントローラスペックかと思います。 これだけでHTTP通信をモックしたテストになっています。

vcr: { cassette_name: 'refund-success' }は次のことを設定しています。

  • このコンテキストVCRを有効にする
  • カセットファイルはspec/vcr/refund-success.yaml

カセットファイル名は指定しなくてもコンテキスト名で作ってくれる機能あります。
ただ、昔VCRをアップデートしたら2バイト文字があるとうまく動かないトラブルに見舞われたことがあり、それ以来私は指定しています3

spec/vcr/refund-success.yamlの中身は上記のカセットの例になります。
初めて実行したときには実際にStripeのAPIにアクセスし、カセットファイルに記録します。2回目以降はカセットが再生されます。

とても簡単ですね。webmockのようにモックを準備するコードは一切なしでOKです。
カセットファイルを作り直したいときには、カセットファイルを削除して再実行するだけです。

注意点

とっても楽にWeb APIをモックしたテストが実装できるのですが、利用には注意が必要です。

それはモックはモック。フェイクであって本物ではないということです。

これはVCR特有ではなくモックに共通した注意点です。
モックしたAPIの仕様の変更は常にチェックが必要です。当たり前の話ですが、モックは作ったときの仕様のままです。
APIの仕様が変更されればそれに追従する必要があります。
さもないとテストは通るけど本番適用すると動かないというトラブルに繋がります。

外部に公開されているAPIなら移行期間を設けると思うので、すぐに古い仕様のAPIが使えなくなることはないと思いますが、放置すると廃止される可能性もあるので注意が必要でしょう。

それ以上に注意が必要なのは非公開の社内APIだったりします。
移行期間など設けず、変更してしまうなんてことが起こりがちです。 それが利用者にとって破壊的な変更だとトラブルになります。 それぞれのプロダクトのチーム間で変更について共有し、注意しあえるコミュニケーションが重要です。

まとめ

VCRでWeb APIのモックを楽しよう!

最後に

アクトインディではエンジニアを募集しています。


  1. VCR=Video Cassette Recorderからのメタファーですね。

  2. レスポンスボディがUTF-8のJSONと決めつけた実装になっていますが、APIの仕様によります。でもまあ、一般的でしょう。

  3. 今はもう修正されているのでしょうか。未確認です。コンテキスト名を変更したらカセットファイル名も変更必要になるのでその理由からも指定したほうがいいと思いっています。

Stripeを使った決済処理を調べてみた。

morishitaです。

今回はStripeについて調べてみたのでそのことを書きます。

Stripeとは

Stripeは決済処理を代行してくれるサービスです。
サービス内でなにか販売をしようとするとユーザーから代金をいただく決済の仕組みを用意する必要があります。

ネットサービスだとまずはクレジットカード決済のサポートが必須となると思います。
通常は、各クレジッドカードブランドと直接やり取りすることはなく、代行処理サービスを使います。 Stripeもそんな代行処理サービスのひとつです。

決済機能はクレジットカード番号というセンシティブな情報を扱うため、代行処理サービスを使ってもやり取りが複雑になりがちです。
私自は過去に国内の同様のサービスを使ってクレジットカード決済を実装したことありましたが、 今回Stripeを使ってみてその実装の容易さに感動しました。

Rails のアプリケーションに組み込む前提で調査したのでご紹介します。

最初の一歩

Stripe: Registerでアカウントを作って開発用のAPIキーを取得したら、ざっくり決済をどうやって実装するのか感じを掴むために次のドキュメントをやってみることをおすすめします。

stripe.com

掲載されているコードのコピー&ペーストで下のようにRails上で決済できるところまで実装できます。

rails newからで所要15-30分くらいです。 ローカルマシン上で動かせます(rails sを使います)。

出来上がると次の様にCheckout.jsというStripe社が用意するJSライブラリで作られたクレジットカードフォームを使って決済を行えます。

f:id:HeRo:20190525151945g:plain
Stripeデモ

クレジットカード情報はRailsに送信されません。 Checkout.jsが直接StripeのAPIサーバに送信して、stripeTokenを取得します。

RailsにはこのstripeTokenを送信して、決済処理に利用します。

f:id:HeRo:20190525151906p:plain
シーケンス

こうしてクレジットカード情報というセンシティブな情報を自社で運用するサーバには一切送らずに決済処理ができるというわけです。

決済処理を行うために必要最低限のRails側のコードは次の通りです1

def create
  charge = Stripe::Charge.create({
    source: params[:stripeToken],
    amount: 500, # 決済金額
    currency: 'jpy',
  })
  # 〜決済結果を保存する処理など〜
end

とてもシンプルな実装で決済処理を実現できます。

Checkout.js

クレジットカード情報はCheckout.jsでハンドリングすると前述しましたが、そちら側の実装も簡単です。

上のGIFアニメのフォームを表示するには次の様に実装するだけです。

  <script src="https://checkout.stripe.com/checkout.js" class="stripe-button"
          data-key="<%= Rails.configuration.stripe[:publishable_key] %>"
          data-description="A month's subscription"
          data-amount="500"
          data-locale="auto"></script>

data-属性で言語や通貨の指定や請求先住所の入力欄の追加などが簡単にできるようになっています。

Stripe.js と Elements

Checkout.jsを利用するとほぼ実装なしと言えるくらい簡単ですが、実際には自社のサービスに合わせたデザインにしたいでしょう。

そのためにStripe.jsというJavaScriptライブラリの中にプリミティブなUIコンポーネントであるElementsが用意されています。実装量は増えるものの、こちらを使えば自由にデザイン可能です。

使いやすいと思ったところ

実際に試してみていいと思ったところ、よく考えられているなと思ったところを紹介します。

カード情報を保存できる。複数カードも可能

StripeではCustomerを登録することでお客様情報を管理できます。
そしてCustomerに対してカード情報を登録し管理できます。
これにより、リピーターとなってくれたお客様が毎回カード番号を入力する手間から解放できます。
複数カードの登録も可能なので複数のカードを使い分けたいというニーズにも対応できます。

クレジットカード番号の追加時も自社サーバにはカード情報は送信しません。やはりJavaScriptで実装したフォームから直接Stripeへ送信して得られたトークンでCustomerに紐づけます。カード情報の一部、ブランド、有効期限といったお客様がカードを選択するのに必要な情報はAPIで取得できるのでそれを示して選択してもらいます。
また、決済に使用するデフォルトカードも設定可能なので、「いつものカードで決済」といったショートカットも用意できます。

部分払い戻しが可能

1回の決済で複数の物品を購入し、その一部をキャンセルしたい場合があります。
その様なユースケースにも対応できます。

1つの決済を何回にも分けて払い戻すことが可能です。 当たり前ですが決済額を超えて払い戻そうとするとエラーが発生します。

冪等なリクエストが可能

UIのレスポンスや通信の遅れで購入確定のボタンを複数回押してしまうことがあります。また、世の中には基本ボタンは連打する人もいます。
リクエスト回数だけ処理するのがサーバとしては普通なのですが、ユーザ視点に立つと意図しない購入、請求に繋がります。

なので決済処理ではサーバサイドで2度めのリクエストは無視するなどの処理が必要になりますが、Stripeはこれにも対応しています。

次の様にidempotency_key(冪等キー)をStripe APIへのリクエストに付加してやることで、冪等にリクエストが処理されます。 つまり、同じリスクエストを2回送ってしまっても2回目以降は同じレスポンスが帰って来て、Stripe側では1つしか決済が作られません。

charge = Stripe::Charge.create({
  amount: 2000,
  currency: "jpy",
  source: @stripe_token,
  description: "何らかのお支払"
}, {
  idempotency_key: @unique_key
})

自社サービス側も冪等に実装しておくことにより、ユーザが意図しない決済が発生することを防げます。

メタデータを保存可能

Stripeに登録する決済(Charge)、カスタマー(Customer)、払い戻し(Refund)などのリソースには、メタデータとして自由にキー:バリューなデータを保存できます。

自社システム側で振り出したIDなどを保存しておけば、システム間でのデータの突き合わせに役立ったりします。
保存したデータはWebコンソールで参照できます。

開発環境が整っている

開発用のAPIキー発行は管理コンソールから簡単にできます。 また、いくつでも発行できます。

実際に請求はされませんが、決済処理を行いそのデータも管理コンソールですぐに確認可能です。

テスト時の決済に使える各種ブランドのカード番号も用意されています。

stripe.com

エラーが発生して決済できない番号も用意されているので様々な挙動を確認することができます。

各種言語用のクライアントライブラリも用意されており、ドキュメントも揃っています。

Stripe API Reference

webhookもある

Stripeで決済Chargeを登録する。払い戻しRefundするといったイベントごとにWebhookにリクエストを送ることも可能となっています。

次の様な用途に利用できるでしょう。

  • お客様サポートのために決済エラーを監視する
  • お客様の購買行動からKPIをリアルタイムで把握する

まとめ

  • クレジットカード情報を一切自社サービスシステムに送らず決済可能
  • 一部払い戻し、ユーザの複数カードの管理など柔軟な決済機能が実装可能
  • 開発環境も整っており実装しやすい

Stripeのサブスクリプションについて調べたことは次のエントリに書いたのでこちらもどうぞ!

tech.actindi.net

最後に

どうしてStripeについて調査したのかというと先日リリースしたいこーよでのチケット販売サービス「すぐいこ」のためです。

「すぐいこ」は実現したいことのまだ一部しかできてません。 アクトインディでは一緒に「すぐいこ」を開発してくれるエンジニアを募集しています。


  1. Rails Checkout Guideの例から更に削って本当の必要最小限のコードとなっています。

Simplecovで一部クラスでカバレッジが計測されない場合の解決方法

morishitaです。

アクトインディではRailsアプリケーションのテストをRSpecで書いています。
そして、テストカバレッジをSimpleCovを使って計測しています。

何故かカバレッジが計測できないクラスがあったのですが、計測できるように解決した件を紹介します。

tl;dr

  • SimpleCov.startは次のコードの前に実行する
require File.expand_path('../../config/environment', __FILE__)
  • .simplecov ファイルにSimpleCov.startを実行するコードを書いてRSpecの起動するクリプトで最初にrequire 'simplecov'したほうが楽。

改善前の状況

例えばいこレポではこんな感じでプルリクエストにPushするとテストが実行され、 その結果がカバレッジとともにSlackに通知される仕組みになっています1

f:id:HeRo:20190513211431p:plain
改善前

そこそこ高いカバレッジを維持できているのですが、 一部どうしても計測できないコードがあって悩んでいました。

f:id:HeRo:20190513211530p:plain
改善前 モデル

Userモデルはログインユーザを表します。
なのでテストで使わないわけがないのですが 0.0 % となっています。

SimpleCov.start の位置が重要

結論から言うと計測したいクラスがロードされるよりも先に SimpleCov.startが実行される必要があります。

そんなつもりはないのに、SimpleCov.startの前にロードしちゃっているありがちな例は次のケースです。

  1. FactoryBotのFactoryのStatic attributesでロードしてしまっている
  2. Initilizerでロードしてしまっている

1.のStatic attributes は FactoryBot 5.0以降で廃止されているので、最新のFactoryBotを使っていれば関係ないはずです。
Static attributes ってどんなの? って方はDeprecating static attributes in factory_bot 4.11を参照ください。対処法も載っています。

2.のケースですがrspec-railsを利用している場合、あんまり意識しなくても、spec_helper.rb にSimplecovの設定を実装していればテスト対象のコードより先にロードされます。 というのもrails generate rspec:installコマンドで生成される rails_helper.rbは最初にspec_helper.rb をロードするようになっているからです。

しかし、rspec-railsの3.6.0未満のバージョンで生成したrails_helper.rbを使っているとハマる可能性があります。私はこれにやられました。

解決するにはrails_helper.rbの次のコードより前にSimpleCov.startを実行すれば良いです。

require File.expand_path('../../config/environment', __FILE__)

つまり、rails_helper.rbの上記コード行より前でspec_helper.rbrequireしてやればいいのです。

test-queue

一方、アクトインディではCI環境だと test-queueを使ってRSpecを並列実行しています。 その場合、rspecコマンドではなく、TestQueue::Runner::RSpecを継承したクラスを実装して独自のRSpec起動スクリプトを用意して使います。そこでもハマっていました。

その起動スクリプト内でも次のコードが存在していました。

require File.expand_path('../../config/environment', __FILE__)

特に test-queueSimpleCov.start を実行しなくても、rails_helper.rbは各specファイルでrequireしていて読み込まれます。
それで見落としがちなのですが、やはりここでも上記コードより前にSimpleCov.startしてやる必要あるのです。

.simplecovによる設定

SimpleCov.startを実行すると言っても1行書くだけではないと思います。

例えば、上記のような設定で計測したいとします。

  • 結果出力のフォーマッタを指定
  • Railsの標準的な設定を導入
  • 7行以下のファイルは無視する

すると次のようなコードが必要になります。

require 'simplecov-json'
SimpleCov.formatters = [
  SimpleCov::Formatter::HTMLFormatter,
  SimpleCov::Formatter::JSONFormatter,
]
SimpleCov.start 'rails' do
  add_filter do |source_file|
    source_file.lines.count < 7
  end
end

前述の通り、最初に実行する必要があるため、複数のRSpec起動するスクリプトがあるとそれぞれに記載する必要があります。
それぞれにコピー&ペーストするのはDRYでないのでファイルを分けてrequireすると思います。
しかし、.simplecovという名前でプロジェクトのルートに置けばrequire 'simplecov'simplecov のロードと同時に .simplecov の内容を実行してくれます。
各起動スクリプトの最初でrequire 'simplecov'してやればいいだけなので、この方法で設定したほうがシンプルでいいと思います。

改善した結果

さて、こうして改善した結果を確認してみましょう。

f:id:HeRo:20190513211607p:plain
改善後

お、カバレッジが少し大きくなりました。
で、問題のUserクラスはというと…。

f:id:HeRo:20190513211626p:plain
改善後 モデル

やりました! 計測できています!

実は…

このエントリで紹介した解決方法はSimpleCovのREADMEに書いてありました。
やはりドキュメントは時々ちゃんと読まないとダメだなと反省しました。

最後に

アクトインディでは計測しながらコードを改善していきたいエンジニアを募集しています。

Active Flagで効率的にフラグを実装する

morishitaです。

今回はActive FlagというGemを紹介します。
このGemはActiveRecordのモデルでBIt Arrayなカラムを扱いやすくしてくれます。

github.com

こういう要件ってありますよね?

  • ON/OFFできるユーザ設定をたくさん持たせたい
  • 選択肢を複数選択できる選択項目を持たせたい

これらをDBに保存できるように実現するにはどのような実装をするでしょうか?

前者の場合、素朴に実装するとBooleanを格納する属性を設定項目分だけ作る方法が考えられます。
1つ、2つの項目ならそれでもいいでしょう。
でもそれ以上になると、テーブルのカラムがやたら増えてしまうのでもっとスッキリ実装できないかなぁと考えてしまいます。

なんとか1カラムに押し込めようとすると、JSONにしたり、true/falseのカンマ区切りリストを格納する方法もあるでしょう。
そして、そんなことをすると後で設定項目を見直す際に「ある設定をONにしているユーザはどれくらいいるのか集計してほしい」とか言われ、SQLで解決しようとするとLIKEや正規表現を駆使することになります。
JSONのキーの命名次第では割と辛かったりしますし、true/falseのカンマ区切りリストだと…考えたくもないですね。
(モデルにロードして全件スキャンっていう手もありますが、小さいサービスならそれもいいでしょう。でも数百万とかになると…。うわぁ)

後者の場合、選択された項目のIDをカンマ区切りで格納したりするのでしょうか?
これも、抽出や集計する必要があるとなかなか辛いと思います。 選択肢そのものが別モデルとして独立できるほどであれば、別テーブルにして1対多の関係で管理してもいいかと思います。
でも、ON/OFF を管理したいだけで別テーブル作るのもなぁ…って場合もありますよね。

Bit Arrayという解決

Bit Arrayを使えば、テーブル的にはスッキリ解決できます。

例えば、Personモデルがあり、話せる言語を英語スペイン語中国語フランス語日本語 から選択するケースを考えます。もちろん複数選択ありです。

このデータを格納するためにinteger型のカラム languages を設けます。

それぞれの言語を次の数で表します。

  • 英語 english = 1
  • スペイン語 spanish = 2
  • 中国語 chinese = 4
  • フランス語 french = 8
  • 日本語 japanese = 16

例えば、英語が話せるPersonlanguages=1、英語と中国語と日本語が話せればlanguages=21というふうに上記で示した数を合計した数を格納します。これで複数の言語が選択された場合も問題なくデータを保存できます。

ん? それで、ちゃんとどんな選択の場合も表現できるの? って思うかもしれませんが、大丈夫です。
それぞれの言語の数字を2進数に変換するとよくわかります。

言語 10進数表現 2進数表現
英語 english 1 00001
スペイン語 spanish 2 00010
中国語 chinese 4 00100
フランス語 french 8 01000
日本語 japanese 16 10000

で、英語中国語日本語を選択する場合は次のようになります。

english:  00001
chinese:  00100
japanese: 10101
----------------
ビットOR    10101 (2進数) => 21 (10進数)

各言語の数字を2進数で表すと各桁がそれぞれの言語を表しているとみなせます。
そしてビットORを取ることにより複数の選択肢の選択状態を表現できます。

これですべての選択の組み合わせを表現できるということがわかりますね。

クエリー

検索するときにはビットANDを使うことにより検索可能です。

例えば、スペイン語(2)またはフランス語(8)のどちらかを選択しているpersonを抽出するには次の様なSQLとなります。

SELECT * FROM person WHERE languages & 10 > 0;

スペイン語(2)とフランス語(8)の両方を選択しているpersonを抽出するには次の様なSQLとなります。

SELECT * FROM person WHERE languages = 10;

メリット・デメリット

  • メリット
    • 1カラムで複数の選択肢の選択状態を格納できる
    • 選択肢が増えてもDBのスキーマは変わらない
    • JSONなどで格納する場合に比べ検索しやすい
      • index を付けても肥大しにくい

  • デメリット
    • DBの値からどれが選択されているのかぱっと見わかりにくい
    • ビット計算に慣れないと値の格納、レコード抽出が分かりづらい

Active Flag

さて、この様なカラムの実装をサポートしてくれるライブラリがactive_flagです。

Active Flagを使った実装

まずはGemfileに次を追加して bundle install でインストールします。

gem 'active_flag'

そしてマイグレーションですが、Bit Arrayを格納するカラムは単なる整数型のカラムを使います。 次のような感じで実装します。

class CreatePerson < ActiveRecord::Migration[5.2]
  def change
    create_table :peaple, comment: 'パーソン' do |t|
      t.integer :languages, null: false, default: 0, limit: 8
    end
  end

次にモデルの実装は、次のとおりです。:languagesに対する選択肢を定義するだけです。簡単ですね。

class Person < ApplicationRecord
  flag :languages, [:english, :spanish, :chinese, :french, :japanese]
end

これで、インスタンスとクラスには次のメソッドが追加されます。

# インスタンスメソッド
## 設定されている値の取得
profile.languages                   #=> #<ActiveFlag::Value: {:english, :japanese}>
profile.languages.english?          #=> true
profile.languages.set?(:english)    #=> true
profile.languages.unset?(:english)  #=> false

## 値のセット、アンセット
profile.languages.set(:spanish)
profile.languages.unset(:japanese)
profile.languages.raw               #=> 3
profile.languages.to_a              #=> [:english, :spanish]

## 複数の値の同時設定
profile.languages = [:spanish, :japanese]

# クラスメソッド
## 選択肢の取得
Profile.languages.maps              
#=> {:english=>1, :spanish=>2, :chinese=>4, :french=>8, :japanese=>16 }
Profile.languages.humans            
#=> {:english=>"English", :spanish=>"Spanish", :chinese=>"Chinese", :french=>"French", :japanese=>"Japanese"}
Profile.languages.pairs             
#=> {"English"=>:english, "Spanish"=>:spanish, "Chinese"=>:chinese, "French"=>:french, "Japanese"=>:japanese}

# スコープメソッド
## 抽出
Profile.where_languages(:french, :spanish)  
#=> SELECT * FROM profiles WHERE languages & 10 > 0

## バルクセット
Profile.languages.set_all!(:chinese)        
#=> UPDATE "profiles" SET languages = COALESCE(languages, 0) | 4

## バルクアンセット
Profile.languages.unset_all!(:chinese)      
#=> UPDATE "profiles" SET languages = COALESCE(languages, 0) & ~4

どの選択肢の値がいくつなのかを自分で管理しなくてもよしなにやってくれます。 そして自分でビット演算子を使ってクエリを作らなくても便利なメソッドで値の取得・設定、レコードの抽出が可能となります。 しかもi18nにも対応しており、言語ファイルを用意すれば複数言語対応で表示も可能です。

プロダクトでの利用

いこレポでは現在サイトリニューアル中です。
記事の種別や分類を整理して訪問ユーザが記事の分類や目的別に記事を探しやすくなる予定です。
段階的にリリースするスケジュールですでに一部は公開済みです。

そのため、Active Flagを使って記事にフラグを追加し、分類に利用しています。

report.iko-yo.net

最後に

いこレポももうすぐ2周年。おかげさまで月間数百万PVを超えるサイトに成長しました。

アクトインディでは一緒にサービスをグロースさせていくエンジニアを募集しています。

Railsで2段階認証を試してみました

こんにちは、nakamuraです。 securityって大事ですよね。少し前からRailsで2要素認証ってどうやるんだろうと気になっていたので、google-authenticator-railsを使ってどんな風に実装できるのか試してみました。基本的には google-authenticator-rails Readmeに記載してある通りに実装していけばOKでした。

Gemをインストール

はじめに、Gemfileにgoogle-authenticator-railsを追記し、bundle installを実行します。

gem 'google-authenticator-rails'

Userモデルを作成

次に、Userモデルにgoogle-authenticator-railsで必要なカラムを追加します。 通常は、passwordカラムなど他にもカラムがあると思いますが、今回はお試しなので、割愛します。

$ rails g model user email:string google_secret:string

Userクラスにacts_as_google_authenticatedを設定します。 これで、google-authenticator-railsのメソッドが使えるようになります。

class User < ApplicationRecord
  acts_as_google_authenticated
end

Readme によるとGoogleAuthenticatorRailsはアカウント区別するためにlabelが必要とのことで、デフォルトではemailカラムを使用しますが、他にもcolumn_nameオプションで別カラムを指定したり、procでカスタマイズも可能とのこと。とりあえず、今回はデフォルトを使用します。

そうすると、下記のように、User#set_google_secret でUserモデルのgoogle_secretカラムに値が設定され、User#google_qr_uri メソッドでQRコードが表示できるようになります。

Loading development environment (Rails 5.2.3)
irb(main):001:0> user = User.new
irb(main):002:0> user.email = "test@example.com"
irb(main):003:0> user.set_google_secret
irb(main):004:0> user.google_qr_uri
=> "https://chart.googleapis.com/chart?cht=qr&chl=otpauth%3A%2F%2Ftotp%2FTEST%2520MY%2520APP%3Atest%40example.com%3Fsecret%3Dedotpifgivmj2lmf%26issuer%3DTEST%2BMY%2BAPP&chs=200x200"
irb(main):005:0>

サンプルアプリの作成

それでは、以降で、簡単なQRコードを表示して、スマホアプリのGoogleAuthenticatorでコードが表示されるところまで実装して行きたいと思います。 まず、Userモデルを下記のように書き換えます。本来であれば、 validationなども定義しないとですが、今回は割愛します。

class User < ApplicationRecord
  acts_as_google_authenticated drift: 10, issuer: 'TEST MY APP'
  after_create { |record| record.set_google_secret }
end

driftはアプリで表示されたコードを何秒間遅延して許容するかどうかの設定とのことで、デフォルトは5秒とのこと。issuerはアプリで表示される発行元の名称です。

次に、UserMfaSessionモデルを作成します。 Readmeにある通り、GoogleAuthenticatorRails::Session::Base継承し、コードの記載は不要です。

# app/models/user_mfa_session.rb

class UserMfaSession < GoogleAuthenticatorRails::Session::Base
  # no real code needed here
end

次に、コントローラーを作成します。 User#google_authentic? でフォームに設定されたコードが正しいかを確認できるようになります。

# app/controllers/user_mfa_session_controller.rb

class UserMfaSessionsController < ApplicationController
  skip_before_action :check_mfa

  def new
    @user = current_user
  end

  def create
    @user = current_user # grab your currently logged in @user
    if @user.google_authentic?(params[:mfa_code])
      UserMfaSession.create(@user)
      redirect_to root_path
    else
      flash[:error] = "Wrong code"
      render :new
    end
  end
end

そして、#check_mfaメソッドを記載し、認証が済んでいない場合は、認証ページにリダイレクトさせます。

# app/controllers/application_controller.rb

class ApplicationController < ActionController::Base
  before_action :check_mfa

  private
  def check_mfa
     if !(user_mfa_session = UserMfaSession.find) && (user_mfa_session ? user_mfa_session.record == current_user : !user_mfa_session)
      redirect_to new_user_mfa_session_path
    end
  end
end

認証ページは以下のように設定します。

# app/views/user_mfa_sessions/new.html.slim

img src="#{@user.google_qr_uri}"
br
= form_tag user_mfa_session_path, method: :post do
  .actions
    = text_field_tag :mfa_code
    = submit_tag 'authenticate'

Welcomeコントローラーを作成し、ルーティングを設定します。

# app/controllers/welcome_controller.rb

class WelcomeController < ApplicationController
  def index
  end

  def logout
    UserMfaSession.destroy
    redirect_to :root
  end
end
# config/routes.rb

Rails.application.routes.draw do
  root 'welcome#index'
  get 'logout' => 'welcome#logout'
  resource :user_mfa_session, only: %i(new create)
  resources :users
end

これで、localhost:3000にアクセスするとQRコードが表示されるので、アプリで読み込むと、下記のようにアプリでコードが表示されるようになります。

f:id:iam-nakamura:20190414115031j:plain

最後に

アクトインディでは、エンジニアを募集しています。 actindi.net

働きやすい環境で世の中に価値を残すことができるサービスを一緒に作りましょう!

Serverless Framework Jets Rubyを触ってみました

こんにちは。Webエンジニアのnakamuraです。

Serveless Frameworkを勉強するにあたって、Ruby製のサーバーレスフレームワークJetsを使ってみました。 サーバーレスフレームワークと言っても、どこから始めていったらいいのかわからなかったので、RailsライクなJetsは非常に助かりました。 今回は、Dockerでの環境構築でJetsを動かすまでを紹介します。

Docker環境でJetsをインストール

まず、ローカル環境に作業用ディレクトリを準備します。

$ mkdir src
$ cd src

次に、Dockerでruby2.6のコンテナを作成。

$ docker run -it --rm -v ${PWD}:/usr/src/app ruby:2.6 bash

コンテナが作成されたらyarnをインストールします。

root@4aac8522c43b:/# apt-get update -qq && apt-get install -y build-essential
root@4aac8522c43b:/# curl -sS https://dl.yarnpkg.com/debian/pubkey.gpg | apt-key add -
root@4aac8522c43b:/# echo "deb https://dl.yarnpkg.com/debian/ stable main" | tee /etc/apt/sources.list.d/yarn.list
root@4aac8522c43b:/# apt-get update && apt-get install yarn

コンテナ側の作業ディレクトリに移動して、jetsをインストールします。

root@3883f5341486:# cd /usr/src/app
root@3883f5341486:/usr/src/app# gem install jets

ここで一旦、exitしてコンテナから抜けます。

root@3883f5341486:/usr/src/app# exit

docker-composeを準備

まず、空のdockerfileを準備し、編集します。

FROM ruby:2.6

RUN apt-get update -qq && apt-get install -y build-essential libpq-dev
RUN curl -sL https://deb.nodesource.com/setup_10.x | bash - && \
    apt-get install nodejs
RUN curl -sS https://dl.yarnpkg.com/debian/pubkey.gpg | apt-key add - && \
    echo "deb https://dl.yarnpkg.com/debian/ stable main" | tee /etc/apt/sources.list.d/yarn.list && \
    apt-get update && apt-get install -y yarn

RUN mkdir /usr/src/app
WORKDIR /usr/src/app
COPY Gemfile* /usr/src/app/
RUN bundle install
COPY . /usr/src/app/

次にdocker-compose.ymlを以下のように編集。

version: '3'
services:
  db:
    image: mysql:5.7.17
    ports:
      - "3306:3306"
    volumes:
      - db-data:/var/lib/mysql
    env_file:
      - .env.development

  web:
    build: .
    command: /bin/sh -c "bundle exec jets server --host '0.0.0.0'"
    ports:
      - "8888:8888"
    volumes:
      - .:/usr/src/app
    environment:
      RAILS_ENV: development
    env_file:
      - .env.development
    depends_on:
      - db

volumes:
  db-data:

database.ymlのpassword部分を変更。

password: <%= ENV['MYSQL_ROOT_PASSWORD'] %>

env.developmentにMYSQL_ROOT_PASSWORDとDB_HOSTを追記。

MYSQL_ROOT_PASSWORD=長いパスワード
DB_HOST=db

docker-compose upを実行し、JetsのWelcomeページが表示されれば成功です。

アプリケーション作成開始

scaffoldで雛形を作成してみます。

$ docker-compose exec web jets generate scaffold Post title:string

すると、railsと似たようなファイルが作成されます。

      invoke  active_record
      create    db/migrate/20190201085745_create_posts.rb
      create    app/models/post.rb
      invoke  resource_route
       route    resources :posts
      invoke  scaffold_controller
      create    app/controllers/posts_controller.rb
      invoke    erb
      create      app/views/posts
      create      app/views/posts/index.html.erb
      create      app/views/posts/edit.html.erb
      create      app/views/posts/show.html.erb
      create      app/views/posts/new.html.erb
      create      app/views/posts/_form.html.erb
      invoke    helper
      create      app/helpers/posts_helper.rb

migrateを実行し、http://localhost:8888/postsにアクセスするとお馴染みのCMSが表示されるはずです。

$ docker-compose exec web jets db:create db:migrate

参考URL

参考にさせていただいたサイト

Jets Ruby Serverless Framework

Ruby 製サーバーレスフレームワークの Jets を検証してみたら、Rails ライクに使えていい感じだった - Qiita

最後に

JetsはRailsを触ったことがある人ならかなり学習コスト少なめで始められるのではと感じました。 引き続き、Jetsを触りながら、AWSデプロイ周辺を学習していこうと思います。

アクトインディでは、エンジニアを募集しているので、興味のある方はご連絡よろしくお願いします!

actindi.net

いこレポのRailsを5.1.2から5.2.2にアップグレードしました

morishitaです。

Rails 6のリリースも見えてきたので、やっといこレポのRailsをアップグレードしました。

具体的には、Rails 5.1.2 → 5.2.2 のアップグレードを実施しました。それについて紹介します。

アップグレードまでの道のり

アップグレードはセオリー通り次のステップで進めました。

  1. テストカバレッジを上げる
  2. Railsのバージョンを保ったまま、他のgemをアップデートする
  3. Railsをアップグレードする

テストカバレッジを上げる

アップグレードの事前準備ですね。 いこレポでは RSpecでテストを実装しており、SimpleCovでカバレッジを計測しています。
これで計測できるのは行カバレッジですが、次のような状況なので拠り所としてよかろうと判断しました。

  • if文がむちゃくちゃ多い処理はない
  • あるif文による分岐の結果が次の分岐に影響するような処理も少ない

目標カバレッジは90%としました。それに対して計測値は約85%。

どこが足りてないのかと調べたらモデルスペックもある主要なモデルで計測されていないものがあります。

設定ミスもなさそうでした。 test-queueが原因かと思い、やめてみても結果は変わらず。
とりあえず、他にすべきことを先に片付けようとFactoryBot の DEPRECATION WARNING1に対応しました。

> DEPRECATION WARNING: Static attributes will be removed in FactoryBot 5.0. Please use dynamic attributes instead by wrapping the attribute value in a block

すると、何ということでしょう!カバレッジも一気に上がりました。
確認すると、計測されていなかったモデルのカバレッジが計測されています。

どうやらSimpleCov.startより先にロードされたクラスは測定対象にならないようです。
Factoryから静的属性の値として参照されているクラスは SimpleCov.start より先にロードされてしまうために測定対象から外れていたようです。
問題のモデルも定数のいくつかが静的属性の値として参照されていました。

それが動的属性に変更することにより最初にFactoryが利用されるときに評価されるようになったため SimpleCov.start よりあとでロードされ計測対象として扱われるようになったようです。

これによりカバレッジ92%を超え、目標をクリアしました。

gemをアップデート

準備は整ったので、どんどんgemをアップデートしていきました。 もともと、gemのアップデートをサボりがちだったので古くなっているものが結構多かったです。

本番環境の動作に影響が少ないdevelopmentグループとtestグループのgemからアップデートしました。 各gemのアップデートは次の手順で進めました。

  1. 変更履歴を確認し、破壊的な変更が入っていないか確認
  2. アップデートして、Rspecをすべてパスさせる
  3. ステージングにデプロイして動作確認
  4. 問題なければ本番環境に適用。

ほとんどコードの変更無しでアップデートできましたが、uglifier を 1.3.0 -> 4.1.202 にアップデートしたら次のエラーでプリコンパイルできなくなりました。

Uglifier::Error: Unexpected token: keyword (const). To use ES6 syntax, harmony mode must be enabled with Uglifier.new(:harmony => true)

この対処として config/environments/production.rb で、次の様に変更しました3

#config.assets.js_compressor = :uglifier #変更前
config.assets.js_compressor = Uglifier.new(harmony: true) # 変更後

Webpackerのアップグレード

一番手間がかかったのはWebpackerでした。

Webpacker2.0.0 からのアップデートでしたが 3.0.0台に上げると設定ファイルがいろいろ変わってしまうので先送りしていました。

面倒とはいえ、ビルドの仕組みなのでJSとSASSがエラーなくトランスパイルできれば、その後の問題は少ないのではないかとも思っていました。 そして、アップデートするからには Webpack4Babel7 にしようとも決めました。

アップデート作業の時点で、Webpack4に対応予定の Webpacker4はまだ正式リリースされておらず4.0.0.pre.3が最新リリースでした。トランスパイルしたものの動作に問題がなければ、正式版でなくても構わないと割り切りました。 Webpacker3に一旦アップデートするステップも意味がないと思ったのでスキップしました。 ということで 2.0.0 → 4.0.0.pre.3 のアップデートです。

次の手順でアップデートを進めました。

  1. Webpackerに関わる設定ファイルを一旦全部捨る。
  2. Webpacker4bundle exec rails webpacker:install:vueで設定ファイル群を生成
    • 既存設定を変更していくより、Webpacker4の設定を変更したほうが効率的だろうと判断。
  3. 生成した設定に追加で必要な設定を足していく
    • 次に関する設定を追加 Pug, SASS, Workbox, splitChunks

PugとSASSはVueモジュールの中で使っています。以前は必要なNPMパッケージをインストールするだけで、設定しなくてもvue-loaderがよしなにやってくれた気がするのですが4、それぞれローダーの設定ファイルを追加して動くようにしました。

Workboxは組み込む際のインタフェースの変更に伴う修正はしましたが、設定内容そのものは既存の設定から変更なく組み込めました。

splitChunksの導入

Webpack4 では CommonsChunk が廃止され、splitChunks に変わりました。もともと、CommonsChunkを使っていたので、splitChunksに移行したいと思ったのですが、Webpacker4.0.0.pre.3では、まだ未サポートでした。

でも、ドキュメントには設定方法が記載されているしなーと思ったら、4.0.0.pre.3より進んでいる作業当時のmasterではすでに実装が含まれる様子。まあ、どうせ導入しようとしているのもpreリリース版だし、動けばいいのだ! とWebpacker4のバージョンをその時のmasterHEADに変更しsplitChunksも使えるようになりました。

bundle exec rails webpacker:install:vueがインストールするVue.jsも当時の最新版で、もともと使っていたバージョンより新しいことを途中まで失念していました。が、特に動作に問題なさそうだったので結果オーライでそのままとしました(Vue.js 2.3.4 -> 2.5.20, Vuex 2.3.1 -> 2.5.0のアップデートとりました)。

Railsのアップデート

さていよいよRails(5.1.2 → 5.2.2)のアップデートです。

永久保存版Railsアップデートガイド - pixiv insideを参考に、次の2つを拠り所に作業を進めました。

Rails 5.1.6.1までのアップデートはパッチバージョン以下のアップデートなので大きな影響はなかろうと思われたので次の2フェーズに分けました。

  • 第1フェーズ:Rails 5.1.2 -> 5.1.6.1
  • 第2フェーズ:Rails 5.1.6.1 -> 5.2.2

各フェーズとも、Railsのリリースバージョンを1つづつアップデートしては動作確認していきました。

第1フェーズ Rails 5.1.2 -> 5.1.6.1

Rails 5.1.5 にアップグレードしたところで、次のようなCapybaraのバージョン不整合が発生しました。

can't activate capybara (~> 2.13), already activated capybara-3.12.0. Make sure all dependencies are added to Gemfile. System test integration requires Rails >= 5.1 and has a hard dependency on a webserver and capybara, please add capybara to your Gemfile and configure a webserver (e.g. Capybara.server = :webrick) before attempting to use system tests.

調べると、RailsのコードでCapybaraのバージョンを~> 2.13に指定してしまってました5

RSpecがFailするどころかはじまりもしないので困りました。Capybaraのバージョン下げる以外にどうにも対処法がなさそうなので、せっかく3.12.0にアップでデートしたのに2.13に下げました。

これ以外は特に詰まるところなく Rails 5.1.6.1 までアップデートできました。 第一フェーズではRails 5.1.6.1での動作確認後、すぐに本番適用しました。

第2フェーズ Rails 5.1.6.1 -> 5.2.2

Railsのアップデート自体では特に詰まるところなくアップデートできました。

Rails 5.2で導入されたいくつかの機能がありますが、次の様に対処しました。

  • Active Storage: 見送り
    • ディスコンと言いながらセキュリティアップデートやAWS SDKの変更への対応など必要なメンテナンスは続いているためPaperClipの利用を継続
  • HTTP/2 Early Hints:見送り
    • 導入したかったのですが、AWS ALBがサポートしていないので。
  • Content Security Policy:ちょっと先送り
    • すでに導入済みのネットワーク広告への影響を検証してから導入予定
  • Credentials:導入済
    • あとあと変更するのは面倒だっったので encrypted secrets から移行

まあ、保守的ですが、上記のとおりです。

Credentials は encrypted secretsと違って Rails.env で値を切り替える仕組みがないのがちょっと不便ですね。

リリース

Rails 5.2.2へのアップグレードのリリースに際しては、予期せぬエラーが発生すればすぐに戻せるよう次のような手順で実施しました。

  1. 旧環境を複製して新環境を作る
  2. 新旧の環境へのトラフィックをRoute53Weighted Routingを利用して振り分けられる様に設定する
  3. トラフィックを徐々に旧環境から新環境に移動させる。

いこレポは Elastic Beanstalkを利用しています。なので、旧環境を複製してもう1つ同じ環境を作るのは、数クリック、5,6分の作業です。

Route53Weighted Routingを設定したら、ClouwdWatchでエラーの発生を監視しつつ新環境の重みを増やしつつ、旧環境の重みを増やしていきました。もしもエラーが想定以上に発生すればすぐに旧環境に戻すことができます6

今回の場合、エラーは発生しなかったのでスムーズに新環境に移行できました。

最後に

アクトインディでは既存サービスもしっかりメンテしながら日々開発を進めています。 一緒に開発してくれるエンジニアを募集しています。


  1. 次の警告が出ていました。

  2. だいぶサボっておりました。

  3. https://github.com/lautis/uglifier/issues/127

  4. うろ覚えです。

  5. https://github.com/rails/rails/blob/0ae59ea828ed20141af0d4c9ed9130eb47ce55f3/actionpack/lib/action_dispatch/system_test_case.rb#L1

  6. 実際にはWeighted RoutingのTTL=60secのタイムラグは発生します。

DockerでRails newしてみました

こんにちは。Webエンジニアのnakamuraです。

Dockerには日々お世話になっているのですが、そもそもDockerについても何もわかっていなかったので、自分でDockerfileを書いてみました。 また、弊社では、現在、育成枠でのエンジニア採用も検討中ということもあり、Dockerに関する入門編、投稿します。

Dockerインストール

公式サイトからダウンロードしてインストールしてください。 (開発環境はOSXを想定しています。)

https://docs.docker.com/docker-for-mac/install/

新規アプリ用のディレクトリを作成

まず、ローカル環境に作業用のディレクトリを用意します。

$ mkdir src
$ cd src

新規Railsアプリの作成(DockerでRails new)

次に、rubyのimageを使ってコンテナを起動します。

$ docker run -it --rm -v ${PWD}:/usr/src/app ruby:2.5 bash

-itはインタラクティブモードで起動してね。という意味で、コンソールからの入力が可能になります。このオプションを指定しないとコンテナはすぐに終了してしまいます。

-v ${PWD}:/usr/src/appでローカルの今いるディレクトリにコンテナ上の/usr/src/appをマウントしてという意味です。

成功すると↓のような感じでコンソールが表示されるので、/usr/src/appに移動してください。

$ root@f332b4bd3dbf:/# cd /usr/src/app

次に、Rails gemをインストールします。

$ root@f332b4bd3dbf:/usr/src/app# gem install rails

次に、Rails newコマンドでアプリを作成します。

$ root@f332b4bd3dbf:/usr/src/app# rails new myapp --skip-test --skip-bundle --database=mysql

exitでコンソールからぬけます。

$ root@f332b4bd3dbf:/usr/src/app# exit

すると、ローカル環境に戻るので、lsコマンドでmyappが作成されているか確認してください。

$ ls
myapp
$ cd myapp
aipc-157:myapp nakamura.masayuki$ ls
Gemfile     Rakefile    bin     config.ru   lib     package.json    storage     vendor
README.md   app     config      db      log     public      tmp
$

これで、ローカル環境にはRailsをインストールすることなく、Rails新規アプリを作成することができました。Docker便利。

Dockerfile作成

ローカル環境に、空のDockerfileを準備します。

$ touch Dockerfile

次に、お使いのエディターで、以下のように追記します。

FROM ruby:2.5.3

RUN apt-get update -qq && apt-get install -y build-essential mysql-client nodejs

RUN mkdir /usr/src/app
WORKDIR /usr/src/app
COPY Gemfile* /usr/src/app/
RUN bundle install
COPY . /usr/src/app/

DB周辺設定

ここでデータベースの作成に必要なファイルを準備します。

$ mkdir -p .env/development
$ touch .env/development/database

databaseを以下のように編集し、Gitを使用している場合は、.gitignoreファイルに.envを追記してください。

MYSQL_ROOT_PASSWORD=適当に長いパスワード

myapp/config/database.ymlを開いて、以下のように編集します。 ポイントはhostの箇所で、このあとdocker-compose.ymlのservicesで使用します。

default: &default
  adapter: mysql2
  encoding: utf8
  pool: 5
  username: root
  password: <%= ENV['MYSQL_ROOT_PASSWORD'] %>
  host: db

Docker composeファイル作成

docker-compose.ymlを作成し、以下のように編集します。

versionなどは適宜、修正してください。

version: '3'
services:
  db:
    image: mysql:5.7.17
    ports:
      - "3306:3306"
    volumes:
      - db-data:/var/lib/mysql
    env_file:
      - .env/development/database

  web:
    build: .
    command: /bin/sh -c "rm -f tmp/pids/server.pid && bundle exec rails s -p 3000 -b '0.0.0.0'"
    ports:
      - "3000:3000"
    volumes:
      - .:/usr/src/app
    environment:
      RAILS_ENV: development
    env_file:
      - .env/development/database
    depends_on:
      - db

volumes:
  db-data:

volumesにある- db-data:/var/lib/mysqlはデータ永続化のための設定です。

-b '0.0.0.0'は、rails serverがローカルマシーン上の全てのIPv4に接続するための設定です。

ローカルからのhttp://localhost:3000リクエストは、Docker Engineに転送され、コンテナ上のrails serverにリクエストします。しかし、コンテナ上のrails serverはlocalhostのリクエストしかlisteningしていないので、異なるIPアドレスのリクエストに応答できません。よって、-b '0.0.0.0'でコンテナ上のrails serverを全てのIPアドレスにバインドします。

コンテナ起動

以下のコマンドでコンテナを起動します。

$ docker-compose up

DB作成

docker-compose up後は、別のコンソールを開いて、以下のコマンドでデータベースを新規作成します。

$ docker-compose exec web bin/rails db:create

もし、コンテナが起動してない状態で、データベースの新規作成を行う場合は、以下のコマンドを実行してください。

$ docker-compose run --rm web bin/rails db:create

ブラウザで確認

ブラウザでhttp://localhost:3000にアクセスし、Railsの起動画面が表示されていればOKです。

開発スタート

あとは、コンテナが起動している時は、以下のようにexec webに続いてrailsのコマンドを実行していけば、scaffoldやmigrationの実行が可能です。

$ docker-compose exec web bin/rails g scaffold User first_name:string last_name:string

マイグレーションの場合は、以下のような感じです。

$ docker-compose exec web bin/rails db:migrate

コンテナを終了させたい場合は、以下のコマンドになります。

$ docker-compose down

Gemを追加したときは、イメージを再構築する必要があるので、以下のコマンドを実行してください。

$ docker-compose build web

最後に

アクトインディでは、エンジニアを募集しています!

サッカー好きの方、お待ちしております!!