rake kamal:ssh, kamal:console などを作っておくと便利
Kamal で、コンテナ作業をしたいとき kamal app exec -i --reuse "bin/rails console" などと入力するのは大変です。
以下のような rake タスクを作っておくと、rails kamal:console などで実現できて便利かと思います。
lib/tasks/kamal.rake
namespace :kamal do task :ssh do sh 'kamal app exec -i --reuse bash' end task :console do sh 'kamal app exec -i --reuse "bin/rails console"' end namespace :maintenance do # using turnout gem task :start do sh 'kamal app exec --reuse --roles=web "bin/rails maintenance:start"' end task :end do sh 'kamal app exec --reuse --roles=web "bin/rails maintenance:end"' end end end
Fly.io からアイデアを拝借しています。ありがとうございます。
Elasticsearch v7 最新版と elasticsearch-sudachi が入った Docker イメージを作る
大変便利な elasticsearch-sudachi (analysis-sudachi) ですが、 2024/1/28 現在ですと Elasticsearch の v7 最新版 7.17.17 に対応したパッケージが提供されていません。
そこで、analysis-sudachi を自前ビルドする Dockerfile を作りました。
README に書かれている
./gradlew -PengineVersion=es:x.x.x build
ですとビルドに失敗したため、
./gradlew -PengineVersion=es:x.x.x build -x integration:test
のように、integration:test タスクを除いてビルドしています。(なぜこのタスクの除く必要があるのか、わかる方がいたら教えてほしいです)
Dockerfile
# syntax = docker/dockerfile:1 ARG ES_VERSION=7.17.17 ARG SUDACHI_DIC_VERSION=20240109 FROM gradle:8 AS build # Build analysis-sudachi ARG ES_VERSION RUN git clone https://github.com/WorksApplications/elasticsearch-sudachi.git \ && cd elasticsearch-sudachi \ && ./gradlew -PengineVersion=es:$ES_VERSION build -x integration:test FROM elasticsearch:$ES_VERSION # Install anaylsis-icu plugin RUN ./bin/elasticsearch-plugin install analysis-icu # Install analysis-sudachi plugin ARG ES_VERSION COPY --from=build /home/gradle/elasticsearch-sudachi/build/distributions/elasticsearch-$ES_VERSION-analysis-sudachi-*.zip /tmp/analysis-sudachi.zip RUN ./bin/elasticsearch-plugin install file:///tmp/analysis-sudachi.zip \ && rm /tmp/analysis-sudachi.zip # Install sudachi dictionaries ARG SUDACHI_DIC_VERSION RUN curl -O http://sudachi.s3-website-ap-northeast-1.amazonaws.com/sudachidict/sudachi-dictionary-$SUDACHI_DIC_VERSION-core.zip \ && curl -O http://sudachi.s3-website-ap-northeast-1.amazonaws.com/sudachidict/sudachi-dictionary-$SUDACHI_DIC_VERSION-full.zip \ && unzip sudachi-dictionary-$SUDACHI_DIC_VERSION-core.zip \ && unzip -o sudachi-dictionary-$SUDACHI_DIC_VERSION-full.zip \ && mkdir ./config/sudachi \ && mv sudachi-dictionary-$SUDACHI_DIC_VERSION/*.dic ./config/sudachi/ \ && rm -rf sudachi-dictionary-$SUDACHI_DIC_VERSION*
Dockerfile の書き方については、
が参考になりました。ありがとうございます。
Rails + Kamal + Cloudflare で訪問者の IP アドレスを取得する
Cloudflare を使っていると、Rails の request.remote_ip に訪問者の IP アドレスではなく、Cloudflare の IP アドレスが入ってきてしまいます。
cloudflare-rails という gem でこの問題が解消されるはずなのですが、なぜかうまくいかず悩んでいました。
原因は、Kamal が使っている Traefik が、Rails に X-Forwarded-For ヘッダを渡していないためのようでした。
というわけで、対応策を書いていきます。Kamal のバージョンは 1.3.1、Traefik のバージョンは 2.9 です。
まず、cloudflare-rails gem を入れます。
Gemfile
group :production do gem 'cloudflare-rails' end
次に、Traefik が X-Forwarded-* ヘッダを渡すように、Kamal で起動オプションを設定します。
信頼できない IP アドレスの除去は cloudflare-rails gem がやってくれます。
config/deploy.yml
traefik: args: entrypoints.http.address: ':80' entrypoints.http.forwardedHeaders.insecure: true
- Traefik EntryPoints Documentation - Traefik
- Get real IP in access log when using CloudFlare or other load-balancers · Issue #10002 · traefik/traefik · GitHub
Traefik を再起動して、デプロイします。
kamal traefik reboot kamal deploy
以上で、request.remote_ip が訪問者の IP アドレスを返すようになるはずです。
Sidekiq でリトライが起きたときだけ Sentry に送信する
エラートラッキングサービスの Sentry ですが、 一時的な通信エラーなど、Sidekiq で 1 回リトライすれば解決するようなエラーまで捕捉されると、通知が多くて厄介です。
そこで、リトライが起きたときだけ Sentry に送信されるようにしてみます。
まず、sentry-sidekiq gem を入れます。
Gemfile
gem 'sentry-rails' gem 'sentry-ruby' gem 'sentry-sidekiq'
次に、Sentry 設定の before_send を使ってフィルタ設定をします。
config/initializers/sentry.rb
Sentry.init do |config| ... if Sidekiq.server? config.before_send = ->(event, hint) { sidekiq_context = event.contexts[:sidekiq] # sidekiq_context['retry_count'] が nil => リトライ前 (sidekiq_context && sidekiq_context['retry_count'].nil?) ? nil : event } end end
以上で、リトライが起きたときだけ Sentry にエラーが送信されるようになります。
Ferrum を使った DOM 要素の可視性チェック
Ruby で Chrome を操作する gem Ferrum で、DOM 要素が表示されているかどうかを調べる方法です。
次のような visible? メソッドを作ります。
def visible?(selector) expr = <<~JS function(node) { return window.getComputedStyle(node).getPropertyValue('display') !== 'none' } JS @browser.evaluate_func expr, @browser.at_css(selector) end @browser = Ferrum::Browser.new @browser.go_to 'https://example.com/' @browser.quit
これを使って、たとえば h1 要素が表示されるまで待つ処理は、次のように書けます。
sleep 0.1 until visible?('h1')
実際には無限ループに陥らないよう、タイムアウト処理を入れたほうがよいかと思います。
Rails アプリの開発で Browsersync を使用する
追記(2022/12/24): proxy を使用しない方法に書き換えました。
Rails v7.0.4 で、sprockets-rails, jsbundling-rails を使用している前提です。
Browsersync は v2.28.1 で確認しています。
まずは、プロジェクトのルートでインストール。
yarn add browser-sync --dev
続いて、設定ファイル (bs-config.js) をプロジェクトのルートに置きます。
ドキュメントを参照し、お好みの内容に書き換えてください。
module.exports = { files: [ 'app/views', 'app/helpers', 'app/assets/builds/application.{js,css}' 'app/components' ], port: 3001, ghostMode: false, notify: false, injectChanges: false // css 更新時もリロード };
app/views/application/_browsersync.html.slim
javascript id="__bs_script__":
(function() {
try {
var script = document.createElement('script');
if ('async') {
script.async = true;
}
script.src = 'http://HOST:3001/browser-sync/browser-sync-client.js?v=2.28.1'.replace("HOST", location.hostname);
if (document.body) {
document.body.appendChild(script);
}
} catch (e) {
console.error("Browsersync: could not append script tag", e);
}
})()
app/views/layouts/application.html.slim
body ... = render 'browsersync' if Rails.env.development?
※ slim で書いています。
最後に、Procfile.dev へ以下を追記します。
browsersync: yarn browser-sync start --config bs-config.js
あとは、いつものように bin/dev を起動して開発するだけです。
slack-notifier で autolink を無効にする
slack-notifier で、URL がリンクになるのを防ぐ方法です。

payload の parse を none にしてあげます。
notifier = Slack::Notifier.new('WEBHOOK_URL') notifier.ping 'example.com', parse: 'none'
参考