Rails 5.2 の足音 ~ Active Storage を試してみる ~

こんにちは、 id:eitoball です。

この記事は、Misoca Advent Calendar 2017 の15日目の記事です。

Rails 5.2 は、beta1 と beta2 が出てきて、正式なリリースも近々のようですね。皆さん、更新の準備はできていますでしょうか?今回は、Rails 5.2 の 目玉機能の一つである Active Storage を試してみようと思います。

Active Storage とは?

Active Storage makes it simple to upload and reference files in cloud services like Amazon S3, Google Cloud Storage, or Microsoft Azure Storage, and attach those files to Active Records. Supports having one main service and mirrors in other services for redundancy. It also provides a disk service for testing or local deployments, but the focus is on cloud storage.

Active Storage は、AWS S3 や Google Cloud Storage などクラウドストレージサービスへのファイルをアップロードをするための機能です。 carrierwavepaperclip といった gem と同じような機能を提供します。 Rails 標準なので、モデルとの連携がシームレスに行うことができるのが特徴だと思います。

Active Storage を試してみる

今回は、Active Storage プロジェクトの README.md に記載されているように User モデルに avatar という属性でファイルを添付するようにしていきます。添付したファイルは、 AWS S3 に保存するように設定したいと思います。

Rails アプリケーションの作成

はじめに rails gem (5.2.0.beta2) を追加します。今回は、せっかくなので、2017年12月25日にリリース予定の ruby 2.5.0dev を使用します。

$ ruby -v
ruby 2.5.0dev (2017-12-14 trunk 61215) [x86_64-darwin16]
$ gem install rails -v 5.2.0.beta2
…
Fetching: rails-5.2.0.beta2.gem (100%)
Successfully installed rails-5.2.0.beta2
28 gems installed

サンプルのアプリケーションを作成します。Rails 5.1 からの更新を想定したいので、--skip-active-storage で、後から、Active Storage を追加するようにします。*1

$ rails new --skip-active-storage -S --webpack=vue sample-application
…
* bin/rake: spring inserted
* bin/rails: spring inserted

作成したアプリケーション内に移動して、Active Storage をインストールします。config/application.rb 内で、 require "active_storage/engine" の行がコメントアウトされているので、コメントを外して、bin/rails active_storage:install を実行します。

$ cd sample-application
$ nvim config/application.rb
...
$ bin/rails active_storage:install
Copied migration 20171214014543_create_active_storage_tables.active_storage.rb from active_storage

データベースを作成して、マイグレーションを実行します。

$ bin/rails db:create db:migrate
Created database 'db/development.sqlite3'
Created database 'db/test.sqlite3'
== 20171214014543 CreateActiveStorageTables: migrating ========================
-- create_table(:active_storage_blobs)
   -> 0.0013s
-- create_table(:active_storage_attachments)
   -> 0.0014s
== 20171214004543 CreateActiveStorageTables: migrated (0.0028s) ===============

active_storage_blobsactive_storage_attachments というテーブルが作成されました。これらのテーブルにファイルのメタ情報などが記録されるようですね。

開発用サーバーを実行して、アプリケーションが動作することを確認します。

$ bin/rails server -b 0.0.0.0

ブラウザで、http://localhost:3000/ にアクセスして、”Yay! You’re on Rails!” と表示されていれば、インストールが成功しています。確認できたらサーバーは停止しておきます。

ユーザーモデルの作成

User モデルと関連するコントローラやビューを scaffold を使って作成します。あと、マイグレーションを実行します。

$ bin/rails generate scaffold user name:string address:string
      invoke  active_record
…
      create    app/assets/stylesheets/scaffold.css
$ bin/rails db:migrate

app/models/user.rb を編集して avatar 属性(has_one_attached :avatar)を追加します。

class User < ApplicationRecord
  has_one_attached :avatar
end

新しく追加した avatar 属性を表示したり保存したりできるようにします。

app/views/users/show.html.erb には、13行目あたりに次の行を追加します。

<% if @user.avatar.attached? %>
  <p>
    <strong>Avatar:</strong>
    <%= image_tag url_for(@user.avatar) %>
  </p>
<% end %>

app/views/users/_form.html.erb には、24行目あたりに次の行を追加します。

<div class="field">
  <%= form.label :avatar %>
  <%= form.file_field :avatar %>
</div>

最後に app/controllers/users_controller.rb では、72行目あたりを以下のように :avatar を追加します。

def user_params
  # params.require(:user).permit(:name, :address)
  params.require(:user).permit(:name, :address, :avatar)
end

開発用サーバーを立ち上げて、http://localhost:3000/users/new にアクセスして avatar のフィールドが追加されていることを確認してください。

f:id:eitoball:20171214130815p:plain

AWS S3 サービスの設定

ファイルを AWS S3 へアップロードするための設定を行います。Rails 5.2 では、秘密にしたい設定情報を暗号化することができるようになりました。AWS の認証情報はその機能を使って保存します。

