Linkers Tech Blog

リンカーズ株式会社の技術ブログです。

S3互換ストレージMinIOの開発環境(Rails)への導入

はじめに

この記事は、Ruby on Railsの既存プロジェクトにMinIOを実際に環境に導入し、その際つまずいたところを記録した記事です。

MinIO導入の経緯から、ローカルの開発環境にコンテナとして導入して、最後はCIが緑になって開発ブランチにマージされるまでを書いています。

対象読者

  • Railsプロジェクトの環境にMinIOの導入を検討している人(どんな障壁があったか知りたい人)
  • RailsプロジェクトでMinIOを導入しようとして詰まっている人
  • MinIOをDockerのコンテナ上で動かしたい人
  • MinIOを知らない人

MinIOとは何か

MinIO Object Storage(以下MinIO)はMinIO, Inc.によって開発されているOSSです。Apache 2.0ライセンスで公開されています。

元々プライベートクラウド上で使えるオブジェクトストレージとして設計されていましたが、AWS S3のAPIと互換性があるため、例えば、本番環境はS3を使って、ローカルやCIの環境はMinIOのコンテナで……というように、ローカルやCIでS3の環境を用意したい人にとっても便利に使うことができます。

GitHubでコードを公開しているだけではなく、公式でDockerイメージまでDockerHubに用意してくれており、大変便利に使うことができます。MinIO, Inc.に足を向けて寝れません。

下の画像は、ローカルで起動しているMinIOにアクセスした画面です。シンプルなデザインがお洒落で素敵ですね。

f:id:jesus_isao:20200227102752p:plain:w500
ログイン画面

f:id:jesus_isao:20200227102817p:plain:w500
管理画面

導入のきっかけと目的

これまでの開発環境/本番環境(概略)

私達のプロジェクトのMinIO導入前の本番環境/開発環境について、今回関わる部分のみ簡単にご説明します。

本番環境

f:id:jesus_isao:20200227102500p:plain:w500
本番環境

本番環境は上記のような一般的な構成です。EC2上にRailsのアプリケーションが起動しており、ユーザーが画像やPDFをアップロードすると、Railsがそれを受け取って、Shrineというgemに後は任せます。Shrineは、ファイルをS3やAPサーバー内に保存してくれるgemです。アップロードされたファイルのダウンロードは、後からユーザーがRailsでレンダリングされたWebページを見て、そこにあるS3へのリンクをクリックしダウンロードする、という構成です。

開発環境

そしてMinIO導入以前のローカルの開発環境は、下記のような構成でした。

f:id:jesus_isao:20200227102541p:plain:w500
開発環境

この画像には3つのローカル端末が描かれていますが、全て同一のMacBook Proです。弊社の開発者にはMacBook Proが支給されます。

この画像を見て分かる通り、本番環境と明らかに違うところがあります。本番環境ではS3が画像やPDF等のファイルを返していましたが、この開発環境ではRailsがファイルを返しています。開発環境と本番環境でそれぞれ、S3を見に行くか、それとも自分自身のサーバー内を見に行くかはShrineが抽象化しているため、ソースコード自体はシンプルです。しかし本番環境と開発環境の仕組みの違いが、後々問題を引き起こします。

本番のみ出てしまう挙動

例えば、実際にあった例としてこんな問題がありました。 「自分でアップロードしたCSVファイルを後からダウンロードしようとすると外部サイト移動の警告が出る」という不具合です。

画像は実際にそれが出ていた時のスクリーンショットです。

f:id:jesus_isao:20200227102929p:plain:w500
不具合

開発環境のURLは./bucket/hoge.csvのような相対パスで、本番環境のURLはhttps://aws.s3.domain/bucket/hoge.csvのような異なるURLが生成されていたため、開発環境中では判定の誤りに気づけず、リリースがされてしまった……。という悲しい不具合でした。

環境の差異によって見過ごされていた不具合は他にも出てくる可能性は十分に考えられるため、少しでもローカルで本番環境に近い構成にしたい、というのがMinIOを導入した動機です。

以上、前置きでした。

MinIOが動くまで

[開発環境] docker-compose.ymlに追加

色々と省略していますが、MinIOに関連するところだけ抽出するとこのようになりました。

# docker-compose.yml
version: "3.3"

services:
  minio:
    image: minio/minio:latest
    networks:
      - local_dev
    ports:
      - "9000:9000"
    volumes:
      - ./docker/volume/minio:/data
    command: server /data
    environment:
      MINIO_ACCESS_KEY: minio_access
      MINIO_SECRET_KEY: minio_secret

networks:
  local_dev:

そして、MinIOに初期処理を行う部分は別のymlに切り出しました。

