Skip to content
RailsアプリケーションでSlack通知を実装する記事のサムネイル

RailsアプリケーションでSlack通知を実装する ​

最近は業務でRuby on Railsを用いた開発を行なっており、個人開発でもRailsを使い始めました。 業務でもSlack通知機能をいくつか実装しましたが、既存の実装やSlack Botがすでに用意された環境だったため、通知処理を書き足すだけで済んでいました。 今回は個人で開発しているアプリケーションにもSlack通知を入れたかったので、Slack Botの作成からRailsアプリケーションでの実装までを一通り行ないました。その備忘録になります。 なお、Rails環境の構築についてはここでは触れませんのでご自身で用意してください。

Slack Botの準備 ​

まずはSlack Botの準備から進めます。

Botの作成とトークン取得 ​

Slack API: Applicationsページで「Create New App」ボタンを押下します。 今回は通知を送るだけが目的なので「Starter app」を選択しました。アプリ名を入力し、追加先のWorkspaceを選択します。 作成できたらアプリのページに遷移するので、サイドバーの「OAuth & Permissions」を押下し、Bot User OAuth Tokenをコピーしておきます。 このトークンは後でRailsアプリケーション内で使用します。

Bot User OAuth Tokenの取得

INFO

メッセージの送信にはchat:writeスコープが必要です。 「Starter app」から作成した場合は最初から付与されていますが、「From scratch」で作成した場合は同じ「OAuth & Permissions」ページのBot Token Scopesにchat:writeを追加し、Workspaceへインストールしてください。

チャンネルIDの取得 ​

通知先にしたいチャンネルのIDを取得します。 チャンネル詳細を開くと一番下にチャンネルIDが表示されているので、コピーしておきます。

チャンネルIDの取得

Botの招待 ​

作成したSlack Botを通知先のチャンネルに招待します。 チャンネル詳細の「エージェントとアプリ」タブを押下し、そこから追加できます。

Botの招待

実装 ​

ここまででBotの準備ができましたので、あとはRails側の実装です。

gemのインストール ​

今回インストールするgemは下記の2つです。

  • slack-ruby-client 3.2
  • config 5.6

コマンドは環境によって変わると思いますが、私はDocker環境ですので下記コマンドでインストールします。

bash
docker compose exec api bundle add slack-ruby-client config

続けてconfig gemを初期化します。

bash
docker compose exec api bin/rails generate config:install

INFO

config gemは必須ではありません。credentialsや環境変数を直接参照する形でも実装できます。 この記事ではconfig + credentialsを用いるので、別の方法で実装する場合は適宜読み替えてください。

credentialsにトークンを保存する ​

credentialsは暗号化されているため、専用コマンドで編集します。 私はVSCodeで編集したいので、このコマンドだけはコンテナ内ではなくホスト側で実行しています。 コンテナ内で編集する場合は他のコマンドと同じようにdocker compose exec apiを付けて、EDITOR=vimのようにコンテナ内で使えるエディタを指定してください。

bash
EDITOR="code --wait" bin/rails credentials:edit

エディタが開いたら、先ほど取得したトークンを記述します。

yaml
slack:
  api_token: xoxb-取得したBotトークン

なお、環境ごとに別のBotを使いたい場合は--environment developmentのように指定すると、環境別のcredentialsを作れます。この記事では1つで進めます。

credentialsをsettings.ymlから参照する ​

config/settings.ymlの中身を下記に置き換えます。

yaml
slack:
  api_token: <%= Rails.application.credentials.dig(:slack, :api_token) %>

config gemはYAMLを読み込む前にERBを評価するので、<%= %>の中にRubyを書けます。これでcredentialsの値がSettings経由で参照できるようになります。 digを使うと、未設定のときに例外ではなくnilが返ります。

読めているか確認します。

bash
docker compose exec api bin/rails runner 'p Settings.slack.api_token&.slice(0, 5)'
# "xoxb-"

チャンネルIDを設定 ​

取得したチャンネルIDを設定します。 チャンネルIDは秘匿情報ではないのでconfig/settings.ymlに直接書きます。 ローカルとプロダクションで通知先を変更したい場合は、config/settings/production.ymlにプロダクション用のチャンネルを設定してください。

yaml
slack:
  api_token: <%= Rails.application.credentials.dig(:slack, :api_token) %>
  channel: "C0BQC9412CY"

Slack APIのラッパーを作成する ​

ファイルはapp/lib/slack/api_client.rbに作ります。lib/ではありません。 Railsはapp/直下のサブディレクトリを自動的にオートロード対象にするので、app/lib/に置けば何も設定せずにSlack::ApiClientを参照できます。一方でlib/はデフォルトではオートロードされないため、そのまま置くとNameErrorになります。

