Railsじゃないサービスの管理/集計画面をRailsのActiveAdmin使って作る


railsじゃないサービスの管理/集計画面を作りたくなる事があったのでメモ。on AWS。

前提

  • 管理対象:例えばec2にアプリケーションサーバ(java)、rdsのDB(mysql)で構築されている。
  • 管理画面:別のec2と別のrds(同じrdsでもDB分ければいいけど)に作る。
  • 開発環境の構築は省略。

デプロイ先のシステム構成と主なgem

  • centOS 6.5
  • ruby 2.2.2
  • rails 4.1.6
  • nginx 1.9.11
  • unicorn 5.0
  • activeadmin 1.0.0pre2
  • devise 3.5.5
  • cancancan 1.13
  • whenever 0.9

開発

管理対象と管理画面自体のDBが異なるけどどうするか?

コードで接続先を分ける事が必要。それぞれの接続先を定義したdatabase.ymlを作った上で、どちらに接続するかをModel単位で分けるイメージ。具体的には、

database.yml
default: &default
  adapter: mysql2
  encoding: utf8
  pool: 5
  username: {ユーザ}
  password: {パスワード}
  socket: /tmp/mysql.sock

# 管理/集計画面用DBの接続先
development:
  <<: *default
  host: {管理・集計画面用DB/開発環境のエンドポイント}

production:
  <<: *default
  host: {管理・集計画面用DB/本番環境のエンドポイント}
  database: {DB名}
  username: {ユーザ}
  password: {パスワード}


# 集計対象DBの接続先
target_development:
  <<: *default
  host: {集計対象DB/開発環境のエンドポイント}

target_production:
  <<: *default
  host: {集計対象DB/本番環境のエンドポイント}
  database: {DB名}
  username: {ユーザ}
  password: {パスワード}

とした場合、集計対象用のDBのModelを次のクラスを継承して作る。

target.rb
class Target < ActiveRecord::Base
  self.abstract_class = true

  if Rails.env == 'production'
    establish_connection(:target_production)
  else
    establish_connection(:target_development)
  end
end

管理・集計用のDBは普通にActiveRecord::Baseを継承して作ればOK。

認証機能が欲しい

認証機能はdeviseで簡単にできる。
activeadminインストールで自動生成されたadmin_user含む3つのモデルについて、マイグレーションとモデルを作成する。admin_userは管理/集計画面のユーザ、admin_roleは権限、admin_user_admin_roleは中間テーブル。

db/migrate/〜_devise_create_admin_users.rb
class DeviseCreateAdminUsers < ActiveRecord::Migration
  def change
    create_table(:admin_users) do |t|
      ## Database authenticatable
      t.string :email,              null: false, default: ""
      t.string :encrypted_password, null: false, default: ""

      ## Recoverable
      t.string   :reset_password_token
      t.datetime :reset_password_sent_at

      ## Rememberable
      t.datetime :remember_created_at

      ## Trackable
      t.integer  :sign_in_count, default: 0, null: false
      t.datetime :current_sign_in_at
      t.datetime :last_sign_in_at
      t.string   :current_sign_in_ip
      t.string   :last_sign_in_ip

      ## Confirmable
      t.string   :confirmation_token
      t.datetime :confirmed_at
      t.datetime :confirmation_sent_at
      t.string   :unconfirmed_email # Only if using reconfirmable

      ## Lockable
      t.integer  :failed_attempts, default: 0, null: false # Only if lock strategy is :failed_attempts
      t.string   :unlock_token # Only if unlock strategy is :email or :both
      t.datetime :locked_at


      t.timestamps null: false
    end

    add_index :admin_users, :email,                unique: true
    add_index :admin_users, :reset_password_token, unique: true
    add_index :admin_users, :confirmation_token,   unique: true
    add_index :admin_users, :unlock_token,         unique: true
  end
end
model/admin_user.rb
class AdminUser < ActiveRecord::Base
  has_many :admin_user_admin_roles
  has_many :admin_roles, through: :admin_user_admin_roles

  # Include default devise modules. Others available are:
  # :confirmable, :lockable, :timeoutable and :omniauthable
  devise :database_authenticatable,
         :recoverable, :rememberable, :trackable, :validatable,
         :confirmable, :lockable, :timeoutable

  def has_role?(admin_role_sym)
    admin_roles.any? { |r| r.name.underscore.downcase.to_sym == admin_role_sym }
  end
end
db/migrate/〜create_admin_roles.rb
class CreateAdminRoles < ActiveRecord::Migration
  def change
    create_table :admin_roles do |t|
      t.string :name

      t.timestamps
    end
    add_index :admin_roles, :name, :unique => true
  end
end
model/admin_role.rb
class AdminRole < ActiveRecord::Base
  has_many :admin_user_admin_roles
  has_many :admin_users, through: :admin_user_admin_roles
end
db/migrate/〜create_admin_user_admin_roles.rb
class CreateAdminUserAdminRoles < ActiveRecord::Migration
  def change
    create_table :admin_user_admin_roles do |t|
      t.integer :admin_role_id
      t.integer :admin_user_id

      t.timestamps
    end
    add_index(:admin_user_admin_roles, :admin_role_id)
    add_index(:admin_user_admin_roles, :admin_user_id, unique: true)
  end
