Quantcast
Channel: Ruby on Railsの記事一覧|TechRacho by BPS株式会社
Viewing all 1388 articles
Browse latest View live

週刊Railsウォッチ(20210412前編)Active Record属性暗号化機能がRails 7にマージ、RailsNew.ioでrails newオプションを生成ほか

$
0
0

こんにちは、hachi8833です。

週刊Railsウォッチについて

  • 各記事冒頭には🔗でパーマリンクを置いてあります: 社内やTwitterでの議論などにどうぞ
  • 「つっつきボイス」はRailsウォッチ公開前ドラフトを(鍋のように)社内有志でつっついたときの会話の再構成です👄
  • お気づきの点がありましたら@hachi8833までメンションをいただければ確認・対応いたします🙇

TechRachoではRubyやRailsなどの最新情報記事を平日に公開しています。TechRacho記事をいち早くお読みになりたい方はTwitterにて@techrachoのフォローをお願いします。また、タグやカテゴリごとにRSSフィードを購読することもできます(例:週刊Railsウォッチタグ)

🔗Rails: 先週の改修(Rails公式ニュースより)

今週は以下のコミットリストのChangelogを中心に見繕いました。

🔗 Cache-Controlヘッダーにprivate, no-storeを指定できるようになった


つっつきボイス:「Cache-Controlヘッダーは前にも改修がありましたね」「あ、そういえばありました(ウォッチ20201012)」「以前はno-storeを指定したら他の設定を含めないように変えられてたけど、今回の修正でprivate, no-storeを指定できるようになったということは、この組み合わせに意味があるということなんでしょうね」

#39461ではCache-Controlヘッダーのno-storeディレクティブで他の指定を含まないようにした(つまりCache-Controlヘッダーに’private, no-store’を指定しても’no-store’だけになる)。privateは不要であることが多いが、常に不要とは限らない。
たとえば、Fastlyドキュメントには「現在はno-storeno-cacheディレクティブを尊重しない」かつ「FastlyとWebブラウザの両方でキャッシュを行わないようにする必要がある場合は、privateディレクティブにmax-age=0no-storeを組み合わせることを推奨する」とある。
#39461で変更されたディレクティブを減らす振る舞いはオーバーライドできないので、FastlyユーザーがRailsをアップグレードできなくなっている。
このプルリクは、privateを指定した場合はprivate, no-storeヘッダーを設定できるように変更する。no-cacheの場合にpublicを指定できるのと同様だが、デフォルトではない。
修正されるissue: #40798

「FastlyのドキュメントにCache-Control: private, no-storeには意味があると書かれてた↓」「使いたい組み合わせを使えるようにしたという修正ですね」

参考: Configuring caching | Fastly Help Guides
参考: Cache-Control - HTTP | MDN

「変更点を見ると、privateno-storeを同時に指定できるように変わった↓」「今までは両方指定してもno-storeだけになってたのか」

# actionpack/lib/action_dispatch/http/cache.rb#L198
+         options = []
+
          if control[:no_store]
-           self._cache_control = NO_STORE
+           options << PRIVATE if control[:private]
+           options << NO_STORE
          elsif control[:no_cache]
-           options = []
            options << PUBLIC if control[:public]
            options << NO_CACHE
            options.concat(control[:extras]) if control[:extras]

-           self._cache_control = options.join(", ")
          else
            extras = control[:extras]
            max_age = control[:max_age]
            stale_while_revalidate = control[:stale_while_revalidate]
            stale_if_error = control[:stale_if_error]

-           options = []
            options << "max-age=#{max_age.to_i}" if max_age
            options << (control[:public] ? PUBLIC : PRIVATE)
            options << MUST_REVALIDATE if control[:must_revalidate]
            options << "stale-while-revalidate=#{stale_while_revalidate.to_i}" if stale_while_revalidate
            options << "stale-if-error=#{stale_if_error.to_i}" if stale_if_error
            options.concat(extras) if extras

-           self._cache_control = options.join(", ")
          end

🔗 ActiveSupport::TimeWithZone.nameが非推奨化


つっつきボイス:「今までのnameメソッドは”Time”という文字列をそのまま返してたの?↓」「えぇ?😳」「プルリクにも何でこういう実装になったのかわからないって書かれてますね」「文字列リテラルの”Time”を返す実装、マジで謎」

# activesupport/lib/active_support/time_with_zone.rb#L43
    def self.name
+     ActiveSupport::Deprecation.warn(<<~EOM)
+       ActiveSupport::TimeWithZone.name has been deprecated and
+       from Rails 7.1 will use the default Ruby implementation.
+     EOM
+
      "Time"
    end

「機能をdeprecatedにするときはこういうふうに書くのか↑」「この書き方は定番ですね」

c00f2d2でnameメソッドが実際のクラス名”ActiveSupport::TimeWithZone”ではなく”Timeを”返すようオーバーライドされていた。このようにした理由が不明だし、nameが実際のクラス名を返すことを期待するる開発者が混乱する可能性がある。この変更が追加された理由がわからないので、ひとまず非推奨化して開発者からのフィードバックを待ち、issueが上がったら適宜修正することにする。
同PRより大意

ActiveSupport::TimeWithZone.nameが非推奨化されてRails 7.1でデフォルトの実装に戻される予定なのか」「次のRailsメジャーバージョンは7だから、リリースごとに非推奨化->削除が最短で行われれば7.1で削除されるでしょうね」


「ところで、Railsのメジャーバージョンが7に上がる理由のひとつに、おそらくRailsで必要な最小限のRubyバージョンが上がるからというのもあるんじゃないかな」「Rails 7だとRuby 3.0が最小バージョンになるんでしょうか?」「Rails 7で最小バージョンをいきなり3.0にしたら追従できない機能やgemが続出すると思うので、そこまではやらないでしょうね」「たしかに」

後でedgeガイドを見ると以下のように書かれていました↓。と思ったら以前のウォッチでも言及していました(ウォッチ20210208)。

  • Rails 7 requires Ruby 2.7.0 or newer.
  • Rails 6 requires Ruby 2.5.0 or newer.
  • Rails 5 requires Ruby 2.2.2 or newer.

以下はつっつき後に教わったツイートです。

🔗 Active Record属性暗号化がついにマージされた


つっつきボイス:「少し前のウォッチ↓ではマージ前だったActive Record属性暗号化がついにマージされました」「お、きたか🎉」「これは嬉しい😋」「思ったよりスムーズにマージされましたね」

週刊Railsウォッチ(20210330後編)Active Recordモデル属性暗号化が標準で入る可能性、Flipper Cloud、awesome_printほか

「Changelogがコンパクトにまとまっててありがたい↓」「HEYでセキュリティ監査を経て実績を積んだ属性暗号化機能が入るのはいいですね👍

属性暗号化のサポート
暗号化された属性はモデルレベルで宣言される。これらは背後に同じ名前のカラムを備えた正式なActive Record属性である。「データベース保存前の属性暗号化」や「値読み出し前の解読」はシステムが透過的に行う。

class Person < ApplicationRecord
  encrypts :name
  encrypts :email_address, deterministic: true
end

詳しくは以下のガイドを参照。
Jorge Manrubia
Changelogより大意

「属性暗号化のガイドもまるまる追加されたんですね」「edgeガイドには実用的なコード例も丁寧に書かれていますし、このまま使えそうなくらい充実してる👍」「key_providerの指定や暗号化キーのローテーションもちゃんとサポートしてる↓」「お〜優秀!」「キーのローテーションは後付けでやろうとするとつらいんですよ…」「Rails 7の目玉機能のひとつですね」

# edgeガイドより
config.active_record.encryption.previous = [ { key_provider: MyOldKeyProvider.new } ]
# edgeガイドより
active_record
  encryption:
    master_key:
        - bc17e7b413fd4720716a7633027f8cc4 # Active, encrypts new content
        - a1cc4d7b9f420e40a337b9e68c5ecec6 # Previous keys can still decrypt existing content
    key_derivation_salt: a3226b97b3b2f8372d1fc6d497a0c0d3

参考: 鍵のローテーション  |  Cloud KMS ドキュメント  |  Google Cloud


「ところでRails 7、いつ出るのかな」「時が来たらとしか言いようがない😆」「マイルストーン↓を見るとまだ10件しかプルリクがないので、しばらくは出なさそうですね」

「お、以前話題になったRails Conductorが7に持ち越されてる↓」「そういえばRails Conductorってありましたね(ウォッチ20190311)」

🔗 ActiveSupport::Cachewritefetchexpires_at:で絶対時刻のTTLを設定できるようになった

ActiveSupport::Cachewritefetchに、キャッシュエントリのTTLを絶対時刻で設定するexpires_at:引数を追加。

Rails.cache.write(key, value, expires_at: Time.now.at_end_of_hour)

Jean Boussier
同Changelogより大意


つっつきボイス:「キャッシュを絶対時刻で期限切れにするexpires_at:オプションが追加された、なるほど」「何月何日の何時というふうに指定できるようになったんですね」「今までだと『あと60分』みたいな相対指定しかできなかったので、絶対時間にするなら自分で計算して渡すことになる」「と思ったら改修も絶対時間を計算してますね↓」「あると嬉しい機能👍

# activesupport/lib/active_support/cache.rb#L805
-     # +:compress+, +:compress_threshold+, +:version+ and +:expires_in+.
-     def initialize(value, compress: true, compress_threshold: DEFAULT_COMPRESS_LIMIT, version: nil, expires_in: nil, **)
+     # +:compress+, +:compress_threshold+, +:version+, +:expires_at+ and +:expires_in+.
+     def initialize(value, compress: true, compress_threshold: DEFAULT_COMPRESS_LIMIT, version: nil, expires_in: nil, expires_at: nil, **)
        @value      = value
        @version    = version
-       @created_at = Time.now.to_f
-       @expires_in = expires_in && expires_in.to_f
+       @created_at = 0.0
+       @expires_in = expires_at&.to_f || expires_in && (expires_in.to_f + Time.now.to_f)

        compress!(compress_threshold) if compress
      end

🔗 update_alldelete_alldestroy_allの後で@cache_keysをクリアするようになった

calculatedなキャッシュキーはリレーションにキャッシュされるが、リレーションでコレクションに改変が発生した場合に再計算する方法がない。
@cache_keysは少なくともupdate_alldelete_alldestroy_allではクリアすべき。
関連issue: #41784
同PRより大意


つっつきボイス:「以下みたいに1リクエストの処理中でrelationを再利用する場合に、最後のscoped.destroy_allで消されたときのkeyがキャッシュに残ってしまう問題に対応したようですね」「なるほど」

scoped = Hoge.where(name: 'piyo')
scoped.destroy_all
pp scoped.map(&:name)

「今までこれを回避するために何かトリッキーなことしてた覚えがありますけど、それをやらなくてよくなった😂」「少なくとも*_all系のメソッドでは@cache_keysがクリアされるようになったので、その種の対応が少し楽になりそう👍

「1レコードを指すActive Recordオブジェクトは何度も参照されることが多いので速度の関係からキャッシュヒットさせたいけど、複数レコードを対象とするRelationオブジェクトの更新系メソッドに対して、更新前に参照していたキャッシュを参照させたいというようなニーズはまずあり得ないだろうということですね、わかる」

🔗Rails

🔗 Tailwind CSS JITでCSSコンパイルを20倍高速化


つっつきボイス:「TechRachoのRails系翻訳記事でお世話になっているEvil Martiansの記事です」「Evil Martiansさんは最近Tailwind CSSがお気に入りみたい」

tailwindlabs/tailwindcss - GitHub

「Tailwind CSSって何だっけと思ったらBootstrapみたいなCSSフレームワークなんですね」「はい」「最近CSSフレームワークを選ぶ機会があったんですけど、これも検討しとけばよかったかな…」「CSSフレームワークみたいなものは作業者が慣れてないとよさを発揮しにくい面もあるので、Bootstrapみたいに長く使われているものが今も幅広く使われていますね」「それもそうか」

「ちなみにCSSフレームワークはBootstrap 5にしちゃいました」「お、5がもう出たんですか?」「実は5-beta3でした(後で正式版にする前提で)」「公式サイト↓のバージョンがv5.0.0-beta1と表示されているので正式版まであともう少しですね」

「そういえばBootstrap 5はIEサポートやめたってどこかに書かれてました」「IEは今やTwitterすら満足に表示できないのでサポート打ち切られても仕方ないでしょうね」「あ、そういえばそんな話もあったかも」「今どきのIE対応は特別に指定がない限り基本的にはサポートしない流れでいいと思います」

Internet Explorer はサポート外です。 Internet Explor のサポートが必要な場合は Bootstrap v4 を使ってください。
ブラウザとOS · Bootstrap v5.0より

🔗 RailsライブラリなしでRubyのWebアプリを作る(Ruby Weeklyより)


つっつきボイス:「Railsライブラリを使わずにRubyでWebアプリを作るという企画か」「おぉ、TCPServerを立ち上げるところから始めている↓」「何とプリミティブな😳」「ここはソケットプログラミングで最初にやるところですね」「Rackサーバーもない状態から始めるのが徹底してる」「without Rails libraryどころか、Ruby付属のgem以外使わないぞという勢いですね」

# 同記事より
#Use Rubys Socket Library
require 'socket'

server = TCPServer.new(1337)

参考: class Socket (Ruby 3.0.0 リファレンスマニュアル)

「なかなか長い記事ですね」「真ん中過ぎぐらいでやっとRackが登場↓」「文明が来た」

# 同記事より
require 'rack/handler/puma'

class HelloWorld
  def call(environment)
    status  = 200
    headers = { 'Content-Type' => 'text/plain' }
    body    = ['Hello', ' world!']

    [status, headers, body]
  end
end

Rack::Handler::Puma.run(HelloWorld.new)

rack/rack - GitHub

「最終的にこういう感じになったんですね↓」

「Webアプリをスクラッチで作ったことがない人やソケットプログラミングをやったことのない人にとっては、かなりいい勉強になりそう👍」「たしかに!」「Railsのようなフレームワークで覆い隠されている各種レイヤをこうやって実際に触ってみると何かしら学びになると思います」

参考: ソケットプログラミング

🔗 Railsでビューのテストを書く理由(Ruby Weeklyより)

# 同記事より
# spec/views/books/index.html.erb_spec.rb

require 'rails_helper'

RSpec.describe "books/index", type: :view do
  before(:each) do
    assign(:books, [
      Book.create!(
        title: "Title",
        description: "MyText",
        download_url: "Download Url",
        status: "Status"
      ),
      Book.create!(
        title: "Title",
        description: "MyText",
        download_url: "Download Url",
        status: "Status"
      )
    ])
  end

  it "renders a list of books" do
    render
    assert_select "tr>td", text: "Title".to_s, count: 2
    assert_select "tr>td", text: "MyText".to_s, count: 2
    assert_select "tr>td", text: "Download Url".to_s, count: 2
    assert_select "tr>td", text: "Status".to_s, count: 2
  end
end

つっつきボイス:「ビューのテストを書く理由を自分なりに考えてみた記事のようです」

「たしかにビューのテストは実際には書かないことも多いんですが、view specが必要な場合なら書くのはありだと思います」「どんな場合でしょう?」「たとえば外部からクローリングされることが前提のビューだと、ユーザーにとっての見た目よりもHTMLのDOM構造自体が仕様になるので、そういう場合はview specでDOM構造をテストしたいですよね」「あ、そうか」「あるいはSEO要件で特定のHTML構造が必要な場合とか」「なるほど」「必要な理由はいろいろ考えられると思うので、頭の体操的に考えてみるのも楽しいですよ」

🔗 Railsのアレを生成するWebサイト2つ


つっつきボイス:「ruby-jp Slackで見かけました」「1つ目のrails.helpというサイトは、Railsのモデルやマイグレーションのジェネレータ文字列をGUIで生成できるんですね↓」「こういうの地味に嬉しいかも」「いつもテキストにメモしてからやってました」「textフィールドにlimitを指定できるとは知らなかった」


rails.helpより

「主にRailsを学び始めたばかりの人が、ジェネレータのタイプミスで失敗してやる気をくじかれるのを防ぐのに便利そう」「たしかに」「マイグレーションでどのフィールドタイプにどんなオプションを指定できるかをGUIで確認できるのも教育向けによさそう: decimalを指定したときはprecisionとscaleも必要になる、とか」「なるほど」「なお、自分は空のマイグレーションファイルを作ってそこに書く派です」


「2つ目のrailsnew.ioはrails newコマンドのオプションを生成するジェネレータサイトです」「omakaseオプションなんてのがあるのね」


railsnew.ioより

「へ〜、--skip-gemfileなんてオプションがあるとは」「Gemfileなしってどんなときに使うんでしょうね」「システムインストールされるgemを使う前提のときとかかな?」「minitestをスキップできない、と思ったらminimalにすると全部スキップされました」

「railsnew.ioは慣れた人にとっても便利そう👍」「rails newは使う頻度が少ないので、turbolinksをオフにするのとか忘れがちですよね」「あ、それ自分もこないだ忘れてました😆

🔗 devise-two-factor: Deviseで二要素認証(Ruby Weeklyより)

tinfoil/devise-two-factor - GitHub


つっつきボイス:「Deviseで二要素認証が使えるgemだそうです」「ちょっと嬉しいかも: 今度使ってみようかな」

参考: 多要素認証 - Wikipedia

「だいぶ昔ですけど、sshに二要素認証を付けてみたのを思い出しました」「sshでですか?」「すぐ飽きてやめちゃいましたけど😆

参考: sshの二段階認証(二要素認証)設定の方法3個 | 俺的備忘録 〜なんかいろいろ〜


前編は以上です。

バックナンバー(2021年度第1四半期)

週刊Railsウォッチ(20210407後編)エイプリルフールのRuby構文プロポーザル、AWSのVPC Reachability Analyzerほか

今週の主なニュースソース

ソースの表記されていない項目は独自ルート(TwitterやはてブやRSSやruby-jp SlackやRedditなど)です。

Rails公式ニュース

Ruby Weekly


週刊Railsウォッチ(20210413後編)RubyMineのRBSサポートとCode With Me、GitHub ActionとDockerレイヤキャッシュほか

$
0
0

こんにちは、hachi8833です。

週刊Railsウォッチについて

  • 各記事冒頭には🔗でパーマリンクを置いてあります: 社内やTwitterでの議論などにどうぞ
  • 「つっつきボイス」はRailsウォッチ公開前ドラフトを(鍋のように)社内有志でつっついたときの会話の再構成です👄
  • お気づきの点がありましたら@hachi8833までメンションをいただければ確認・対応いたします🙇

TechRachoではRubyやRailsなどの最新情報記事を平日に公開しています。TechRacho記事をいち早くお読みになりたい方はTwitterにて@techrachoのフォローをお願いします。また、タグやカテゴリごとにRSSフィードを購読することもできます(例:週刊Railsウォッチタグ)

🔗Ruby

🔗 RubyMineがRBSに対応


つっつきボイス:「お〜、Ruby 3のRBSがRubyMineでサポートされた🎉」「MDN ドキュメントのバンドルやパーシャルのプレビューなど他にもいろいろ機能が増えてますね: パーシャルのプレビューはどのぐらいうまくできるかわかりませんが」

ruby/rbs - GitHub

「Dockerサポートもいろいろ改善されたようですね↓: 今すべての作業をDockerベースにしているので即役に立ちそう」

参考: Docker RubyMine 2021.1 Beta 4: Code With Me and Improvements for Docker | The RubyMine Blog


「ちなみに最近のDocker Desktop for Windowsは随分よくなってきていて、DockerをRubyMineと組み合わせてHyper-V抜きで快適に使えるようになったのがホントありがたい🙏」「以前のDocker Desktop for Windowsからそんなに変わったんですか」「それまではHyper-VにインストールしたManjaro LinuxにRDP接続してGUIを使っていましたが、WindowsネイティブのGUIを使うのに比べて操作遅延が数フレームあったので、それがなくなって快適に開発できるようになりましたね👍

参考: Windows に Docker Desktop をインストール — Docker-docs-ja 19.03 ドキュメント

🔗 JetBrains IDEの「Code With Me」機能は優秀

「お〜、Code With Me機能もRubyMineで正式にリリースされたのか、これも嬉しい🎉」「これは?」「自分のIDE環境の操作をリモートの相手と共有して同時編集もできる、ペアプロなどで便利な機能です」「なるほど、VSCodeのLive Shareみたいですね」

参考: Code With Me: The Ultimate Collaborative Development Service by JetBrains

「Code With Meは、単なる画面共有やブラウザ共有と違って参加者全員がカーソルを動かせますし、JetBrains IDEのコード補完などの機能も参加者全員が使えます」「スゴい」「しかもホスト役の人がRubyMineでCode With Meで共有すれば、他の参加者はRubyMineを持っていなくてもプレーヤー的なクライアントソフトをダウンロードするだけで使えます」「ますますスゴい」


「今ちょっと自分のRubyMineをアップデートしてCode With Meを動かしたので入ってみてください(invitation linkを共有): さて誰が最初にこのRubyMineのscratch bufferに書き込むかな?」(しばし一同で動かす)

「シェルが開いてクライアントのダウンロードに30秒ほどかかりましたが、今入れました!」「こちらも成功です」「入れました」「動いた動いた、コード補完も使える」「チーム系ツール以外はすべて使えますよ」「音声や動画も出せるんですね」「ゲストをfull syncモードにすると画面切り替えなども許可できます」(しばし一同で動かす)

「というように、Code With MeはIDEの機能ごと共有できるのが、単なるブラウザ画面共有などと違う点です」「いろいろスゴい」「IDE内ターミナルの利用も個別に許可できますし、ホストが”Force all to follow you”を実行すればゲストを黙らせて見るだけに切り替えることもできます」「そんな機能まで😆」「レスポンスもローカルとほとんど変わらないぐらいスムーズ👍」「ペアプロもはかどりそう」「ちなみにCode With Meは一定時間何も操作しないとオフになります」

ペアプロを極めて最強の開発チームをつくる(1/4)ペアの組み方(翻訳)

「RubyMineにCode With Meが入ったのはかなり大きいと思います」「たとえばruby-jp Slackのような場で質問するときにも、その人の環境でCode With Meが使えたらinvitationリンクを投げるだけで環境を共有できるので、サポートする側にも質問する側にも便利かなと思いました」「たしかに」「質問側は現象や環境を正確に記述するのが大変ですし、回答側はそれを再現するのが大変なんですが、そういう時間を大幅に節約できそう」

🔗 JetBrains IDEを使うときのコツ

「何だかRubyMineを買いたい気持ちがにわかに高まってきました」「JetBrains IDEはサムライズムさん経由で日本円で買うと本家より安いですよ↓💰」「まずはダウンロードしてみます〜」「ちなみに自分はだいぶ前からIntelliJ IDEA Ultimateを年払いで使ってますけど、JetBrainsのIDEは長く使うほど年払い額がディスカウントされるのも嬉しい👍

参考: 株式会社サムライズム - 開発者向け生産性向上ツール、サービス

「とりあえずRubyMineダウンロードしました」「JetBrains IDEでおすすめの設定は、とりあえずVMに割り当てるメモリを増やすことですね↓」「やっておきま〜す」

参考: 起動オプション .vmoptions ファイルの場所 – 株式会社サムライズム

「デフォルトのキー操作をカスタマイズするとググラビリティが下がるので、可能ならJetBrains標準のキー操作に慣れておく方が良いと思います」「了解です!」「あと、機能名を正式な英語名で覚えておくようにしておくとShiftキーを2回押せばすべての機能を名前で呼び出せます(Search Everywhere): ショートカットキーやメニュー位置がうろ覚えでも使えるので便利👍


以下はつっつき後に見つけたツイートです。

🔗 Rubyist: iOSで実行できるRuby環境


つっつきボイス:「Rubyのどのバージョンが動くんでしょう?」「MRuby 3 VMだそうです」

mruby/mruby - GitHub

「Rubyのコードをショートカットに登録して実行できるのか」「ちょっと動かしてみた感じではhttpリクエストも動かせるみたいですね」

後で自分のiPhoneで動かしてみました。

「App Storeはこういうアプリを許可するようになったんでしたっけ?」「私もそこが気になりました」「たしか以前は任意のコードを実行できるアプリはApp Storeで許可が出ませんでしたよね」「そうでした」「それが嫌だったのでMacもiPhoneも使ってません😆

後で調べてみると、現在のガイドラインは以下のようになっています(以前のガイドラインについては不明)。ここを見る限りでは、コードを実行可能なアプリは条件付きで許可される可能性はありそうですね。

2.5.2
Appはバンドル内で完結している必要があります。他のAppを含め、指定されたコンテナエリア外に対するデータの読み書き、またはAppの特徴や機能を導入したり変更したりするコードをエリア外からダウンロード、インストール、実行することは許可されません。実行形式のコードの学習や開発、学生によるテストを目的とした教育用Appでは、コードが他の目的で使用されないという、限られた状況での使用に限り、コードのダウンロードが許可される場合があります。こうしたAppでは、ユーザーがApp上でソースコードの全体を確認し、編集できることを許可しておく必要があります。
App Store Reviewガイドライン - Apple Developerより

🔗 debugger-tatakidai


つっつきボイス:「_ko1さんが作っているデバッガーの叩き台」「早速名前がdebuggerからdebugger-tatakidaiに変わってますね」「皆さまのフィードバックお待ちしてますとありました」

「このツールはRubyをリモートデバッグできるんですね: READMEを見た限りでは、標準のdebugライブラリのreplacementとして使える、よりリッチなデバッガライブラリという感じかなと思います」「最終的にRubyにバンドルされるといいですね」

参考: library debug (Ruby 3.0.0 リファレンスマニュアル) — 従来のデバッガ

「ちなみにJetBrains IDEにもリモートデバッガが入ってます↓: RubyMineではdebaseというdebug.rbのfaster implementationを使うようになっていて、ruby-debug-ideというgemを使ってIDEからTCP経由でattachするような構成になっています」

参考: リモートデバッグ | RubyMine

ruby-debug/ruby-debug-ide - GitHub

🔗 その他Ruby(Ruby Weeklyより)


つっつきボイス:「RubyCards?」「RubyやRailsコードの問題を数日おきぐらいにツイートしているアカウントだそうです」「面白そう〜、フォローしました」「Ruby Goldの試験対策に使えるかも」

参考: Ruby技術者認定試験

🔗クラウド/コンテナ/インフラ/Serverless

🔗 GitHub ActionsのイメージビルドにDockerレイヤキャッシュを効かせる


つっつきボイス:「TechRachoのRails関連翻訳記事でよくお世話になっているEvil Martiansの記事です」「そういえばDockerのレイヤキャッシュはGitHub Actionsで使えますね」

参考: Dockerfile のベスト・プラクティス — Docker-docs-ja 19.03 ドキュメント

「こんなふうにGitHub Actionsの設定を書けるのか↓」「お〜」

# 同記事より: docker/build-push-actionの公式サンプル
name: ci

on:
  push:
    branches:
      - "master"

jobs:
  docker:
    runs-on: ubuntu-latest
    steps:
      # Check out code
      - name: Checkout
        uses: actions/checkout@v2
      # This is the a separate action that sets up buildx runner
      - name: Set up Docker Buildx
        uses: docker/setup-buildx-action@v1
      # So now you can use Actions' own caching!
      - name: Cache Docker layers
        uses: actions/cache@v2
        with:
          path: /tmp/.buildx-cache
          key: ${{ runner.os }}-buildx-${{ github.sha }}
          restore-keys: |
            ${{ runner.os }}-buildx-
      - name: Login to DockerHub
        uses: docker/login-action@v1
        with:
          username: ${{ secrets.DOCKERHUB_USERNAME }}
          password: ${{ secrets.DOCKERHUB_TOKEN }}
      # And make it available for the builds
      - name: Build and push
        uses: docker/build-push-action@v2
        with:
          context: .
          push: false
          tags: user/app:latest
          cache-from: type=local,src=/tmp/.buildx-cache
          cache-to: type=local,dest=/tmp/.buildx-cache-new
        # This ugly bit is necessary if you don't want your cache to grow forever
        # till it hits GitHub's limit of 5GB.
        # Temp fix
        # https://github.com/docker/build-push-action/issues/252
        # https://github.com/moby/buildkit/issues/1896
      - name: Move cache
        run: |
          rm -rf /tmp/.buildx-cache
          mv /tmp/.buildx-cache-new /tmp/.buildx-cache