ruby
module Slack
  class ApiClient
    def initialize(token: Settings.slack.api_token)
      @client = Slack::Web::Client.new(token:)
    end

    def send_message(channel, text)
      @client.chat_postMessage(channel:, text:)
    end
  end
end

ここまで実装できたら、実際に通知が飛ぶか試してみましょう。 下記コマンドを実行し、Slackに通知が届けば成功です。

bash
docker compose exec api bin/rails runner 'Slack::ApiClient.new.send_message(Settings.slack.channel, "テスト通知")'

通知処理をJobに切り出す ​

送信先は外部APIなので失敗することがあります。呼び出し元と切り離し、非同期実行やリトライに載せられる形にしておきます。 下記コマンドでファイルを作成します。

bash
docker compose exec api bin/rails generate job slack/notify

ファイルを作成したら中身を下記のように変更します。 module Slackで囲むことでSlack::ApiClientと同じ名前空間に入るため、ApiClientだけで参照できます。 ジェネレータはclass Slack::NotifyJobというコンパクトな形式で生成しますが、この書き方だとレキシカルスコープにSlackが入らず、ApiClientだけではNameErrorになります。そのためmodule Slackで囲むネスト形式に書き換えています。

ruby
module Slack
  class NotifyJob < ApplicationJob
    queue_as :default

    def perform(text)
      ApiClient.new.send_message(Settings.slack.channel, text)
    end
  end
end

Jobに切り出しておくと、下記のようにリトライも宣言できます。必要に応じて追加してみてください。

ruby
retry_on Slack::Web::Api::Errors::HttpRequestError, wait: :polynomially_longer, attempts: 3

slack-ruby-clientはタイムアウトや接続断、Slack側の5xxといった通信まわりの失敗をSlack::Web::Api::Errors::HttpRequestErrorにまとめてくれるので、Job側ではこれを拾えば一時的な失敗を再試行できます。 逆に権限不足やチャンネル不正のように、待っても結果が変わらないエラーはリトライしても無駄なので対象にしません。 なおレート制限(HTTP 429)はSlack::Web::Api::Errors::TooManyRequestsErrorとして飛んできますが、こちらはHttpRequestErrorとは別系統のため上記のretry_onでは拾えません。 SlackがRetry-Afterヘッダーで待つべき秒数を返してくれるので、厳密に対応するなら固定のバックオフではなくerror.retry_afterを見て待つ実装にするのが良いかもしれません。

ではJobを実行してみます。この記事ではすぐに結果を確認したいので、以降も同期実行のperform_nowを使用します。 実際に非同期で動かす場合は、Solid Queueなどのキューアダプタを設定したうえでperform_laterに変更してください。

bash
docker compose exec api bin/rails runner 'Slack::NotifyJob.perform_now("Jobからの通知")'

rakeタスクから実行する ​

rakeタスクはlib/tasks/配下に置きます。ここに置いた.rakeファイルはrakeコマンドの実行時にRailsが読み込んでくれるので、app/libと違ってオートロードの設定は不要です。 bin/rails generate taskというコマンドもありますが、生成されるのはlib/tasks/<namespace名>.rakeで、引数を受け取る形にも対応していません。今回はほとんど書き換えることになるので手動で作成します。

ruby
# frozen_string_literal: true

namespace :slack do
  desc "Slackに通知する。例: bin/rails 'slack:notify[こんにちは]'"
  task :notify, [:text] => :environment do |task, args|
    text = args[:text].presence || "定期通知です"

    Rails.logger.info("#{task.name}: Start")
    Slack::NotifyJob.perform_now(text)
    Rails.logger.info("#{task.name}: End")
  end
end

rakeのnamespace :slackはタスク名にslack:という接頭辞を付けるだけで、Rubyのモジュールとは無関係です。そのためnamespace :slackの中にいてもSlack::は省略できず、Slack::NotifyJobとフルネームで書く必要があります。 また:environmentを付けないとRailsアプリケーションが読み込まれず、Slack::NotifyJobもSettingsも参照できません。 ブロックの第1引数に渡ってくるのはタスク名の文字列ではなくRake::Taskオブジェクトなので、タスク名はtask.nameで取り出しています。 なおRails.loggerの出力はターミナルではなくlog/development.logに書き込まれます。確認したい場合はログファイルを見てください。 では引数ありとなしで実行してみます。

bash
docker compose exec api bin/rails slack:notify
bash
docker compose exec api bin/rails 'slack:notify[記事のテスト通知]'

これで手動実行までできました。 実際に定期通知として運用する場合は、cronやホスティング先のスケジューラにrakeタスクを登録する方法や、Solid Queueを導入してconfig/recurring.ymlにJobを直接登録する方法などがあります。今回はここまでを範囲とします。


いかがだったでしょうか。今回は単純なテキスト送信だけでしたが、Slack通知では特定のユーザーやグループへのメンションなども行なえます。必要であれば調べて実装してみてください。