bin/rails credentials:showconfig/credentials.yml.enc 内に保存されている情報を見ることができます。

$ bin/rails credentials:show
# aws:
#  access_key_id: 123
#  secret_access_key: 345

# Used as the base secret for all MessageVerifiers in Rails, including the one protecting cookies.
secret_key_base: abc123

bin/rails credential:edit で編集します。aws: からの部分のコメントを外して、AWS のアクセスキーIDとシークレットアクセスキーを記載します。EDITOREmacsVim などをコンソールベースのエディタを指定して編集して下さい。

$ EDITOR=nvim bin/rails credentials:edit
…
$ bin/rails credentials:show
aws:
  access_key_id: 123
  secret_access_key: 345

# Used as the base secret for all MessageVerifiers in Rails, including the one protecting cookies.
secret_key_base: abc123

Active Storage のストレージの設定は config/storage.yml に記載します。ここに AWS S3 の情報を追加します。amazon: からの行をコメントアウトして regionbucket を適宜修正して下さい。

$ nvim config/storage.yml
…
$ cat config/storage.yml
…
amazon:
  service: S3
  access_key_id: <%= Rails.application.credentials.dig(:aws, :access_key_id) %>
  secret_access_key: <%= Rails.application.credentials.dig(:aws, :secret_access_key) %>
  region: us-east-1
  bucket: active_storage-test
…

そして、 config/environments/development.rb を編集して、開発環境で、AWS S3 ストレージを使うようにします。

$ nvim config/environments/development.rb
…
$ cat config/environments/development.rb
…
  # Store uploaded files on the local file system (see config/storage.yml for options)
  config.active_storage.service = :amazon
…

最後に aws-sdk-s3 gem を Gemfile に追加して、bundle install します。

$ nvim Gemfile
…
gem ‘aws-sdk-s3’
$ bundle install
…

開発用サーバーを起動して、http://localhost:3000/users/new にアクセスして実際にユーザーを作成して画像をアップロードします。

$ bin/rails server -b 0.0.0.0
=> Booting Puma
…
Use Ctrl-C to stop

f:id:eitoball:20171214130606p:plain

大きい画像をアップロードするとこのようになってしまいます。ですので、縮小できるようにします。

app/views/users/show.html.erbavatar 画像を表示している部分を変更します。

