ウェブサービスを作っています。

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 からアイデアを拝借しています。ありがとうございます。

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 の書き方については、

が参考になりました。ありがとうございます。

gihyo.jp

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 を再起動して、デプロイします。

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 の parsenone にしてあげます。

notifier = Slack::Notifier.new('WEBHOOK_URL')
notifier.ping 'example.com', parse: 'none'

参考

api.slack.com