参考: Build and push Docker images · Actions · GitHub Marketplace

「初歩的な質問ですけど、これができるとどんな点が嬉しいんでしょうか?」「GitHub ActionsでCIを回すときにキャッシュが残っていればそこから取れるので速くなりますね」「あ、なるほど!」「いわゆるCIキャッシュです」

「記事の最後の方にどのぐらい速くなったかが載ってる」「お〜、2分以上短縮されることもあるらしい」「bundle installyarn installのような時間がかかりがちなコマンドにキャッシュが効いて速くなる、たしかに」

「この記事はキャッシュによる速度差が具体的にわかるのがいいですね👍」「これ翻訳してください」「了解です〜!」

🔗CSS/HTML/フロントエンド/テスト/デザイン

🔗 Chrome DevToolsの機能


つっつきボイス:「Chrome DevToolsのtips記事です: 知らないものもあったので取り上げてみました」

「へ〜、capture node screenshotという機能があるのか」「おぉ?」「特定の要素だけをきれいにスクショで撮れる機能ですね」

「お、コンソールで$_が使えるそうだ」「これは?」「$_は直前の評価値を返す変数です: シェルやRubyにもありますし、他の言語にも似たようなものがあります」「あ、なるほど」「JavaScriptのコンソールならこれがあっても不思議じゃないですね」

[Ruby] Kernelの特殊変数をできるだけ$記号なしで書いてみる

「XHRリクエストのreplayは有名かな」「コンソールでcopyすると変数をクリップボードにコピーできるのか、これは知っておくと便利そう👍」「配列を表に整形する機能はJypiter Notebook↓を思わせますね」「要素を非表示にするショートカットもある」

参考: Project Jupyter | Home

「割と長いので翻訳記事なのかなと思ったら、最後まで見ると翻訳って書いてありました😅」「Chrome DevToolsにはいろいろ機能が隠れているから、こうやったらできるかなと思ってつついてみると意外にできたりしますね」「DevTools、要素を見るときぐらいしか使ってなかった…」

🔗 Chrome 90のDevToolsで使える新機能

「ChromeのDevToolsといえば最近感心したのは、弊社CTOのbabaさんがSlackに貼ってくれた、CSSのflexboxのオプションをビジュアル表示する機能ですね↓」

「これは?」「よく見てもらえればわかると思いますが、writing-modeがたとえばvertical-rlかそうでないかで、flexboxの各種オプションに表示される方向の絵(赤枠)までちゃんと切り替わってくれるんですよ」「あ、たしかにflex-directionがrl(right to left)のときは矢印のアイコンが右から左に変わってる!」「お〜」

「top to bottom、left to right、right to leftはもちろん、btt(bottom to top)みたいに下から上に進むパターン↓まであるので、たぶんflexboxの全パターンを網羅していると思います」「これはスゴいな〜」「かなりスゴい」「flexboxのこういう挙動って忘れがちなので助かる👍

「おや、自分のChromeでは表示されないな…」「今Slackでbabaさんに聞いてみたら、Canary版のChrome↓でやってたそうです」「あ、そういえばbabaさんは普段からCanary版Chrome使ってますね」

参考: Chrome Canary のデベロッパー向け機能 - Google Chrome

「以下の記事によると、Chrome 90(未リリース)新機能のflexbox debugging toolsというのがそれみたいです↓」

参考: What’s New In DevTools (Chrome 90) - Chrome Developers

「Canary版Chromeをインストールしたら出せました」「出た🎉」「左のサジェスチョンは前から使えますが、右のアイコンの矢印がちゃんとrlに合わせて右から左になってる」「お〜見やすい」

「Chromeは着々と強くなってますね」「そしてChromeが強くなるとElectronも強くなってくれるので、Slackもよくなってくる」「そうそう」

🔗 その他

ケーブル保護チューブ


つっつきボイス:「これちょっと欲しいです」「工具を使ってケーブルをびしっとまとめられる業務用のチューブ、ありますね」「チューブの途中からも線を出せるのがよさそう」「ホームセンターで買えるかな?」「ダイソーに類似品があるらしいという噂がありますね」


後編は以上です。

バックナンバー(2021年度第1四半期)

週刊Railsウォッチ(20210412前編)Active Record属性暗号化機能がRails 7にマージ、RailsNew.ioでrails newオプションを生成ほか

今週の主なニュースソース

ソースの表記されていない項目は独自ルート(TwitterやはてブやRSSやruby-jp SlackやRedditなど)です。

Ruby Weekly

Railsの技: ナビゲーションリンクで便利なlink_to_unless_currentメソッド(翻訳)

$
0
0

概要

原著者の許諾を得て翻訳・公開いたします。

Railsの技: ナビゲーションリンクで便利なlink_to_unless_currentメソッド(翻訳)

Railsの古典的なlink_toヘルパーメソッドは皆さんもとっくにお馴染みでしょう。しかしlink_to_unless_currentという変種はご存知ですか?

link_to_unless_currentメソッドの動作は、「リンク先がブラウザの現在のURLと同じ場合はリンクを生成しない」「それ以外の場合はlink_toと同じ」です。

使い方

link_tolink_to_unless_currentに置き換えるだけで完了です。そのページを表示していない場合は<a>タグをレンダリングし、そのページを表示している場合はリンクテキストだけをレンダリングします。

<nav class="flex flex-col space-y-1">
  <%= link_to_unless_current "Dashboard", dashboard_path %>
  <%= link_to_unless_current "Team", teams_path %>
  <%= link_to_unless_current "Projects", projects_path %>
  ...
</nav>

/dashboardを開いている場合は、上のテンプレートから以下のHTMLが生成されます。

<nav class="flex flex-col space-y-1">
  Dashboard
  <a href="/teams">Teams</a>
  <a href="/projects">Projects</a>
  ...
</nav>

リンク名の代わりにブロックを渡してレンダリングすることもできます。これは共有ビューを作成するときに重宝します。たとえば、Postのアクションを持つ以下のようなパーシャルを作成できます。

<%= link_to_unless_current "+ Comment", new_comment_path do
  link_to "Back", posts_path
end %>

類似のヘルパーメソッド

ご想像のとおり、link_to_iflink_to_unlessといった類似のヘルパーメソッドを使えばリンクを生成するかどうかの条件を指定できます。

以下のように「通常はPostのリストをすべて表示する」「ただしadminとしてログインしている場合はリストのタイトルをeditページへのリンクに変える」ことも可能です。

<% @posts.each do |post| %>
  <%= link_to_if @user.admin?, post.title, manage_post_path(post) %>
<% end %>

参考資料

関連記事

Railsの技: pluckはActive RecordモデルでもEnumerableでも使える(翻訳)

Rails 7でActiveStorage::Streamingサポートが追加(翻訳)

$
0
0

概要

原著者の許諾を得て翻訳・公開いたします。

Rails 7でActiveStorage::Streamingサポートが追加(翻訳)

Active Storageが登場したことで、Railsのファイル保存が多くの点で改善されました。しかしファイルの「抽出方法」「ダウンロード方法」「ストリーミング方法」についてのサポートはわずかでした。

ダウンロードなどの基本的な機能についてはアプリで簡単にサポートできますが、より大きなファイルや動画ファイルはストリーミングにできると便利です。Netflixの時代はコンテンツを見るのに1分以上待ってくれる人などいません。配信はただちに行う必要があります。

これがストリーミングの利点です。ダウンロード方式ではファイルが最後までローカルコンピュータに読み込まれるまで待つことになりますが、ストリーミングならデータの一部(チャンク)だけを表示できるので、ユーザーの待ち時間が大幅に短縮されるのが便利です。Railsでストリーミングをサポートする方法を見てみましょう。

Active Storageからストリーミングするコントローラを独自に作りたい場合、ストリーミングを正しく行うメソッドがあると便利です。ストリーミングについてはActiveStorage::BaseControllerでもある程度のレベルまでサポートされていましたが、データが抽出されていないため使い勝手がよくありませんでした。

変更前

アップデート前のストリーミングで利用できるのは、いくつかの大まかな機能だけでした。

ブラウザがストリーミングファイルの表示機能をサポートしている場合は、以下のように書きます。

response.headers["Content-Type"] = @model.image.content_type
response.headers["Content-Disposition"] = "inline; #{@model.image.filename.parameters}"

@model.image.download do |chunk|
  response.stream.write(chunk)
end

この場合、ブラウザが表示すべきファイルの種類を認識するために、レスポンスヘッダーを手動で設定する必要がある点にご注目ください。ありがたいことに、今ならもっと良い方法があります。

変更後