<% if @user.avatar.attached? %>
  <p>
    <strong>Avatar:</strong>
    <%= image_tag url_for(@user.avatar.variant(resize: '128x128') %>
  </p>
<% end %>

#variant には他にも色々な変換ができるようです。詳しくは、https://github.com/rails/rails/blob/master/activestorage/app/models/active_storage/variant.rb を参照して下さい。

Gemfile を編集して、 mini_magick を追加して、bundle install します。*2

$ nvim Gemfile
…
gem ‘mini_magick'
$ bundle install
…

サーバーを再起動して、 http://localhost:3000/users/11 の部分は、先ほど作成したユーザーのIDです)にアクセスすると画像が縮小されていることがわかります。

f:id:eitoball:20171214130609p:plain

さいごに

今回は、近々リリース予定の Rails 5.2 での新しく導入される Active Storage を使って、ファイルを AWS S3 へアップロードするようにしてみました。今回は試していませんが、Google Cloud Storage などの他のクラウドストレージサービスやローカルファイルシステムにもアップロードしたりもできます。また、複数とストレージに同時に保存(mirroring)もできるようです。また、画像以外にも動画やPDFを扱うこともできるようです。

正式にリリースされていないため文書などが少なく、 carrierwave や paperclip と比べると機能はまだ少なく導入・移行は大変そうです。しかしながら、Rails の他の機能とシームレスに連携できるのは、とても魅力的だと思います。

明日、16日目は、merotan ( @renyamizuno_ ) が、HTML と CSS を使って究極の何かを作ったことについて語ってくれるそうです。

*1:webpacker を使っているので yarn が必要です。

*2:別途、ImageMagick をインストールしておく必要があります。

🔥カンバンを完了させるということ

こんにちは、mzpです。 Misoca Advent Calendar 2017 の13日目の記事を書きます。

🔖Misocaのカンバン

Misocaでは、各プロジェクトごとに付箋が作られ、壁に貼っていた。台紙となっている模造紙のことを名古屋ではB紙と呼ぶので、「MisocaのB紙」と呼んでいた。

f:id:mzp:20171212170223p:plain

名古屋オフィス外で働いている人と情報共有が難しかったため今はTrelloに移行している。

f:id:mzp:20171212132922p:plain

🚀完了したプロジェクト

完了したプロジェクトは場所でまとめて管理していた。 Doneの杜と呼んでいた。最初は「いっぱい終わったなー」と満足して見ていたが、だんだんと片づけたい気持ちが増してきた。

f:id:mzp:20171212133328p:plain

出雲大社への奉納

ちょうどその時期にRubyWorld Conference 2017のために島根に行くことが決まっていたので、出雲大社に奉納できないかと思って調べてみた。

FAQを確認すると

古い御神札・御守をお返しされる場合、境内に納め所がありますので、そちらにお納めお返し下さい。焼納祭を奉仕いたし、お焚き上げをさせていただきます。

と書いてあった。 念のため御札以外の紙でもよいかと問合せてみたが、問題ないとのことだった。

さっそく出雲まで持っていくことにした。

🐾奉納までの様子

はがす様子。

f:id:mzp:20171212134022p:plain

飛行機での移動。

f:id:mzp:20171031013943j:plain

RubyWorld Conference 2017に参加。

f:id:mzp:20171031025021j:plain

出雲大社への移動。

f:id:mzp:20171103040706j:plain

納める場所があった。

f:id:mzp:20171103042234j:plain

投入。

f:id:mzp:20171103042330j:plain

💗所感

  • 壁がすっきりしてよかった。
  • ただの付箋だが、それなりに苦労したプロジェクトばかりなので、ゴミ箱に捨てるのは偲びなかった。奉納する、という儀式をすることで、自分の気持ちと折り合いをつけることができた。
  • スピリチュアルエンジニアリングのための徳も高めることができた。

🏢採用

Misocaでは松江で働きたいソフトウェアエンジニアを募集しています。

Misocaの開発環境2017冬

はじめに

こんにちは、Misoca開発チームの洋食(yoshoku)です。

この記事は、Misoca Advent Calendar 2017 の8日目の記事です。

さて、Misocaでは、PCやエディタなど、開発環境を自由に選択することができます。 今回は、開発チームメンバーの開発環境を、一問一答形式で紹介したいと思います。

めろたん(@renyamizuno_)

メインの開発マシンは?

普段どのエディタ/IDEでコード書いてる?

  • Visual Studio Code
    • 最高。
  • Atom
    • teletype を試し始めて、最強。
    • 神ってる。
  • vim
    • たまにちょろっと直したい時にエディターに戻るのが面倒な時にサッと使ったりする。

好きなキーバインドは?

  • vim
    • ただ最近はエディターのキーバインドとぶつかったりしてなんか上手く使えないことが多々あるので、抜いてる。

お勧めのツールは?

黒曜(@kokuyouwind)

メインの開発マシンは?

普段どのエディタ/IDEでコード書いてる?

  • IntelliJ IDEA
    • コードジャンプとかリファクタ機能が超便利
  • emacs
    • ちょっとした実験スクリプトを書くときとか、サーバ上作業とかではこっち

好きなキーバインドは?

お勧めのツールは?

  • Database tool window
    • IDE内でレコードを確認したりSQLを書いて実行したりできるので便利
    • 設定すればport forwardしたりHeroku Postgresに繋いだりもできる
    • adapterも充実してるので大体のデータベースに繋げる(MySQLとPostgresくらいしか使ってないけど)
  • Railways
    • RailsのroutesをIDE内で確認できるようになる
    • Quick Navigationも入るので、~_urlみたいなのを打つのがすごく楽になる

ころさん(@corocn)

メインの開発マシンは?

普段どのエディタ/IDEでコード書いてる?

好きなキーバインドは?

お勧めのツールは?

  • IntelliJのサブツール群
    • 使ってみると意外と便利で Terminal, DB, VCS(Git), Debugger等にお世話になっている
  • Karabiner-Elements
    • これがないと生きていけない

他に何かこだわりは?

  • IntelliJはあんまりカスタマイズせずに使ってます。IdeaVimも入れましたが、しっくりこなくてやめてしまった。(既存のショートカットとconflictする部分が辛かった)

@sunflat

メインの開発マシンは?

普段どのエディタ/IDEでコード書いてる?

  • RubyMine

好きなキーバインドは?

お勧めのツールは?

  • RubyMine + IdeaVim
  • Karabiner-Elements
  • Clipy

他に何かこだわりは?

  • 普通のテキストはMacVimで書いてます
  • JISキーボードでIdeaVimを使うと、Ctrl+[ をESCとして扱ってくれないので、Karabiner-Elementsでそのように設定してます

@eitoball

メインの開発マシンは?

普段どのエディタ/IDEでコード書いてる?

  • neovim

好きなキーバインドは?

お勧めのツールは?

  • Alfred、Dash、MacBreakZ、Bartender、f.lux、hammerspoon

他に何かこだわりは?

  • できるだけ、マウスを使わないように努力しています。

@enda531

メインの開発マシンは?

普段どのエディタ/IDEでコード書いてる?

好きなキーバインドは?

お勧めのツールは?

@hidakatsuya

メインの開発マシンは?

普段どのエディタ/IDEでコード書いてる?

好きなキーバインドは?

お勧めのツールは?

  • Atom Package だと go-to-definition, ruby-test, vim-mode-plus, cursor-history, linter-rubocop, teletype あたり
  • iTerm, tmux, Pixelmator, Karabiner-Elements

@y0shoku

メインの開発マシンは?

普段どのエディタ/IDEでコード書いてる?

好きなキーバインドは?

  • vim、キーボードは英字配列です

お勧めのツールは?

  • bash-it、tmux、Sequel Pro、Jupyter Notebook

他に何かこだわりは?

  • マウスではなくトラックボール使ってます。
  • 机の上をスッキリさせたいので、できるだけ無線のものを選んでます。

おわりに

たまたま、アンケートに回答したメンバーが全員Macユーザーでしたが😅Thinkpadを愛用しているメンバーもいます👍 二年前のアンケートと比べてみると、時代の変化を感じられるかも⁉️

tech.misoca.jp

Misoca Advent Calendar 2017、明日の担当は @toyoshi です。

採用

Misocaでは開発者を募集しています。

開発生産性向上の取り組みについて

id:eitoball です。

この記事は、Misoca Advent Calendar 2017 の記事として書いています。

昨日、NGK2017B 昼の部 で「開発生産性向上の取り組みについて」という内容のLTをしてきました。今回は、LTで話したことについて書いていきます。

開発生産性向上の取り組みとは?

いきなりですが、

If I had 8 hours to chop down a tree, I would spend 6 of those hours sharpening my axe. (もし8時間、木を切る時間を与えられたら、そのうち6時間を私は斧を研ぐのに使うだろう。)

という言葉をご存じでしょうか?第16代アメリカ合衆国大統領エイブラハム・リンカーンが残したと伝えられている名言の1つです。

斧で木を切る際、斧の刃をしっかり研いでよく切れるようにすると早く効率よく木を切ることができるようになりますね。物事に取りかかる際には、しっかり準備をして取りくむと生産性が上がるという意味です。

Misoca では、開発生産性を向上させる、もしくは、停滞させないようにいろいろな取り組みをしてます。具体例としては、

という取り組みをしています。以下の技術ブログの記事はこの取り組みの成果になります。

継続的な向上

Misoca では、以前から有志がJenkinsとrrrspecと私に書かれているような CI 環境のビルド時間を短縮するなど生産性の向上に単発的に取り組んでいました。木を切り続けていると鋭かった斧の歯はだんだん鈍ってしまうように、開発チームの規模が大きくなるに従い実行するテストの数が多くなっていき、早かった CI 環境はだんだんと遅くなり、不満が上がるようになりました。

そのため、2017年5月頃より、生産性向上の取り組みを機能開発と同じようにプロジェクトとして、期間と人数を決めて、継続的に実施するようになりました。プロジェクトなので、最初に インセプションデッキ を書いて、事後検証をして終了します。

開発生産性向上のプロジェクトでは、基本的に期間を優先しています。2週間と決めたら成果が出る・出ないに関わらず、終了します。成果の出すのが難しいテーマの場合は、期間に合ったスコープ(ゴール)を決めながら、段階的に進めていきます。 CI のビルド時間の短縮の場合、

といった取り組みを行っていき、20分以上だったビルド時間を約5分程度まで短縮しました。

まとめ

物事を効率的に進めるには、事前準備と継続的な取り組みが必要です。Misoca では、開発生産性向上を機能開発と同様に扱って実施しています。

明日は、 merotan こと @renyamizuno_が「すごーい」何か書いてくれるそうです。楽しみです。

自動esaやり機とクリーンアーキテクチャ

🎄 Advent Calendar

もう12月ですね。
あっという間に、今年も残り1ヶ月となりました。

f:id:kokuyouwind:20171129095835j:plain

12月といえばクリスマス。
そしてアドベントカレンダーです。

f:id:kokuyouwind:20171129095858j:plain

そんなわけで、今年はMisocaでもアドベントカレンダーを作りました!

この記事はMisoca Advent Calendar 2017の1日目です。
中の人がそんなにいないので、同じ人が何度か出てくることになります。
途切れず25日完走できるか、乞うご期待。

あ、ちなみに初日の担当は@kokuyouwindです。
BOOTHで技術同人誌を頒布してるのでよろしくおねがいします。
コミケにも持っていきます。初日のキ-53aです。

🐤 esa.io

Misocaでは情報共有にesa.ioを使っています。
ポストをWIPで作ってから書き足していけるため、気軽に情報をアウトプットでき大変便利なツールです。

✍️ 寄せ書きポスト

Misocaでのesaの使い方の一つに「枠だけ作ったポストに各自が書き足す」場合があります。
寄せ書きみたいですね。

例えば、議題を持ち寄って話すミーティングでは、議事録のポストを作っておいて各自が議題を書き足していきます。
こうすると、当日は書いてある内容に沿って進行できますし、議事録の手間をだいぶ軽減できます。

また各自の日報についても、WIPで作って書き足していくスタイルの人がいたりします。
これは好みが分かれますが、自分は帰り際に記憶を掘り起こす必要がないのでこの方が好きです。

🔄 ポストの自動作成

WIPで書き足していく方法はかなり便利なのですが、「誰がポストを作るか」という問題があります。
特に週次で開催されるミーティングの場合、毎週だれかが作るのは結構面倒です。
また、うっかりすると2人が重複して新しいポストを作ってしまいます。

既にあるポストに書き足すだけで良いのが理想的なので、決まった周期で自動的にポストを作れると便利そうです。
残念ながらesa.ioにはそういった機能はありませんが、esa APIを使えば外部からポストを作ることができます。

そういった経緯で作ったのが自動esaやり機(esa_feeder)です。

🐣 自動esaやり機

自動esaやり機ではesa.ioの投稿テンプレートにタグを付与することで自動作成の指定を行います。

  • #feed_* をつけると指定した曜日に記事が作られる
    • #feed_mon で月曜日、#feed_tueで火曜日、etc...
    • #feed_wdayで平日指定(祝日を除く月〜金)
  • #slack_* をつけると指定したSlackチャンネルに記事作成通知
  • #me_* をつけると指定ユーザでポストを作成
    • 未指定だと esa_bot ユーザで作られる
  • ユーザごとの投稿テンプレートに対応
    • この場合は #me_* タグの指定に関わらず、そのユーザでポストが作られる

例えばtemplates/週次報告/%{Year}-%{month}-%{day} #feed_thu #slack_generalという投稿テンプレートがあった場合、

  • 毎週木曜日に 週次報告/2017-11-30 などのポストが作られる
  • 作ったポストがSlackの #general チャンネルに通知される

という挙動になります。

f:id:kokuyouwind:20171130163541p:plain

から

f:id:kokuyouwind:20171130163029p:plain

が作られる感じです。

⚙ 仕組み

内部処理は非常に単純で、当日の曜日に該当するタグをクエリとして、APIでテンプレートを取得し、それぞれのテンプレートごとにポストを作成しているだけです。
この処理はthorタスクで起動できるようにしており、Heroku Schedulerで毎日9時に実行するようにしています。

当初はデータベースにポストの作成設定を持たせたりWebUIをつけたりしようと考えていたのですが、必要な機能を絞った結果、非常にシンプルな構成になりました。

🌏 クリーンアーキテクチャ

esa_feederは単機能の小さなリポジトリですが、せっかく新しいリポジトリを作ったので、試験的にクリーンアーキテクチャに沿ってレイヤを切り分けてみました。

以下の画像はThe Clean Architectureのブログ記事からの引用です。

f:id:kokuyouwind:20171129105311j:plain

クリーンアーキテクチャではこの図のようにビジネスルールを中央に置き、フレームワークや外部APIなどの詳細には直接依存しないようにします。

📁 ディレクトリ構成

esa_feederbundle gemコマンドで初期構成を作ったため、lib/esa_feeder以下に主要なコードが配置されています。
lib/esa_feeder以下のディレクトリ構成は以下のとおりです。

esa_feeder
├── entities
│   └── esa_post.rb
├── use_cases
│   ├── feed.rb
│   └── source_tag.rb
├── gateways
│   ├── esa_client.rb
│   └── slack_client.rb
└── version.rb

entitiesは図中のEntities(黄色の層)に対応しており、関心の対象になるクラスを配置します。
今回はesaのポストを表わすEsaPostを作っています。

use_casesは図中のUse Cases(赤色の層)に対応しており、アプリケーションの振る舞いを扱うクラスを配置しています。
今回は「日付に該当するタグを取得する」と「指定したタグ群の記事を作成する」という2つの振る舞いクラスを作っています。

gatewaysGateways(緑色の層)とExternal Interface(青色の層)に対応しており、外界とやりとりするコードを置いています。
今回はesa.ioとSlackのAPIを利用するため、それらのクライアントを置いています。

もちろん、ユースケースではesaのテンプレートを取得したり記事を作ったりする必要があり、gatewayに配置されたクラスのインスタンスを知る必要があります。
しかしクリーンアーキテクチャの原則では内側のレイヤから外側のレイヤに依存することはできないため、ユースケースの初期化時に依存性注入(Dependency Injection, DI)するようにしています。

module EsaFeeder
  module UseCases
    class Feed
      def initialize(esa_port, notifier_port)
        @esa_port = esa_port
        @notifier_port = notifier_port
      end

      def call(tags)
        # ...
        post = esa_port.create_from_template(template, feed_user)
        # ...

🙆 メリット

クリーンアーキテクチャにしたことで各クラスの責務が明確になり、テストが書きやすくなりました。
特にユースケースが外界と接続する部分は必ずDIされているため、allow_any_instanceなどを使わずmockでテストでき、意図しない動作を起こしにくくなっていると思います。

# 直接依存しているとこうなりがち
before do
  allow_any_instance_of(Esa::Client)
    .to receive(:posts)
    .and_return(posts)
end
subject { described_class.new.call }

# DIするとこう書ける
let(:esa_client) { double('esa client', posts: posts) }
subject { described_class.new(esa_client).call }

また変更で影響を受ける層が限られるため、機能追加や修正の見通しが良くなります。

例えば#me_*タグでポストの作成ユーザを変えれるようにした際は、

  • #me_*という特殊タグの追加」というEntities層の変更
  • 「ポストを作る際に作成者を切り替える」というUse Cases層の変更

にとどまり、Gateways層には影響を与えずに修正できました。

🙅 デメリット

当然ですが責務をかなり細かく切り分けることでクラス数も多くなるため、コード量自体は多くなります。
またテストコードではモックが増え、特にUse Cases層ではGateways層とのやり取りを全てモックすることになるため、今回のようにAPIを順に叩くだけだと「これは何のテストだっけ…」という気持ちになってきます。

今回は実験的な意味もありクリーンアーキテクチャに沿った設計にしましたが、この規模のリポジトリではメリットよりもデメリットが大きいかもしれません。

❓ 迷い

クリーンアーキテクチャには境界をまたがるデータについて、以下のように書かれています。

典型的には、境界をまたがるデータは、シンプルなデータ構造だ。 (中略) われわれは、ズルをして、エンティティやデータベースの行を渡すべきではない。

これはもっともな気もするのですが、ユースケースgatewayから返ってきた値を毎回entitiesに変換するというのも筋が悪いような気がします。
そもそも緑色のInterface Adapter層は

ユースケースとエンティティにもっとも便利な形式から、データベースやウェブのような外部の機能にもっとも便利な形式に、データを変換する

のが責務と書かれているので、ここがEntitiesへの変換まで受け持ったほうが見通しが良くなるのではと思い、現状のesa_feederではgatewaysからentitiesを触っています。
自分の理解が浅いというのもありますが、このあたりは結構どうするか迷っている部分です。

💪 今後の課題

自動esaやり機はesa.ioとSlack通知を前提に作っていますが、Gatewayを切り替えれば他の情報共有アプリケーションで同様の記事作成を行ったり、通知先をSlack以外に切り替えたりできるはずです。
このあたりはRubotyの仕組みを参考にしてプラグイン方式にできないかと画策しています。

また細かい話ですが、議事録を事前に作ると記事中の日付が作成日になってしまうため、実際に会議を行う日とズレてしまいます。
このあたりをうまくサポートできる機能が付けられないかなーというのも考えています。

💬 余談

自動esaやり機を作るにあたりesa APIで「テンプレートから記事を作る機能」が欲しいなーと思い問い合わせたところ、その日のうちに実装完了メールが来てびっくりしました。
その後もパラメタ周りの問題を問い合わせたら1時間後には修正がリリースされていたりして、最高のスピード感でした。

📢 宣伝

Misoca Advent Calendar 2017、明日の担当はころちゃんです。

Misocaではesa APIをハックしたいエンジニアを募集しています!

📱MisocaアプリのiPhone X対応

mzpです。こんにちは。

MisocaのiPhoneアプリiPhone Xに対応しました。

🎨 デザイン変更

単純に最新のXcodeでビルドしなおしてみたが、iPhone Xのホームインジケータとボタンが被ってしまった。

f:id:mzp:20171116135049p:plain:w200

こういった画面はデザインを変更し、ボタンの下に余白を設けた。

f:id:mzp:20171116135803p:plain:w250

同様の位置にボタンがある画面がいくつかあったので、それらの画面も修正した。

📝更新通知画面

今回のバージョンから以下のような更新内容を通知する画面を導入した。

f:id:mzp:20171116140943p:plain:w250

実機で動かしてみたら、想像以上に格好よくて楽しかった。この写真のときは不具合修正のところに虫のピクトグラムを表示していたが、虫が気持ち悪いと言われたのでリリース版では魔法の杖に変えた。

f:id:mzp:20171115110809j:plain:w400

この画面を表示するかどうかの判断は以下のようにしている。前回起動したときとバージョンが異なっている場合は、この画面を出すようにした。

enum UpdateType {
    case initial
    case update
    case normal
}

class UpdateInfo {
    private let key = "latest_version"

    func requestUpdateType(f: (UpdateType) -> Void) {
        if !Auth.isLoggedIn() {
            // 未ログイン状態なら初期画面を表示する
            UserDefaults.standard.set(currentVersion, forKey: key)
            f(.initial)
            return
        }

        if currentVersion == previosVersion {
            // 前回起動したときと同じバージョンなら通常処理をする
            f(.normal)
        } else {
            // 前回起動したときと違うバージョンなら更新通知画面を出す
            UserDefaults.standard.set(currentVersion, forKey: key)
            f(.update)
        }
    }

    private lazy var currentVersion: String? = {
        guard let shortVersion = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String else {
            return nil
        }
        guard let version = Bundle.main.infoDictionary?["CFBundleVersion"] as? String else {
            return nil
        }
        return "\(shortVersion)-\(version)"
    }()

    private lazy var previosVersion: String = {
        return UserDefaults.standard.string(forKey: key) ?? ""
    }()
}

👈文書ボタンの長押し

文書作成ボタンやタブバーを長押しすると、作成する文書の種類を選べるようにした。

f:id:mzp:20171116151905j:plain:w250

これは UILongPressGestureRecognizer で実現している。

let view = navigationItem.rightBarButtonItem?.value(forKey: "view") as? UIView
view?.addGestureRecognizer(
  UILongPressGestureRecognizer(target: self, 
                               action: #selector(DocumentsViewController.addLongpressAction(_:))))

📢採用

MisocaではiPhoneアプリ開発に携わりたいエンジニアを募集中です。

特定のブランチをもとに本番同様の動作確認ができる「レビュー環境」の話

こんにちは。tkykです。

みなさん、コードレビューしていますか?今日はMisocaのレビュープロセスで用いられている、とっても便利な「レビュー環境」について紹介します。

Misocaのレビュー体制とその課題

MisocaではPull Request(以下、PR)ベースの開発体制をとっており、必ず他のエンジニアによるPRのレビューを経てから、masterへマージすることになっています。

レビュー時に動作確認をするには、エンジニア各自がローカル環境にブランチをチェックアウトして行うのですが、時にはそれだけでは不都合なケースもあります。

  • 非エンジニアにも動作確認をしてほしい
  • 動作確認をするための条件を整えたい
    • 最終的にはマージされないコードを一時的に追加したい
    • 依存するライブラリのバージョンを変更したい
    • RAILS_ENV=production でビルド・実行したい
    • などなど

このようなケースに対応するために、「レビュー環境」という仕組みを作りました*1

レビュー環境とは

レビュー環境とは、特定のブランチに基づいて構築された、独立したアプリケーションの実行環境です。専用のURLが割り当てられ、本番環境と同様に動作しますが、サーバリソースは独自に確保されており、自由な操作によるテストが可能です。

レビュー環境の使い方はとても簡単です。例えば今、 awesome-feature ブランチで新機能の開発を行っているとしましょう。このブランチを元にレビュー環境を構築するには、 review/[任意の名前] という名前のブランチをリポジトリにpushします。

git push origin awesome-feature:review/awesome

構築は自動で行われ、完了するとslackにそのレビュー環境専用に割り当てられたURLが通知されます。このURLにはGoogle認証によるアクセス制限がかかっていますが、Misocaの開発メンバーなら誰もが自由にアクセスすることができます。

(当初はPRが作られるたびに自動で環境を構築する方式も検討しましたが、すべてのPRが専用の環境を必要とするわけではなく、リソースの無駄が大きくなるので、ブランチ名で明示する方式になりました)。

仕組み

レビュー環境の正体はDockerコンテナです。コンテナの管理はAWS ECS(Elastic Container Service)で行っています。

全体の構造は次の通りです。GitHub Webhookを起点にAWS CodeBuildでDockerイメージの構築を行い、AWS Lambdaでコンテナの起動やslackへの通知を行います。

(prprについては過去の記事をご覧ください)

f:id:tkykmw:20171107151244p:plain

今回はCodeBuildの設定と、ECSを管理するLambdaの処理内容について、少し詳しく説明します。

CodeBuildによるDockerイメージの構築

CodeBuildではDockerイメージの構築と、ECR(Amazon EC2 Container Registry)への登録を行います。一連の処理の流れは、公式ドキュメントのサンプルとほぼ同じです。

# appspec.yml

phases:
  pre_build:
    commands:
      - echo Logging in to Amazon ECR...
      - $(aws ecr get-login --region $AWS_DEFAULT_REGION)
  build:
    commands:
      - echo Building the Docker image...
      - docker build -t $IMAGE_REPO_NAME:$IMAGE_TAG -f config/docker/Dockerfile.review .
      - docker tag $IMAGE_REPO_NAME:$IMAGE_TAG $AWS_ACCOUNT_ID.dkr.ecr.$AWS_DEFAULT_REGION.amazonaws.com/$IMAGE_REPO_NAME:$IMAGE_TAG
  post_build:
    commands:
      - echo Pushing the Docker image...
      - docker push $AWS_ACCOUNT_ID.dkr.ecr.$AWS_DEFAULT_REGION.amazonaws.com/$IMAGE_REPO_NAME:$IMAGE_TAG
      - curl -X POST -d "{\"repo_name\":\"$IMAGE_REPO_NAME\",\"image_tag\":\"$IMAGE_TAG\"}" https://xxxxxxxx.execute-api.$AWS_DEFAULT_REGION.amazonaws.com/build-review-container

イメージのタグとして、レビュー環境の名前(ブランチ名の review/[この部分])を割り当てます。ファイル中では $IMAGE_TAG という変数の部分です。

ECRへのイメージ登録が完了したら、後続のLambdaを実行するために、API Gatewayに対するHTTP POSTリクエストを発行します。このとき、レビュー環境の名前をパラメータとして渡します。

LambdaによるECS管理

ECSでは、コンテナの定義情報(タスク定義)と実行情報(サービス)を分けて管理します。実際に起動されるコンテナは「タスク」として管理されます。

レビュー環境を構築するためのLambdaの処理は、次の通りです。

  1. タスク定義を作成・更新する
  2. サービスを作成・更新する
  3. (必要な場合)既存のタスクを終了する

順にコードを見ていきます。Lambdaのランタイムはnode.js 6.10を使用し、エラー処理は省いています。

タスク定義

タスク定義は単純なJavaScriptオブジェクトとして作成し、registerTaskDefinition で新規作成または更新します。

/*
 * タスク定義には、
 * - Dockerイメージのパス
 * - ポートマッピングの設定
 * - 環境変数の設定
 * - ログ出力の設定
 * - etc.
 * が含まれる
 */
const taskDefinition = (repository, tag, hostname, port) => {
  return {
    containerDefinitions: [ {
      family: taskName(tag),
      image: `000000000.dkr.ecr.ap-northeast-1.amazonaws.com/${repository}:${tag}`,
      portMappings: [ { hostPort: port, containerPort: 3000, protocol: 'tcp' } ],
      entryPoint: [ 'bin/launch' ],
      name: tag,
      environment: [
        { 'name': 'RAILS_ENV', 'value': 'production' },
        { 'name': 'APPLICATION_HOST_WITH_PORT', 'value': hostname },
        { 'name': 'MYSQL_DATABASE_PRODUCTION', 'value': `misoca_review_${tag}` }
      ],
      logConfiguration: {
        logDriver: 'awslogs',
        options: { 'awslogs-group': 'ECS', 'awslogs-region': 'ap-northeast-1', 'awslogs-stream-prefix': 'misoca-review' }
      },
      memoryReservation: 256
    } ],
    networkMode: 'bridge',
    placementConstraints: [],
    volumes: [],
    essential: true,
    volumesFrom: []
  };
};

/*
 * タスク定義の更新 or 新規作成
 * `event` はCodeBuildからLambdaに渡されたイベントオブジェクト
 * ホスト名とポート番号はタグ名をもとに生成する前提
 */
ecs.registerTaskDefinition(
  taskDefinition(
    event.repo_name,
    event.image_tag,
    hostname(event.image_tag),
    port(event.image_tag)
  ), (err, data) => {
  updateOrCreateService(event.image_tag);
});

タスク定義の内容に変更がない場合、毎回更新する必要はないのですが、処理を単純化するために常に registerTaskDefinition を実行するようにしました。

サービス

タスク定義の作成(更新)に成功したら、その定義を参照するサービスを作成(更新)します。レビュー環境一つにつき、一つのタスク(=コンテナ)を起動したいので、desiredCountは1に設定します。

const updateOrCreateService = (tag) => {
  ecs.describeServices({ services: [serviceName(tag)], cluster: CLUSTER }, (err, data) => {
    // 削除されたサービスは 'INACTIVE' ステータスとして見える
    if(data.services.length > 0 && data.services[0].status !== 'INACTIVE') {
      ecs.updateService({
        cluster: CLUSTER,
        service: serviceName(tag),
        taskDefinition: taskName(tag),
        desiredCount: 1
      }, (err, data) => {
        stopRunningTask(tag);
      });
    } else {
      ecs.createService({
        cluster: CLUSTER,
        serviceName: serviceName(tag),
        taskDefinition: taskName(tag),
        desiredCount: 1
      });
    }
  });
};

サービスが作成されると、タスクの起動はECSが自動で行ってくれます。しかしすでにタスクが起動していた場合、そのままでは新たなイメージがロードされないので、stopTask で起動済みタスクを終了します。するとECSが新たなイメージとタスク定義に基づいて、タスクを再起動してくれます。

/*
 * 常に1件のみ、実行中であることを想定する
 */
const stopRunningTask = (tag) => {
  ecs.listTasks({
    maxResults: 1,
    cluster: CLUSTER,
    desiredStatus: 'RUNNING',
    serviceName: serviceName(tag)
  }, (err, data) => {
    ecs.stopTask({
      task: data.taskArns[0],
      cluster: CLUSTER,
      reason: 'Review deploy'
    });
  });
};

まとめ

いかがでしたでしょうか。

Dockerコンテナによる軽量な実装と、AWS APIを活用した構築の自動化によって、細かな目的ごとに、本番同様に動く環境を作れるようになりました。今ではレビュー目的に限らず、エンジニア各自が自由な実験を行うためにも用いられ、開発生産性の向上に寄与しています。

採用

Misocaでは日々の開発生産性の向上に取り組みたいエンジニアも募集中です!

*1:この名前と機能は、Heroku Review Appsにインスパイアされたものです