ここに出てくるmcはminio-clientの略で、起動しているminioのコンテナに対してアクセスし、hogeという名前のバケットを削除したり新規作成したりしています。ここに出てくるminio/mcというイメージも、公式から提供されています。

# 別のファイル docker-compose.createbuckets.yml
version: "3.3"

services:
  createbuckets:
    image: minio/mc
    networks:
      - local_dev
    depends_on:
      - minio
    entrypoint: >
      /bin/sh -c "
      until (/usr/bin/mc config host add myminio http://minio:9000 minio_access minio_secret) do echo '...waiting...' && sleep 1; done;
      /usr/bin/mc rm -r --force myminio/hoge;
      /usr/bin/mc mb myminio/hoge;
      /usr/bin/mc policy download myminio/hoge;
      echo '-----------------------------------------';
      echo 'バケットの作成を完了しました。';
      echo 'Ctrl + Cでdocker-composeを終了してください……';
      echo '-----------------------------------------';
      exit 0;
      "

networks:
  local_dev:

普段の開発をする際のdocker-compose upをする前に、初回のみ下記のコマンドを実行します。

docker-compose -f docker-compose.yml -f docker-compose.createbuckets.yml up

複数のファイルを-fで指定してdocker-compose upすると、ファイルの中身がマージされて実行されます。

このコマンドを初回だけ実行することで、初期処理が行われてバケットが作成され、MinIOに書き込み・読み込みができるようになります。以降は普通のdocker-compose upで開発環境が起動します。

ハマった:foreman、突然の死

これはdocker-compose.createbuckets.ymlにMinIOの初期処理を分離する前、docker-compose.ymlにバケットの作成の処理を詰め込んでいた時の話です。

私達のチームではforemanというプロセス管理ツールを使っていました。これはHerokuのProcfileをもとにプロセスを実行してくれるツールで、私達はこれで、docker-compose upしたりrails sしたりしていたのですが、バケットの作成の処理が入っていた場合にbundle exec foreman startしても停止してしまうという問題が発生しました。

理由はforemanが、「全てのプロセスが起動し続ける」前提で動いているツールであったことでした。 docker-compose.createbuckets.ymlに書いてある内容を読めば分かるのですが、ここでexit 0;しています。これによってこのminio-clientのコンテナは停止しますが、それをforemanが検知するとforemanは全てのプロセスを終了しようとします。よって、docker-compose.createbuckets.ymlに書いてあるような内容は、少なくともforemanの外で実行する必要があります。

[CI環境] gitlab.ymlに追加

私達のチームはGitLab CIを使用しています。E2Eテストでもまた、MinIOを使いたいので、Rspecが動くJobなどでMinIOも動かせるよう設定をする必要があります。

# .gitlan-ci.yml

# 予め作っておいたimage。この中にminio-clientも入っている。
# これを各Jobで使う
image: gitlab.example.com/my/image:latest

rspec:
  services:
    - name: minio/minio
      command: ["server", "/data"]
      alias: "minio.example.com"
  script:
    - sh -c "until (/usr/bin/mc config host add myminio http://minio.example.com:9000 minio_access minio_secret) do echo '...waiting...' && sleep 1; done; /usr/bin/mc rm -r --force myminio/hoge; /usr/bin/mc mb myminio/hoge; /usr/bin/mc policy download myminio/hoge; exit 0;"
    - bundle exec rspec

It works!

動きました! 後は、赤くなっているテストを修正します。

テスト(Rspec)が通るまで

素直な修正

これまで"rspec用"として書いていたコードが、より本番環境に近いコードに修正されることになります。

例えば、これまではhref='/bucket/6f4648d.png'を期待していたところを、href='http://localhost:9000/bucket/6f4648d.png'に直すような修正です。より本番環境に近い状態でテストができるようになったため、こうしたテストを修正するのは当然のことでしょう。

Aws::S3::Errors::RequestTimeTooSkewed

Timecopというgemがあります。これによって、Time.nowなどをした時の時間を操作することができます。 しかしRspec内の時間が固定されていたことによって、Minioとの時間差が大きくなりすぎてアップロードできなくなるケースが出てきました。

0 byteのファイルアップロードできないと発覚

MinIOに変えたことで0 byteのファイルをアップロードできない不具合が見つかりました。自動テストで0 byteのダミーファイルをアップロードしていて、そこがErrorになったことで判明しました。S3を使っている本番環境でも同様の問題が発生していると分かったため、MinIOを入れたことで隠れていた問題が浮き上がりました。

調べると、これはShrineの不具合でした。issue #440で報告がされており、2020/01/16にリリースされたShrine v3.2.1だと問題なくアップロードができると分かりました。

ActionController::RoutingError

これまで、開発環境や自動テスト環境ではファイルをダウンロードする時に、相対パスにあるファイルをRspecが読みに行っていました。

例えば、下記は「test.ppt」というパワポのファイルへのリンクが画面に表示されていて、それをクリックしたらパワポのファイルをダウンロードができることをテストしているRspecです。

click_on 'test.ppt'
expect(page.response_headers['Content-Type']).to eq('application/vnd.ms-powerpoint')

しかし、このプロジェクトではファイルをMinIOが返すように変更しました。つまり、Railsがファイルを返すわけではなくなったため、pageで中身を取得することはできなくなりました。

この問題に対してはKernel#openでファイルをダウンロードし、その中身をテストするという方針に変更しました。

System Specのhave_linkでリンクが突然探し出せなくなった

下記のようなテーブルの構造で、hrefの中身のURLが適切に生成されているかをテストしていました。

<table>
  <tbody>
    <tr>
      <td>
        <div>
          <span>
            <a href="./bucket/hogehoge.csv">hogehoge.csv</a>
            <p>something</p>
          </span>
        </div>
      </td>
    </tr>
  </tbody>
</table>

これまでは、withinで<tr>のところまで降りてきて、下記のコードでテストできていました。

expect(td[0]).to have_link 'hogehoge.csv', href: something_expected_url

しかし、MinIOはS3と互換性があり、URLにもS3と同様の長いクエリがついて、最終的には長大なURLになります。たとえば、X-Amz-Credentialや、X-Amz-Signatureなどのパラメータが付与されます。それが原因なのか、have_linkで深いところにあるURLをいきなり探りあてることができなくなりました。 そのため、URLのあるタグまでfindして判定することにしました。

expect(td[0].find('div > span > a')[:href]).to eq something_expected_url

修正完了

テストを修正し、CIの全てのテストが緑になりました。

導入完了

GitLabのマージリクエストで他の開発者にレビュー依頼をし、LGTMをもらい、開発ブランチに変更をマージしました。これにて導入が完了しました。

本流とは関係のないところですが、MinIOとはあまり関係のないところで課題となったところもいくつかありました。

  • ローカル環境でランダムで発生するrspecのエラーが表出した(元々のテストにバグが仕込まれていた)
  • 開発ブランチへのマージ後、ある開発者のみMinIOでアップロードができない状態に陥った(何かの拍子でローカルのgem、aws-sdk-s3の中身が書き換わっていたことが判明した)

MinIOを導入することで、これまで潜んでいた色々な問題が表出する可能性もあるため、既存プロジェクトに導入する際にはそれなりの時間を見積もっていた方が安全かもしれません。

とはいえ、これでやっと導入は完了しました。お疲れ様でした!🎉 最終的に、開発環境は画像のようになりました。

f:id:jesus_isao:20200227103014p:plain:w500
最終的な開発環境

良い開発ライフを!

Meetup for Rails engineers -メドピア×リンカーズ×Classi開発事例- 開催レポート

f:id:ryota_suzuki_linkers:20191120163040j:plain

リンカーズは去る10月30日に行われた "Meetup for Rails engineers -メドピア×リンカーズ×Classi開発事例-" を、メドピア社・Classi社と共に開催しました。弊社からは3名が登壇しましたので、その模様を紹介します。

(書き手: リンカーズ 鈴木竜太)

  • 礒飛拓也「リンカーズの紹介」
  • 大河原修「実践Railsアプリケーション設計」
  • 鈴木竜太「安定したFeature Specを助ける3つの処方箋」
  • 発表を終えて
    • 次回予告
続きを読む

JavaScriptでのイベントキャンセルの落とし穴

Railsで開発していると、デフォルトのSubmitイベントを上書きしたい時に遭遇することがあると思います。 そういった場合は、Clickイベントなどを利用して、デフォルトのSubmitイベントはキャンセルし、上書きした処理を実行することになります。 しかし、イベントリスナーの設定方法やキャンセル方法によっては、予期せぬ動作を引き起こすことがあります。 JavaScriptのイベント伝播の仕組みを理解していれば、予期せぬ動作を未然に防ぐことが出来ます。

  • イベントの伝播の仕組み
    • デフォルトアクションのキャンセルとバブリングの停止
  • 例:二重送信の防止
  • まとめ
続きを読む

Slack始めました MattermostからSlackへの移行手順

リンカーズでは最近コミュニケーションツールを Mattermost から Slack に切り替えました。当ブログでは2回に分けてSlack移行についてお伝えしたいと思います。まず今回はMattermostからデータを抽出し、Slackにインポートする手順です。

  • 概要
  • 移行できなかったもの
  • 作業
    • export
    • 手元に移動
    • convert
    • import
  • 自動化できなかった作業
    • カスタム絵文字
    • チャンネル
      • mattermostにはなかったチャンネルを追加
      • プライベート化、アーカイブ
  • おわりに
続きを読む

リモートオフィスでの仕事を紹介! 2019年3月Matz会ダイジェスト

こんにちは、リンカーズ情報システム部の鈴木です。今回は弊社に残るMatz会アーカイブから2019年3月開催分をお送りします。

  • 似た企業名を探す
    • アルゴリズムの紹介
    • パフォーマンス計測
    • Ruby本体での採用例
  • リモート勤務3年目
    • 東海オフィスの環境
    • メリット・デメリット・注意していること
    • まつもとさんから
  • まとめ
  • We are hiring!!
続きを読む

イベントトラッキング用gem ahoyで、deviseの複数のユーザモデルを取り扱う

ahoy導入の経緯と課題

ahoyはRailsアプリケーションにアナリティクスツールを組み込むためのgemで、Railsアプリケーション上でユーザ行動ログを取得し、保存することができます。データの保存先としてActiveRecordを選択することができるので、最小構成のインフラでも簡単に導入することができます。また、Railsアプリケーションの一部として動くため、Google Analyticsへの接続を制限している環境のユーザの行動も補足することができるというメリットもあります。

開発中のRailsアプリケーションは認証用のライブラリとしてdeviseを利用しており、お客様と社内のスタッフをテーブル単位で分けています。ahoyは標準では複数のモデルに対応していないため、導入するにあたってこの問題を解決する必要がありました。

GitHub上に、deviseの複数のユーザモデルに対応させたいというIssue( Multiple Devise models · Issue #38 · ankane/ahoy · GitHub )が上がっており、 polymorphic関連で解決する、というgemの作者からの回答があるのですが、その具体的な手続きまでは書かれていません。 今回はその手順をまとめました。

ahoyの導入方法

公式のREADMEか、わかりやすいQiita記事を読んでください。

github.com qiita.com

複数のdeviseのモデルのユーザをトラッキングする設定の手順

ここでは、deviseのユーザモデルが、staff, cutomerの二種類あるとして話を進めます。

まずREADMEのcustom user methodにあるように、コントローラ内でアクセス中のユーザのデータにアクセスするための設定を変更します。

# config/initializers/ahoy.rb
Ahoy.user_method = ->(controller) { current_staff || current_customer }

Ahoy::Visit, Ahoy::Eventのモデルのbelong_toの部分をポリモーフィック関連に置き換えます。

# event.rb
class Ahoy::Event < ApplicationRecord
  include Ahoy::QueryMethods
  self.table_name = 'ahoy_events'
  belongs_to :visit
  belongs_to :user, optional: true, polymorphic: true
end
# visit.rb
class Ahoy::Visit < ApplicationRecord
  self.table_name = 'ahoy_visits'
  has_many :events, class_name: 'Ahoy::Event'
  belongs_to :user, optional: true, polymorphic: true
end

データベースに保存するので、カラムの追加が必要ですね。追加しましょう。

class AddUserTypeToAhoyVisitsAndEvents < ActiveRecord::Migration[5.2]
  def change
    add_column :ahoy_visits, :user_type, :string, after: :visitor_token
    add_column :ahoy_events, :user_type, :string, after: :visit_id
  end
end

また、eventに保存されるデータにカラムを追加する手続きが必要になります。

# config/initializers/ahoy.rb
class Ahoy::Store < Ahoy::DatabaseStore
  def track_event(data)
    data[:user_type] = controller.current_staff ? 'Staff' : 'Customer'
    super(data)
  end
end

以上で完成です。 あとは、好きなページにトラッキングコードを入れることで、複数のユーザモデルに対応した状態でユーザの行動を分析できるようになります。

自己紹介

浜田です。開発に関わるいろんなお仕事とサーバのお守りを担当しています。

「ポストMatz」はいつ、誰が? 2019年2月Matz会ダイジェスト

こんにちは、情報システム部の鈴木です。

弊社では技術顧問: まつもとゆきひろ氏を招いて定期的に勉強会(通称: Matz会)を行なっています。お待たせしました、今回は今年2月開催分のレポートです。

今回のテーマはこちらです。詳しいレポートは「続きを読む」より。

  • Rubyで音楽を奏でる
    • 目指す挙動と実装方針
    • 実装と課題
    • まつもとさんによる補足
  • "xruby syndrome"
    • エンジニアへの提言
    • Pythonコミュニティで起きたこと
    • Ruby長期運用計画
  • まとめ
  • We are hiring!!
続きを読む