コントローラでのファイルストリーミングをネイティブにサポートするメソッドが追加されました(#41440)。これによって導入されるActiveStorage::Streamingモジュールを任意のコントローラにincludeして#send_blob_streamメソッドを利用できるようになります。このメソッドには、クラウドストレージからblobをストリーミングするActionController::Base#send_streamがラップされています(changelog)。

コード例を見てみましょう。ここでは仮に「next Netflix」サービスを構築中だとしましょう。当然、動画のストリーミング機能が必要ですが、#send_blob_streamメソッドを用いて動画ファイルを渡すだけでできるようになります。このメソッドのパラメータでContent-Dispositionヘッダーも指定できます。指定なしの場合はContent-Dispositionが”inline”になります。

class VideoController < ApplicationController
  include ActiveStorage::SetBlob, ActiveStorage::Streaming

  def show
    http_cache_forever(public: true) do
      send_blob_stream @video, disposition: params[:disposition]
    end
  end
end

以上で完成です。これはRailsアプリケーションでストリーミングをネイティブにサポートする手軽な方法です。

関連記事

Railsの技: ナビゲーションリンクで便利なlink_to_unless_currentメソッド(翻訳)

Webpacker v6.0.0.beta.6の現時点の変更点について

$
0
0

こんにちは、hachi8833です。

以下の記事を翻訳しようと思って動作確認するうちにWebpacker 6.0の開発が進められていることに気づいたので、変更点がどのぐらいあるかをとりあえずメモしてみました。Webpacker 5.0からの変更点、割と多い…😅

Webpacker 6.0が正式にリリースされたら試してみようと思います。

本記事の内容は流動的であり、Webpacker 6.0のリリースまでに変わる可能性があります。

Webpacker 6.0は現在beta.6

rails/webpacker - GitHub

本記事執筆時点では、v6.0.0.beta.6タグ(2021/02/27)まで進んでいます。マイルストーンなどは見当たらないので具体的なリリース予定日は不明です。

また、Webpacker 6はwebpack 5と互換だそうです↓。

なお、Rails本家のWebpackerガイドは今のところWebpacker 5のままのようです↓。

Changelogより

未確定ですが、現時点のChangelogをメモしました。

  • node_modulesは今後デフォルトではコンパイルされなくなる。これにより、主にRailsの#35501やWebpackerの多数のissueが修正される。無効になったローダーは、引き続き以下のように明示的なrequireが必要な可能性がある。
const nodeModules = require('@rails/webpacker/rules/node_modules.js')
environment.loaders.append('nodeModules', nodeModules)
  • environment.jsにenvironment.loaders.delete('nodeModules')を追加した場合は削除しなければならない(でないとItem nodeModules not foundエラーが発生する)。
  • インストールタスクではすべての環境のextract_cssがデフォルトでtrueに設定される。また、デフォルトのapplication packに代えて個別のapplication.cssファイルが生成される。これはWebpacker 5.0.0で導入されたエントリごとの複数ファイルサポートによる(#2608)。
  • WebpackerのsplitChunks() APIのラッパーはデフォルトのruntimeChunk: 'single'になる(ページごとのマルチエントリポイントを使う場合の潜在的な問題防止に役立つ)(#2708)。
  • @babel/preset-envのオプションが、Babelドキュメントの推奨値であるautoに変更される(#2709)。
  • Yarn 2のサポート(experimental)。.yarnrc.ymlファイルにnodeLinker: node-modulesを手動で設定しなければならない点に注意。
  • webpack-dev-serverの問題を修正(#2898

breaking changes

  • webpackのコンフィグがシンプルになる
  • 統合インストーラが削除される
  • Splitchunks がデフォルトで有効に
  • extract_cssがデフォルトで有効に
  • オプショナルのCSSサポート

Webpacker 6.0アップグレードガイドより

6.0アップグレードガイドには、以下のようにさまざまな変更が記載されています。


設定ファイル v5 v6
config/webpacker.yml source_path: app/javascript source_path: app/packs
config/webpacker.yml source_entry_path: packs source_entry_path: entrypoints

app/javascript/packsディレクトリもapp/packs/entrypointsに変えることになります。


以下の既存の設定ファイルやディレクトリの内容が変更されるので、アップグレード時に一時的にリネームが必要です。

  • config/webpack/(ディレクトリ)
  • config/webpacker.yml(ファイル)

v5の以下のメソッドはv6で置き換えが必要です。

v5 v6
javascript_packs_with_chunks_tag javascript_pack_tag
stylesheet_packs_with_chunks_tag stylesheet_pack_tag

CSSやReactやTypeScriptの6.0でのインテグレーション方法については以下を参照。


従来のconfig/webpack/environment.jsはconfig/webpack/base.jsに変わります。これに伴い、environmentをbase.jsではwebpackConfigに置き換えます。

// config/webpack/base.js
const { webpackConfig, merge } = require('@rails/webpacker')
const customConfig = require('./custom')

module.exports = merge(webpackConfig, customConfig)

.browserlistrcファイルが存在する場合は"browserlist"キーをpackage.jsonに転記して.browserlistrcを削除します(ちなみに.browserlistrcが残っているとエラーになりました)。


webpacker.ymlからextensions項目が削除されます。既存のextensionsをカスタマイズした場合は以下のようにresolveの下に移転する必要があります。詳しくはWebpackの設定を参照。

{
  resolve: {
      extensions: ['.ts', '.tsx']
  }
}

関連記事

Rails: Webpacker 5.1.0でcheck_yarn_integrityオプションが廃止された

【保存版】Rails 5 Webpacker公式ドキュメントの歩き方+追加情報

週刊Railsウォッチ(20210419前編)RailsのN+1クエリを定番以外の方法で修正する、GitLabのセキュリティ修正リリースほか

$
0
0

こんにちは、hachi8833です。RailsConf 2021をすっかり見逃してました😇


つっつきボイス:「RailsConf 2021、そういえば開催時期だったか」「つっつき中の今日(04/15)がラス日でした🙇」「キーノートのページにコミットでよく見かける顔が並んでますね」

「RailsConf 2021のfaqを見た感じでは有料イベントのようですね」「あ、そうでしたか」「参加者は後で動画を見られるそうですが、一般公開するとは書かれてない: たしかに有料でしかもリモート開催のイベントの動画を後で無料公開したら有料で登録する人がほとんどいなくなってしまいますよね」「それもそうか…」「スライドだけでも見られればと思ったけどしょうがないですね」


以下はつっつき後に見つけたツイートです。

週刊Railsウォッチについて

  • 各記事冒頭には🔗でパーマリンクを置いてあります: 社内やTwitterでの議論などにどうぞ
  • 「つっつきボイス」はRailsウォッチ公開前ドラフトを(鍋のように)社内有志でつっついたときの会話の再構成です👄
  • お気づきの点がありましたら@hachi8833までメンションをいただければ確認・対応いたします🙇

TechRachoではRubyやRailsなどの最新情報記事を平日に公開しています。TechRacho記事をいち早くお読みになりたい方はTwitterにて@techrachoのフォローをお願いします。また、タグやカテゴリごとにRSSフィードを購読することもできます(例:週刊Railsウォッチタグ)

🔗Rails: 先週の改修(Rails公式ニュースより)

今回は以下のコミットリストのChangelogを中心に見繕いました。

🔗 エラーページのCSSとアクセシビリティを改善


つっつきボイス:「developmentモードで表示されるエラーページのスタイルをインライン(style=による直書き)ではなくちゃんとCSSのクラスで書き直したのか↓」「開発中によく見かける赤いエラーページですね」

# actionpack/lib/action_dispatch/middleware/templates/rescues/_message_and_suggestions.html.erb#L13
    <% corrections.each do |correction| %>
-     <li style="list-style-type: none"><%= h correction %></li>
+     <li class="correction"><%= h correction %></li>
    <% end %>

<div>タグも、意味を表せるHTML5の<main>タグに置き換えてる↓」「考えてみれば、developmentモードのエラー画面をスクリーンリーダーで読む人がいる可能性もありそう」「だからアクセシビリティ改善なんですね」

# actionpack/lib/action_dispatch/middleware/templates/rescues/blocked_host.html.erb#L4
-<div id="container">
+<main role="main" id="container">
  <h2>To allow requests to <%= @host %>, add the following to your environment configuration:</h2>
  <pre>config.hosts << "<%= @host %>"</pre>
-</div>
+</main>

参考: HTML: アクセシビリティの基礎 - ウェブ開発を学ぶ | MDN


「ところで、accessibilityをa11yって略さなくてもいいんじゃないかしら」「i18n(internationalization)やm17n(multilingualization)ぐらい長ければ略すのもやむなしな気もしますけど」「Kubernetesも大して長くないけどK8sと略されたりしますね」

🔗 ActiveSupport::Duration#partsのハッシュをfreeze


つっつきボイス:「partsが返すハッシュをfreezeして返すことで高速化したらしい」「ベンチマークでも速くなってますね」「ライター系メソッドも削除したのか」「freezeしたんだからライター系メソッドはあったらマズいでしょうね」「あ、たしかに」

# activesupport/lib/active_support/duration.rb#L212
-   def initialize(value, parts) #:nodoc:
+   def initialize(value, parts, variable = nil) #:nodoc:
      @value, @parts = value, parts
      @parts.reject! { |k, v| v.zero? } unless value == 0
+     @parts.freeze
+     @variable = variable
+
+     if @variable.nil?
+       @variable = @parts.any? { |part, _| VARIABLE_PARTS.include?(part) }
+     end
+   end
+
+   # Returns a copy of the parts hash that defines the duration
+   def parts
+     @parts.dup
    end

「Active SupportのDurationは処理によっては利用頻度が高そうなので、高速化されるのはありがたい🙏


「DateやTimeの差からDurationを得られるのは便利ですよね」「他の言語だといったんmsecに変換するとかいろいろ面倒になりがちですが、ActiveSupport::Duration.build(time1 - time2)のようにするとDurationが得られますね」

「もっとも、たとえばActiveSupport::Duration.build(time1.beginning_of_day - time2.beginning_of_day)がサマータイム切り替わりをはさんだときに常に日単位になってくれるかどうかは解釈によっては難しいですよね: 1日が23時間や25時間でも1日と解釈するのか、それともあくまで1日=24時間として解釈するのか、とか」「う、そうですね😅」「日付は難しい…」

DurationはValue Objectであり、ミューテーションされるべきではない。
この変更によって、Durationが変数であるかどうかにかかわらずキャッシュできるようになり、ActiveSupport::Duration+メソッドや-メソッドのパフォーマンスが改善される。さらにany?やインラインのarray定義を避けられるので、arrayが2個アロケーションされるのも回避できる。アプリ全体のパフォーマンスが著しく改善されるとまではいかないが、duration_of_variable_length?メソッドは2.5倍速くなる。

Warming up --------------------------------------
             current   325.663k i/100ms
                 new   828.177k i/100ms
Calculating -------------------------------------
             current      3.274M (± 2.9%) i/s -     16.609M in   5.076744s
                 new      8.337M (± 1.9%) i/s -     42.237M in   5.067887s

Comparison:
                 new:  8337279.2 i/s
             current:  3274297.7 i/s - 2.55x  (± 0.00) slower

parts=メソッドとvalue=メソッドはDurationで決して使うべきではないので、削除した。

さらにpartsがfrozen hashのコピーを返すようにし、内部でdupを使わずにpartsにアクセスできるよう_partsリーダーも追加した。
同PRより大意

🔗 ActiveSupport::TimeZone#utc_to_localを修正

utc_to_local_returns_utc_offset_timesがfalseでtimeインスタンスに小数の秒がある場合、Time.utc コンストラクタが小数秒の値ではなくusec(マイクロ秒)の値を取るため、新しいUTC timeインスタンスが1,000,000のファクターでずれていた。
Changelogより大意


つっつきボイス:「usecって何だったかなと思ったらマイクロ秒(μsec)の簡易表記なんですね」「そうそう」

参考: ミリ秒とは - IT用語辞典 e-Words

「ローカルのシステムはマイクロ秒も扱えたと思うので、手元のWSLのコンソール(Ubuntu 20)でdateコマンドやってみよう」(しばし操作)「dateにナノ秒の書式(%N)があったのでナノ秒まで出せた↓(マイクロ秒の書式は見当たらず)」

$ date +"%Y-%m-%d %H:%M:%S.%N"
2021-04-15 20:20:15.172406400

「で、従来はTime.utcで小数点以下の値が小数秒として扱われていなかったので* 1_000_000を追加して修正したようですね↓」「このバグよく見つけたな〜」

# activesupport/lib/active_support/values/time_zone.rb#L513
    def utc_to_local(time)
      tzinfo.utc_to_local(time).yield_self do |t|
        ActiveSupport.utc_to_local_returns_utc_offset_times ?
-         t : Time.utc(t.year, t.month, t.day, t.hour, t.min, t.sec, t.sec_fraction)
+         t : Time.utc(t.year, t.month, t.day, t.hour, t.min, t.sec, t.sec_fraction * 1_000_000)
      end
    end

🔗 Rack::Runtimeミドルウェアが非推奨化

  • Rack::RuntimeFakeRuntimeに置き換えられる。FakeRuntimeはリクエストを渡すだけのダミーのミドルウェアで、ミドルウェア操作では利用できない。
  • ミドルウェア操作(相対的なinsertmoveなど)でRack::Runtimeを使うとFakeRuntimeを使うようdeprecation warningを表示する。
  • アプリケーションが(useunshiftなどで)Rack::Runtimeを明示的に追加する場合はdeprecation warningは表示されず、FakeRuntimeは無視される。
  • RailsガイドはRack::Runtimeを参照しないよう更新される。
    同PRより大意
# actionpack/lib/action_dispatch/middleware/stack.rb#L6
module ActionDispatch
  class MiddlewareStack
+   class FakeRuntime
+     def initialize(app)
+       @app = app
+     end
+
+     def call(env)
+       @app.call(env)
+     end
+   end
+

つっつきボイス:「Rack::Runtimeって何をするんだろう?」「このissue↓を見ると、HTTPのX-RUNTIMEヘッダーを追加するミドルウェアみたいですね」「X-RUNTIMEでググると結構古い記事が出てきた」「元々デバッグ用なのかも?」

X-RUNTIMEはサーバーの処理にかかった時間を返すんですね」「今まではRack::RuntimeX-RUNTIMEを返してたけど、X-RUNTIMEが必要な人はほとんどいなさそうだし、セキュリティ上の懸念もあるので削除しようという流れっぽい」「こんなふうにRack::Runtimeに依存したコンフィグを書いてる↓人がいる可能性もあるので、いったんdeprecationをはさんでから削除するんでしょうね」

# #38412コメントより
config.middleware.insert_before(::Rack::Runtime, SomeCoolMiddleware.new)

参考: x-runtime は消すべきなのか - Qiita

「このプルリクを見なかったらRack::Runtimeを知ることは一生なかったかも😆」「X-RUNTIMEを使う人って相当マニアックな感じしますね」「New RelicのようなアプリケーションメトリクスツールならX-RUNTIMEヘッダーを使うことがあるかもしれませんね」「あ、ありそう!」

参考: New Relic | パフォーマンス分析プラットフォーム

🔗 番外: Webpackerがいつの間にかv6.0.0.betaに

rails/webpacker - GitHub


つっつきボイス:「Evil Martiansの以下の記事↓を翻訳しながらチェックしていたら、Webpacker 6の開発が思ったより進んでいることに気づいて、今記事を書きかけています(公開済み)」

参考: Set up Tailwind CSS JIT in a Rails project to compile styles 20x faster — Martian Chronicles, Evil Martians’ team blog

「Webpacker 5がリリースされたのは昨年ぐらいでしたっけ?」「リリースタグを見ると2020年3月だから1年前ぐらいですね」「早いな〜」「Webpacker 5から6への移行がそんなに手間でなければ大丈夫かなと思いますけど」「でも現時点の6.0アップグレードガイド↓を見ると割と変更点あるようです」「あ〜、source_pathとかも変わるのか」「コンフィグ洗い替えが必要っぽいですね」「Webpacker 5を頑張って導入してカスタマイズしていたら大変そう…」「容赦なき変更」


つっつき後、以下の記事に現状をメモしました↓。

Webpacker v6.0.0.beta.6の現時点の変更点について

🔗Rails

🔗 マネーフォワードのマイクロサービス化インタビュー記事


つっつきボイス:「マネーフォワードの中の人へのインタビューが本当に生々しかったので取り上げてみました」「なるほど、現在はリクエストの途中にRailsをはさんでいるのを将来Goで直接処理しようとしてるんですね」

中出: まず前提として先ほど申し上げたように、まずRuby on Railsでできた巨大なSaaSがありまして、それを今マイクロサービスに置き換えている最中です。現在のアーキテクチャとしては、ユーザーのリクエストがRailsに届き、それがGoでできたマイクロサービスを呼び出すという形になっています。将来的には、Railsを通さずに直接Goのマイクロサービスを呼び出す形に移行していくことを考えています。
同記事より

「マイクロサービス化はまだ始まったばかりだそうです」「マネーフォーワードのようにお金に関連するサービスだと大変そう」「マネーフォワードのサービス内容を考えると、周辺機能はともかくコア機能はマイクロサービスにしにくそうな気がしますね: 機能を横断しないと取れないものはマイクロサービス化が難しくなる」「そうですよね…」


「ところで、ひと頃のマイクロサービス化しようぜみたいな話も最近だいぶ落ち着いてきた感じはありますね」「言われてみれば前より静かになったかも」「表に出ていない部分も含めて、マイクロサービス化に挑戦して失敗したところは結構あるんじゃないかな」

「もしかすると本当に欲しかったのはマイクロサービスじゃなくて『多少モジュラライズされたモノリス』だったのかも」「気持ちわかります」「巨大モノリスのままだとつらいのは確かなので、モノリスにマイクロサービスのパラダイムを一部取り入れることで、モノリスを適切に分割するときの設計上の参考にするのはありだと思いますし、上の記事はそういうときに参考になりそうですね👍


つっつきの後で、以下の「シタデルアーキテクチャ」をやっと思い出しました↓。

Rails: AppSignalが採用する「シタデルアーキテクチャ」(翻訳)

🔗 rollout gemで「フィーチャーゲート」を実現する

fetlife/rollout - GitHub

fetlife/rollout-ui - GitHub


つっつきボイス:「Engine Yardの記事です」「フィーチャーゲートはいわゆるフィーチャーフラグ的な機能かな: この機能はいろんな呼び方があるんですよ」「あ、そうなんですね」

参考: フィーチャートグル - Wikipedia

「ちなみに、ちょうどこの構成によく似た案件をこれから始めるところです↓」「へ〜」「RailsのAPIをURLベースでカレントから新しいものに切り替える感じで、よく行われる作業ですね」


同記事より


「そういえば自分も今Rails 4の案件やってます」「Rails 4ぐらい古いものをアップグレードする場合、小さなアプリだったら頑張って少しずつRails 6までアップグレードするより、rails newしてそっちにコードを移す方が早いんじゃないかって思いますね」「そうそう、少しずつアップグレードするのって意外としんどいですよね」「その途中でテスト系のgemがつかえたりするとアプリ本来の挙動と関係ないところで力を使わないといけなくなってつらい」

「でもアプリが大きいとなかなかそうもいかないので、順当に少しずつアップグレードすることになりますけど」「手間はかかりますけど、ステップバイステップでアップグレードする方が明らかに安全ですよね」「進捗も出しやすいですし」「rails newしてコードをコピペする方式だと、完全に移し終わるまで全体をテストできないんですよね」「コードベースが大きいとリスクが高すぎる」「そういうことがありうるので巨大モノリスにするのは避けたい気持ちがあります」

🔗 .countでRailsアプリが落ちることがある(Ruby Weeklyより)


つっつきボイス:「.countでそんなに遅くなるものかな?」「あ、テーブルの行数がでかいのか」「30秒経過してもクエリが返ってこないのはつらそう」「クエリが返るまで数分かかるRedmineの対応したことならありますよ」「お〜」「なかなか最適化しがいのあるクエリでした😆

「特定のクエリだけがものすごく遅い場合なら、頑張って最適化すればたいてい速くできますし、結果の整合性も取りやすいですね」「お〜」「逆に、複雑にテーブルをまたぐようなN+1クエリを手動で定数回に最適化する方がしんどい: 数百msecのクエリが数百件とか」「たしかに」「N+1の最適化は油断すると結果を変えてしまうことがあるんですよ」

「記事の結論は、.count > 0.count.positive?をもっと速いメソッドに置き換えましょうということか」「遅いクエリの最適化は頑張りが大事」「たしかActiveRecord::Relationempty?はレコードが1件あるかどうかがわかればいいので、COUNT()を使わずLIMIT 1を使った実装になっていて速かった覚えがありますね」「なるほど」

参考: 【Ruby on Rails】ActiveRecordのexists?の逆はempty?を使う - Qiita

🔗 N+1クエリを定番以外の方法で修正する


つっつきボイス:「ちょうどN+1クエリの記事です」「non standardなN+1クエリ修正ですか」

「ちなみに最初の見出し『N+1 queries 101』の101は、英語圏では『入門』の意味ですね」「そうそう、海外の大学のシラバスだと最初に履修する授業の番号が101になってることが多い」「へ〜、101ってそんな意味なんですね」

# 同記事より
# app/models/post.rb

class Post < ApplicationRecord
  belongs_to :user

  def author_name
    user.name
  end
end

# app/models/user.rb

class User < ApplicationRecord
  has_many :posts
end

# app/controllers/posts_controller.rb

class PostsController < ApplicationController
  def index
    @posts = Post.published
  end
end

「入門の次は、Scout APM↓を使ってメトリクスを取ってる」「APM(Application Performance Monitoring)ツールはN+1クエリの検出には有効ですね: N+1クエリが確実に発生していて、かつ実際に遅くなっていることを確かめてから対策する方がいい👍」「そうですね」「N+1クエリが発生していても実用上問題ない速度が出ていれば、結果が変わってしまう可能性のある更新を行わずに済むときもあります」


「方法その1は、Active Recordのキャッシュを作って手動でヒットさせる方式↓」「たしかにRailsだとnon standard」「この方法が有効なケースは、限定的ですがたしかにあります: SQLでJOINを駆使してもきれいに取れないケースがたまにあるんですが、そういうときはこのように展開済みの一時的なハッシュを作ったりしますね」「なるほど!」「これをRedisに入れられればリクエストをまたいで共有できるので便利です」

# 同記事より
class PostsController < ApplicationController
  def index
    @posts = Post.published.includes(:comments)
    @users = User.active
    @users_cache = @users.reduce({}) do |agg, user|
      agg[user.id] = user
      agg
    end
  end
end

「Rails標準の方法だとeager loadingするんですが、それが効かないケースにもこういう書き方をすることがありますね」


「方法2は、引数や戻り値をオブジェクトではなくプリミティブな値にする ↓: スコープでpostオブジェクトを渡すとpost.user.idの部分でクエリが発生するので、生のuser_idを受け取る方がいい、たしかに」「あ〜、なるほど」「効くかどうかは内部の処理にもよりますし、呼び出し方も変えないといけないので、効果を確かめてから使う方がいいと思います」

# 同記事より
class Post < ApplicationRecord
  scope :by_author, -> (user_id) {
    where(user_id: user_id)
  }
end

「方法3はリレーションシップのショートカットを作る」「この手法もときどき使いますね: リレーションを普通にたどるとuser.account.activity.last_active_atのように長くなりますけど、UserのidをActivityにも置くことで以下のようにactivity.last_active_atと書けるようにする↓」

# 同記事より
class Post < ApplicationRecord
  belongs_to :user
  belongs_to :activity

  def author_last_active_at
    activity.last_active_at
  end
end

「正規化するならuser.accountをたどればUserのidを取れるのでActivetyにUserのidを置く必要はないんですが、置くことで参照の距離を縮めるという発想ですね」「お〜、なるほど!」

「マルチテナントのアプリケーションなどでこの方法を使うことがあります: テナントのidは利用頻度が高いんですが、きれいに正規化すると距離が離れてしまってJOINを何度も行わないと取れなくなるので、テナントのidを途中のテーブルにも持たせたりします」「記事には『やりすぎ注意』とありますね」「そう、この方法だと正規化を崩すことになるので、データ不整合が発生したら悲惨なことになります😇」「たしかに」「あと、外部キーを付けすぎるとINSERTが重くなる可能性もあります: いずれにしろ十分検討してからにしましょう」


「方法4の『リレーションシップのデータ複製』もよく使いますね: マテリアライズドビューあたりがこれに近い方法かな」「なるほど」「テーブルパーティショニングもこの一種でしょうね: 直近1か月のデータだけ別テーブルに切り出してすぐ取り出せるようにするとか」「ログデータなんかもそうやったりしますね」

参考: マテリアライズドビュー(マテビュー)とは - IT用語辞典 e-Words
参考: 5.11. テーブルのパーティショニング


「記事タイトルはnon standard wayとあるけど、これはRailsの標準ではないというだけで、Railsに限らない一般的なN+1クエリの解決ではどれも普通に使われる方法だと思います」「あ、そういうことでしたか」「むしろRailsでN+1解決というとすぐeager loadingの話になる方が一般的でないんじゃないかな」「まあたしかに😅」「記事で紹介されている方法は少なくともバッドプラクティスではありませんね: いい記事👍

🔗 GitLabのセキュリティ修正がリリース


つっつきボイス:「さっきHacklinesに流れてたので取り上げました」「あ、ログインユーザーが画像アップロードしてコード実行できるのは結構エグい!社内GitLabサーバーにも赤ランプ点灯してるし↓、週末に適用しなきゃ」

つっつき後の日曜日に無事アップデート完了しました。


前編は以上です。

バックナンバー(2021年度第1四半期)

週刊Railsウォッチ(20210413後編)RubyMineのRBSサポートとCode With Me、GitHub ActionとDockerレイヤキャッシュほか

今週の主なニュースソース

ソースの表記されていない項目は独自ルート(TwitterやはてブやRSSやruby-jp SlackやRedditなど)です。

Rails公式ニュース

Ruby Weekly

週刊Railsウォッチ(20210420後編)ShopifyのJITコンパイラYJIT、PicoRuby、DynamoDBの3つの制約ほか

$
0
0

こんにちは、hachi8833です。

週刊Railsウォッチについて

  • 各記事冒頭には🔗でパーマリンクを置いてあります: 社内やTwitterでの議論などにどうぞ
  • 「つっつきボイス」はRailsウォッチ公開前ドラフトを(鍋のように)社内有志でつっついたときの会話の再構成です👄
  • お気づきの点がありましたら@hachi8833までメンションをいただければ確認・対応いたします🙇

TechRachoではRubyやRailsなどの最新情報記事を平日に公開しています。TechRacho記事をいち早くお読みになりたい方はTwitterにて@techrachoのフォローをお願いします。また、タグやカテゴリごとにRSSフィードを購読することもできます(例:週刊Railsウォッチタグ)

🔗Ruby

🔗 YJIT: もうひとつのJITコンパイラ(Ruby Weeklyより)


つっつきボイス:「YJITのYはyet another」「Shopifyがやっているそうです」「Shopifyは例のSorbet↓をはじめ、いろいろ手広くやってますね」

「Shopifyが自分たち向けにRubyをカスタマイズしている様子は、FacebookがPHPをカスタマイズしてHack言語を作ったのを思わせますね: ShopifyはRuby標準にこだわらないけどRuby関連イベントにも頻繁に登場していますし、いいものはRubyに還元しようというスタンスも感じられます」「たしかに」「Hackっていう言語名、もうちょっと何とかして欲しかった」

参考: Hack (プログラミング言語) - Wikipedia

「ちなみにRuby Weeklyに載っていたのは上の動画でしたが、リポジトリとしては以下のドキュメントとyjit-benchしか今のところ見つかりませんでした↓」「early stageとあるのでこれからかな」「MJITだけだと限界に突き当たるかもしれないので、YJITのようないろんなアプローチがあるのはいいと思います👍

Shopify/yjit-bench - GitHub


「ところで、今のCRubyの構成だとJITは大変そうなので、今後は言語のエンジン(VM)もJITに適したものに仕様が変わっていくこともあり得るかなと思いました」「ふむふむ」「JRubyが速いのは、JVMの上で動いているからというのも大きいですよね」

「最近のRubyでは、従来C言語で書かれていたメソッドをRubyで書き直すことがちょくちょく行われていますけど、あれはJITを効きやすくするという側面もあると思います」「たしかに最近よくやってますよね」「Cより速くなることもあると何かで見ました」「CライブラリのままだとJITでのメモリ再配置が難しいと思うので、 C -> Rubyへの書き換えはそれはそれで進みつつ、今後どこかのタイミングでVM自体もJITが効きやすくなる変更を入れていこう、みたいな話になるのかもしれませんね」

🔗 PicoRuby: ワンチップマイコン向けRuby


つっつきボイス:「ピコルビー?」「ラズパイとかに乗っける用ですか?」「Raspberry Piよりもっともっと制約の大きいワンチップマイコン向けですね: ラズパイならピコどころかメガ・ギガの世界ですよ」「ホントだ、ROM 256KB以下、RAM 64KB以下とか書かれてる」

参考: Raspberry Pi - Wikipedia

「PicoRubyに近いのは、いわゆるmrubyではなくmruby/cの方でしょうね↓: PicoRubyはGPIOにも対応していますし、チップに焼き込む前提っぽい」

mrubyc/mrubyc - GitHub

参考: GPIO - Wikipedia

「記事ではキーボードファームウェアをRubyで作っていて、その中のkeymapなどのカスタマイズ部分について解説しているようですね」「自作キーボード勢の間でこうやってキーボードファームウェアを作るのが流行ってるみたいですよ」


同記事より

🔗 mechanize:クローラ作りに便利なgem(Ruby Weeklyより)

sparklemotion/mechanize - GitHub


つっつきボイス:「mechanize、久しぶりに見たな」「あ、これ有名なgemなんですか?」「mechanizeはまさにクローラ的なインターフェイスを持っているので、昔からRubyでクローラを作るのによく使われます」「へ〜!」「nokogiri gemはHTTPをパースするのによく使われますが、mechanizeはクローラ専用と言ってもいいぐらい特化しているのが特長です」

sparklemotion/nokogiri - GitHub

「mechanizeにあるこういうfield_withcheckbox_withみたいなメソッドは、クローラやE2E自動テストみたいなブラウザの挙動を模した操作ができるインターフェースですね↓」「お〜」

# https://docs.seattlerb.org/mechanize/Mechanize/Form.html#method-i-field_with-21-28criteria-29
form.field_with(:id => "exact_field_id").value = 'hello'
# https://docs.seattlerb.org/mechanize/Mechanize/Form.html#method-i-checkbox_with-28criteria-29
form.checkbox_with(:name => /woo/).check

「おそらくCapybaraなどでやるより速いんじゃないかな: Capybaraはもう少し上のレイヤの動きも模倣しますけど、mechanizeはクローラ的なこと以外はやらないのでその分速そう」「そういえば最近クローラ作ってないな、また作ろうかな」

teamcapybara/capybara - GitHub

🔗DB

🔗 知っておきたいDynamoDBの3つの制約(DB Weeklyより)

参考: Amazon DynamoDB(マネージド NoSQL データベース)| AWS

  1. The item size limit;
  2. The page size limit for Query and Scan operations
  3. The partition throughput limits
    同記事より

🔗 1. The item size limit

つっつきボイス:「最大400KB/行なのか」「これに引っかかるほど大きなデータは普通入れないと思いますけどね」「MongoDBの16MBより小さいですね」「MongoDBのつもりで使うとハマりそう」「巨大なJSON blobをツッコむなと記事にも書かれていますね」「そもそもDynomoDBはMongoDBみたいなネステッドなクエリをあまりサポートしていなかったと思うので、MongoDB的な使い方は普通しないんじゃないかな」

参考: BLOB(Binary Large OBject)とは - IT用語辞典 e-Words

🔗 2. The page size limit for Query and Scan operations

「2.にもクエリの制約の話があるように、DynamoDBにはあまりにも巨大なデータを入れない方がいいでしょうね」「小さいものをたくさん入れるのはいいけど、バカでかいものを入れるなよと」「それにDynamoDBは、レコード数があまりに多いときにインデックスにヒットしないクエリを投げてしまうとリソースを一瞬で使い切ってしまいます」「そうそう!」「記事で1リクエストあたり最大1MBとあるのが2.で言うクエリのpage size limitです」

「DynamoDBはread capacity unitの設定が難しいんですよ: 内部的にはHTTPリクエストを細かく分割したような感じでページングというかブロック転送的にデータを取ってくるんですけど、ヒットした結果が大きいと1リクエストに入れられるレコード数が減ってしまうんですよ」「ああなるほど!」「逆にヒットした結果が小さければ1リクエストに入れられるレコード数は増えます: read capacity unitは課金にも関連しますし、単純にレコード数だけで見積もれないので行サイズなども考慮しないといけないのが面倒」

参考: Managing Settings on DynamoDB Provisioned Capacity Tables - Amazon DynamoDB

「2.のスキャンはどのキーでも検索できるんですが、テーブルの全行をスキャンするのでテーブルサイズが巨大になるとすごいことになります」「へ〜」「2.のクエリは必要なものだけを取れますが、partition keyが設定されているものしか絞り込みができない」「なるほど」「このあたりはDynamoDBを一度使ってみるとわかります」

🔗 3. The partition throughput limits

「3.のpartition throughput limitは、記事によると1パーティションあたりのreadとwriteのcapacity unitにそれぞれ上限があるのか」

🔗 DynamoDBのモード

「DynamoDBにはオンデマンドキャパシティーモードとプロビジョニング済みキャパシティモード(デフォルト)という2種類の課金体系があって↓、後者は消費したユニット数が単位時間ごとに回復するんですが、完全に消費するとAPIエラーになるのがつらい」「本番で起きたら悲しいですね…」

参考: 料金 - Amazon DynamoDB | AWS

「一方前者のオンデマンドキャパシティーモードにはその制限はなく、完全に従量課金になります」「なるほど〜」「使い方に応じてどちらかを選ばないといけません」

「なお以下は2018年の記事↓なので現在の金額と違うと思いますが、料金のグラフの伸び方などは参考になると思います」「ふむふむ」

参考: Amazon DynamoDBをオンデマンド(従量課金)で利用した場合の料金体系まとめ #reinvent | DevelopersIO

「プロビジョニング済みキャパシティモードはlimit exceededエラーになる可能性がある点がつらいので、よほど余裕があるかエラーが起きても構わないものでもない限り、ユーザーリクエストベースのクエリに使うのは怖い」「ごもっともです」「TerraformやCloudFormationのログ置き場とか、バッチ処理のログ置き場に使うなら、プロビジョニング済みキャパシティモードでもまず制限は超えないと思います」

参考: AWS CloudFormation(テンプレートを使ったリソースのモデル化と管理)| AWS

「一般にDynamoDBは、productionに導入する前に自分である程度触っておくのがよいと思います」「そうですね」「いきなり使うと想定外の事態に出くわす可能性がありますし、DynamoDBのクエリ形式は特殊なので慣れないと使いにくいかもしれません」「DynamoDB、いろいろ懐かしいです」「DynamoDBはAWSの中でも歴史が相当長いサービスで、公開前にもAWS内部で相当使われていたはずですね」

🔗 その他DB


つっつきボイス:「遅れてきたエイプリルフールねたで恐縮ですが、SQLクエリにPLEASEを付けないと遅くなるというジョーク記事に、後から続々とそれを実装しましたという追記が伸びてました」「そうそう、SQLは問い合わせ(query)のための言語ですからポリティカルコレクトネスということにしておく感じで」

🔗 pleaseと英語のニュアンスよもやま話

「ところで上の記事では誰も触れていなかったんですが、実は英語のニュアンスとしては、命令形にpleaseを付けても丁寧になるわけではないんですよ」「えぇ!そうなんですか?」「たしかにplease付けても命令形であることは変わらない😆」「丁寧に言うときはWould you~とかCould you〜で始めたりしますよね」

参考: 【英語で依頼】丁寧さを使い分け!ネイティブがお願いに使う表現と、ビジネスメールの依頼文

「日本人としてはついpleaseを日本語の『〜してください』と同じように考えがちなんですけど、どちらかというと『お願いだからやって!』みたいな強調のニュアンスに近くて、結局命令には変わりないそうです」「やべぇ、気をつけようっと😅」「逆に英語の命令形だからといって必ずしもぞんざいとも限らなかったりするのがややこしい…」「しょうがないですよね、自然言語だから」「結局文脈次第ですね」


「それで思い出したんですが、以前ある技術書籍の翻訳許可をメールで出したときにすごく丁寧なお断りメールをもらったことがあって、文面で1箇所たりともnotが使われていないのに全体としてはちゃんとお断りになっていて、これがビジネス英語かと逆に感心した覚えがあります」「notを使わない、わかる!」「ご多幸をお祈りしますメールみたいな」「イギリス英語はそういう婉曲的表現が発達してる印象ありますよね」「イギリス英語はよくわかってないんですがそう思います」

参考: イギリス英語と京都弁の共通点(小野昌弘) - 個人 - Yahoo!ニュース

🔗クラウド/コンテナ/インフラ/Serverless

🔗 Lambda@Edgeの課金変更


つっつきボイス:「Lambda@Edgeの課金の時間が1msec単位になった!」「お〜」

「AWS CloudFrontでBASIC認証を使おうとするとLambda@Edgeに置くしかないんですけど、BASIC認証の処理は数行しかないので一瞬で終わるんですよ」「たしかに10msecもかからなさそう😆」「そんなふうに50msecもかからない処理って割とよくあるので、50msec分も課金されなくなったのはありがたい🙏

参考: Basic認証 - Wikipedia

「Lambdaも少し前にミリ秒単位課金になったので、ごく短時間で終わる処理を書いたときの課金の違いは大きいですね」

参考: 料金 - AWS Lambda |AWS

🔗言語/ツール/OS/CPU

🔗 ジュニアと達人

SuperPaintman/the-evolution-of-a-go-programmer - GitHub

# 同リポジトリより: ジュニアの場合
package fac

func Factorial(n int) int {
    res := 1

    for i := 1; i <= n; i++ {
        res *= i
    }

    return res
}
# 同リポジトリより: シニアおよびRob Pikeの場合
package fac

// Factorial returns n!.
func Factorial(n int) int {
    res := 1

    for i := 1; i <= n; i++ {
        res *= i
    }

    return res
}

つっつきボイス:「Go言語で書かれた素朴な処理なんですが、ジュニアが書いたコードとGoの作者であるRob Pikeのコードがコメント以外まったく同じになっていてウケました」「なるほど、ジュニアの愚直なコードが、関数型やジェネリクスを覚えたりマルチスレッド化対応やasyncをやったりするうちにどんどん複雑になっていく、あるある」「これは面白い😆」「同じコードをRob Pikeが書くと何も言われないのに、ジュニアが書くとレビューで修正依頼が来たりして」「誰が書いたかで変わるのもあるあるですね」

「ちなみにこれは前からあるネタですね↓」「なるほど!これをGo言語でもじったのか」「高校生、大学生、新人と進んで、マスターになるとスゴいのを書いてる」「そしてグルはecho "Hello, world."だけでおしまい😆

参考: The Evolution of a Programmer

「その下の管理職のコードをよく見ると、部下にやっといてメールを出してるだけじゃないですか」「最後のChief Executiveのコード↓が最高」「その辺も含めてもろもろネタですね」

# 同サイトより: 役員の場合
  % letter
  letter: Command not found.
  % mail
  To: ^X ^F ^C
  % help mail
  help: Command not found.
  % damn!
  !: Event unrecognized
  % logout

🔗 その他

🔗 くずし字を読み取るアプリ


つっつきボイス:「お〜、きれい✨」「くずし字を読み取るとふわっとタブレットに文字が浮かび上がるあたりが、Outer Wildsというゲームで古代種族が石版に記録した文字を古代文字スキャナーで読み取るシーンを思わせますね↓」「Outer Wildsもう終わったんですか?」「まだです: このゲームはネタバレされる前にやっとくのがおすすめ」「よ〜し」

参考: 22分後、太陽系は消滅する。宇宙を舞台にしたオープンワールド探索アドベンチャー『Outer Wilds』がNintendo Switchで2021年夏配信決定。 | トピックス | Nintendo

🔗 VSCodeで小説支援

ttrace/vscode-language-japanese-novel - GitHub


つっつきボイス:「VSCodeで小説!」「こんな拡張があるんですね」「青空文庫記法の対応とか便利そう」「こちらの@ttraceことSF作家の藤井太洋氏は、実は大学で3つ下ぐらいの後輩でして」「へ〜!」「デビュー作の『Gene Mapper』という小説は、たしか3.11をきっかけに通勤電車の行き帰りにiPhoneで書いたそうです」

参考: 藤井太洋 - Wikipedia

「しかも小説を書く前は、むしろShade↓という3Dモデリングソフトの専門家というかエバンジェリストとしてその道では有名だったそうです」「Shadeって今も使われてますよね」「そうそう」「大学の頃、彼がAppleの初代ノートパソコンのPowerbookを背中にしょったまま3Dレンダリングしながら学内を駆け回ってたのを思い出します」

参考: Shade3D 公式 | 3DCGソフトウェア
参考: PowerBook - Wikipedia


後編は以上です。

バックナンバー(2021年度第1四半期)

週刊Railsウォッチ(20210419前編)RailsのN+1クエリを定番以外の方法で修正する、GitLabのセキュリティ修正リリースほか

今週の主なニュースソース

ソースの表記されていない項目は独自ルート(TwitterやはてブやRSSやruby-jp SlackやRedditなど)です。

Ruby Weekly

DB Weekly

db_weekly_banner

Railsの技: excerptヘルパーでテキストの一部を抜粋する(翻訳)

$
0
0

概要

原著者の許諾を得て翻訳・公開いたします。

Railsの技: excerptヘルパーでテキストの一部を抜粋する(翻訳)

Railsのexcerptヘルパーは、テキストの中から特定のフレーズにマッチするチャンク(chunk: 小さな塊)を抜粋します。フレーズは文字列のどこにあっても構いません。

たとえば、特定の検索語句とマッチするメールのリストを表示したいとしましょう。

こんなときは、レコードをフィルタしてからメール本文に対してexcerptを実行します。

利用法

仮に、以下のようなビューでこの機能を作るとしましょう。

<%= link_to email do %>
  <div class="flex justify-between items-center">
    <p class="text-sm font-medium text-gray-900">
      <%= email.sender_name %>
    </p>
    <%= local_time_ago email.received_at %>
  </div>
  <p class="text-sm text-gray-700">
    <%= email.subject %>
  </p>
  <p class="text-sm text-gray-500">
    <%= excerpt(email.body.to_plain_text, params[:q], radius: 15) %>
  </p>
<% end %>

Action Textを使っている場合はto_plain_textを呼んでおくのがポイントです(さもないとHTMLタグが閉じないままになる可能性があります)。一般にリッチテキストの抜粋を表示する場合は、書式設定を抜粋全体には適用しないでしょう。

excerptヘルパーを以下のhighlightヘルパーと組み合わせると、さらにいい感じになります。

Railsの技: highlightヘルパーで検索結果を強調表示する(翻訳)

オプション(1)

@product.description = "A one-of-a-kind vintange DHH coffee mug signed by the man himself!"

excerpt(@product.description, "coffee", radius: 10)
=> "...tange DHH coffee mug signe..."

excerptヘルパーは大文字小文字を区別しません。正規表現を渡すこともできます。

@product.description = "A one-of-a-kind vintange DHH coffee mug signed by the man himself!"

excerpt(@product.description, "COFFEE", radius: 10)
=> "...tange DHH coffee mug signe..."

excerpt(@product.description, /tea|coffee/, radius: 10)
=> "...tange DHH coffee mug signe..."

オプション(2)

radius:オプションは、マッチした箇所の前後何文字を抜粋するかを指定します(デフォルトは100)。

@product.description = "A one-of-a-kind vintange DHH coffee mug signed by the man himself!"

excerpt(@product.description, "coffee", radius: 5)
=> "...DHH coffee mug..."

excerpt(@product.description, "coffee", radius: 25)
=> "...e-of-a-kind vintange DHH coffee mug signed by the man hi..."

省略を表す...を変更したい場合は、omission:オプションで指定します。

@product.description = "A one-of-a-kind vintange DHH coffee mug signed by the man himself!"

excerpt(@product.description, "coffee", radius: 10)
=> "...tange DHH coffee mug signe..."

excerpt(@product.description, "coffee", radius: 10, omission: "---")
=> "---tange DHH coffee mug signe---"

excerpt(@product.description, "coffee", radius: 10, omission: "<snip>")
=> "<snip>tange DHH coffee mug signe<snip>"

参考資料

Rails APIドキュメント: TextHelper#excerpt

Rails APIドキュメント: ActionText#to_plain_text

関連記事

Railsの技: pluckはActive RecordモデルでもEnumerableでも使える(翻訳)


Railsの技: Attributes APIでPOROの属性を自動的にキャストする(翻訳)

$
0
0

概要

原著者の許諾を得て翻訳・公開いたします。

日本語タイトルは内容に即したものにしました。

Railsの技: Attributes APIでPOROの属性を自動的にキャストする(翻訳)

Railsアプリでは、ロジックを抽出してPORO(Plain-Old Ruby Object)にすることがよく行われます。しかしコントローラでparamsから直接これらのオブジェクトにデータを渡していることも多く、その場合はデータが文字列になってしまいます。

class SalesReport
  attr_accessor :start_date, :end_date, :min_items

  def initialize(params = {})
    @start_date = params[:start_date]
    @end_date = params[:end_date]
    @min_items = params[:min_items]
  end

  def run!
    # クールな何かを実行する
  end
end

report = SalesReport.new(start_date: "2020-01-01", end_date: "2020-03-01", min_items: "10")

# しかしデータがstringとして保存されてしまう☹
report.start_date
# => "2020-01-01"
report.min_items
# => "10"

普通ならstart_dateはDateで欲しいでしょうし、min_itemsはIntegerで欲しいでしょう。以下のように初歩的な型キャストをコンストラクタに追加する方法も考えられます。

class SalesReport
  attr_accessor :start_date, :end_date, :min_items

  def initialize(params)
    @start_date = Date.parse(params[:start_date])
    @end_date = Date.parse(params[:end_date])
    @min_items = params[:min_items].to_i
  end

  def run!
    # クールな何かを実行する
  end
end

これも悪くありませんが、RailsのAttributes APIを利用してキャストを自動化すればさらによくなります。

利用法

RailsのAttributes APIは、ActiveRecordモデルの舞台裏で属性の型キャストに使われています。たとえば、データベースにdatetimeカラムがあるモデルにクエリをかけて取り出したRubyオブジェクトにはDateTimeフィールドがあります。これはAttributes APIの働きによるものです。

以下のようにActiveModel::ModelモジュールとActiveModel::Attributesモジュールをincludeすると、SalesReportモデルをいい感じに引き締められます。

class SalesReport
  include ActiveModel::Model
  include ActiveModel::Attributes

  attribute :start_date, :date
  attribute :end_date, :date
  attribute :min_items, :integer

  def run!
    # クールな何かを実行する
  end
end

report = SalesReport.new(start_date: "2020-01-01", end_date: "2020-03-01", min_items: "10")

# 属性がネイティブ型になってくれた!

report.start_date
# => Wed, 01 Jan 2020
report.min_items
# => 10

このパターンは、RailsアプリのForm ObjectやReport Objectはもちろん、その他モデル系のRubyクラスにありがちなコードを減らすのに最適です。型キャストを手作りで再実装せずに、フレームワークにお任せできます。

オプション

原注

このモジュールはRails 6.1の時点ではprivate APIです1。ご利用は自己責任でお願いします。

Attributes APIは、ほとんどのプリミティブな型キャストを自動的に処理します。基本的な型はすべて扱えます。

attribute :start_date, :date
attribute :max_size, :integer
attribute :enabled, :boolean
attribute :score, :float

すぐ使える型の全リストはactivemodel/lib/active_model/typeで参照できます。

この機能で最も素晴らしい点は、これらの型で入力を受け付ける方法がきわめて堅牢にできていることです。たとえば、boolean型の属性は、falseに相当する以下のどの値にも対応しています。

FALSE_VALUES = [
  false, 0,
  "0", :"0",
  "f", :f,
  "F", :F,
  "false", :false,
  "FALSE", :FALSE,
  "off", :off,
  "OFF", :OFF,
]

以下のようにすることで、castserializeを実装したカスタム型も登録できます。

ActiveRecord::Type.register(:zip_code, ZipCodeType)

class ZipCodeType < ActiveRecord::Type::Value
  def cast(value)
    ZipCode.new(value) # 扱いが特殊なZipCodeクラスにキャストする
  end

  def serialize(value)
    value.to_s
  end
end

さらに、Attributes APIを用いてデフォルト値も設定できます。

attribute :start_date, :date, default: 30.days.ago
attribute :max_size, :integer, default: 15
attribute :enabled, :boolean, default: true
attribute :score, :float, default: 9.75

参考資料

関連記事

Rails5: ActiveRecord標準のattributes APIドキュメント(翻訳)


  1. ActiveModel::Attributes# :nodoc:と記載されていることを指していると思われます(メソッドの可視性 — API ドキュメント作成ガイドライン)。 

週刊Railsウォッチ(20210426前編)Hotwireの詳細な解説記事3本、Rails 7に入る予定の機能ほか

$
0
0

こんにちは、hachi8833です。

週刊Railsウォッチについて

  • 各記事冒頭には🔗でパーマリンクを置いてあります: 社内やTwitterでの議論などにどうぞ
  • 「つっつきボイス」はRailsウォッチ公開前ドラフトを(鍋のように)社内有志でつっついたときの会話の再構成です👄
  • お気づきの点がありましたら@hachi8833までメンションをいただければ確認・対応いたします🙇

TechRachoではRubyやRailsなどの最新情報記事を平日に公開しています。TechRacho記事をいち早くお読みになりたい方はTwitterにて@techrachoのフォローをお願いします。また、タグやカテゴリごとにRSSフィードを購読することもできます(例:週刊Railsウォッチタグ)

🔗Rails: 先週の改修(Rails公式ニュースより)

今回は以下のコミットリストのChangelogを中心に見繕いました。

🔗 disable_joins:オプションが追加

  • 関連付けのjoinsを無効にするオプションを追加

マルチプルデータベースでは、関連付けをデータベース間でjoinできない。
Railsでこのオプションを設定すると、関連付けのJOINを生成する代わりに、2つ以上のクエリを生成するようになる。

このオプションは、以下のようにhas_namy through関連付けに設定する。

class Dog
  has_many :treats, through: :humans, disable_joins: true
  has_many :humans
end

これにより、SQLでJOINを生成するのではなく@dog.treatsで2つのクエリが使われる。

SELECT "humans"."id" FROM "humans" WHERE "humans"."dog_id" = ?  [["dog_id", 1]]
SELECT "treats".* FROM "treats" WHERE "treats"."human_id" IN (?, ?, ?)  [["human_id", 1], ["human_id", 2], ["human_id", 3]]

Eileen M. Uchitelle, Aaron Patterson, Lee Quarella
changelogより大意


つっつきボイス:「disable_joinsはマルチプルデータベースでは欲しい機能でしょうね」「Active Recordが賢くJOINを生成するのを黙らせるのね」

「コミットメッセージを見ると、GitHubの中で使っているコードを切り出したと書かれてました↓」「お〜、GitHubもRailsのマルチプルデータベース機能を使ってるのかな?」

このコードはGitHub内部で用いているgemから切り出したものである。つまりこの実装はproduction環境で常用しているものであり、実験的コードではない。
同コミットメッセージより

「たしかPostgreSQLやMySQLでは同一RDBMS上のデータベース間でJOINが可能ですね」

参考: 4 Methods for joining data from multiple PostgreSQL databases
参考: mysql - Joining two tables from different Databases [SOLVED] | DaniWeb

🔗 新機能: Enumerable#soleが追加

ActiveRecord::FinderMethods#soleに対応するEnumerable#soleを追加。
このメソッドは、enumerableから1個だけ項目を返す。項目がない場合や2個以上の場合はエラーになる。
Asherah Connor
同Changelogより


つっつきボイス:「そういえばActive Recordにもsoleが追加されていましたね(ウォッチ20210112)」「あったあった」「それのEnumerable版なのか」「1件だけあるかどうかをチェックするコードは自分でもよく書くので、Enumerableにもこの機能があるのはいいですね👍」「知っておくとありがたいメソッドになりそう」

「Active Recordのsoleは該当が1件でない場合はエラーになるんですね」「たしかメソッド名をどうするかが議論になってfind_sole_byfind_solely_byなどの候補が出ていた覚えあります」「そうそう」

🔗 ロケールファイルのパスをデフォルトで再帰探索するようになった


つっつきボイス:「デフォルトのロケール読み込みパスが*.{rb,yml}から**/*.{rb,yml}に変わってyamlの再帰読み込みが可能になったんですね↓」「そういえば今までは手動でパスを書いていたかも」「/en//ja/のように言語ごとにロケールのyamlファイルをネストする方が便利ですし、既にベストプラクティスとしてそう書いている人も多いと思うので、再帰読み込みの方をデフォルトにしようということでしょうね」

# railties/lib/rails/engine/configuration.rb#L57
          paths.add "config"
          paths.add "config/environments", glob: "#{Rails.env}.rb"
          paths.add "config/initializers", glob: "**/*.rb"
          paths.add "config/locales",      glob: "*.{rb,yml}"
          paths.add "config/locales",      glob: "**/*.{rb,yml}"
          paths.add "config/routes.rb"
          paths.add "config/routes",       glob: "**/*.rb"

🔗Rails

🔗 willnetさんのHotwire記事


つっつきボイス:「これ、とてもいい記事だと思いました」「そうそう、Hotwireそのものの解説も丁寧ですし、Turbolinksが流行らなかった理由や歴史的経緯も詳しく書かれていてありがたい: Hotwireとは何かという解説だけだとわかりにくくなりがち」「たしかに」

「たしか続き物記事でしたよね?」「はい、『次回に続く』となっていました」「これは楽しみ😋

同記事で紹介されていたwillnetさんの以下の記事もよさそうですね。

参考: TurbolinksからTurboへの移行 - おもしろwebサービス開発日記


速報: Basecampがリリースした「Hotwire」の概要

🔗 Evil MartiansとEngine YardのHotwire記事


つっつきボイス:「こちらはEvil MartiansによるHotwireの解説記事ですね」「あとEngine YardもHotwire推しの記事を書いているのを見つけました↓」

「今週はHotwire記事が3本も出揃いましたね」「Hotwireのように、HTMLを動的に部分更新をするときにもHTML partialのレンダリングは全部サーバーサイドでやりたいというのは、クライアントJavaScriptとサーバーサイドViewの間をあっちこっち行き来して苦労したことのある人なら一度は思ったことがあるんじゃないかな」「たしかに」「実際似たようなことは昔から行われていて、render_async gem↓もそれに近いと思いますし、大昔のRailsでもlink_to_remoteでパーシャルを直接取得してDOMを更新するなど、泥臭い感じでやってましたね」「ふむふむ」

Rails: render_async gemでレンダリングを高速化(翻訳)

参考: jQueryで link_to_remoteを使う - satake7’s memo

「その意味でHotwireはまったく新しい技術ではないと思いますが、以前は個別に泥臭くやっていた方法を統合された方法で、かつモダンなHTMLでやれるのがメリットなのだろうと考えています」「なるほど!」

🔗 Rails 7に入る機能(Hacklinesより)


つっつきボイス:「Rails 7にどんな機能が入るかを現時点でまとめた記事だそうです」「Railsウォッチで見たことのあるものが大半のようですが、知らない機能もありそうかな」


「画像をlazy loadingする機能が入る↓」「imgタグにloading="lazy"属性を追加できるんですね: ブラウザの画像lazy loading機能を有効にするだけなのでRailsアプリケーションが頑張る話ではなかったか」

参考: Chromeに実装される新機能『loading属性』について解説、ついにブラウザがネイティブで遅延ロードをサポート | コリス


「お、Active Recordにinvert_whereが入るのか↓」「これは知らなかった」「スコープのWHERE条件にNOT ()を付けるんですね」「使うかどうかは微妙かな」

# 同記事より
good_students = Student.where(grade: 80..100)
# SELECT \"students\".* FROM \"students\" WHERE \"students\".\"grade\" BETWEEN 80 AND 100

bad_students = good_students.invert_where
# SELECT \"students\".* FROM \"students\" WHERE NOT (\"students\".\"grade\" BETWEEN 80 AND 100)

has_one: throughも使えるようになるのね↓」「今までできなかったのか!」

# 同記事より
class Dog
  has_many :toys
  has_one :toy_box
end

class Toy
  belongs_to :dog
  has_one :toy_box, through: :dog
end

class ToyBox
  belongs_to :dog
end
toy.build_toy_box
# <ToyBox:0x00007f572007e170 id: nil, dog_id: 3>

toy.create_toy_box
# <ToyBox:0x00005601f2ac09a0 id: 5, dog_id: 3>

「新機能はRails 7が出るまでにまだまだ増えたり変わったりするので、そのつもりで読むのがよいと思います」「そうですね」

🔗 ActiveModel::Attributesの隠された型システム


つっつきボイス:「Active Modelの内部にはビルトイン型がある↓という記事ですね: 内部を解説しているのは貴重かも」「カスタム型の登録をたまに自力でやることがありますけど、Active Model wayでカスタム型を登録できるんですね」「昨年1月の記事ですが、このあたりは大きく変わらないと思うのでよいと思います👍

# 同記事より
register(:big_integer, Type::BigInteger)
register(:binary, Type::Binary)
register(:boolean, Type::Boolean)
register(:date, Type::Date)
register(:datetime, Type::DateTime)
register(:decimal, Type::Decimal)
register(:float, Type::Float)
register(:immutable_string, Type::ImmutableString)
register(:integer, Type::Integer)
register(:string, Type::String)
register(:time, Type::Time)

前編は以上です。

バックナンバー(2021年度第1四半期)

週刊Railsウォッチ(20210420後編)ShopifyのJITコンパイラYJIT、PicoRuby、DynamoDBの3つの制約ほか

今週の主なニュースソース

ソースの表記されていない項目は独自ルート(TwitterやはてブやRSSやruby-jp SlackやRedditなど)です。

Rails公式ニュース

Ruby Weekly

Hacklines

Hacklines

週刊Railsウォッチ(20210427後編)RactorでUDPサーバーを作る、JSONシリアライザalba gem、AppleのAirTagほか

$
0
0

こんにちは、hachi8833です。次回の週刊Railsウォッチはゴールデンウィーク翌週の5/10(月)を予定しています。

週刊Railsウォッチについて

  • 各記事冒頭には🔗でパーマリンクを置いてあります: 社内やTwitterでの議論などにどうぞ
  • 「つっつきボイス」はRailsウォッチ公開前ドラフトを(鍋のように)社内有志でつっついたときの会話の再構成です👄
  • お気づきの点がありましたら@hachi8833までメンションをいただければ確認・対応いたします🙇

TechRachoではRubyやRailsなどの最新情報記事を平日に公開しています。TechRacho記事をいち早くお読みになりたい方はTwitterにて@techrachoのフォローをお願いします。また、タグやカテゴリごとにRSSフィードを購読することもできます(例:週刊Railsウォッチタグ)

🔗Ruby

🔗 Ruby 3でコアクラスをサブクラス化したときの挙動が変わった件について(Ruby Weeklyより)


つっつきボイス:「Ruby 3.0でいろいろ機能が追加されましたが、そういえばこの変更もあったなと思って取り上げました」「ArrayStringのようなRubyのコアクラスをサブクラスに継承して使うのはやめておく方がいいよという記事↓も紹介されてる」「継承でない方法にしましょうということですね」

参考: Why you shouldn’t inherit from Ruby’s core classes (and what to do instead) – avdi.codes

以下はつっつき後に手元で確認した動作です。

class Bar < String
end

bar = Bar.new

# Ruby 2.7までの場合
bar.upcase.class #=> Bar
# Ruby 3.0の場合
bar.upcase.class #=> String

参考: プロと読み解く Ruby 3.0 NEWS - クックパッド開発者ブログ

  • Arrayのサブクラスのメソッドが、サブクラスではなく、Arrayクラスのオブジェクトを返すようになった
  • Stringのサブクラスのメソッドが、サブクラスではなく、Stringクラスのオブジェクトを返すようになった
    同記事見出しより

「そういえばRailsにもこのあたりに関連した修正が入ってましたね」「そうだったかも」「探してみます」「知らないうちに型が変わるのはしんどいので、この挙動変更は知っておく必要がありますね」


後で探すと、銀座Rails#28の以下のスライドにありました↓。同ページのリンクも抜き書きしました。

🔗 alba: 高速JSONシリアライザ(Ruby Weeklyより)

okuramasafumi/alba - GitHub


つっつきボイス:「大倉さんの作ったJSONシリアライザgemがRuby Weeklyに取り上げられていました」「albaのリリースを見ると2020年1月からなので割と新しいgemなんですね」「fastest!」「JSONのシリアライズはとてもよく使われるので、シリアライズが高速化されるのはいいですね👍」

「ちなみにJSONシリアライザにはいくつか定番があるんですよ↓」

参考: 2020年のRuby/RailsのJSONシリアライザは何を使うべきか問題 - Bouldering & Com.

「Railsに入っているjbuilderは昔から遅いので有名(最近はわかりませんが)↓: それもあって他のJSONシリアライザがいろいろ登場したという側面はありますね」「なるほど」「自分は速度をそれほど気にする方でもないんですが、それでもjbuilderを最初に使ったときに遅いと思ったほどでした: jbuilderはpublic APIなどに利用してpage cacheなどを効かせられるのであれば問題なさそうですが、キャッシュに乗せられないようなAPIで使うには主に速度面でちょっと辛いかもしれないとは感じました」

rails/jbuilder - GitHub

「active_model_serializer↓は昔から定番で自分もよく使ってましたが、記事にもあるように最近は少し更新頻度が下がってるかな」

rails-api/active_model_serializers - GitHub

「amatsudaさんのjbは、jbuilder的にビューファイルを作るタイプですね↓」

amatsuda/jb - GitHub

「JSON APIはプログラムから操作することがほとんどなので、求められる速度やレスポンスの要求が通常のHTMLビューよりもシビアなんですよ」「たしかに」「JSONシリアライザが遅いのはよろしくないので、それもあってactive_model_serializerを使ってました」


以下はつっつき後に見つけたツイートです。

🔗 RactorでUDPサーバーを作る(Ruby Weeklyより)


つっつきボイス:「割と短い記事です」「お、RactorでUDPサーバーですか」「RactorでWebサーバーを書いた人がいたので(Writing a Ractor-based web server · Kir Shatrov)、UDPサーバーを作ってみたということみたいですね」

「UDPソケットを待ち受けるRactor listenerを1つ作って、そこで受信したデータグラムの処理をメッセージ処理用のRactor pipeで受信し、Ractor workerをCPU_COUNT数だけ動かして処理を振り分けている感じですね↓」「ふむふむ」「Ractorの参考になるコード例が増えるのはいいですね👍」

🔗 その他Ruby

つっつきボイス:「そういえば例年なら今頃RubyKaigiが開催されている時期ですよね」「あぁ、そうだった…」「オンラインのイベントや勉強会が主流になったことで参加の敷居は下がったのはよいことだと思いますけど、人に会ったりおいしいものを食べたりできないのが寂しい」「ですよね」

🔗クラウド/コンテナ/インフラ/Serverless

🔗 CloudflareのWorkers UnboundがGAに(StatusCode Weeklyより)


つっつきボイス:「Cron Triggersで15分かかるような処理がWorkers Unboundだと扱えるとありますね」「あ、なるほど」「昨年のCloudflare記事が予告的な感じで読みやすそうです↓」

参考: Workers Unboundのご紹介

Workers Unboundは、オリジナルのCloudflare Workers(現在、Workers Bundledと呼んでいます)と似ていますが、さらに長い実行時間を必要とするアプリケーション向けです。処理負荷がどれだけ高くても、お客様がそれをWorkersに移動させることができるようにCPU制限を拡張しています。そのため開発者は、Edgeで高速かつシンプルな作業をするか、リソースが無制限の集中型クラウドで重いコンピューティングを実行するか、どちらかを選ぶ必要がなくなります。
Workers Unboundのご紹介より

「以前からあるCloudflare WorkersはAWS Lambda@Edgeと競合するサービスですね: Workers Unboundは時間制限を緩和したもので、日本語記事によるとCloudflare Workersとは別サービスということらしい」「なるほど」「そこまで時間のかかる処理をクラウドにキャッシュしたいケースはすぐに思い当たらないけど、画像処理やAIから巨大な予測値を返すような処理が想定されているようなので、そうした用途には悪くないかもしれませんね」

参考: Lambda@Edge | AWS

🔗 AppleのAirTag


つっつきボイス:「AppleのAirTag、Tileより高いらしいですね」「4個で1万円超えか」「値段的にもTileで十分なので自分はAirTagが欲しいとは思わないかな」「性能より個数が欲しい方なのでTileにしたいです」

「AirTagはApple純正な分、初めて使うときの設定はやりやすそうなので、たとえば自分の親に使ってもらうときには楽かもと思いました」「あ、それもそうか」「Appleはそういう使い勝手の敷居を下げるのがうまいですね」「AirTagは向きまで特定できるのがちょっとスゴい」

「あとAirTagもオンラインで買うと刻印入れてくれるらしいですよ↓」「あ、物理の刻印なんですね」「刻印サービス、なぜかあまり人気がないんですよ」「MacBookやiPhoneに刻印入れると後で転売しにくくなりますし」

参考: 無料のメッセージ刻印とギフト包装 - Apple(日本)

「AirTagのようなものだと落としたときに名前がないと誰のものかわからなくなるので、そういう場合に刻印が役に立ちそう」「刻印にはApple絵文字も使えますよ」「Appleがプライバシー方面に力を入れている点は安心材料かな」

「Tileはラインナップも豊富で安いですし、この種のメーカーとしては一番長くやっていますよね」「そうそう、他の製品だとサービス終了したり」「Tileはマニア層だけではなく一般レベルで使われていますね」


「そういえば自転車にTile付けようかなと思ってたところです」「バイクの盗難防止的なやつですね」「自分も自転車と自動車とキーにTile付けてます」「心配なのはバッテリーがどのぐらい持つかなんですが」「Tileは余裕で1年は持ちますし、電池も交換可能なタイプも前から出ていますよ」「お、そうなんですね」

「ちなみにAirTagで使えるCR2032という電池はコンビニでも手に入るんですけど、ちょっと大きめで厚みがあるんですよ」「Wikipedia見てて気づいたんですけど、型番がそのままサイズを反映してるんですね↓」「ホントだ、CR2032だと20mm × 32mmなのか」

参考: コイン形リチウム電池 - Wikipedia

🔗言語/ツール/OS/CPU

🔗 研究名目でLinuxカーネルに意図的に不具合パッチを投稿したセキュリティ研究者が問題に

つっつきボイス:「既にあちこちで語られていますけど、Linuxに意図的に脆弱性を埋め込むのはもう言い訳しようがない」「これはそうとしか言いようがないですね」「コードやレビュー手順を解析して脆弱性を指摘する研究とかならまだしも、実践するのは完全にアウト」

「今の時点で(注: 2021/04/22 20:00頃)、ミネソタ大ドメインから投稿された全てのコミットをLinuxリポジトリからRevertするパッチができたようですね↓」「パッチを作ったGregさんは温厚な人柄で知られているんですけど、そのGregさんを怒らせるとは」

参考: [PATCH 000/190] Revertion of all of the umn.edu commits - Greg Kroah-Hartman
参考: グレッグ・クロー=ハートマン - Wikipedia

「今は情報がいろいろ出ている最中ですが、もう少し経てば情報をまとめたブログが出てくると思います」「本トピックのタイトル付け、悩んでます…」


以下はつっつき後の最新記事です。

参考: University of Minnesota security researchers apologize for deliberately buggy Linux patches | ZDNet
参考: Linux kernel team rejects University of Minnesota researchers’ apology | Ars Technica

🔗 その他CPU

つっつきボイス:「トランジスタでCPUを作ってロボットを動かしてる」「キットが5万円で、今品切れだそうです」「これで何するんですか?」「こういうのは作るだけで楽しいものなんですよ😋」


後編は以上です。

バックナンバー(2021年度第1四半期)

週刊Railsウォッチ(20210426前編)Hotwireの詳細な解説記事3本、Rails 7に入る予定の機能ほか

今週の主なニュースソース

ソースの表記されていない項目は独自ルート(TwitterやはてブやRSSやruby-jp SlackやRedditなど)です。

Ruby Weekly

StatusCode Weekly

statuscode_weekly_banner

クジラに乗ったRuby: Evil Martians流Docker+Ruby/Rails開発環境構築(翻訳)

$
0
0

概要

原著者の許諾を得て翻訳・公開いたします。

更新情報

  • 2019/09/06: 初版公開
  • 2021/04/20: 原文更新を反映

訳注

Ruby on Whalesは、Ruby on Railsの他に、もしかすると「Boy on a Dolphin」にかけているのかもしれないと思いました。Whales(クジラ)はもちろんDockerのシンボルです。

まえがき

本記事は、私がRailsConf 2019で話した「Terraforming legacy Rails applications」↑の、いわばB面に相当します。この記事を読んで、皆さんがアプリケーション開発をDockerに乗り換えるとまでは考えていません(皆さんが以下の動画で若干言及しているのをご覧になっていたとしても)。本記事の狙いは、私が現在のRailsプロジェクトで用いている設定を皆さんと共有することです。それらのRailsプロジェクトは、Evil Martiansのproduction development環境で生まれたものです。どうぞご自由にお使いください。

クジラに乗ったRuby: Evil Martians流Docker+Ruby/Rails開発環境構築(翻訳)

原文免責事項: 英語版記事は最新の推奨事項に合わせて更新を繰り返しています。詳しくは記事末尾のChangelogをご覧ください(参考: 原文Changelog)。


私がdevelopment環境でDockerを使い始めたのは、かれこれ3年ほど前の話です(それまで使っていたVagrantは4GB RAMのノートパソコンではあまりに重たかったのでした)。もちろん、最初からバラ色のDocker人生だったわけではありません。自分のみならず、チームにとっても「十分にふさわしい」Docker設定を見つけるまでに2年という月日を費やしました。

私の設定を、(ほぼほぼ)すべての行に解説を付けてご覧に入れたいと思います。「Dockerをわかっている」前提のわかりにくいチュートリアルはもうたくさんですよね。


本記事のソースコードは、GitHubのevilmartians/terraforming-railsでご覧いただけます


本記事の例では以下を用います。

  • Ruby 2.6.3
  • PostgreSQL 13
  • NodeJS 11 & Yarn(Webpackerベースのアセットコンパイル用)

🔗 Evil Martians流Dockerfile

Railsアプリケーションの「環境」は、Dockerfileで定義します。サーバーの実行、コンソール(rails c)、テスト、rakeタスク、開発者としてコードとのインタラクティブなやりとりは、ここで行います。

ARG RUBY_VERSION
# 後述
FROM ruby:$RUBY_VERSION-slim-buster

ARG PG_MAJOR
ARG NODE_MAJOR
ARG BUNDLER_VERSION
ARG YARN_VERSION

# 共通の依存関係
RUN apt-get update -qq \
  && DEBIAN_FRONTEND=noninteractive apt-get install -yq --no-install-recommends \
    build-essential \
    gnupg2 \
    curl \
    less \
    git \
  && apt-get clean \
  && rm -rf /var/cache/apt/archives/* \
  && rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* \
  && truncate -s 0 /var/log/*log

# mimemagic gem用のMIMEタイプデータベースをダウンロード
RUN apt-get update -qq && DEBIAN_FRONTEND=noninteractive apt-get -yq dist-upgrade && \
  DEBIAN_FRONTEND=noninteractive apt-get install -yq --no-install-recommends \
    shared-mime-info \
  && cp /usr/share/mime/packages/freedesktop.org.xml ./ \
  && apt-get remove -y --purge shared-mime-info \
  && apt-get clean \
  && rm -rf /var/cache/apt/archives/* \
  && rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* \
  && truncate -s 0 /var/log/*log \
  && mkdir -p /usr/share/mime/packages \
  && cp ./freedesktop.org.xml /usr/share/mime/packages/

# PostgreSQLをソースリストに追加
RUN curl -sSL https://www.postgresql.org/media/keys/ACCC4CF8.asc | apt-key add - \
  && echo 'deb http://apt.postgresql.org/pub/repos/apt/ buster-pgdg main' $PG_MAJOR > /etc/apt/sources.list.d/pgdg.list

# NodeJSをソースリストに追加
RUN curl -sL https://deb.nodesource.com/setup_$NODE_MAJOR.x | bash -

# Yarnをソースリストに追加
RUN curl -sS https://dl.yarnpkg.com/debian/pubkey.gpg | apt-key add - \
  && echo 'deb http://dl.yarnpkg.com/debian/ stable main' > /etc/apt/sources.list.d/yarn.list

# アプリケーションの依存関係
# 外部のAptfileでやってる(後ほどお楽しみに!)
COPY Aptfile /tmp/Aptfile
RUN apt-get update -qq && DEBIAN_FRONTEND=noninteractive apt-get -yq dist-upgrade && \
  DEBIAN_FRONTEND=noninteractive apt-get install -yq --no-install-recommends \
    libpq-dev \
    postgresql-client-$PG_MAJOR \
    nodejs \
    yarn=$YARN_VERSION-1 \
    $(grep -Ev '^\s*#' /tmp/Aptfile | xargs) && \
    apt-get clean && \
    rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* && \
    truncate -s 0 /var/log/*log

# bundlerの設定
ENV LANG=C.UTF-8 \
  BUNDLE_JOBS=4 \
  BUNDLE_RETRY=3

# bundler設定をプロジェクトのルートに保存する場合は以下をコメント解除
# ENV BUNDLE_APP_CONFIG=.bundle

# `bin/`や`bundle exec`を付けずにbinstabを実行したい場合は以下をコメント解除
# ENV PATH /app/bin:$PATH

# RubyGemsをアップグレードして必要なバージョンのbundlerをインストール
RUN gem update --system && \
    gem install bundler:$BUNDLER_VERSION

# appコードを置くディレクトリを作成
RUN mkdir -p /app

WORKDIR /app

このDockerfileの設定は必要不可欠なもののみを含んでいますので、これを元にカスタマイズできます。このDockerfileで何が行われるかを解説します。

最初の2行に少々妙なことが書かれています。

ARG RUBY_VERSION
FROM ruby:$RUBY_VERSION-slim-buster

FROM ruby:2.6.3みたいに適当な安定版Rubyのバージョンを書いておけばよさそうなものですよね。ここではDockerfileを一種のテンプレートとして用い、環境を外部から設定可能にしたいのです。

  • ランタイム依存の厳密なバージョンは、docker-compose.ymlの方で指定することにします(後述)。
  • aptコマンドでインストール可能な依存のリストは、これも別ファイルに保存することにします(これも後述)。

また、PostgreSQLなどの他の依存関係に対応する正しいソースを追加するためにDebianのリリース(buster)を明示的に指定しています。

上に続く以下の4行は、PostgreSQL、NodeJS、Yarn、Bundlerのバージョンを定義します。

ARG PG_MAJOR
ARG NODE_MAJOR
ARG BUNDLER_VERSION
ARG YARN_VERSION

今どきDockerfileをDocker Composeなしで使う人などいないという前提なので、Dockerfileではデフォルト値を指定しないことにします。

続いて実際のイメージビルドが行われます。Dockerイメージのサイズを削減するためにslimベースのDockerイメージを使うので、最初にシステム共通の依存関係(GitやcURLなど)を手動でインストールします。

# 共通の依存関係
RUN apt-get update -qq \
  && DEBIAN_FRONTEND=noninteractive apt-get install -yq --no-install-recommends \
    build-essential \
    gnupg2 \
    curl \
    less \
    git \
  && apt-get clean \
  && rm -rf /var/cache/apt/archives/* \
  && rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* \
  && truncate -s 0 /var/log/*log

システム依存のインストールの詳細については、アプリケーション固有のものについて説明するときに後述します。

次に、mimemagic gemで使うMIMEタイプデータベースをダウンロードするためだけにRUNコマンドを実行します(mimemagic gemのライセンス問題はご存知ですか?)。

RUN apt-get update -qq && DEBIAN_FRONTEND=noninteractive apt-get -yq dist-upgrade && \
  DEBIAN_FRONTEND=noninteractive apt-get install -yq --no-install-recommends \
    shared-mime-info \
  && cp /usr/share/mime/packages/freedesktop.org.xml ./ \
  && apt-get remove -y --purge shared-mime-info \
  && apt-get clean \
  && rm -rf /var/cache/apt/archives/* \
  && rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* \
  && truncate -s 0 /var/log/*log \
  && mkdir -p /usr/share/mime/packages \
  && cp ./freedesktop.org.xml /usr/share/mime/packages/

mimemagic gemを使わないのであれば、上のブロックは削除して構いません。

PostgreSQL、NodeJS、Yarnをaptコマンドでインストールするために、それらのdebパッケージのリポジトリをソースリストに追加する必要があります。

RUN curl -sSL https://www.postgresql.org/media/keys/ACCC4CF8.asc | apt-key add - \
  && echo 'deb http://apt.postgresql.org/pub/repos/apt/ buster-pgdg main' $PG_MAJOR > /etc/apt/sources.list.d/pgdg.list

原注: ここが、OSリリースをbusterにしたことを利用している箇所です。

RUN curl -sL https://deb.nodesource.com/setup_$NODE_MAJOR.x | bash -
RUN curl -sS https://dl.yarnpkg.com/debian/pubkey.gpg | apt-key add - \
  && echo 'deb http://dl.yarnpkg.com/debian/ stable main' > /etc/apt/sources.list.d/yarn.list

今度は依存関係のインストールです(apt-get installの実行など)。

COPY Aptfile /tmp/Aptfile
RUN apt-get update -qq && DEBIAN_FRONTEND=noninteractive apt-get -yq dist-upgrade && \
  DEBIAN_FRONTEND=noninteractive apt-get install -yq --no-install-recommends \
    libpq-dev \
    postgresql-client-$PG_MAJOR \
    nodejs \
    yarn \
    $(grep -Ev '^\s*#' /tmp/Aptfile | xargs) && \
    apt-get clean && \
    rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* && \
    truncate -s 0 /var/log/*log

まずAptfileという裏技について解説します。

COPY Aptfile /tmp/Aptfile
RUN apt-get install \
    $(grep -Ev '^\s*#' /tmp/Aptfile | xargs) && \

Aptfileというアイデアはheroku-buildpack-aptから拝借しました。heroku-buildpack-aptは、Herokuに追加パッケージをインストールできます。このbuildpackを使っていれば、同じAptfileをローカルでもproduction環境でも再利用できます(buildpackのAptfileの方が多くの機能を提供していますが)。

私たちのデフォルトAptfileに含まれているのパッケージは、たったひとつです(私たちはRailsのcredentialの編集にVimを使っています)。

vim

私が携わっていた直前のプロジェクトでは、LaTeXやTexLiveを用いてPDFを生成しました。そのときのAptfileは、さしずめ以下のような感じにできたでしょう(当時私はこの技を使っていませんでしたが)。

vim
texlive
texlive-latex-recommended
texlive-fonts-recommended
texlive-lang-cyrillic

このようにすることで、タスク固有の依存関係を別ファイルに切り出し、Dockerfileの普遍性を高めています。

DEBIAN_FRONTEND=noninteractiveという行については、「answer on Ask Ubuntu」という記事をご覧になることをおすすめします。

--no-install-recommendsスイッチを指定すると、推奨パッケージのインストールを行わなくなるので容量を節約でき、ひいてはイメージをもっとスリムにできます。詳しくは「Xubuntu Geek: Save disk space with apt-get option “no-install-recommends” in Xubuntu」をご覧ください。

RUNの最後の部分(apt-get clean && rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* && truncate -s 0 /var/log/*log)も目的は同じです。取得したパッケージファイルのローカルリポジトリ(ここまでで必要なものはすべてインストールできているので、これらはもはや不要です)や、インストール中に作成されたすべての一時ファイルやログをクリーンアップします。 特定のDockerレイヤにごみを残さないようにするため、このクリーンアップ作業は同じRUNステートメントの中で行う必要があります。

最後の部分は、もっぱらBundlerのためのものです。

# bundlerの設定
ENV LANG=C.UTF-8 \
  BUNDLE_JOBS=4 \
  BUNDLE_RETRY=3

# bundler設定をプロジェクトのルートに保存する場合は以下をコメント解除
# ENV BUNDLE_APP_CONFIG=.bundle

# `bin/`や`bundle exec`を付けずにbinstabを実行したい場合は以下をコメント解除
# ENV PATH /app/bin:$PATH

# RubyGemsをアップグレードして必要なバージョンのbundlerをインストール
RUN gem update --system && \
    gem install bundler:$BUNDLER_VERSION

LANG=C.UTF-8は、デフォルトロケールをUTF-8に設定します。これを行わないとRubyが文字列でUS-ASCIIを使ってしまうので、かわいいかわいい絵文字たちとおさらば👋になってしまいます。

プロジェクト固有のbundler設定(プライベートなgemで使うcredentialなど)の保存先に<root>/.bundleフォルダを使う場合は、BUNDLE_APP_CONFIGが必要です。デフォルトのRubyイメージではこの変数が定義されていて(#129)、bundlerがローカル設定にフォールバックしないようになっています。

また、bundle execを付けずにコマンドを実行できるよう、PATH変数に<root>/binフォルダを追加することも可能です。これはデフォルトではオフにしてありますが、理由はマルチプロジェクト環境(Railsアプリ内でローカルのgemやエンジンを使う場合など)でコードが動かなくなる可能性があるためです。

🔗 Evil Martians流docker-compose.yml

Docker Composeは、コンテナ化された環境をオーケストレーションするツールで、これを用いてコンテナ同士を接続し、永続化ボリュームやサービスを定義できます。

以下は、データベースにPostgreSQL、バックグラウンドジョブの処理にSidekiqを用いた、Railsアプリケーションの典型的な開発環境のためのdocker-compose.ymlです。

version: '2.4'

services:
  app: &app
    build:
      context: .dockerdev
      dockerfile: Dockerfile
      args:
        RUBY_VERSION: '2.6.3'
        PG_MAJOR: '13'
        NODE_MAJOR: '11'
        YARN_VERSION: '1.13.0'
        BUNDLER_VERSION: '2.0.2'
    image: example-dev:1.0.0
    environment: &env
      NODE_ENV: ${NODE_ENV:-development}
      RAILS_ENV: ${RAILS_ENV:-development}
      YARN_CACHE_FOLDER: /app/node_modules/.yarn-cache
    tmpfs:
      - /tmp

  backend: &backend
    <<: *app
    stdin_open: true
    tty: true
    volumes:
      - .:/app:cached
      - rails_cache:/app/tmp/cache
      - bundle:/usr/local/bundle
      - node_modules:/app/node_modules
      - packs:/app/public/packs
      - .dockerdev/.psqlrc:/root/.psqlrc:ro
    environment:
      <<: *env
      REDIS_URL: redis://redis:6379/
      DATABASE_URL: postgres://postgres:postgres@postgres:5432
      BOOTSNAP_CACHE_DIR: /usr/local/bundle/_bootsnap
      WEBPACKER_DEV_SERVER_HOST: webpacker
      WEB_CONCURRENCY: 1
      HISTFILE: /app/log/.bash_history
      PSQL_HISTFILE: /app/log/.psql_history
      EDITOR: vi
    depends_on:
      postgres:
        condition: service_healthy
      redis:
        condition: service_healthy

  runner:
    <<: *backend
    command: /bin/bash
    ports:
      - '3000:3000'
      - '3002:3002'

  rails:
    <<: *backend
    command: bundle exec rails server -b 0.0.0.0
    ports:
      - '3000:3000'

  sidekiq:
    <<: *backend
    command: bundle exec sidekiq -C config/sidekiq.yml

  postgres:
    image: postgres:13
    volumes:
      - .psqlrc:/root/.psqlrc:ro
      - postgres:/var/lib/postgresql/data
      - ./log:/root/log:cached
    environment:
      PSQL_HISTFILE: /root/log/.psql_history
      POSTGRES_PASSWORD: postgres
    ports:
      - 5432
    healthcheck:
      test: pg_isready -U postgres -h 127.0.0.1
      interval: 5s

  redis:
    image: redis:3.2-alpine
    volumes:
      - redis:/data
    ports:
      - 6379
    healthcheck:
      test: redis-cli ping
      interval: 1s
      timeout: 3s
      retries: 30

  webpacker:
    <<: *app
    command: ./bin/webpack-dev-server
    ports:
      - '3035:3035'
    volumes:
      - .:/app:cached
      - bundle:/usr/local/bundle
      - node_modules:/app/node_modules
      - packs:/app/public/packs
    environment:
      <<: *env
      WEBPACKER_DEV_SERVER_HOST: 0.0.0.0

volumes:
  postgres:
  redis:
  bundle:
  node_modules:
  rails_cache:
  packs:

このdocker-compose.ymlでは8つのサービスを定義しています。サービスを8つも定義しているのを不思議に思うかもしれませんが、その一部は単に他と共有する設定を定義しているだけで(appbackendといった抽象サービス)、残りはアプリケーションコンテナを用いる特定のコマンド(runnerなど)のためのものです。

私たちはDocker Composeのバージョン2を意図的に使っています。開発目的にはバージョン2が適しています。詳しくはDockerのissueをご覧ください(#7593)。

このアプローチでは、アプリケーションをdocker-compose upで実行するのではなく、docker-compose up railsのように、実行したいサービスを常にピンポイントで指定するようにしています。development環境ではWebpackerやSidekiqなどを全部立ち上げる必要はめったにないので、合理的です。

それでは各サービスを詳しく見ていくことにしましょう。

🔗 app

appサービスの主な目的は、(上のDockerfileで定義した)アプリケーションコンテナの構築に必要な情報をすべて提供することです。

build:
  context: .dockerdev
  dockerfile: Dockerfile
  args:
    RUBY_VERSION: '2.6.3'
    PG_MAJOR: '13'
    NODE_MAJOR: '11'
    YARN_VERSION: '1.13.0'
    BUNDLER_VERSION: '2.0.2'

contextディレクトリは、Dockerのbuild contextを定義します。これはビルドプロセスで用いる一種のワーキングディレクトリであり、COPYコマンドなどで用いられます。また、イメージがビルドされるたびにパッケージ化されてDockerデーモンに送信されるので、イメージのサイズはできるだけ小さくしておくのが望まれます。

ログや一時ファイルによってプロジェクトディレクトリのサイズが非常に大きくなるとビルドが遅くなる可能性があります。.dockerignoreファイルでサイズを減らすか、.dockerdevのような小さなディレクトリを指定しましょう。

私たちの設定ではDockerfileへのパスを明示的に指定しています。理由は、私たちはDockerファイルをプロジェクトのルートディレクトリに配置するのではなく、.dockerdevという隠しディレクトリの中に他のすべてのDocker関連ファイルと一緒に配置しているからです。

前述したように、Dockerfileではargsを用いて依存関係の正確なバージョンを指定しています。

ここでひとつ注意すべきは、イメージにタグ付けする方法です。

image: example-dev:1.0.0

Dockerを開発に用いるメリットのひとつは、設定の変更を自動的にチーム全体で同期できることです。これは、ローカルイメージ(引数や、イメージが依存するファイルでもよい)を変更するたびにローカルイメージのバージョン番号を常にアップグレードしておきさえすれば可能です。逆に最悪なのは、ビルドタグにexample-dev:latestを使うことです。

イメージのバージョン番号が正しく管理されていれば、異なる2つの環境同士で余分な追加作業を一切行わずに済むようにもできます。たとえば、長期間実行するchore/upgrade-to-ruby-3ブランチで作業している最中に、いつでもmasterブランチに切り替えて古いイメージや古いRubyを利用できます。しかもリビルド不要で。


重要: docker-compose.yml内のイメージでlatestタグを使うのは最悪です。


次に、共通の環境変数を追加します。この環境変数は、RailsやWebpackerなど複数のサービスで共有されます。

environment: &env
  NODE_ENV: ${NODE_ENV:-development}
  RAILS_ENV: ${RAILS_ENV:-development}
  # 高速化のため、マウントしたボリュームにYarnキャッシュを保存
  YARN_CACHE_FOLDER: /app/node_modules/.yarn-cache

ここでもいろんなことが行われていますが、1つ説明しておきたい点があります。

まずX=${X:-smth}という構文についてです。これは「コンテナ内の変数Xで使う値は、ホストマシンに環境変数Xがあればそれを使い、ない場合は指定の値を使う」という意味です。これによって、RAILS_ENV=test docker-compose up railsのようにコマンドで別の環境を指定してサービスを実行できるようになります。

なお、environmentのフィールドがリスト形式(- NODE_ENV=xxx)ではなく辞書形式(NODE_ENV: xxx)になっていることにご注意ください。辞書形式にすることで、共通設定を再利用できるようになります(後述)。

他にも、コンテナ内で/tmpフォルダにDockerのtmpfsマウントを用いるように指定することでスピードアップしています。

tmpfs:
  - /tmp

🔗 backend

いよいよ本記事で一番美味しい部分にたどり着きました。

このbackendサービスは、あらゆるRubyサービスで共有する振る舞いを定義します。

まずはvolumes:を見てみましょう。

volumes:
  - .:/app:cached
  - bundle:/usr/local/bundle
  - rails_cache:/app/tmp/cache
  - node_modules:/app/node_modules
  - packs:/app/public/packs
  - .dockerdev/.psqlrc:/root/.psqlrc:ro

volumes:リストの最初の項目「- .:/app:cached」では、現在のワーキングディレクトリ(つまりプロジェクトのルートディレクトリ)をコンテナ内の/appフォルダにマウントし、かつcached戦略を用いています。このcachedという修飾子は、MacOSでのDocker環境の効率を高めるうえで重要なポイントです。cachedについては別記事を書いていますので😉、本記事ではこれ以上は深堀りしません。詳しくはこちらの「公式ドキュメント」をご覧ください。

その次の行では、/bundleという名前のボリュームに/urs/local/bundleの内容を保存するようコンテナに指示しています(gemはデフォルトでここに保存されます)。私たちはこのようにして、gemのデータを永続化して複数の実行で使えるようにしています。docker-compose.ymlで定義されたすべてのボリュームは、docker-compose down --volumesを実行するまで持続します。

以下の3行も、「DockerがMacだと遅い」という呪いをお祓いするために書かれています。私たちは、生成されたファイルをすべてDockerボリュームに配置することで、ホストマシンでディスク操作が重くなるのを回避しています。

- rails_cache:/app/tmp/cache
- node_modules:/app/node_modules
- packs:/app/public/packs

ポイント: macOSでDockerを十分高速に動かすには、ソースファイルを:cachedでマウントし、かつ、生成されたコンテンツ(アセットやbundleなど)の保存にはボリュームを使うこと。


末尾の3行では、特定のpsql設定をコンテナに追加しています。私たちはほとんどの場合、コマンド履歴をアプリのlog/.psql_historyに保存することで永続化する必要があります。psqlをRubyのコンテナに追加している理由は、rails dbconsoleを実行するときに内部で使われるからです。

私たちが追加している.psqlrcファイルには、履歴ファイルを環境変数経由で指定できるようにするために以下の仕掛けが施されています。履歴ファイルへのパスをPSQL_HISTFILE環境変数で指定できるようにし、利用できない場合は$HOME/.psql_historyにフォールバックします。

\set HISTFILE `[[ -z $PSQL_HISTFILE ]] && echo $HOME/.psql_history || echo $PSQL_HISTFILE`

環境変数について説明します。

environment:
  <<: *env
  REDIS_URL: redis://redis:6379/
  DATABASE_URL: postgres://postgres:postgres@postgres:5432
  WEBPACKER_DEV_SERVER_HOST: webpacker
  BOOTSNAP_CACHE_DIR: /usr/local/bundle/_bootsnap
  HISTFILE: /app/log/.bash_history
  PSQL_HISTFILE: /app/log/.psql_history
  EDITOR: vi
  MALLOC_ARENA_MAX: 2
  WEB_CONCURRENCY: ${WEB_CONCURRENCY:-1}

冒頭の<<: *envで、共通の環境変数を「継承」しているのがポイントです。

DATABASE_URL変数、REDIS_URL変数、WEBPACKER_DEV_SERVER_HOST変数は、Rubyアプリケーションを別のサービスに接続しますDATABASE_URL変数はRailsのActive Recordで、WEBPACKER_DEV_SERVER_HOST変数はRailsのWebpackerでいつでもサポートされます。ライブラリによってはREDIS_URL変数もサポートします(Sidekiq)が、どのライブラリでもサポートされているとは限りません(たとえばAction Cableでは明示的に設定が必要です)。

私たちはbootsnapを用いてアプリケーションの読み込みを高速化しています。bootsnapのキャッシュはBudlerのデータと同じ場所に保存しています。理由は、このキャッシュに含まれている内容のほとんどがgemのデータだからです。つまり、たとえばRubyを別のバージョンにアップグレードする場合は、それらを一括廃棄するべきということです。

HISTFILE=/app/log/.bash_historyは、開発者のUXにとって重要な設定です。この設定によってbashの履歴が特定の場所に保管され、永続化されるようになります。

EDITOR=viは、たとえばrails credentials:editコマンドでcredentialファイルを管理するのに用います。

末尾の2つの設定であるMALLOC_ARENA_MAXWEB_CONCURRENCYは、Railsのメモリハンドリングをチェックしやすくするためのものです。

他にbackendサービスで説明すべきは以下の行だけです。

stdin_open: true
tty: true

この設定によって、サービスをインタラクティブ(TTYを提供するなどの対話的な操作)にできます。私たちの場合、たとえばRailsコンソールやBashをコンテナ内で実行するのに必要です。

これは、-itオプションを付けてDockerコンテナを実行するのと同じです。

🔗 webpacker

webpackerで言及しておきたいのはWEBPACKER_DEV_SERVER_HOST=0.0.0.0という設定だけです。これによって、Webpack dev serverに「外部から」アクセスできるようになります(デフォルトではlocalhostで実行されます)。

🔗 runner

このrunnerサービスの目的を説明するために、私がDockerを開発に用いるときの段取りについて説明させてください。

  • 私はDockerデーモンの起動で以下のようなカスタムdocker-startスクリプトを作って実行しています。
#!/bin/sh

if ! $(docker info > /dev/null 2>&1); then
  echo "Docker for Macを開いています..."
  open -a /Applications/Docker.app
  while ! docker system info > /dev/null 2>&1; do sleep 1; done
  echo "Docker準備OK!"
else
  echo "Dockerは実行中です"
fi
  • 次に、コンテナのシェルにログインするために、プロジェクトでdcr runnerを実行します(dcrdocker-compose runのエイリアス)。つまりdcr runnerは以下のエイリアスになります。
$ docker-compose run --rm runner
  • 後はこのコンテナの中でほとんどの作業を行います(テストやマイグレーションやrakeタスクなど何でも構いません)。

以上でおわかりのように、私は何かタスクを1つ実行する必要が生じるたびにいちいちコンテナを1つ立ち上げたりせず、いつも同じ設定でやっています。

つまり私は、なつかしのvagrant sshと同じ感覚でdcr runnerを使っているのです。

私がこれをshellと呼ばずにrunnerと呼んでいる理由はただひとつ、コンテナの中で任意のコマンドをrunするのにも使えるからです。

メモ: このサービスをrunnerと呼ぶかどうかは好みの問題であり、(デフォルトのcommand/bin/bash)は別としても)webサービスと比べて何ひとつ目新しい点はありません。つまり、docker-compose run runnerdocker-compose run web /bin/bashと完全に同じです(ただし短い😉)。

🔗 ヘルスチェック

Railsでdb:migrateなどのコマンドを実行するときには、DBが起動していてコネクションを受け付けられる状態にしておきたいものです。依存するサービスが起動するまで待つようにDocker Composeに指示したい場合は、healthcheckが使えます。

既にお気づきかと思いますが、depends_on定義に書かれているのは単なるサービスリストではありません。

backend:
  # ...
  depends_on:
    postgres:
      condition: service_healthy
    redis:
      condition: service_healthy

postgres:
  # ...
  healthcheck:
    test: pg_isready -U postgres -h 127.0.0.1
    interval: 5s

redis:
  # ...
  healthcheck:
    test: redis-cli ping
    interval: 1s
    timeout: 3s
    retries: 30

メモ: ヘルスチェックはDocker Composeファイルフォーマットv2.1以降でのみサポートされています。私たちがヘルスチェックを開発環境でしか使っていない理由はこれです。

🔗 おまけ: Evil Martians特製のdip.ymlについて

訳注

dipについては以下の記事もどうぞ。

docker-composeを便利にするツール「dip」を使ってみた

Docker Compose式のやり方がまだ難しいとお思いの方に、Dipというツールをご紹介します。これは開発者がスムーズなエクスペリエンスを得られるようにと、Evil Martiansのあるメンバーがこしらえたものです。

dip.ymlは、複数のcomposeファイルを使い分ける場合や、プラットフォームに依存する複数の設定を使い分ける場合に特に便利です。dip.ymlはそれらをまとめて、Dockerでの開発環境を管理する一般的なインターフェイスを提供できるからです。

dip.ymlについては別記事にて詳しく説明しようと思います。どうぞご期待ください!

追伸

本記事のtipsを共有してくれたSergey PonomarevMikhail Merkushinに感謝いたします🤘

元記事のトップ画像のクレジット: © NASA/JPL-Caltech, 2009

🔗 原文Changelog

1.1.3 (2021-03-30)
minimagic gemのライセンス問題緩和のためDockerfileを更新(#35
docker-compose設定の環境変数を辞書形式に変更(#6
1.1.2 (2021-02-26)
依存関係のバージョンを更新(#28
Aptfileにコメントを書けるようになった(#31
Dockerfile内のAptfileへのパスを修正(#33
1.1.1 (2020-09-15)
.dockerdevディレクトリをプロジェクトディレクトリではなくビルドコンテキストとして使用(#26
1.1.0 (2019-12-10)
Rubyのベースイメージをslimに変更
Rubyバージョン用のDebianリリースを明示的に指定し、busterにアップグレード
bundlerのパスを/bundleからbundler標準の/usr/local/bundleに変更
Docker Composeのファイル形式はv2.4を使用
postgresサービスとredisサービスにヘルスチェックを追加

訳注

以下のスライドも合わせて読むことで、より理解が進むと思います。

関連記事

Rails 6のB面に隠れている地味にうれしい機能たち(翻訳)

The post クジラに乗ったRuby: Evil Martians流Docker+Ruby/Rails開発環境構築(翻訳) first appeared on TechRacho.

Rails 7のActive Recordにinvert_whereメソッドが追加される(翻訳)

$
0
0

概要

原著者の許諾を得て翻訳・公開いたします。

Rails 7のActive Recordにinvert_whereメソッドが追加される(翻訳)

Railsアプリケーションでwhere句の条件の否定を取りたくなるケースがよくあります。

たとえば、ユーザーのメールアドレスと電話番号の両方を検証する必要があるシステムがあるとします。アカウントを検証するために、Userモデルに2つのカラムemail_verifiedphone_verifiedを追加します。

ユーザーは、システムによってユーザーのメールアドレスと電話番号の両方が検証された場合にのみ検証済みとなります。

変更前

Rails 7以前は、システム内の検証済みユーザーや未検証ユーザーの詳細を取得するために、Userモデルにverifiedスコープとunverifiedスコープを追加していました。

class User < ApplicationRecord
  scope :verified, -> { where(email_verified: true, phone_verified: true) }
  scope :unverified, -> { where.not(email_verified: true, phone_verified: true) }

  scope :with_verified_email, -> { where(email_verified: true) }
  scope :with_unverified_email, -> { where.not(email_verified: true) }
end
User.verified
# SELECT "users".* FROM "users" WHERE "users"."email_verified" = $1 AND "users"."phone_verified" = $2 /* loading for inspect */ LIMIT $3  [["email_verified", true], ["phone_verified", true], ["LIMIT", 11]]

User.unverified
# SELECT "users".* FROM "users" WHERE NOT ("users"."email_verified" = $1 AND "users"."phone_verified" = $2) /* loading for inspect */ LIMIT $3  [["email_verified", true], ["phone_verified", true], ["LIMIT", 11]]

User.with_verified_email
# SELECT "users".* FROM "users" WHERE "users"."email_verified" = $1 /* loading for inspect */ LIMIT $2  [["email_verified", true], ["LIMIT", 11]]

User.with_unverified_email
# SELECT "users".* FROM "users" WHERE "users"."email_verified" != $1 /* loading for inspect */ LIMIT $2  [["email_verified", true], ["LIMIT", 11]]

上のように、verifiedというスコープでemail_verifiedカラムとphone_verifiedカラムがtrueであるかどうかをチェックしています。しかし、unverifiedのユーザーを取得するためにwhere.not句を用いるスコープも別途導入する必要があります。こうしてwhere.notを使うメソッドがRailsコード内で公開されてしまいます。

メールアドレスについても同様に、検証済みのメールを持つユーザーと、未検証のメールアドレスを持つユーザーのそれぞれについてスコープを追加することになります。

変更後

Rails 7に、すべてのスコープ条件を反転させる#invert_whereメソッドが ActiveRecordに追加されました(#40249)。

否定条件を持つunverifiedスコープと with_unverified_emailスコープを両方作成する代わりに、以下のようにverifiedスコープと with_verified_emailスコープにそれぞれinvert_whereをチェインできます。

class User < ApplicationRecord
  scope :verified, -> { where(email_verified: true, phone_verified: true) }

  scope :with_verified_email, -> { where(email_verified: true) }
end
User.verified
# SELECT "users".* FROM "users" WHERE "users"."email_verified" = $1 AND "users"."phone_verified" = $2 /* loading for inspect */ LIMIT $3  [["email_verified", true], ["phone_verified", true], ["LIMIT", 11]]

User.verified.invert_where
# SELECT "users".* FROM "users" WHERE NOT ("users"."email_verified" = $1 AND "users"."phone_verified" = $2) /* loading for inspect */ LIMIT $3  [["email_verified", true], ["phone_verified", true], ["LIMIT", 11]]

User.with_verified_email
# SELECT "users".* FROM "users" WHERE "users"."email_verified" = $1 /* loading for inspect */ LIMIT $2  [["email_verified", true], ["LIMIT", 11]]

User.with_verified_email.invert_where
# SELECT "users".* FROM "users" WHERE "users"."email_verified" != $1 /* loading for inspect */ LIMIT $2  [["email_verified", true], ["LIMIT", 11]]

関連記事

Rails 7でActiveStorage::Streamingサポートが追加(翻訳)

The post Rails 7のActive Recordにinvert_whereメソッドが追加される(翻訳) first appeared on TechRacho.

Railsセキュリティ修正6.1.3.2/6.0.3.7/5.2.4.6/5.2.6がリリースされました

$
0
0

Ruby on Rails セキュリティ修正6.1.3.2/6.0.3.7/5.2.4.6/5.2.6がリリースされました。詳しくは公式の各種リリース情報をご覧ください。

今回リリースされた以下の4つのRailsリリースには重要なセキュリティ修正が含まれており、上記リリース情報でもアップグレードが求められています。

🔗 セキュリティ修正の概要

上記セキュリティ修正では、以下の4件の修正が行われています。各CVEリンクには、セキュリティ修正を適用できない事情がある場合のための回避方法も記載されています。

🔗 1. Action Dispatchが悪意のあるMIMEタイプでDoS攻撃を受ける可能性

影響を受けるRailsバージョン
v6.0.0以降
影響を受けないRailsバージョン
v6.0.0未満
修正済みバージョン
v6.0.3.7、v6.1.0.2

影響:
Action DispatchにDoS(Denial of Service)脆弱性の可能性がある。巧妙に作られたAcceptヘッダーによって、Action DispatchのMIMEタイプパーサーで正規表現エンジンの「Catastrophic Backtracking」が引き起こされる可能性がある。
CVE-2021-22902より大意

参考: Runaway Regular Expressions: Catastrophic Backtracking
参考: ReDoS - Wikipedia

🔗 2. config.hostの設定次第ではリクエストのHostヘッダーで任意のサイトにリダイレクトされる可能性

影響を受けるRailsバージョン
v6.1.0.rc2以降
影響を受けないRailsバージョン
v6.1.0.rc2未満
修正済みバージョン
v6.1.3.2

影響:
CVE-2021-22881に類似している。特殊な細工を施したHostヘッダーと特定の”allowed host”フォーマットの組み合わせによっては、Action PackのHost Authorizationミドルウェアから悪意のあるWebサイトにリダイレクトされる可能性がある。
rails/rails@9bc7ea5以降のRailsでは、冒頭にドットのない文字列がconfig.hostsに含まれていると、文字列が正しくエスケープされずに正規表現に変換される。これによって、たとえばconfig.hosts << “sub.example.com”という設定がsub-example.comという値のHostヘッダーを持つリクエストを許してしまう。
CVE-2021-22903より大意

🔗 3. url_forに渡されるArrayに信頼できない文字列があると意図しない情報公開やメソッド実行につながる可能性

影響を受けるRailsバージョン
2.0.0以降
影響を受けるRailsバージョン
2.0.0未満
修正済みバージョン
v6.1.3.2、v6.0.3.7、v5.2.4.6、v5.2.6

影響: 信頼できないユーザー入力をredirect_topolymorphic_urlに渡すと、意図しない情報公開やメソッド実行につながる可能性のある脆弱性がAction Packで見つかった。脆弱なコードは以下のような感じになる。

redirect_to(params[:some_param])

該当するリリースのユーザーはただちにアップグレードするか、CVE-2021-22885に記載されているいずれかの回避方法をただちに適用すべき。
CVE-2021-22885より大意


url_forは引数のarray(通常はシンボルかレコード)経由でポリモーフィックなURLをビルドできる。開発者がユーザー入力のarrayを渡すと、文字列によって意図しないルーティングヘルパーが呼び出される可能性がある。
同Changelogより大意

🔗 4. Action Controllerのトークン認証が特定のパターンで遅くなるDoS脆弱性の可能性

影響を受けるRailsバージョン
4.0.0以降
影響を受けるRailsバージョン
4.0.0未満
修正済みバージョン
v6.1.3.2、v6.0.3.7、v5.2.4.6、v5.2.6

影響:
authenticate_or_request_with_http_tokenまたはauthenticate_with_http_tokenでリクエストを認証するコードに影響が生じる。影響を受けるコードは以下のような感じになる。

class PostsController < ApplicationController
  before_action :authenticate

  private

  def authenticate
    authenticate_or_request_with_http_token do |token, options|
      # ...
    end
  end
end

該当するリリースのユーザーはただちにアップグレードするか、CVE-2021-22885に記載されているいずれかの回避方法をただちに適用すべき。
CVE-2021-22904より大意


TechRachoではRubyやRailsの最新情報などの記事を平日に公開しています。TechRacho記事をいち早くお読みになりたい方はTwitterにて@techrachoのフォローをお願いします。また、タグやカテゴリごとにRSSフィードを購読することもできます(例:Railsリリース情報タグ)

関連記事

速報: Rails 6.1.2がリリースされました

The post Railsセキュリティ修正6.1.3.2/6.0.3.7/5.2.4.6/5.2.6がリリースされました first appeared on TechRacho.

週刊Railsウォッチ(20210510前編)属性メソッドをキャッシュして最適化、Railsのガバナンスに関する声明、bundle install高速化ほか

$
0
0

こんにちは、hachi8833です。

週刊Railsウォッチについて

  • 各記事冒頭には🔗でパーマリンクを置いてあります: 社内やTwitterでの議論などにどうぞ
  • 「つっつきボイス」はRailsウォッチ公開前ドラフトを(鍋のように)社内有志でつっついたときの会話の再構成です👄
  • お気づきの点がありましたら@hachi8833までメンションをいただければ確認・対応いたします🙇

TechRachoではRubyやRailsなどの最新情報記事を平日に公開しています。TechRacho記事をいち早くお読みになりたい方はTwitterにて@techrachoのフォローをお願いします。また、タグやカテゴリごとにRSSフィードを購読することもできます(例:週刊Railsウォッチタグ)

🔗Rails: 先週の改修(Rails公式ニュースより)

今回は以下のコミットリストのChangelogのうち、セキュリティ修正以外のものから見繕いました。

🔗 stylesheet_link_tagextnameオプションが追加


つっつきボイス:「javascript_include_tagにもextnameオプションがあるのでstylesheet_link_tagにも付けようという感じのようです」「extnameはextension nameつまり拡張子名を指定するのね」

CSSのパスにデフォルト.cssを追加するのをスキップするextname:オプションをstylesheet_link_tagに追加。

# 変更前:
stylesheet_link_tag "style.less"
# <link href="/stylesheets/style.less.scss" rel="stylesheet">
# 変更後
stylesheet_link_tag "style.less", extname: false, skip_pipeline: true, rel: "stylesheet/less"
# <link href="/stylesheets/style.less" rel="stylesheet/less">

Abhay Nikam
同Changelogより大意

「CSSの拡張子を.lessとかにしたい場合にデフォルトで.css拡張子を付けないようにするオプションか、たしかにできないと困る」「ところでlessって使ったことないんですけど、どのぐらい使われているのかな?」「言われてみれば自分の身の回りでは見かけないかも」「MIMEタイプも"stylesheet/less"になるのか」「使っている人がいる以上欲しいオプションですね👍

参考: Getting started | Less.js

🔗 生成された属性メソッドをキャッシュおよび再利用する最適化


つっつきボイス:「attributes系メソッドをキャッシュする、なるほど」「キャッシュしたことでメモリがだいぶ節約できたみたいですね」「METHOD_CACHESを追加してこれを参照するようにしたのか↓」「define_method_attributeというprivateメソッドがgemで使われていると動かなくなる可能性があるとプルリクメッセージに書かれてました」

# activemodel/lib/active_model/attribute_methods.rb#L360
      private
-       class CodeGenerator
+       class CodeGenerator # :nodoc:
+         class MethodSet
+           METHOD_CACHES = Hash.new { |h, k| h[k] = Module.new }
+
+           def initialize(namespace)
+             @cache = METHOD_CACHES[namespace]
+             @sources = []
+             @methods = {}
+           end
+
+           def define_cached_method(name, as: name)
+             name = name.to_sym
+             as = as.to_sym
+             @methods.fetch(name) do
+               unless @cache.method_defined?(as)
+                 yield @sources
+               end
+               @methods[name] = as
+             end
+           end

「このキャッシュが効くシチュエーションは多そう: Railsワーカーのメモリを減らしてくれるいい高速化だと思います👍


同PRより


追いかけボイス:「同issueのコメントが興味深い: memory_profilerというgemはIMEMO領域の分は見てくれないのでこれで計測するとむしろ劣化したように見えるけど、heap-profilerを使うとちゃんとIMEMOを含んだallocated sizeが出るのでメモリが削減されたことが分かるそうです」

SamSaffron/memory_profiler - GitHub

Shopify/heap-profiler - GitHub

参考: Reducing Memory Usage in Ruby | Tenderlovemaking — IMEMOの解説あり

「heap-profilerのREADMEにmemory_profilerとの違いも解説してある:ObjectSpace.each_objectでは拾いきれないものがObjectSpace.dump_allだと取れるらしい(以下の記事にもObjectSpace.dump_allの解説あり↓)」

Rubyのヒープをビジュアル表示する(翻訳)

🔗 ActiveSupport::Safebufferのstringへの暗黙の型強制を非推奨化

ActiveSupport::SafeBufferでオブジェクトからstringへの正しくない暗黙の型強制を非推奨化。

オブジェクトを文字列操作でStringに暗黙的に変換するためには#to_strを実装しなければならない(String#%など一部のメソッドを除く)。ActiveSupport::SafeBufferは、特定の状況でそうしたオブジェクトに対して誤って明示的な変換メソッド (#to_s) を呼び出していた。この動作が非推奨化された。
Jean Boussier
Changelogより大意


つっつきボイス:「implicit coercionはいわゆる暗黙の型強制」「coercionってそういう意味だったんですか」「MySQLなどでもcoercionという用語を見かけますね」

coercion {名-1} : 強制、無理強い

参考: Type coercion (型強制) - MDN Web Docs 用語集: ウェブ関連用語の定義 | MDN
参考: c# - EF Core to Mysql table binding - Stack Overflow

「ソースを見るとSafeBufferはStringを継承している↓: Ruby 3でStringのサブクラスを継承したときの振る舞いが変わったことに関連しているかなと思ったけど(ウォッチ20210427)、ここでは関係なさそうかな」

# activesupport/lib/active_support/core_ext/string/output_safety.rb#L133-L134
module ActiveSupport #:nodoc:
  class SafeBuffer < String
  ...

「issue #22947を見るとp("".html_safe + Object.new)のようなケースで暗黙の型強制による問題があったんですね↓」「html_safeを呼んで+で結合すると何でもstringに変換されちゃってたのか」「to_sだとどんなオブジェクトでもtype conversionできてしまうのでエラーになるべき(to_strが実装されていないなら文字列として結合されるべきではない)という意図かなと思いました」「なるほど」「現在動いているコードはたぶん影響を受けなさそうに見えますね」「よかった〜」

# #22947より
gem "activesupport", "4.2.5"
require "active_support/core_ext/string/output_safety"

p("".html_safe + nil) #=> ""
p("".html_safe + 123) #=> "{"
p("".html_safe + Object.new) #=> "#<Object:0x007fe664939778>"

🔗 ActiveSupport::Cacheでキャッシュエントリのシリアライザをカスタマイズ可能に


つっつきボイス:「2つのプルリクのうち上が本体で、下がRailsガイドの更新でした」「キャッシュのシリアライズにたしかデフォルトでMarshal.dumpを使っていますね」「シリアライザとデシリアライザはここではCoderっていう名前なのか: 単にシリアライズ・デシリアライズするだけならSerializerでしょうけど、gzipすると書かれているので圧縮・解凍も行うという意味でCoderなのかも」「なるほど、圧縮解凍もやってるんですね」

  • :coderオプションはキャッシュエントリのデフォルトシリアライズメカニズムをカスタムのものに置き換えられる。coderdumploadに応答する必要があり、カスタムコーダーを渡すと自動圧縮は無効になる。
    guides/source/caching_with_rails.md更新部分より

config.active_support.cache_format_versionというコンフィグが互換用に追加されてますね↓」

ActiveSupport::Cacheシリアライズにより高速でコンパクトなフォーマットが追加された。

これはconfig.active_support.cache_format_version = 7.0またはconfig.load_defaults(7.0)で有効にできる。Active Support 7.0は設定に関わらずActive Support 6.1でシリアライズされたキャッシュエントリを読み込めるので、キャッシュを無効にすることなくアップグレード可能。ただしRails 6.1は新しいフォーマットを読み込めないので、新しいフォーマットを有効にする前にすべてのリーダーをアップグレードする必要がある。
Jean Boussier
同Changelogより大意

🔗Rails

🔗 Railsのガバナンスに関する声明


つっつきボイス:「上は連休前に持ち上がったBasecampの一連の騒ぎの後で出された声明ですね」「Railsの運営は(Basecampのような)特定メンバーや特定企業の一存だけで決まるものではないということを改めて確認する内容になっていました」「実際以前からそうなっていますが、Railsの今後についての不安を解消するためにも改めて声明を出したという流れですね」

「経緯についてはdiscuss.rubyonrails.orgの書き込みの冒頭にひととおりまとまっているようです↓」「もう落ち着いたのかな?」「そのうち詳しいまとめ記事が出ると思うのでそちらに期待しましょう」

「今回Basecampから退社した人々の中にはRailsの一部のコアライブラリを業務としてメンテナンスしていた人たちもいましたが、声明にもあるようにRailsはこれまでも今後もBasecampだけのものではありませんし、多くの企業がRailsを使い続けているのも確かなので、Railsがこれで終わるというものではないと考えてよいと思います」「BasecampはBasecamp、RailsはRailsですよね」

参考: プロジェクト管理の老舗Basecampで「社員の政治的意見表明禁止」により社員3分の1が退社 | TechCrunch Japan


つっつきの後で、BasecampのCEOが一連の件について謝罪を表明したという記事が出ました↓。

参考: Basecamp CEO apologizes to staff in new post: ‘We have a lot to learn’  - The VergeRuby Weeklyより)

🔗 Railsでビューコンポーネントのライブラリを構築する(Ruby Weeklyより)


つっつきボイス:「Railsのビューコンポーネントの記事を久しぶりに見たので取り上げてみました」「この記事で使われているStorybookというJavaScriptライブラリはときどき目にしますね↓」「iOSにもStorybookってあったかも」「そうそう、紛らわしい」

参考: Storybook: UI component explorer for frontend developers

「記事はステップバイステップで進めている感じ」「以下のgemでビューコンポーネントとStorybookをつないでいるそうです↓」

jonspalmer/view_component_storybook - GitHub

「ビューコンポーネントは、こういうふうにRubyのコードでCSSを記述するスタイル↓を採用する決心がつくかどうかがポイントでしょうね」「あ、こういうふうに書くんですか」「クラスの行数が増えるとRuboCopに怒られそう」

# 同記事より: app/components/button_component.rb
 frozen_string_literal: true

class ButtonComponent < ViewComponent::Base
  attr_accessor :type

  PRIMARY_CLASSES = %w[
    disabled:bg-purple-300
    focus:bg-purple-600
    hover:bg-purple-600
    bg-purple-500
    text-white
  ].freeze
  OUTLINE_CLASSES = %w[
    hover:bg-gray-200
    focus:bg-gray-200
    disabled:bg-gray-100
    bg-white
    border
    border-purple-600
    text-purple-600
  ].freeze
  DANGER_CLASSES = %w[
    hover:bg-red-600
    focus:bg-red-600
    disabled:bg-red-300
    bg-red-500
    text-white
  ].freeze
  BASE_CLASSES = %w[
    cursor-pointer
    rounded
    transition
    duration-200
    text-center
    p-4
    whitespace-nowrap
    font-bold
  ].freeze

  BUTTON_TYPE_MAPPINGS = {
    primary: PRIMARY_CLASSES,
    danger: DANGER_CLASSES,
    outline: OUTLINE_CLASSES
  }.freeze

  def initialize(type: :primary)
    @type = type
  end

  def classes
    (BUTTON_TYPE_MAPPINGS[@type] + BASE_CLASSES).join(' ')
  end

end

「この記事でやっているのはサーバーサイドのビューコンポーネントということになりますね」「たしかに」「Rails側からは扱いやすいでしょうけど、フロントエンドエンジニアがどう思うかかな: この書き方にしないといけない理由をフロント側から聞かれたときに答えにくそう」「それですよね」

🔗 Value Objectをクラスで定義してプリミティブな値と戦う


つっつきボイス:「Value Objectを使いたくなる気持ちはわかる」「Value Objectをイミュータブルにする、なるほど」

# 同記事より
class AnswerScore
  def initialize(skill_id, score)
    @skill_id = skill_id
    @score = BigDecimal(score.to_s)
  end

  attr_reader :skill_id, :score

  def +(other)
    raise ArgumentError unless self.class === other
    raise ArgumentError if self.skill_id != other.skill_id

    ScoreSum.new(skill_id: skill_id, sum: score + other.score, n: 2)
  end

  def average_score
    score.round(2)
  end

  def ==(other)
    other.class === self &&
      other.hash == hash
  end

  alias eql? ==

  def hash
    [skill_id, score].join.hash
  end
end

「この記事のようにValue ObjectをStructとかではなくクラスとしてちゃんと定義しているのはいいですね👍」「なるほど、Structではなくクラスですか」「RailsでValue ObjectというとStructで手軽に作って使い捨てるのを割と見かけますが、それだとハッシュで定義するのとあまり変わらないかなという気持ちがありますね: こういうふうにクラスとして定義できるValue Objectなら値に意味を持たせられるので好ましいと思います」「ふむふむ」

「既存にない概念ならこうやってValue Objectのクラスにする意味があると思いますし、Rubyだと比較的やりやすいんですが、後はValue Objectにする意義がどのぐらいの頻度で生じるかかな」「メンテナンスのしやすさとのバランスでしょうね」

🔗 その他Rails


つっつきボイス:「bundle install--jobsオプションでパラレルダウンロードできるとは知らなかった」「パラレルダウンロードの依存関係はちゃんと解決されるのかな?」「依存関係が衝突したら捨てるしかないでしょうね」

「記事によると、やってみたけど思ったほど速くならなくて、ネイティブgemのmake--jobsオプションを付ける方が効いたそうです↓」「ああたしかに、ネイティブ系gemのコンパイルをパラレルにする方が効くでしょうし副作用もなさそう👍」「言われてみれば」

# 同記事より
$ time MAKE="make --jobs 8" bundle install
<SNIP>
bundle install
133.25s user 35.47s system 468% cpu 36.004 total

前編は以上です。

バックナンバー(2021年度第1四半期)

週刊Railsウォッチ(20210427後編)RactorでUDPサーバーを作る、JSONシリアライザalba gem、AppleのAirTagほか

今週の主なニュースソース

ソースの表記されていない項目は独自ルート(TwitterやはてブやRSSやruby-jp SlackやRedditなど)です。

Rails公式ニュース

Ruby on Rails Discussions

Ruby Weekly

The post 週刊Railsウォッチ(20210510前編)属性メソッドをキャッシュして最適化、Railsのガバナンスに関する声明、bundle install高速化ほか first appeared on TechRacho.


週刊Railsウォッチ(20210511後編)AWS Lambda関数ハンドラをDSLで書けるyake gem、VPC Peeringが同一AZ転送量無料化ほか

$
0
0

こんにちは、hachi8833です。

週刊Railsウォッチについて

  • 各記事冒頭には🔗でパーマリンクを置いてあります: 社内やTwitterでの議論などにどうぞ
  • 「つっつきボイス」はRailsウォッチ公開前ドラフトを(鍋のように)社内有志でつっついたときの会話の再構成です👄
  • お気づきの点がありましたら@hachi8833までメンションをいただければ確認・対応いたします🙇

TechRachoではRubyやRailsなどの最新情報記事を平日に公開しています。TechRacho記事をいち早くお読みになりたい方はTwitterにて@techrachoのフォローをお願いします。また、タグやカテゴリごとにRSSフィードを購読することもできます(例:週刊Railsウォッチタグ)

🔗Ruby

🔗 yake: AWS Lambda関数ハンドラをrake風に書ける(Ruby Weeklyより)

amancevice/yake - GitHub


つっつきボイス:「いぇーく?」「なるほど、handler DSLに続けてコードを書くとLambda Functionのコードに展開してくれる感じですね↓」「おぉー」「API Gatewayを統合する形でも書けるらしい」

# 同リポジトリより
# ./lambda_function.rb
require "yake"

handler :lambda_handler do |event|
  # Your code here
end

# Handler signature: `lambda_function.lambda_handler`
# 同リポジトリより
# ./lambda_function.rb
require "yake/api"

header "content-type" => "application/json"

get "/fizz" do
  respond 200, { ok: true }.to_json
end

handler :lambda_handler do |event|
  route event
rescue => err
  respond 500, { message: err.message }.to_json
end

# Handler signature: `lambda_function.lambda_handler`

参考: AWS Lambda を Amazon API Gateway に使用する - AWS Lambda

「こんなふうにSinatra風にルーティングを書くこともできる↓: yakeを使うとデプロイも含めてLambdaやAPI Gatewayを簡単に使えるようになるんでしょうね」

# 同リポジトリより
delete "/…" do |event|
  # Handle 'DELETE /…' route key events
end

get "/…" do |event|
  # Handle 'GET /…' route key events
end

head "/…" do |event|
  # Handle 'HEAD /…' route key events
end

options "/…" do |event|
  # Handle 'OPTIONS /…' route key events
end

patch "/…" do |event|
  # Handle 'PATCH /…' route key events
end

post "/…" do |event|
  # Handle 'POST /…' route key events
end

put "/…" do |event|
  # Handle 'PUT /…' route key events
end

「デプロイ方法はどこだろう?」「API Gatewayの定義もRubyで書いているから、デプロイ機能はあるはずですね(しばらく探す)READMEの下の方に、version.rbのバージョンを更新してからbundle exec rake releaseすると書かれてた」「あ、ここですか」「シンプルに使えるのはよさそう👍

🔗 AWS LambdaでRuby 2.5サポートが間もなく終了

「Lambdaといえば、AWS LambdaでのRuby 2.5のサポート終了がアナウンスされましたね↓」「おぉ、ついに」「AWSからのお知らせメールで知りましたが、おそらくRuby 2.5レイヤを使っているユーザーにしかこのメール通知は来ていないと思います」

参考: ランタイムサポートポリシー - AWS Lambda

「サポート終了のフェーズ1が2021年7月30日から開始、フェーズ2が2021年8月30日から開始なのか」「もうすぐじゃないですか」「まだちょっと時間はあるかな」「なおRubyコミュニティでのCRuby 2.5のサポートは今年4月で既に終了しています↓」

参考: Ruby 2.5.9 Released

「AWS公式のLambdaランタイムのRubyは2.7なのかー」「そう、Lambda公式のRubyは最新安定版の3.0じゃないんですよ: なるべく長く使いたいので最新安定版で始めたいけど、Ruby 2.7だとその分期間が短くなるのが残念」


「ちなみにAWS Lambdaは昨年Rubyのマイナーバージョンを2.5.7から2.5.8に予告なしでアップグレードしたことがあって、めちゃくちゃ困りました」「えぇ!いきなりLambda関数動かなくなるのはつらい…」「gemをbundleしているコードだったんですが、Ruby本体のbundled gemであるjson gemのバージョンが上がったことでGemfile.lockの記述との差違が出てしまい、突然『何もしてないのに動かなくなった』という事態に陥りました」「おぉ…」「そのときにAWSのサポートに詳しくフィードバックしたおかげか、今回はちゃんと通知されました👍」「フィードバックは大事ですね」

🔗 ビットフィールドをRubyで扱う(Ruby Weeklyより)


つっつきボイス:「ビットフィールドといえば、C言語にあるけどあまり使ったことのない機能」「C書いたことないです…」「ビットフィールドはビットマスクの演算に使ったりしますね」

参考: ビット演算 - Wikipedia

「そうそう、こういう0oで始まるRubyの8進数リテラル、あまり見かけないけどありますね↓」

# 同記事より
irb> 0o644.to_s(2).rjust(12, "0").each_char.map { |b| b.to_i == 1 }
=> [false, false, false, true, true, false, true, false, false, true, false, false]

参考: リテラル (Ruby 3.0.0 リファレンスマニュアル)

「Rustだとこういう感じでビットフィールドを定義できるのね↓」

// 同記事より
#[macro_use]
extern crate bitflags;

bitflags! {
    struct Flags: u32 {
        const A = 0b00000001;
        const B = 0b00000010;
        const C = 0b00000100;
        const ABC = Self::A.bits | Self::B.bits | Self::C.bits;
    }
}

fn main() {
    let e1 = Flags::A | Flags::C;
    let e2 = Flags::B | Flags::C;
    assert_eq!((e1 | e2), Flags::ABC);   // union
    assert_eq!((e1 & e2), Flags::C);     // intersection
    assert_eq!((e1 - e2), Flags::A);     // set difference
    assert_eq!(!e2, Flags::A);           // set complement
}

「C言語だとまさにこう書く↓」

/* 同記事より */
#define S_IFMT   0170000
#define S_IFLNK  0120000
#define S_IFREG  0100000
#define S_IFDIR  0040000
#define S_IFCHR  0020000
#define S_IFIFO  0010000
#define S_ISUID  0004000
#define S_ISGID  0002000
#define S_ISVTX  0001000

#define S_ISLNK(m) (((m) & S_IFMT) == S_IFLNK)
#define S_ISREG(m) (((m) & S_IFMT) == S_IFREG)
#define S_ISDIR(m) (((m) & S_IFMT) == S_IFDIR)

「へ〜、Sequel::Modelを使うとenumっぽく書けるのか↓: ビットフィールドとenumは本来別物なのが気になるけど、enumっぽく書きたい人にはいいかも」

# 同記事より
class MyModel < Sequel::Model
  plugin :bit_fields, :status_bits, [ :started, :finished, :reviewed ]
end
model = MyModel.create

model.started?        # => false
model.started = true
model.started?        # => true
model.status_bits     # => 1

model.finished?       # => false
model.finished = true
model.finished?       # => true
model.status_bits     # => 3

# let's find all the finished instances
MyModel.finished(true).all

# let's find all unfinished instances
MyModel.finished(false).all

# let's find all the started and the finished instances
MyModel.started.finished.all

jeremyevans/sequel - GitHub

「そしてActive Recordでもbitfieldsというgemを使うとビットフィールドが使えるそうですよ↓」「こんなgemがあったとは」「1個のビットフィールドに最大64ビットまで定義できるらしい」「64ビットを使い切る場面はあまり想像できないかも🤔

# 同記事より
class User < ActiveRecord::Base
  include Bitfields
  bitfield :my_bits, 1 => :seller, 2 => :insane, 4 => :sensible
end

grosser/bitfields - GitHub

「ビットフィールドは、シリアル通信やネイティブのネットワークプログラミングでパケット処理を書くような低レイヤの処理で必要になることがあります」「組み込み系の処理にもよくありますね」「頻度は年に数回ぐらいですが、使うときは使います」

参考: ビットフィールド - Wikipedia

🔗 リモートのzipファイルのファイルリストだけを取り出す(Ruby Weeklyより)

つっつきボイス:「そうそう、zipファイルは冒頭の部分をうまく取り出せば、全体をダウンロードしなくてもファイルリストを取れますね」「それをRubyでやってみた記事だそうです」「記事でzipのファイル構造も引用されてる↓: ファイルエントリはこういうふうにzipファイルの冒頭に集まってます」「お〜」

ZIP-64 Internal Layout
commons.wikimedia.orgより

「この種のテクニックは結構応用が効きます: S3に置かれている超巨大なzipファイルの中身を知りたいときとか」「やりたいときありますね」「mpegなどの動画・音声ファイルもメタ情報はたいていファイルの冒頭に置かれているので、zipと同じ考え方で処理できますね」

🔗 その他Ruby

つっつきボイス:「この記事を書いたBurdette Lamarさんは、最近英語版のRubyドキュメントを熱心に更新しているそうで、その更新履歴です」「ドキュメントが新しくなるのはありがたい!」「偉大な仕事👍

🔗クラウド/コンテナ/インフラ/Serverless

🔗 AWS VPC Peeringの価格変更


つっつきボイス:「BPSの社内Slackに貼っていただいた記事です」「そうそう、VPC Peeringの転送にかかる料金が、同じAZ(Availability Zone)ID内では無料になるというお知らせ」「おー!」

参考: VPC ピア機能とは - Amazon Virtual Private Cloud

「ただし、AZの名前ではなくAZの”ID”を使う点に注意が必要です」「IDですか?」「ap-northeast-1aのようなAZは名前で、apne1-az3のように表記するのがIDです: AZ名がアカウントに依存する論理名だとすると、AZ IDはアカウントと無関係に物理データセンターにマッピングされます」「あ、なるほど」「AZ IDが同じならVPC Peeringのデータ転送が無料になるということですね(VPC Peeringそのものの利用料はかかりますが)」「AWSアカウント間で巨大なデータ転送を行いたいときとかにありがたい🙏」「ちょうど最近VPC PeeringとTransit Gatewayを使い始めたところです」

参考: リソースの AZIDs - AWS Resource Access Manager

🔗 Transit Gateway

「ちなみにVPC Peeringは文字どおり他のAWSアカウントと一対一で接続しますが、Transit Gatewayは、あるAWSアカウントのVPCにTransit Gatewayを作ると他の複数のAWSアカウントから接続できる、ルーター的な接続サービスです」「そんなことができるんですね!」「VPC Peeringは3拠点ぐらいまでなら何とかやれますが、拠点が増えると接続がメッシュ状に増えてしまうので、接続拠点が4つ以上ぐらいになったらTransit Gatewayの方が便利」「ふむふむ」

参考: AWS Transit Gateway(VPC およびアカウント接続を簡単にスケール)| AWS

「Transit GatewayはAWSアカウントのひとつをVPCホストにして他のAWSアカウントのVPC同士をルーターのように接続するので、ルーティングの設定も必要ですし、接続するVPC同士のIPアドレスが衝突してはいけないんですよ」「なるほど」「IPアドレスが衝突するとVPCを作り直さないといけなくなって、配下にあるEC2インスタンスなどもすべて作り直しになるので大変です😢

🔗JavaScript

🔗 Romeプロジェクトが法人化(Publickeyより)


つっつきボイス:「BabelやLintのようなJSツールをRomeに集約するということみたいですね」「ツール事業の法人化は、責任や問い合わせや寄付先が一元化されるという意味ではありがたい」「法人を軌道に乗せられるかどうかが勝負でしょうね」


以下はつっつき後に見つけたツイートです。

🔗 その他JS

つっつきボイス:「JSのArray.prototypeで提案されているイミュータブルなメソッド名が過去形の動詞になっているのが面白いと思って拾ってみました」「なるほど、popped()とかreversed()とかたしかに過去形」「Arrayを返すイミュータブルなメソッドが欲しいのはわかる」「後はこのメソッド名が受け入れられるかどうかでしょうね」


後編は以上です。

バックナンバー(2021年度第1四半期)

週刊Railsウォッチ(20210427後編)RactorでUDPサーバーを作る、JSONシリアライザalba gem、AppleのAirTagほか

今週の主なニュースソース

ソースの表記されていない項目は独自ルート(TwitterやはてブやRSSやruby-jp SlackやRedditなど)です。

Ruby Weekly

Publickey

publickey_banner_captured

The post 週刊Railsウォッチ(20210511後編)AWS Lambda関数ハンドラをDSLで書けるyake gem、VPC Peeringが同一AZ転送量無料化ほか first appeared on TechRacho.

Railsの技: 起動時に環境変数の存在を確認する(翻訳)

$
0
0

概要

原著者の許諾を得て翻訳・公開いたします。

Railsの技: 起動時に環境変数の存在を確認する(翻訳)

Twelove-Factor Appにも書かれているように、Railsにおける外部サービスなどのオプション設定は環境変数で行うのが定番です。こうした環境変数の値は、Gitなどのソースコード管理にチェックインせず、環境ごとに切り替えて使うのが普通です。

Railsには「イニシャライザ」という概念があり、Railsアプリの起動中に実行するコードを指します。

必要な環境変数が設定されているかどうかをチェックするカスタムのイニシャライザを追加することで、後でコードが値の存在を期待して例外が発生するのを防げるようになります。

利用法

アプリのconfig/initializersディレクトリの下に以下のイニシャライザを作成して、必須にする環境変数を配列に追加します。

# config/initializers/01_ensure_environment.rb

if Rails.env.development?
  %w[
    AWS_ACCESS_KEY_ID
    AWS_SECRET_ACCESS_KEY
    S3_BUCKET
    ALGOLIA_ID
    ALGOLIA_API_KEY
    ALGOLIA_SEARCH_KEY
    ALGOLIA_INDEX
    ALGOLIA_CAMPAIGN_INDEX
    TWITTER_API_SECRET
    TWITTER_API_TOKEN
  ].each do |env_var|
    if !ENV.has_key?(env_var) || ENV[env_var].blank?
      raise <<~EOL
      Missing environment variable: #{env_var}

      Ask a teammate for the appropriate value.
      EOL
    end
  end
end

オプション

Railsのイニシャライザファイルは、ABC順に読み込まれて実行されます。これを利用して、01_ensure_environment.rbのような名前でソート順を制御し、このイニシャライザが最初に読み込まれるようにします。

環境変数の値を空にしたサンプルの.env.sampleファイルをgitにチェックインしておけば、新しいチームメンバーがこれを利用して自分の環境を簡単に設定できるようになります。

参考資料

関連記事

Rubyの技: 文字列操作で便利かつ高速なdelete_prefixとdelete_suffix(翻訳)

The post Railsの技: 起動時に環境変数の存在を確認する(翻訳) first appeared on TechRacho.

Rails 7: has_one :through関連付けでコンストラクタが使えるようになる(翻訳)

$
0
0

概要

原著者の許諾を得て翻訳・公開いたします。

Rails 7: has_one :through関連付けでコンストラクタが使えるようになる(翻訳)

RailsのActive Recordモデルでbelongs_to関連付けを宣言すると、宣言したクラスで自動的にbuild_associationメソッドとcreate_associationのメソッドが使えるようになります。

たとえば以下の宣言があるとします。

class Book < ApplicationRecord
  belongs_to :author
end

このとき、Bookモデルの各インスタンスでbuild_authorメソッドとcreate_authorメソッドが使えるようになります。

今度は以下の3つのモデルで考えてみましょう。

# app/models/supplier.rb
class Supplier < ApplicationRecord
  has_one :account
  has_one :account_history, through: :account
end
# app/models/account.rb
class Account < ApplicationRecord
  belongs_to :supplier
  has_one :account_history
end
# app/models/account_history.rb
class AccountHistory < ApplicationRecord
  belongs_to :account
end

サプライヤーのアカウントを作成する場合は、以下のようにSupplierオブジェクトでcreate_accountを呼べます。

Supplier.first.create_account

# Supplier Load  (0.5ms) SELECT "suppliers".* FROM "suppliers" ORDER BY "suppliers"."id" ASC LIMIT $1  [["LIMIT", 1]]

# Account Load  (0.5ms) SELECT "accounts".* FROM "accounts" WHERE "accounts"."supplier_id" = $1 LIMIT $2
[["supplier_id", "6700d0ba-b8ed-4ff5-bb69-c57e7ea7f285"], ["LIMIT", 1]]

has_one: :accountという直接の関連付けは期待どおりに動きますが、AccountHistoryを作成しようとすると以下のようにエラーになります。

Supplier.first.create_account_history

# Supplier Load  (0.5ms) SELECT "suppliers".* FROM "suppliers" ORDER BY "suppliers"."id" ASC LIMIT $1  [["LIMIT", 1]]

NoMethodError: undefined method `create_account_history' for #<Supplier:0x00007f949d6016c8>

このように、has_one :throughリレーションシップを作成してもcreate_account_historyメソッドは使えるようになりません。

変更前

Ruby 7より前は、Supplierモデルにcreate_account_historyメソッドを自分で追加することで上の問題を解決できます。

class Supplier < ApplicationRecord
  has_one :account
  has_one :account_history, through: :account

  def create_account_history
    account = self.account
    account.create_account_history
  end
end

これは期待どおり動作しますが、他のhas_one :through関連付けでも同様のメソッドを自分で追加することになります。

変更後

Rails 7ではこの問題を解決するため、has_one :through関連付けでbuild_<関連付け名>メソッドとcreate_<関連付け名>メソッドが使えるようになりました(#40007)。

以下のようにcreate_account_historyを書けば、SupplierオブジェクトでAccountHistoryを作成できます。

Supplier.first.create_account_history

# AccountHistory Create (1.3ms)  INSERT INTO "account_histories" ("account_id", "user_id") VALUES ($1, $2) RETURNING "id" [["account_id", "5d671a08-1513-442a-8cfa-ac00e128d423"], ["user_id", "24571aff-3456-442a-8cfa-aerte128d423"], ["organization_user_id""6700d0ba-b8ed-4ff5-bb69-c57e7ea7f285"], ["created_at", "2021-04-09 06:26:59.002535"], ["updated_at", "2021-04-09 06:26:59.002535"]]

build_account_historyも同様に利用でき、NoMethodError例外が発生しなくなります。

関連記事

Rails 7のActive Recordにinvert_whereメソッドが追加される(翻訳)

The post Rails 7: has_one :through関連付けでコンストラクタが使えるようになる(翻訳) first appeared on TechRacho.

Rails 6+Webpacker開発環境をJS強者ががっつりセットアップしてみた(翻訳)

$
0
0

概要

原著者の許諾を得て翻訳・公開いたします。

日本語タイトルは内容に即したものにしました。画像はすべて元記事からの引用です。
原文の目次は省略しました。原文の乱れは訳文で修正してあります。以下のRailsガイドもどうぞ。

更新履歴:

  • 2019/11/28: 初版公開
  • 2021/05/06: 更新

Rails 6+Webpacker開発環境をJS強者ががっつりセットアップしてみた(翻訳)

👋皆さんこんにちは。本記事では私がRubyやRuby on Railsを学んだ一環として、ドキュメントに書かれていないことを全部盛りしました。RubyやRailsのエコシステムは初めてなので、皆さまからのフィードバックを心よりお待ちしております。お気づきの点がありましたら元記事にコメントをどうぞ。

Ruby on Railsのためのチュートリアルの大半はAPIの利用方法にフォーカスしていますが、これは理にかなっています(例: Rails をはじめよう - Rails ガイド)。

しかしrails new blogしてコントローラを編集し、git push herokuを何度かやってみるまではいいとして、そこから先はどうすればいいのでしょうか?私はその先に進みたかったので、以下についてのベストプラクティスが欲しかったのです。

  • 開発用セットアップ
  • RubyやRuby on Railsのコードエディタの設定
  • アセットの設定やオートリロード(Webpacker)
  • 再現可能なビルド、安全な環境
  • もう少し学べる書籍をいくつか

自分はこれらの点について相当じたばたしました。というのもオンライン上での議論がほとんど見当たらないからです。つまりこれらの点については「実際に体当たりで使って学ぶのが普通」ということです。

私はRailsアプリケーションを学んでデプロイするときに、自分がハマった小さなワナや、上述の課題をすべて解決するために追加しなければならなかったコード片を、以下のようにすべてREADMEファイルに盛り込むところから始めました。

ある時点からこのリストはどんどん膨れ上がってきたので、次にRailsアプリケーションを始めなければならなくなったときに「自分が使える資料にいつでも手が届く」状態にすべく書き直す必要を感じました。

このメモ書きを公開することで私の知見を共有し、(願わくば)皆さまのお役に立てられればと思います。

エディタとVS Codeのセットアップ

訳注:

参考: 原文の「エディタとVS Codeのセットアップ」セクションは、後に以下の記事に切り出されました。

私はJavaScript開発者だったので、それもあってVisual Studio Codeを使っています。JetBrainsの「Ruby 2019 State of the developer ecosystem」↓を見ると、RubyMineのようにもっとよさげなエディタがあるようですね。お使いの方はご感想を私の記事のコメントまでお知らせください。

エディタの設定

シンプルな割には忘れがちですが、VS CodeはEditorConfigをサポートしています。プロジェクトのルートディレクトリに.editorconfigファイルを作成しておけば、VS Codeを使っているどのユーザーでもインデント設定や改行コードをマスター設定にできます(訳注: VS Codeの場合はEditorConfig拡張のインストールが必要です)。

最小限の.editorconfigファイルを作成します。

# editorconfig.org
root = true

[*]
charset = utf-8
end_of_line = lf
indent_size = 2
indent_style = space
insert_final_newline = true
trim_trailing_whitespace = true

これでプロジェクトの他のコントリビューターもEditorConfigを使えるようになります。

VS Codeの拡張

VS CodeでのRailsやRubyを気持ちよく編集するために、以下の拡張を使っています。

これらの拡張を楽にインストールできるよう以下のフォルダとファイルを作成します。これでRailsプロジェクトのメンバー全員に拡張がおすすめされ、インストールをいちいち依頼しなくてもよくなります。

  • .vscode/extensions.json:
{
  "recommendations": [
    "rebornix.Ruby",
    "kaiwood.endwise",
    "vortizhe.simple-ruby-erb",
    "bung87.rails",
    "ninoseki.vscode-gem-lens"
  ]
}

これで、プロジェクトをVS Codeで開けばこれらのチーム用拡張をインストールするようヒントが表示されます。

VS Codeの設定

さらに、RubyやRailsプロジェクトの開発効率を最大限に高めるため、以下のプロジェクト設定を使っています。

  • .vscode/settings.json
{
  "ruby.useLanguageServer": true,
  "ruby.useBundler": true,
  "ruby.intellisense": "rubyLocate",
  "ruby.format": "rufo",
  "files.associations": {
    "*.html.erb": "html"
  },
  "[html]": {
    "editor.defaultFormatter": "vscode.html-language-features"
  },
  "editor.formatOnSave": true
}

これで、.rbファイルや.erb.htmlをエディタで保存するとRubyやRailsのコードが自動的にフォーマットされるようになります。

これは現時点で私が見出したベストのセットアップです。最終的にはPrettierとそれ用のRubyプラグインがベストなオプションになるかと思いますが、記事執筆時点では.erb.htmlのフォーマットがサポートされていません(#371)。

  • フォーマットがよく効くように以下をGemfileに追加しておきます。
group :development do
  # [...]
  gem 'rufo', '~> 0.7.0', require: false
end

これで、.rbファイルや.erb.htmlファイルを保存するたびに自動フォーマットされるようになります。

Webpackerの使い方

このセクションでは[S]CSSについて説明します。この説明はすべてのSCSSファイルとCSSファイルに適用できます。Webpackerは複数のフォーマットをサポートしています(webpacker/css.md at master · rails/webpacker)。

当初、Webpackerについて以下の理由で混乱しました。

Rails 6からRailsアプリケーションにWebpackが同梱されて組み込めるようになりました。これはWebpacker経由で行われます。Webpackerは、事前に設定されたWebpackにビューヘルパーを加えたものを提供することで、生成されたアセット(JavaScriptや[S]CSSファイルなど)を簡単に対応付けられるようにします。

2019年11月の時点では、デフォルトのRailsアプリケーションのerbテンプレートファイルには以下が含まれています。

  • app/views/layout/application.erb.html
  <%= stylesheet_link_tag "application", media: "all", "data-turbolinks-track": "reload" %>
  <%= javascript_pack_tag 'application', 'data-turbolinks-track': 'reload' %>

2行目は、app/javascript/packs/application.jsというエントリポイントから生成されたコンパイル済みJavaScriptファイルパスをWebpackerから取得します。

しかし1行目は、現在のアセットからのファイル生成を片っ端からアセットパイプラインに指示します。

このようにシステムが二重になっていることで混乱しました。何か理由はあるのかもしれませんが、ここではWebpackerのみを用いてアセットを生成するよう修正しましょう。

[S]CSSファイルをWebpackerで扱う

[S]CSSファイルをWebpackerで扱う(Rails 6でBootstrap 4を使うなど)には、CSSをインクルードする行を以下のように変更する必要があります。

  • app/views/layout/application.erb.html
  <%= stylesheet_pack_tag "application", media: "all", "data-turbolinks-track": "reload" %>

これにより、「application」という名前のパックについてWebpackerがスタイルシートアセットの正しいURLを提供するようになります。現在のメインのパックのファイル名をアプリケーションが実際に参照するときの名前はapp/javascript/packs/application.jsです。

stylesheet_link_tagstylesheet_pack_tagに変更されていることにお気づきでしょうか。

次に、メインのSCSSファイルを作成します(実際のファイル名や置き場所は何でも構いません)。

  • app/javascript/src/style.scss
@import "~bootstrap/scss/bootstrap"; // Bootstrapを使う予定なら

次にJavaScriptのimport文を書きます。

  • app/javascript/packs/application.js
// [...]
import "../src/style.scss";

このスタイルシートは、実際にはこのJavaScriptファイルでは読み込まれません。この記述は、単にWebpackにコンパイルを指示して、Railsアプリケーションのテンプレートでrequireのパスを指定できるようにするためのものです。

Rails 6でBootstrap 4を使う

訳注

現在(2021/05/06)はBootstrap 5がリリースされているので、yarn add bootstrapを実行するとBootstrap 5がインストールされます。

また、Bootstrap 5と併用するpopper.jsについてはyarn add @popperjs/coreでインストールするようです。

先ほどの最後の部分を@import "~bootstrap/scss/bootstrap";に変えて動かすには、Bootstrapをインストールする必要があります。ただしGemfileに書くのではなく、Yarnを使います。~を付けた~bootstrapという記述は、node_modules/bootstrap/...以下からファイルを探すようWebpackに指示するためのものです。それでは追加しましょう。

  • ターミナルで以下を実行します
yarn add bootstrap

BootstrapのJavaScriptを動かすには、JavaScriptの依存関係もインストールする必要があります。

  • ターミナルで以下を実行します
yarn add jquery popper.js

インストールしたファイルをJavaScriptアプリケーションでrequireします。

  • app/javascript/packs/application.js
require("jquery");
require("bootstrap");

[S]CSSやJavaScriptファイルのオートリロード

私にとってWebpackの強みのひとつといえばオートリロード機能ですが、デフォルトのRails 6では無効になっています。オートリロードでは、developmentモードですべてのページの更新が常に読み込まれます。オートリロードを有効にするにはwebpack-dev-serverを使う必要があります。ありがたいことに、webpack-dev-serverは新しいRails 6アプリケーションには既に同梱されているので、これをRailsサーバーと一緒に起動するだけで使えます。

  • ターミナルで以下を実行します
./bin/webpack-dev-server

私はdevelopment用サーバーでは1つのコマンドで実行できるようにするのが好みです。rails server./bin/webpack-dev-serverをパラレルに起動できれば理想的です。昨今ならProcfile.devとovermindで今風にやると楽にできそうです。

DarthSim/overmind - GitHub

overmindは事実上foremanですが、「よくできています」。私はインターネット上のツールを信頼しているのでこれを使っています。必要な作業は以下のとおりです。

  • Procfile.devを作成します
web: rails server
webpacker:  ./bin/webpack-dev-server
  • .envを作成または更新します
OVERMIND_PROCFILE=Procfile.dev

これで、rails serverrails sをローカルで起動する代わりに、以下のようにovermindでやれます。

  • ターミナルで以下を実行します
brew install overmind
overmind start
# またはovermind s

おめでとう、これでhttp://localhost:5000にアクセスすればWebサーバーが実行されます。組み込みのオートリロードも高速です。

(以前はよりシンプルなhivemindをおすすめしていましたが、Railsサーバーをデバッグする必要が生じたときは使えません)


1つ問題があったのは、WebpackerがYarn integrityチェックを頻繁に行うことでdevelopment環境で遅くなる点でした。私のYarn依存関係は最新かつ常に同じに保たれていることを確認してあるので(次の「再現可能な環境」を参照)、Yarn integrityは無効にしました。

Webpackerを使えばいろんなことができるのですが、メインのRailsドキュメントには情報がありません。Webpackerについて学ぶには以下がおすすめです。私はWebpackerについて必要なこと(特にフォルダ構造webpack-dev-server、 CSS)はすべてここで学びました。

訳注

現在はWebpackerガイドがありますので、先にこちらをどうぞ。

以下の記事も参考までにどうぞ。

【保存版】Rails 5 Webpacker公式ドキュメントの歩き方+追加情報

自動テストとデスクトップ通知

私はJavaScript方面から来たので、JestというJavaScript例外テストツールを使っていました。これは今やJavaScriptアプリケーションのテストでは定番です(他のものを使っても別に構いません)が、RubyやRuby on Railsのテストにはさまざまな方法があります。ほとんどの場合自分に必要なのは、テストの自動実行と、テストがパスまたは失敗した場合のデスクトップ通知です。

テストの自動実行ツールは皆さんも既にネットでいろいろ見つけていることでしょう。たとえば「Automating Minitest in Rails 6」はよい出発点です。

でも自分は以下のようなデスクトップ通知がどうしても必要欲しいのです。

これならエディタを切り替えずにテストの状態をチェックできます。

最も簡単に通知を実現する方法はterminal-notifierとterminal-notifier-guardを使うことだとわかってきました。

julienXX/terminal-notifier - GitHub
Codaisseur/terminal-notifier-guard - GitHub

必要な作業は、Gemfileのdevelopmentグループとtestグループに以下を追加することだけです。

gem 'terminal-notifier', '2.0.0'
gem 'terminal-notifier-guard', '1.7.0'

これでbundleを実行すれば、guardがこれらのgemを自動検出してナイスな通知を表示してくれます。


本記事では当初Growlを推していましたが、理由はterminal-notifierがその時点でうまく動かなかったためでした。しかしコメントで再挑戦を勧められて、今度はうまくいきました。

もちろんGrowlでも問題なくやれますので、既にお使いの方はそのままどうぞ。

再現可能な環境

私はNode.jsやJavaScript方面の出身なので、「再現可能な環境」に必要な正しい習慣を身に付けてきました。

再現可能な環境(reproducible environments)についてご存知ですか?Travis CIで動かそうがHerokuで動かそうが、はたまたMacbookで動かそうが、常に同一の依存関係、プラットフォーム、言語バージョンで実行するべきです。MacbookではYarn 1.9にRuby 2.6にNode.js 12.3.4という依存関係で動かしつつ、HerokuではYarn 1.1にRuby 2.4にNode.js 12.1.0という依存関係で動かしたいことなどありえません。理想は、どのバージョンについてもインストールやメンテナンスやアップグレードを簡単に行えることです。

bundler、Rails、Ruby、Node.js、Yarnもこれに該当します。次のセクションで方法を説明します。

再現可能な環境: Ruby編

  1. rbenvでさまざまなバージョンのRubyを簡単にインストールできるようにする
  2. .ruby-versionファイルでRubyバージョンを全プラットフォームで強制的に設定する
  3. Gemfileを変更して、bundler実行時にRubyのバージョンを強制的に設定する
  4. 依存関係のバージョンを抽出する(いわゆる依存関係のピニング(pinning)

1. rbenvをインストールします

2. .ruby-versionファイルを以下の内容で作成します

2.6.5
# 訳注: 2021/05/06時点の最新安定版は3.0.1です

3. GemfileのRubyバージョンを、Bundlerで使うRubyバージョンと合わせます

ruby '2.6.5'
# 訳注: 2021/05/06時点の最新安定版は3.0.1です

4. GemfileのRuby依存関係で正確なバージョンを指定します

gem 'webpacker', '~> 4.0'
# 訳注: 2021/05/06時点では以下のようになります。
# gem 'webpacker', '~> 5.0'

具体的には、上を以下のように変更します。

gem 'webpacker', '4.2.0'
# 訳注: 2021/05/06時点のWebpackerは5.3.0なので以下のように書きます。
# gem 'webpacker', '5.3.0'

ありがたいことにVS Codeにインストールした拡張のおかげで、Ruby依存関係の最新版を簡単に確認できます。Gemfileの該当行にマウスオーバーするだけで以下のように表示されます。

依存関係のピニングはデリケートな問題です。ありがたいことに、Gemfileやbundlerでバージョンを指定するときのさまざまな戦略のメリットやデメリットについてうまくまとめた良記事がありますのでご覧ください。

再現可能な環境: Node.jsとYarn編

RailsがWebpackに依存するようになったことで、Node.jsとYarnにも依存するようになりました。これは驚きですね。つまり、それぞれのバイナリで使うバージョンも正確に指定する必要があります。

以下の要領で進めます。

  1. インストールして使う.Node.jsのバージョンは.nvmrcファイルで指定する。HerokuもTravis CIも、利用するNode.jsのバージョンをこれで推測する。
  2. インストールして使うYarnのバージョンはYarnポリシーで指定する。これはYarn 1.13.0以降であればどこでも使えます。Yarn policyは誕生して1年ですが、安全です。
  3. (Webpacker経由のWebpackなどのように)Node.jsスクリプトを実行するYarnやNode.jsのバージョンは、package.jsonのenginesフィールドで検証される。
  4. 依存関係のバージョンを抽出する

1. nvmをインストールします

続いて以下の内容で.nvmrcファイルを作成します。

12.13.1
# 訳注: 2021/05/06時点のLTS版Node.jsは14.17.0です

本記事をお読みの方は、Node.jsの最新バージョンをNode.jsサイトで確認してお使いください。

2. ターミナルで以下のYarnポリシーを設定します

yarn policies set-version 1.19.1
# 訳注: 2021/05/06時点のYarnは1.22.5です

これでYarn 1.19.1がダウンロードされて.yarnrcファイルが作成され、Yarn 1.19.1を使うようYarnのバイナリに指示されます。いいですね〜❤️。繰り返しますが、本記事をお読みの方は、Yarnの最新バージョンをYarnサイトのリリース情報(およびリリース一覧)で確認してお使いください。

最新のLTS版Node.jsも利用できます。

3. package.jsonを以下のように更新します

{
  // [...],
  "engines": {
    "node": "12.13.1",
    "yarn": "1.19.1"
  }
}

4. package.jsonの依存関係をすべて正確なバージョンにピニングします

"@rails/webpacker": "^4.2.0",

具体的には、上のバージョンを以下のように変更します。

"@rails/webpacker": "4.2.0",

次のセクションでは、依存関係を実際にピニングできるツールについて説明します。

これでマスターとなるNode.jsとYarnの環境ができました!

依存関係の自動アップデート

ご覧のように、再現可能な環境づくりの一環として、私はいついかなるときでも依存関係のバージョンはピンポイントで指定しています。なにかのはずみで、環境やプラットフォームの依存関係バージョンがちょっと違ってしまうという事態になって嬉しい人などいません。そんなことになれば時間をドブに捨てるはめになります。

もちろん、Gemfile.lockやyarn.lockのようなロックファイルのおかげで大半の問題は解決できますが、それだけでは不十分です。ロックファイルを使っても失敗する可能性がある(参考)というだけでなく、私はいついかなる場合でも現在の直接的な依存関係も正確に知っておかないと気が済まないのです。今使える機能や今あるバグの両方について知りたいときに、package.jsonやGemfileの”~2.x”みたいなバージョン表記の背後で何が起きているのかを解明するのに謎のコマンドを調べまくるはめになりたくないのです。

しかし、たまには依存関係のアップグレードもやりたいのです。それも一切つらい目に遭わずに。JavaScriptやRubyの依存関係の自動アップデートについては、Renovateを一度チェックしてみることをおすすめします。

Herokuと、Yarnインストールかぶり現象

Herokuにデプロイする場合、Herokuの以下のガイドがよくできているので、これに従いましょう。

私の場合、Herokuにデプロイ中にYarnインストールが複数回トリガされるというおかしな動作に遭遇しました。ひとつはHerokuでpackage.jsonファイルが検出されたとき、もうひとつはWebpackerでアセットをバンドルしているときです。これでは困るので、これらのタスクを以下のように分割したいと思いました。

  1. Yarn依存関係をインストール
  2. Ruby依存関係をインストール
  3. アセットのコンパイル

タスク1.と2.はHerokuで自動検出されますが、自分で順序を指定することもできます。これを行うには、buildpacksとbuildpacksの順序を自分で設定し、最初にNode.jsのを、次にRubyのを使うようにしなければなりません。

  • ターミナルで以下を実行します
heroku buildpacks:clear
heroku buildpacks:add heroku/nodejs
heroku buildpacks:add heroku/ruby

今度はWebpackerによるYarnインストールをすべて無効にしなければなりません。Rakefileに以下を追加します。

Rake::Task['yarn:install'].clear
namespace :yarn do
  desc "Disabling internal yarn install from Rails"
  task :install => [:environment] do
    puts "Disabling internal yarn install from Rails"
  end
end

Rake::Task['webpacker:yarn_install'].clear
namespace :webpacker do
  desc "Disabling internal yarn install from Rails"
  task :yarn_install => [:environment] do
    puts "Disabling internal yarn install from Rails"
  end
end

この問題(#744)を調べてくれたRebecca Lynn Cremonaに感謝いたします。

設定管理

credential、設定ファイル、環境変数ついては以下の別記事をご覧ください。

RubyやRailsを学ぶのに最適なリソース一覧

これについては別記事に切り出しました。

解説は以上です。

Node.jsの経験を元にRails世界に飛び込んでみましたが、全般的に楽しめました。本記事で指摘した問題がいずれ解消しますように。そのときには本記事を更新いたします。本記事で取り上げたトピックについてもっと詳しい方がいらっしゃいましたら、ぜひ元記事にコメントいただくかvincent@codeagain.comまでお知らせください。

本記事がどこかでお役に立ちましたら、他の方にもおすすめください。

お読みいただいた皆さまに感謝いたします。

おたより発掘

関連記事

Rails 6のDocker開発環境構築をEvil Martians流にやってみた

The post Rails 6+Webpacker開発環境をJS強者ががっつりセットアップしてみた(翻訳) first appeared on TechRacho.

GitHub ActionsのイメージビルドをDockerレイヤキャッシュで高速化(翻訳)

$
0
0

概要

原著者の許諾を得て翻訳・公開いたします。

GitHub ActionsのイメージビルドをDockerレイヤキャッシュで高速化(翻訳)

はじめに: 適切なDockerレイヤキャッシングでGitHub Actionsでイメージを構築する方法を学び、ググる時間を節約しましょう。DockerのBuildKit機能がGitHubのCIランナーで簡単にセットアップできるようになったおかげで、GitHub Actionsネイティブのキャッシュ機構を使って、イメージレイヤをビルドの隙間に収容できます。シングルステージやマルチステージのプロダクションDockerfileを用いる普通のRailsアプリケーションの例と、多くのプラットフォームで使える単一イメージのマトリックスビルドの例をご紹介します。

警告:本記事ではYAML設定がたくさん登場します。

キャッシュやCIについてひととおり理解している方は、ここをクリックして楽しいYAML設定までスキップしてください。

Dockerをローカル開発で使うかどうかは各自の好みでよいと思います(私たちはDocker信者とDocker嫌いを分け隔てしません)が、クラウド時代のデプロイメントでは、スケーラブルなアプリケーションをproduction環境で実行するためにコンテナと付き合わないわけにはいきません。

現代は、ビルドとプッシュが何らかの形で絶え間なく行われている時代です(手作業であろうと、高度に自動化されたDevOpsパイプラインに乗っかっていようと変わりません)。AWS FargateもGoogle Cloud RunもHerokuもコンテナを実行できます(現在のHerokuはDockerデプロイもサポートしています)。クラウドホスティング業界で激烈な競争が繰り広げられているこの時代においては、アプリケーションを仮想マシンにデプロイするという「昔ながらの手法」にこだわってもしょうがありません。

🔗 2021年版「Dockerキャッシュ即席入門」

Dockerイメージがケーキの断面のようなレイヤ(layer: 層)で構成されていることは、今なら小学生も知っています。Dockerファイル内に欠かれるRUNCOPYADDコマンドは、読み取り専用のレイヤ(本質的にはファイルのかたまりでしかありません)を作成します。

Dockerホストはイメージの容量を削減するためにさまざまなレイヤを積み重ね、イメージを実行するときは書き込み可能な薄い「コンテナ」レイヤをその上に追加します。複数のレイヤにあるファイルが組み合わされて、コンテナ用のファイルシステム(ユニオンファイルシステム)が形成されます。

ユニオンファイルシステムを用いる主な理由は、最終的なシステムの中で「最も変更の少ない部分を安全にキャッシュ」し、「以後のビルドで再利用」したり、複数の最終コンテナ間で共有」できるようにするためです。

通信帯域の削減も目的のひとつです。依存するイメージの新バージョンがリリースされた場合に、ネットワーク経由で全レイヤを取得しなくても、変更部分を取得するだけで済みます。


つまり、カスタムDockerfileを正しく書くには、キャッシュが最もよく効くようにレイヤを配置すべきです。Dockerfileの冒頭部分にはなるべく変更が発生しないものを置き、変更が最も多く発生する部分はDockerfileの末尾にまとめるべきです。


通常、デプロイ前の最終的なコンテナで最初にコピーされるのはシステムの依存関係の部分であり、最も変化しやすい部分であるアプリケーションコードは最後にコピーされます。

DevOpsの専門家であれば、おそらくBuildKitについて聞いたことがあるでしょう。BuildKitは2017年に発表され、Docker Engine 18.09以降のすべてのディストリビューションに同梱されるまでに成熟しています。

また、バージョン19.03以降のDocker CEにはbuildxと呼ばれるDocker用のスタンドアロンCLIプラグインが含まれており(experimentalモードのみですが)、docker builddefault builderとして設定できます。

Dockerをカジュアルに使うときにも、docker buildコマンドを実行する前に環境変数DOCKER_BUILDKIT=1を設定しておけば、クールで新しい青みがかったCLIが表示されます(ビルドの出力全体が1つのターミナル画面に収まるようTTY表示に工夫が凝らされています)。これがBuildKit/buildxの動作です。また、Dockerがキャッシュを利用した箇所や、Dockerがゼロからレイヤを構築しなければならなかった箇所がひと目でわかるように出力されます。

DOCKER_BUILDKIT=1 docker build -t myimage:mytag .の出力結果

上は、AnyCable用のデモアプリのイメージをビルド中のスクリーンショットです。アプリケーションのソースコードが変更されるたびにRailsがアセットのコンパイルを再実行するようになっているため、rails assets:precompileを実行するレイヤを除いて、すべてのレイヤがDockerによってCACHEDとして解決されたことがわかります。しかもコンパイル時間はわずか30秒です。キャッシュが効かなければ、私のローカルコンピュータでビルドがすべて完了するまで3分ほど待たなくてはならないでしょう。なお、ビルド時間はビルダーシステムやコンテナ内のアプリケーションのサイズによって変わります。


Action Cableの拡張機能を作った理由やプロジェクトの今後の展開については以下をご覧ください。

AnyCable 1.0: RubyとGoによるリアルタイムWebの4年間(翻訳)

キャッシュ機構がなければ、平均的なDockerユーザーはさらにつらい思いをすることになります(信じていただきたいのですが、MacでしょっちゅうDockerを実行している私たちは嫌というほど身に沁みています)。

BuildKitの最もクールな点はDockerレイヤのアーティファクト(artifact: 成果物)を任意のフォルダに保存できることです。ローカルストレージやリモートレジストリからのエクスポートやインポートも非常に簡単です。

言うまでもありませんが、コンテナ化されたアプリケーションを扱う開発者は、本番用のDockerビルドを自分のローカルマシンで実行しないのが普通です。CIプロバイダを活用すれば、自宅での煩雑なイメージビルド作業を効率の高いデータセンターに移して開発作業を快適にできます。


しかし、実行のたびに破棄されるVM(仮想マシン)上でイメージをビルドすると、キャッシュのメリットがなくなってしまいます。


ほとんどのCIプロバイダーは分単位課金なので、CIを動かすのに十分な稼働時間を確保できます。

しかしおそらく、「ビルドに時間がかかる」「積み上がるキュー」「追加コスト」といった問題で頭の痛い方や、緊急で修正プログラムをリリースしたいのにこれらの問題のせいでインシデントに素早く対応できない悩みを抱えている方もいらっしゃるでしょう。

もちろん、CIプロバイダは自分たちの顧客を困らせるつもりなどありません。少なくともほとんどのCIプロバイダは何らかのキャッシュを実装していますし、特にコンテナのビルドが主要サービスであればなおさらです(こんにちはQuayさん)。専用のコンテナビルドサービスを使っていればこうした問題に気づかずに済むこともありますが、健全性を保つにはサービスを増やすのではなく減らすことが大切です。そこでGitHub Actionsの出番です。

🔗 GitHub Actionsを残らず飲み干す

GitHub Actionsは比較的新しいCIサービスですが、登場後たちまち巨大サービスに成長しました(ゾウあるいは体重千ポンドのゴリラ並ですね)。GitHub Actionsの主な目的は、「コードの共同作業」から「自動コードチェック」「デプロイ」まで、運用パイプライン全体を一手に引き受けることです。あらゆる開発者にとって夢のような世界がやってきました。なお、GitLabには何年も前から「パイプライン」機能がありました(GitLabの実装の方が優れているのは間違いありません)。しかし「git remote」市場の最大手であるGitHubがCI市場に参入してきたことで、今度はCI/CDを単独で提供しているプロバイダーが慌て始めています(Travis CI、お名残り惜しゅうございます)。

GitHubのUbuntuランナーやWindowsランナーにはすべてDockerがプリインストールされています(なぜかmacOSランナーにはありませんが)。したがって、「PRをメインブランチにマージする」「新しいリリースをデプロイする」のと同じように、リポジトリで発生する有意義なイベントに応じてDockerビルドも設定するのは当然の流れです。

必要なのは、ワークフローファイルのどこかにある、Dockerですっかりお馴染みになった以下の一連のステップだけです。

# Your mileage may vary
docker login
docker build -t myimage:mytag -f Dockerfile.prod .
docker push myimage:mytag
# VMはいずれにしろ破棄されるのでログアウトは不要

残念ながらこのままでは、GitHub Actionsでこれを実行するたびに、すべてのビルドがゼロから実行されてしまいます。

少し前に、GitHubワークフローの依存関係にキャッシュ機構が導入されましたが、Dockerレイヤで活用するための公式な方法が提供されていませんでした。カスタムアクションをDockerコンテナとして作成できる点は驚きです。

多くのサードパーティは、docker saveコマンドを中心にソリューションを構築するか、(Dockerでレイヤを再利用するために)新しいイメージをビルドする前に古いイメージを取り出すソリューションを構築しています。すべてのアプローチについて解説しているチュートリアルも一応ありますが、特にデフォルトのDocker機能を実装するタスクでは、いずれのアプローチも不自然に思えます。

率直に申し上げれば、Digital Oceanのドロップレット上に独自ランナーを作成して、実行中はそこにキャッシュを保持する方が理にかなっていると思えました。これは間違いなく実行可能な方法ですが、GitHubのエコシステムの外で独自ランナーを管理しなければならないため、多少手間がかかります。

🔗 いよいよアクションを少し追加する

GitHub Actionsが登場した頃はなかなかわかりませんでしたが、GitHub Actionsの最大の強みは「アクションそのもの」です。Docker社が公式に管理しているアクションの1つにbuild-push-actionというその名のとおりのアクションがあります。

GitHubの公式ドキュメントにも以下があります。

name: Publish Docker image
on:
  release:
    types: [published]
jobs:
  push_to_registry:
    name: Push Docker image to Docker Hub
    runs-on: ubuntu-latest
    steps:
      - name: Check out the repo
        uses: actions/checkout@v2
      - name: Push to Docker Hub
        uses: docker/build-push-action@v1
        with:
          username: ${{ secrets.DOCKER_USERNAME }}
          password: ${{ secrets.DOCKER_PASSWORD }}
          repository: my-docker-hub-namespace/my-docker-hub-repository
          tag_with_ref: true

しかし残念なことに、buildx/BuildKitランナーをVMのコンテキストでセットアップする(つまりDockerのキャッシュがエクスポート可能になり、正しくキャッシュできる)驚くほどシンプルな方法が存在するにもかかわらず、このドキュメントにはそのことが記載されていないのです。

以下は、docker/build-push-actionリポジトリから引用した公式なサンプルです。

name: ci

on:
  push:
    branches:
      - "master"

jobs:
  docker:
    runs-on: ubuntu-latest
    steps:
      # Check out code
      - name: Checkout
        uses: actions/checkout@v2
      # This is the a separate action that sets up buildx runner
      - name: Set up Docker Buildx
        uses: docker/setup-buildx-action@v1
      # So now you can use Actions' own caching!
      - name: Cache Docker layers
        uses: actions/cache@v2
        with:
          path: /tmp/.buildx-cache
          key: ${{ runner.os }}-buildx-${{ github.sha }}
          restore-keys: |
            ${{ runner.os }}-buildx-
      - name: Login to DockerHub
        uses: docker/login-action@v1
        with:
          username: ${{ secrets.DOCKERHUB_USERNAME }}
          password: ${{ secrets.DOCKERHUB_TOKEN }}
      # And make it available for the builds
      - name: Build and push
        uses: docker/build-push-action@v2
        with:
          context: .
          push: false
          tags: user/app:latest
          cache-from: type=local,src=/tmp/.buildx-cache
          cache-to: type=local,dest=/tmp/.buildx-cache-new
        # This ugly bit is necessary if you don't want your cache to grow forever
        # till it hits GitHub's limit of 5GB.
        # Temp fix
        # https://github.com/docker/build-push-action/issues/252
        # https://github.com/moby/buildkit/issues/1896
      - name: Move cache
        run: |
          rm -rf /tmp/.buildx-cache
          mv /tmp/.buildx-cache-new /tmp/.buildx-cache

何ということでしょう。こんな誰でも見えるところに答えがあるのに、これまで誰も気づいていなかったのです。この方法が「現実の」アプリケーションのデプロイでも使えるかどうか確認してみましょう。

🔗 キャッシュ対決

私たちのキャッシュアプローチを実際に(in action)テストするために、Evil Martiansの筆頭バックエンドエンジニアである同僚のVladimir Dementyevによるデモ用Railsアプリケーション (ソース)を使うことにします。

これはよくある「Hello Rails」的なサンプルアプリではありません。RailsネイティブのAction Cableを超高速かつメモリ効率の高いAnyCableでドロップイン拡張した、最新のリアルタイムアプリケーションの機能デモです。

このアプリは大きさも手頃で、Vladimirのいつもの仕事ぶりと同様に、驚くほど念入りにテストされています。以下はrails statsの実行結果です。

rails statsの実行結果

このアプリをforkして、フェイクのproduction向けDockerfileをいくつか追加することにします。

Dockerfile.prod
 標準的な “production” Dockerfile。
一般的なビルドの依存関係をすべて追加し、PostgreSQLクライアントとNode.jsをセットアップし、YarnとBundlerをインストールし、RubyとJSの依存関係をすべてビルドし、ソースコードをイメージにコピーし、最後に静的アセット(JavaScriptとCSS)をコンパイルします。
Dockerfile.multi
マルチステージビルドを用いてさらに進化したDockerfile。
最終的なイメージのサイズは半分強まで削減され、最終ステージには「コード」「アセット」「gem」のみが含まれます。

追加したDockerfileは、それらを用いてビルドしたイメージが単独では動作しないという意味においてのみ「フェイク」です。現実のproduction環境(Kubernetesクラスタなど)でアプリのすべての部分がクリックできるようにするには何らかのオーケストレーションが必要ですが、オーケストレーションの設定については本チュートリアルでは扱いません。

ここで確認したいのは実際のビルド時間の近似値やベンチマークだけなので、フェイクのDockerfileで十分です。

🔗 シングルステージでDockerfileをビルドする

それでは、デプロイをシミュレートするfake_deploy_singlestage.ymlワークフローを作成してみましょう。アプリケーションコードをチェックアウトし、イメージをビルドします。

現実世界であれば、ビルドしたイメージをレジストリにプッシュして、Kubernetesのマニフェストを更新するなり、新しいHelmのデプロイをトリガーするところでしょう。

ワークフローの中に2つのジョブを配置します。ジョブの1つはキャッシュを使い、もう1つは使いません。

GitHub Actions のワークフローではデフォルトでジョブがパラレルに実行されるので、2つのジョブの間に健全な競争心が生まれます。先に踊り終わった方が勝者です。

name: Fake deploy with normal Dockerfile
on: [push]

jobs:
  # This job uses Buildx layer caching
  fake_deploy_cache_single:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v2

      - name: Prepare
        id: prep
        run: |
          TAG=$(echo $GITHUB_SHA | head -c7)
          IMAGE="my.docker.registry/progapangist/anycable_demo"
          echo ::set-output name=tagged_image::${IMAGE}:${TAG}
          echo ::set-output name=tag::${TAG}
      - name: Set up Docker Buildx
        id: buildx
        uses: docker/setup-buildx-action@master

      # Registry login step intentionally missing

      - name: Cache Docker layers
        uses: actions/cache@v2
        with:
          path: /tmp/.buildx-cache
          key: ${{ runner.os }}-single-buildx-${{ github.sha }}
          restore-keys: |
            ${{ runner.os }}-single-buildx

      - name: Build production image
        uses: docker/build-push-action@v2
        with:
          context: .
          builder: ${{ steps.buildx.outputs.name }}
          file: .dockerdev/Dockerfile.prod
          push: false # This would be set to true in a real world deployment scenario.
          tags: ${{ steps.prep.outputs.tagged_image }}
          cache-from: type=local,src=/tmp/.buildx-cache
          cache-to: type=local,dest=/tmp/.buildx-cache-new

        # Temp fix
        # https://github.com/docker/build-push-action/issues/252
        # https://github.com/moby/buildkit/issues/1896
      - name: Move cache
        run: |
          rm -rf /tmp/.buildx-cache
          mv /tmp/.buildx-cache-new /tmp/.buildx-cache

  # This job builds an image from scratch every time without cache
  fake_deploy_no_cache_single:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v2

      - name: Prepare
        id: prep
        run: |
          TAG=$(echo $GITHUB_SHA | head -c7)
          IMAGE="my.docker.registry/progapangist/anycable_demo"
          echo ::set-output name=tagged_image::${IMAGE}:${TAG}
          echo ::set-output name=tag::${TAG}

      - name: Build production image
        id: docker_build
        uses: docker/build-push-action@v2
        with:
          context: .
          file: .dockerdev/Dockerfile.prod
          push: false
          tags: ${{ steps.prep.outputs.tagged_image }}

結果をコミットしてプッシュします。コールドキャッシュの結果は以下でご覧いただけます。

コールドキャッシュでのビルド結果

最初はキャッシュアクションのステップ数が増えるので、その分時間がかかります。今度は空のコミットをプッシュし、キャッシュが効くかどうかを確認してみましょう。

git commit --allow-empty -m "testing cache speed"
git push

さぁ、ドラムロールよろしく。

ウォームキャッシュでのビルド

見事にキャッシュが効きました!もちろん、実際のプロジェクトのリポジトリで空のコミットをプッシュすることはそれほどありません。あるとすれば同じコードで再度デプロイをかけるときぐらいですが、こうした操作は実際よく行われるので、Dockerキャッシュを効かせてビルド時間を2分半短縮できれば文句なしです。

今度はもう少し現実っぽい操作を行ってみましょう。コードを少し変更して(任意のクラスにコメントを追加するなど)、再度プッシュします。

git add . && git commit -m "changing some code..."
git push

アプリケーションのコードが何らかの形で変更されると、DockerfileのRUN bundle exec rails assets:precompileステップが新たに実行されます。これは、新しいアセットを必要とする形でコードが変更された「可能性」があるからです。


キャッシュが適切に効いていれば、キャッシュされたワークフローで再構築されるレイヤは1つだけになるはずです。最も時間のかかる bundle installyarn install のステップにキャッシュが効くはずです。


さて結果はどうでしょうか?

Dockerビルド中に変更されたのは、期待どおりアセットレイヤだけとなりました。ワークフロー内にproductionイメージのビルドステップが1行あるだけで、キャッシュミスが発生します。

上のスクリーンショットで示したように、キャッシュなしのワークフローではDockerイメージのビルドに3分38秒(トータルでは3分43秒)かかったのに対し、キャッシュありのワークフローでは1分29秒でイメージがビルドされました(トータルでは2分03秒)。

これでもビルド時間を半分近く削減できたんですよ!

🔗 マルチステージでDockerfileをビルドする

イメージのサイズが気になる方は(気にするべきです!)、Dockerのマルチステージビルドについて頭を整理しておく必要があります。マルチステージビルドを用いると、最終的なイメージに「デプロイ可能な」コードのみが含まれるようになり、一般的なビルドの依存関係や、Node.jsランタイム(Railsの場合)といった不要なファイルをイメージからすべて排除できます。

私たちのデモアプリのイメージの場合は、マルチステージビルドを使うと391MB、使わない場合は967MBでした。DockerイメージはCIサーバーとproductionサーバーの間で送受信されるので、ディスク容量とネットワーク帯域幅を大幅に削減できます。

それでは、サンプルのDockerfile.multiでマルチステージビルドの方法を確認してみましょう。

このDockerfileでは、productionビルドのdeployステージのみを対象としています。マルチステージのビルドをキャッシュするワークフローはシングルステージの場合とほぼ同じで、注目すべき違いはほとんどありませんが、ここを省略してしまうと失敗したときに脳の血管がブチ切れるかもしれませんので、ご注意ください。

以下の設定をじっくり見てみると、docker/build-push-action@v2ステップのcache-toキーにmode=maxというややこしそうなオプションがあるのがわかります。

name: Fake deploy with multi-stage Dockerfile
on: [push]

jobs:
  fake_deploy_cache_multi:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v2

      - name: Prepare
        id: prep
        run: |
          TAG=$(echo $GITHUB_SHA | head -c7)
          IMAGE="my.docker.registry/progapangist/anycable_demo"
          echo ::set-output name=tagged_image::${IMAGE}:${TAG}
          echo ::set-output name=tag::${TAG}
      - name: Set up Docker Buildx
        id: buildx
        # Use the action from the master, as we've seen some inconsistencies with @v1
        # Issue: https://github.com/docker/build-push-action/issues/286
        uses: docker/setup-buildx-action@master
        # Only worked for us with this option on 🤷♂️‍
        with:
          install: true

      - name: Cache Docker layers
        uses: actions/cache@v2
        with:
          path: /tmp/.buildx-cache
          # Key is named differently to avoid collision
          key: ${{ runner.os }}-multi-buildx-${{ github.sha }}
          restore-keys: |
            ${{ runner.os }}-multi-buildx

      - name: Build production image
        uses: docker/build-push-action@v2
        with:
          context: .
          builder: ${{ steps.buildx.outputs.name }}
          file: .dockerdev/Dockerfile.multi
          # Set the desired build target here
          target: deploy
          push: false
          tags: ${{ steps.prep.outputs.tagged_image }}
          cache-from: type=local,src=/tmp/.buildx-cache
          # Note the mode=max here
          # More: https://github.com/moby/buildkit#--export-cache-options
          # And: https://github.com/docker/buildx#--cache-tonametypetypekeyvalue
          cache-to: type=local,mode=max,dest=/tmp/.buildx-cache-new

      - name: Move cache
        run: |
          rm -rf /tmp/.buildx-cache
          mv /tmp/.buildx-cache-new /tmp/.buildx-cache

今回のベンチマーク結果もだいたい同じなので、ご自分で比較してみてください。

Dockerキャッシュのささやかな実験は最終的に成功しました!


「コミット時にアプリケーションコードだけが変更される」「依存関係は一切変更されない」という最も一般的なシナリオでは、デプロイ時間を半分に短縮できます。


もちろん、デプロイ中にGemfile.lockyarn.lockが変更されればbundle installyarn installのキャッシュレイヤが無効になり、ビルド時間は増加しますが、成熟したアプリケーションではほとんど起きません(依存関係の更新が必要な場合を除く)。

🔗 ボーナス: vendorイメージやライブラリの「マトリックスビルド」

Evil MartiansではFullstaq Rubyを愛用しています。Fullstaq Rubyはjemallocmalloc_trimパッチで最適化され、アプリケーションのメモリ使用量を30%〜50%削減できます。

Kubernetes上のproductionアプリケーションをFullstaq Rubyに移行した成果については、以下の過去記事をどうぞ。

Fullstaq Rubyの第一印象とDocker/Kubenetes Rubyアプリとの統合(翻訳)

Fulllstaq Rubyのメンバーが公式なコンテナ版のプラットフォームに取り組んでいる一方で、Evil Martiansのバックエンドエンジニアや外部コントリビュータはDebian 9 (stretch および stretch-slim) および Debian 10 (buster および buster-slim) 上で動作する Ruby 3.0.0、2.7.2、2.6.6のDockerイメージ↓をメンテナンスしています。

evilmartians/fullstaq-ruby-docker - GitHub

最近私たちがセットアップしたGitHub Actionは、ビルドマトリックス戦略を用いて、24の(わずかに)異なるイメージを同時にビルドします。

このリポジトリに置いたbuild-pushワークフローをご覧いただければ、私たちのアプローチがマトリックスビルドでどのように機能するかを確認できます。


レイヤキャッシュを有効にした場合、ビルド時間は1イメージあたり16~60秒程度です。


キャッシュ機構の旅はこれにておしまいです。お読みいただいた皆さまに感謝いたします。

本記事に掲載されているワークフローの完全版は私たちの以下のリポジトリから自由に取得して利用できます↓。

progapandist/anycable_rails_demo - GitHub

原文注

本記事は、キャッシュの仕組みを超わかりやすく解説したブログ記事やdocker-action-examplesリポジトリを公開したDocker社の皆さまのお力添えなしには実現しませんでした。

🔗 詳しく知りたい方へ

  • Evil MartiansでDockerを「ローカル」開発環境にセットアップする方法については、AnyCableTestProfの作者であるVladimir Dementyev氏による以下の画期的な記事をご覧ください。同記事では、Docker Composeの作業を楽にしてくれる火星印Dipツール(Mikhail Merkushin作)も紹介されています。フレームワークや言語に依存しないDockerとDipのマジックについては、“Reusable development containers with Docker Compose and Dip”もご覧ください。

クジラに乗ったRuby: Evil Martians流Docker+Ruby/Rails開発環境構築(翻訳)

訳注

Dipツールについては以下の記事もどうぞ。

docker-composeを便利にするツール「dip」を使ってみた

🔗 お知らせ

自社Railsアプリケーションの「テラフォーミング」、クラウド向けの堅牢なデプロイメントパイプラインの構築、コンテナ中心の社内エンジニアリング文化の構築で支援をお求めの方は、お気軽にEvil Martiansのフォームまでご相談をお寄せください。Evil MartiansのエンジニアとDevOpsスペシャリストが、お客様のデジタルプロダクトの効率を最大化するためにお力添えいたします。

本記事の翻訳や転載についてのご相談は、まずメールにてお願いします。

The post GitHub ActionsのイメージビルドをDockerレイヤキャッシュで高速化(翻訳) first appeared on TechRacho.

Viewing all 1388 articles
Browse latest View live