end
model/admin_user_admin_role.rb
class AdminUserAdminRole < ActiveRecord::Base
  belongs_to :admin_user
  belongs_to :admin_role
end

admin_user.rbの様々な機能は、activeadminインストール時にコメントアウトされている属性を有効にすると使えるようになる。confirmableとか。合わせてmigrationも有効にしておく。
また、confirmableなどでメール送信できるようにしとかなければいけないのでそれもやっておく。AWSなのでSES使ってaction_mailerでメール送る、など。

以上でmodelが出来上がったので、ユーザの追加編集削除が出来るようadmin_user.rbを好みでカスタマイズして使う。例えば下記は初期パスワードを自動で作ってメールで送信するようになっている。

admin/admin_user.rb
ActiveAdmin.register AdminUser do
  permit_params :email, :password, :password_confirmation, admin_role_ids: []

  index :download_links => false do
    selectable_column
    id_column
    column :admin_roles do |f|
      f.admin_roles.size > 0 ? f.admin_roles.map { |r| r.name }.join(", ") : ''
    end
    column :email
    column :current_sign_in_at
    column :sign_in_count
    column :created_at
    actions
  end

  filter :email
  filter :admin_role
  filter :current_sign_in_at
  filter :sign_in_count
  filter :created_at

  form do |f|
    f.inputs "Admin Details" do
      f.input :email
      f.input :admin_roles
    end
    f.actions
  end

  controller do
    def create
      if params[:admin_user][:password] == params[:admin_user][:email]
        redirect_to new_admin_admin_user_path, alert: 'メールアドレスと同じ文字列をパスワードに設定する事はできません'
        return
      end
      generated_password = Devise.friendly_token.first(9)
      params[:admin_user][:password] = generated_password
      params[:admin_user][:password_confirmation] = generated_password
      InitialPassword.send_generated_password(params[:admin_user][:email], generated_password).deliver
      super
    end
    def update
      if params[:admin_user][:password] == params[:admin_user][:email]
        redirect_to edit_admin_admin_user_path, alert: 'メールアドレスと同じ文字列をパスワードに設定する事はできません'
        return
      end
      if params[:admin_user][:password].blank?
        params[:admin_user].delete("password")
        params[:admin_user].delete("password_confirmation")
      end
      super
    end
  end
end
mailers/admin_user.rb
class InitialPassword < ActionMailer::Base
  default from: '[email protected]'
  default reply_to: '[email protected]'

  def send_generated_password(address, generated_password)
    @generated_password = generated_password
    mail to: address, subject: '初期パスワード'
  end
end

権限管理がしたい

cancancanで出来る。rails g cancancan:abilityで作ったファイルを、super(全操作可能)/admin(ユーザと権限の追加・編集・削除だけNG)/power(generalで閲覧できるページに加えて秘匿性の高いページの閲覧可能)/general(ダッシュボードなど一部のページの閲覧のみ可能)の権限で分ける場合は次のようにする。

models/ability.rb

class Ability
  include CanCan::Ability

  def initialize(user)
    user || AdminUser.new
    send(user.admin_roles.first.name.downcase)
  end

  def super
    can :manage, :all
  end

  def admin
    can :manage, :all
    cannot :manage, AdminUser
    cannot :manage, AdminRole
  end

  def power
    general
    can :manage, ActiveAdmin::Page, name: "秘匿性の高いページ1"
    can :manage, ActiveAdmin::Page, name: "秘匿性の高いページ2"
  end

  def general
    can :manage, ActiveAdmin::Page, name: "Dashboard"
    can :manage, ActiveAdmin::Page, name: "秘匿性の低いページ1"
    can :manage, ActiveAdmin::Page, name: "秘匿性の低いページ2"
  end
end

ちなみに閲覧のみで良いのにpower/generalにmanage権限をつけている理由はcsvダウンロードを許可したかったから。
また、各権限のレコードををadmin_rolesにインサートしておくのを忘れない事。

バッチで定期的に集計値を保存したい

whenever使ったら簡単にできる。集計用テーブルaggregationsを作った上で、データ作成するコードといつ集計するかを決めるコードを作る。

lib/tasks/aggregation.rake
namespace :aggregation do
  desc 'aggregate basic data'
  task :basic => :environment do
    {集計してaggregationsにデータを突っ込むコード}
  end
end
config/schedule.rb
require File.expand_path(File.dirname(__FILE__) + "/environment")

set :environment, @environment
set :output, Rails.root.join('log', 'cron.log')

every 1.day, at: '12pm' do
  rake 'aggregation:basic'
end

これで毎日12:00に集計データを保存する事が可能。bundle exec whenever -s 'environment=production' --update-crontabでcronに設定して終わり。
同じようにして集計結果をメールで送信する事も可能。

補足:デプロイ先に環境構築する時参考にしたページ

ruby。
http://www.task-notes.com/entry/20150624/1435114800
依存関係で足りないものがあって怒られたら指示通り入れる。mysql-develとか。

nginx。yumで。
http://qiita.com/nenokido2000/items/3cbb76dac2b9940f339e