Rails 環境別の設定ごと secrets.yml -> credentialへ移行
こんにちは。MUGENUPの林です。
私の担当サービスは、かつてRails 5.1以下を使っていた名残で、secrets.yml
で秘密情報を管理していました。
※現在の バージョンは 5.2.3 です。
secrets.yml
とdatabase.yml
が手動管理ということは、変更漏れによるエラーが発生してしまう可能性が高く危険です。
credentialへ移行し、変更の必要のないmaster.keyのみ手動管理する方法に切り替えることにしました。
credentialはRails5.2より追加されました。
credential作成
EDITOR="vim" bin/rails credentials:edit
のコマンドで編集画面が表示されます。
EDITOR="vim"
は作業環境でエディター設定をしている場合は省略可能です。
以下、初期の中身です。
# aws: # access_key_id: 123 # secret_access_key: 345 # Used as the base secret for all MessageVerifiers in Rails, including the one protecting cookies. secret_key_base: 〜シークレットキー〜
secret_key_base は残し、既存のconfig/secrets.yml
をそのままコピーしてペーストしましょう。※ 理由は最後に
secret_key_base: 〜シークレットキー〜 default: &default development: <<: *default test: <<: *default staging: <<: *default production: <<: *default
といった形でコピー&ペーストすることで、環境別の設定もそのまま引き継げます。
保存すると、config/credentials.yml.enc
とconfig/master.key
が出来上がります。
.gitignore
では
# Ignore master key for decrypting credentials and more. /config/master.key
が追加されています。
credentialsは、環境別の設定をソースコード側で自動参照してくれないので、
ソースコード内のRails.application.secrets
を
Rails.application.credentials[Rails.env.to_sym]
と置き換えましょう。
config/database.yml は?
host: <%= Rails.application.credentials[Rails.env.to_sym][:db][:host] %> password: <%= Rails.application.credentials[Rails.env.to_sym][:db][:password] %>
host と password だけ credentials に任せる形にして、秘密の情報を無くし、 Gitの追跡対象に追加しました。
.gitignore
から
/config/database.yml
を消すことで、コミットができるようになります。
circleCIは?
設定のEnvironment Variables
からRAILS_MASTER_KEY
を環境変数(Name)として追加できるので、
master.keyの中身をValueに入れて追加します。
capistranoは?
手動管理していたconfig/secrets.yml
, config/database.yml
に対して貼られていたシンボリックリンクを、master.key
に置き換えます。
先に対象サーバーのシンボリックリンク先になるファイルを設置しましょう。
(例: [Railsのソースコードのあるパス]/shared/config/master.key)
config/deploy.rb
でデプロイ時の設定が行われているので、こちらを書き換えましょう。
set :linked_files, fetch(:linked_files, []).push('config/database.yml', 'config/secrets.yml') ↓ set :linked_files, fetch(:linked_files, []).push('config/master.key')
他にもsetup等でconfig/secrets.yml
, config/database.yml
がある場合は、適宜置き換えてください。
以上で完了です!
secret_key_base は、development / test と staging / production で取得方法が変わる
(development / test はダミーの文字列を自動生成して通ってしまう)
ようなので、慎重にご対応ください。
※secret_key_baseだけは特別対応をします!
Before
default: &default development: <<: *default secret_key_base: 〜今までのシークレットキー〜
のように、環境別に secret_key_base を設定していた場合はそちらを消し、
After
secret_key_base: 〜シークレットキー〜 default: &default development: <<: *default
と、defaultの外に、credential作成時に作られた最新のシークレットキーを書きましょう。
- Rails本体が secret_key_base を参照するときの
Rails.application.credentials.secret_key_base
をRails.application.credentials[Rails.env.to_sym].secret_key_base
と置き換えることができない - credentialsを初めて作成したときに secret_key_base が自動生成されることから、 credentialsと secret_key_base は1:1で、環境別に用意するものではなくなったと推測
以上の理由から、 secret_key_base は一つに統一し、Rails.application.credentials.secret_key_base
で呼び出せる形に整えることにしました。
ちなみに、環境別の設定に書くと
Missing 'secret_key_base' for 'staging' environment, set this string with 'rails credentials:edit'
というエラーが出てしまいます。
新規に立ち上げるプロジェクトにWebpackじゃなくてParcelにした理由
お疲れ様です。MUGENUP エンジニアの崔です。今回は新規プロジェクトサービスを立ち上げる時にフロントエンドの構成でWebpackじゃなくてParcelにした理由に対して話します。
フロントエンドの話
新規プロジェクトのフロントエンドはVueにするように決まってVueを使うためにはトランスファイラーが必要でJavaScriptと一緒に組み合わせるためにバンドラーが必要でした。
bundler(バンドラー)の話
最近のウェブフロントエンドをやっている方だったら当然のようにパッケージ管理にnpmを使っているかと思います。 JavaScriptのファイルの間をどう繋ぐかはAMDやCommonJS、ESModuleなど色んな標準がありますけどそれの話はまた別に機会があればやりたいと思います。 で、バンドラーがやることとは様々なJavaScriptファイルを「一つの」JavaScriptファイルにすることです。 一つにするだけじゃなくてファイルを圧縮してサイズを減らしたり、標準のJavaScript、Node.jsでは通用しない文法もトランスファイルする(Babelを使って)こともやります。 まるでCとかC++のコンパイラーがやるコンパイルみたいな気もしましすね。
Webpackの話
Webpackは今は多分JavaScriptのバンドラーの中では結構使われているかと思います。基本的にはWebpack.config.jsファイルを作成してファイルをマージするところのエントリポイントを選択してそこから参照関係を探って一つのファイルにしてくれます。 もちろんCLIを設置してコマンドでも実行はできますけど、大体の場合はConfigファイルを作成しているかと思います
Webpackを使うためにはどのファイルから探ってパッケージを集めるとかSassやVueなどNode.js側でサーポートしていない言語を入れるためにPluginを設定しなきゃダメです。 下の公式ドキュメントから見ると。。
https://webpack.js.org/concepts/configuration/
var path = require('path'); module.exports = { mode: 'development', entry: './foo.js', output: { path: path.resolve(__dirname, 'dist'), filename: 'foo.bundle.js' } };
一見、書きやすいように見えますけど、下の記事みたいにPluginとか細かい設定が入るとコードが長くなります。これはConfigファイルを作った人にもある程度責任はありますね。 でも、そもそも特定のファイル(エントリポイント)からパッケージの依存関係を探るだけの話だったらConfigは必須ではないでしょう?と考えた人がありました。
Parcelの話
ここからが本題のParcelの話です。規模にもよりますけど特にプロジェクトが大きくないとか、まだ初期の段階だとしたらすぐバンドリングをやりたくなりませんか?
バンドラー自体がいらないケースを外すと大体は今でもすぐプロジェクトを立ち上げたくて我慢できないかと思います。
で、Parcelはnpm install -g parcel
をやっただけで使えるようになります。
$ npm i -g parcel $ # index.htmlにはmain.jsを呼び出していると仮定する $ parcel --build index.html Building main.js... ✨ Built in xx.xxs. dist/main.xxxxxx.js.map ⚠️ x.xx MB xxms dist/main.xxxxxx.js xxx.xx KB xx.xxs ...
賢い可愛いParcelはindex.htmlから繋がっているエントリポイントになるJavaScriptのファイルからまたImportしてくるファイルの拡張子を覗いてPluginも、Babelも勝手に設置してBuildしてくれるんです。
開発モードでリアルタイムトランスファイルもできるし、
parcel index.html
JavaScriptをエントリポイントにして不要なMapファイルをBuild結果物から除外するのも可能です
parcel build entry.js --no-source-maps
理由
つまりWebpackじゃなくてParcelにした理由は上でも書いた通り設定ファイルを書かなくてもPluginを入れてくれたり、ファイルを圧縮してくれるなど色々都合いい動きをしてくれたからです。 もちろんこれだけ見るとWebpackオワコンだねとか、Parcelだけ使えばいいじゃんって言えるかも知れませんけど Webpackはファイルを一個にまとめるだけじゃなくて結果対象を複数にできるとか、Configファイルって言ってもJavaScriptのコードなので拡張の可能性が高いと思います。
結論
今のプロジェクトの性格や、チームメンバーなの開発環境を考慮しながらバンドラーを選択するべきかと思います。
DiveでDockerイメージを覗いてサイズを減らす方法
お疲れ様です。MUGENUP エンジニアの崔です。今回はDiveというツールを使ってDockerイメージの中身を見る方法を紹介しようと思います。
なぜ必要?
イメージを作るときにCentOSやDebianのイメージ自体も300から600メガになるし、パッケージを設置したり、重いファイルを入れるとすぐギガバイトまで行くかと思います。
開発環境は多少イメージが多くてもdocker image prune
などの立ち上がっていないイメージを消すコマンドをたたくことで容量を確保しますが、本番環境や他のイメージのベースのなるイメージだったらなるべくイメージのサイズを軽くする必要があると思います。
Diveとは?
Diveは対話型CLIツールでイメージのIDを入れるとイメージのステップごとにその中身のディレクトリー構造が見れるツールです。
https://github.com/wagoodman/dive
まずツールを実行すると二つの窓に別れています。 左の方はステップをキーボードの上下キーで次、前のステップに移動するのができます。 右の方は今選択したステップのイメージレイヤのディレクトリー構造が見れます。 で、お互いの窓をスペースキーで転換できます。
イメージを分析しよう!
ではツールをインストールしたら(MacとかLinuxのメージャーなDistroならパッケージマネジャーから設置できます!)実際これを使って分析しましょう。
まず、ターゲットになるイメージを選択する為にイメージのリストをみましょう。
$ docker images -a REPOSITORY TAG IMAGE ID CREATED SIZE ***_backend latest b9d23b76e550 2 hours ago 333MB ***_console latest b9d23b76e550 2 hours ago 333MB <none> <none> b56bd1bd77f4 2 hours ago 333MB <none> <none> 3a8d411fa803 2 hours ago 333MB <none> <none> 3df2afeb812e 2 hours ago 333MB <none> <none> 628311edb63b 2 hours ago 51.2MB <none> <none> f6c2a2f82ff8 2 hours ago 51.2MB ***_frontend latest 5772f1e7234a 2 hours ago 372MB
で、今回はfrontedイメージの中身を見ましょう
$ dive 5772f1e7234a Image Source: docker://5772f1e7234a Fetching image... (this can take a while for large images)
まず最初に実行した画面です。
見ての通り二つの窓があります。タブキーで左右移動して上下で要素に接近できます。 左からは一個下に進んで見て、右画面にファイルの変更が色に表示されます。 ここで一旦ステップを踏んでみましょう。
ここでは本来ならいないはずのdist
フォルダーのトランスファイルされたファイルたちがイメージに入ってしましましたね。
ここが一旦消すところですね。
ここでもう一個発見しました。node_modules
です。次のステップでどうせnpm install
が走るからnode_modules
が生成されるから除外しなくても良いと思うかも知れませんが、、、
node_modules
には開発環境で一回インストールして要らなくなったパッケージとかあるかも知れないので自分は削除する判断をしました。
削除
削除とは言いましたが、ただイメージに先発見したファイルが入れなくなる過程ですね。
Dockerイメージに入れたくないファイルがあったら.dockerignore
で記載できます。
自分の場合は.dockerignore
ファイルにnode_modules
とdist
とを入れます
$ cat .dockerignore node_modules dist
後、npm install --only=prod
みたいにdevDependencies(開発環境だけ必要なパッケージ、npm install --save-dev
でインストールしたもの)は場外するようにDockerfileも変えれば良さそうですね。
今回の例はそこまで大きいプロジェクトではなかったので多い変化はないけど、イメージが重い!毎回新しいイメージの生成でディスク足りない!!って方はどこか無駄にコピーしているファイルはないか、確認のときにはぜひ使ってみてください。
DiveのCLI画面ではCtrl+Cで終了、Ctrl+Fでファイル名から探すなど必要な機能としては足りないかと思います しかもDiveは効率度(efficiency)を測定したり、CIモード設定でイメージのサイズが一定以上とか、効率度が低くなるとPass/Failをリターンコードとして返してくれるらしいので色々使えるツールかと思います。
これでDockerイメージの内部を見れる良いツールdiveで楽しいDocker開発になったら嬉しいです。最後まで読んでくれてありがとうございます!
CTO Night and Day 2019備忘録
今年初めての参加となりました。 MUGENUPの前島です。
CTO nightとは
AWSが主催する日本のベンチャー企業CTOが一堂に介し、各セッションやディスカッションを通じ学びとネットワークを得るカンファレンスです。今回は約120名の参加がありました。 初参加企業は3割程度です。MUGENUPは代表の伊藤がCTO時代から通じ全て参加しているようです。 総日程は3日間で朝から晩までスケジュールが組まれており観光の時間はありません。
(なので私は朝会場に向かう際や帰り道を徒歩で移動して少しでも京都にいる実感を得ようと足掻きました)
会場はthe sodoh 東山 京都です。
web界隈のエンジニアなら多くの人が知っているであろう方々と接することが出来たのは、まだまだ新参者の私にとってとても視座の上がる刺激的な体験でした。
以下、参加したセッションやキーノートについてつらつら書いていきます
Day 1
CTOのためのamazon カルチャー
ジェフ・ベゾスかかかげるBuilders and Dreamersについての学びや何故Amazonがここまで大きくなったのか?という問いの一部を垣間見た気がしました。
以下はセッションの中で聞いた中で特に自分に取って重要だと感じたトピックです。
- 誰のための事業なのかを考える
- イノベーション文化
- スピードと俊敏性にこだわるための小規模チーム
- 主体性と自律性を重視
- 一つのチーム内で完結できるようにする
amazonの行動指針であるOur Leadership Principlesについてはどれだけ多様性のある人間が集まった組織であっても全員が同じ方向に向かうために改めて「行動指針」が必要だと感じました。 ただエンジニア組織を作り上げていくだけではダメで、今後は組織全体にも行動指針の重要性を説いていきたい!!!
keynote
Gremlin IncのKolton Andrus氏による「カオスを起こすのではなく、カオスを手懐けるためのエンジニアリング」というカオスエンジニアリングについての考え方、Snyk LtdのGuy Podjarny氏によるOSS脆弱性データベースの重要性と変遷、活用方法については初めて聴く内容も多く新鮮でした。
また、ソラコム安川さんのkeynoteで印象に残ったのは「疎結合で非同期なチーム作り」など組織、チーム作りの考え方です。 全てを自社に当てはめることは出来ないけれど、適応出来そうなものがあれば積極的に参考にしていきたいと思いました。 また、AWSにいたご経歴からかソラコムの行動指針にもAWSのOur Leadership Principlesを参考にしている箇所がありこうやって素晴らしい文化が継承されていくのも考え深かったです。
「sansanの今までとこれから」というタイトルでkeynoteを発表した下さったsansan CTO藤倉さんの話ではまさにシード期から現在、そしてこれからのsansanの企業としての在り方をメッセージ性を込めて発信して下さっていると感じました。 印象に残った言葉として、最後の方に仰っていた「技術としては、特別なことを何もしていない」です。
企業として本当に重要なのは技術だけではないということ、足元の課題はなんなのか?誰が使うのか?を徹底して考えテクノロジーをビジネスモデルに順応させ加速させたからこそ今の私にはとても説得力がありました。
Unconference
テーマ別にテーブルが用意され5〜6人でディスカッションする時間です。 私が参加したテーマは「CTOとVPoE」でした。 詳細の議事録をAWSのスタッフの方が取ってくれていたので、 詳細は省きますがCTOとは企業に取って何か?VPoEとの違いや各々の企業の過去の事例を踏まえて議論することが出来たのはとても貴重な時間でした。 今後自分がVPoEとしてやれることと次のステップへ進むための心構えなど、自分の中で方針がいくつか定まりました。
改めて、同じテーブルで拙い私に様々なことを教えて下さり真摯に接してくださった皆様(レクターCEOの松岡さん、グノシーctoの小出さん、テクロスctoの佃さん、LegalForce ctoの時武さん)、本当にありがとうございました!!
CTO MidNight!!!
立食式の交流会の後は、MAHARAJA祇園で毎年恒例Tech二次会とDJブース!!
中でも関西学生エンジニアLT大会は見応えがありました。 今の大学生って凄くスタートアップの世界に入りやすい環境があった羨ましいなと思いつつ、有り余る若い時の熱量を発散できる場所があるのはとてもいいことだなあ・・・
写真はLT大会後のDJタイムの一幕です。 徐々に人数は減っていき、最後は数えられる人数しかフロアにいませんでした。笑
Day 2
day 2ではオムロン社にお邪魔したSINIC理論のワークショックに参加したりCTO DojoやCTO 1000本ノックなど嗜好を凝らしたセッションが多かったです。 自分が参加したCTO Dojoのセッションは「第二新卒としてのCTOというキャリアと実態」、「CTO は「規制」とどう向き合うべきか」、「エンジニア採用サービスを見てきてわかったエンジニア採用がうまくいっている会社の例」、「エンジニアのモチベーション管理」です。
それぞれ一つ一つのセッションについて1記事書けるくらい内容の濃いものなのですが、自分なりに考えをまとめるとどの会社もチームもそれなりに似通った課題に直面していて、大事なのはその課題とどれだけ真摯に向き合えるか、その課題を解決したり上手く切り抜けるために試行錯誤と実験を繰り返して磨かれていったことを継承していくしかないだなと思いました。当たり前と言えば当たり前ですが・・・
私としてはエンジニアもとい、人間が組織で働く上で成果を出し続けるには「考え方×能力×情熱」がバランス良く揃ってないといけない。 これら一つ一つを成長させて上げたり用意したり、自ら引き出してもらったり出来るマネージャーが世の中にもっともと必要だと感じました。 今自分が出来ることをもっともっと見つめ直す良いきっかけに、この3日間はなったと思います。
まとめ
今回、様々なことを学びました。 普段都内だと物理的に近くにいても中々話す機会がない方と深い話が出来たのも、このCTO nightならではの魅力だと思います。 この機会をくださった周りの方々には本当に感謝してもしきれないほどです。 ここで学んだことを活かした事業の成長でこのご恩を少しずつお返しできたらと思います。
AWS AuroraをR3(旧世代)からR5(新世代)に安全に変更する方法
皆さん。こんにちは。MUGENUP エンジニアの崔です。
今回は、AWS AuroraをR3(旧世代)からR5(新世代)に安全に変更する方法をまとめてみました。
AWS Aurora
Amazon Aurora は、MySQL および PostgreSQL と互換性のあるクラウド向けのリレーショナルデータベースであり、従来のエンタープライズデータベースのパフォーマンスと可用性に加え、オープンソースデータベースのシンプルさとコスト効率性も兼ね備えています。
MUGENUPはMySQLと互換性もあるためAuroraを使っています。
そこで中断させないでインスタンスの変更が可能かどうかを調査し、実際にやってみました!
対象:fujossy
月間PV1200万を超える小説投稿サイト https://fujossy.jp
目的
DBインスタンスの変更をサーバーを停止しないままでやりたい
方法
AWS AuroraにはFailover(フェイルオーバー)機能があるので、活用してみましょう。
Failover(フェイルオーバー)?
Failoverは異常が生じたときの予備システムに自動的に切り替えられる機能
手順
>>> 簡単! <<<
実際やってみましょう
まず、インスタンスをコピー
- xxxxxxx-aurora-clusterにもともと紐づいていたxxxxxxx-aurolaからクローンに作成でコピーします
- 設定などは既存の設定と同じなのでそのままクローンしましょう
- クローンに成功したらClusterのページのCloudWatchでもインスタンスの状態を確認できます
Failoverをセット
- 既存に書き込み状態になっている旧インスタンスを選択したフェイルオーバーボタンを推しましょう
- 無事Failover設定に成功するとコピーした新インスタンスが書き込み、旧インスタンスが読み込み状態になります
- そうなると旧インスタンスは変更可能になります
旧インスタンスの変更
- 旧インスタンスを変更する時には常時にConnectionを維持するアプリケーション(Sidekiqとか)は一回Offにする必要があります
- アマゾンによるとインスタンスをライブに変更する時には既存の連結がある時ゆっくりと変更するように現状に発生する問題を最小限にすることをやるのでConnectionをいっぱい持っているアプリケーションは止めておく必要があります
元に戻す
Failoverで書き込みと読み込みが逆だった新インスタンス(アップデートの為の臨時インスタンス)にまた同じくFailoverを設定しましょう そうなると元インスタンスがまた書き込みに、臨時インスタンスは読み込みになるので臨時インスタンスは消去出来るようになります
インスタンス変更の確認
インスタンスの変更は勿論作業しながらすると思いますが、CloudWatchのデータを参考にする方法をここに記録します
まず、上のグラフはDBに接続しているConnectionの数です。途中にメインのインスタンス(元インスタンス、xxxxxxx-aurola)のConnectionをSidekiqをOffにした影響で減っているのを確認できます。そのあと変更後元に戻したらまたConnectionが復旧されます
注意点
- 無中断にインスタンスをアップしたとしても途中で何秒くらい接続が切れるのがユーザー側にも影響を受けるので先にメンテナンスのお知らせが必要になります
- アプリケーションにもDBに接続が切れるように処理されるのでアプリケーションがDBの状態によってパニックが起きて強制的に死ぬシステムなら使えないです
- この方法の良さは中断時間を時間単位(アプリケーションサーバーも中断)から秒単位(DBホストだけ中断、切り替え)にするにあって完全に0秒でシステムのDBがアップデート出来るのではないです
- 今回の件とは別ですけど、インスタンスの名前が間違っていて変更したらホストネーム(サーバーのドメインネーム)が変わるので注意する必要があります
募集
弊社は、一緒に働くエンジニアを募集中です。 ビジョン「創ることで生きる人を増やす」の達成、ご一緒しませんか?
SavePoint の動き方 (テナント編)
皆さん。こんにちは。MUGENUP の osada です。
5月14日(木) に SavePoint(セーブポイント) という新サービスをリリースしました。
MUGENUP がクラウドソーシングで培った進行管理プロセス、そしてその集大成である Workstation を リプレースし、どなたでも使えるように SaaS として提供したプロジェクト管理ツールです。
今回は、SavePoint が導入しているマルチテナントについて解説します。
SavePoint はビルディングに似ています。
SavePoint は、savept.com
というドメインで運用しています。
しかし実際には、mugenup.savept.com
のようなURLでアクセスすることになります。
これは実際にはどうなっているのでしょうか?
図は、SavePoint の構造をビルに例えたものです。
キーワードは、「フロア、入構証、エレベータ」の3つです。
まず SavePoint ビルがあります。
そこにはフロアがありますが、これがそれぞれのサブドメイン(mugenup
の部分)になります
*1。
次に、ユーザーごとに、入構証が発行されます。 これは、メールアドレスとパスワードで構成され、 1メールアドレスごとに、1つ 発行されます。 これを使って 「SavePoint ビルに入る = ログインする」ことができるようになります。
しかしこれだけでは、フロアに入ることが出来ません。 ビルには入れたけど、エレベータが止まらないのです。
フロアに入るには、その入構証にフロアに入る許可を付与する必要があります。 そうすることで、エレベータがそのフロアに止まることになります。 (許可がないフロアには止まりません = アクセスできません)
逆に言うと、フロア許可証があれば、再度ログインし直す必要がありません。 一つの入構証で、自由にフロアを行き来することができます。
フロア許可証には2段階のレベルあります。
フロア許可証があるので、フロアに降りることはできるようになりました。 しかし今度は、部屋(= プロジェクト・案件)に入ることが出来ません。
部屋ごとにセキュリティがあるため、今度は部屋の許可を貰う必要があります。 この許可は、2種類の人からもらうことができます。
一人は、部屋の管理者(=案件管理者: 詳細は次回に解説します)。
もう一人は、フロア管理者 です。
フロア許可証には、通常レベルとは別に、 全てをコントロールできる管理者権限(Level A)を付与することができます。
これを持つ人が、フロア管理者です。 登録サイトから登録したユーザーは、フロア管理者となります。 *2
このレベルでは、全ての部屋(案件)に自由にアクセスしたり、 フロア管理室(後述)に入ることができるようになります。
ログインはできたけど、何もアクセスできない、というときは、 まだ部屋に入れてもらっていない、ということかもしれません。
フロア管理室でできること
部屋の外側のフロア全体に関わることはフロア管理者しか行うことができません。 例えば、
- 入フロア許可の登録・剥奪
- 新しい部屋(案件)の追加・部屋の削除
- 他の誰かをフロア管理者にすること
などです。
データベースの解説
さて、概念を説明したところで、技術の話に移行します。
SaaS のサービスの場合、マルチテナント、というキーワードが出てきます。 しかし今回は、テナントではなく、フロアという例を出しました。 なぜかというと、「ビル入構証」という概念を伝えたかったからです。
純粋にテナント構造にすると、ユーザーはテナントごとに入構証を持ち歩かなければなりません。 首から3枚も4枚もカードをぶら下げて歩くのは、大変です。
よって SavePoint では、ユーザーのみ、テナントごとのデータベースから切り離して管理しています。
クライアントからサーバにアクセスがあると、Rack ミドルウエアの中で、 リクエストからサブドメインを判別し、データベースが切り替わります。 データベース単位で切り替わるため、他のテナントのデータが混ざることはありません。
しかし、ユーザーテーブルのみ、どのテナントからアクセスしても、 必ず一つのデータベースを見に行くようになっています。
これにより、セキュリティを高めながら、利便性を保つことができるようになっています。
まとめ
- SavePoint はビルディング構造に似ています
- 入構証の他に、フロア許可証があります
- ユーザーは異なるフロアにシームレスに移動することができます。
SavePoint は、「発注者の管理を楽にしよう」 ということを追求して作っているサービスです。
ところが、MUGENUP 自体は、発注元からお仕事をいただく制作側の立場です。
もしSavePoint を100社がご導入いただいて、MUGENUPにお仕事を頂く場合、 通常のサービスでは、一人ごとに100アカウント作ることになるかもしれません。
しかし SavePoint なら何社になっても1人1アカウントです。
このように、制作側にとっても使いやすいサービス を目指せるのは、 ドッグフーディングができる弊社の強みだと考えています。
発注者にも、受注者にも使いやすいサービス、を目指して、開発を続けています。
是非お使いください。
募集
弊社は、一緒に SavePoint を作ってくれるエンジニアを募集中です。 無限流開発、ご一緒しませんか?
ActiveJob から見るシリアライズとデシリアライズ
皆さん、こんにちは。プルリクで間違った指摘をして大反省中 の osada です。
プルリクで間違った指摘をして大反省中です。 下記のコードで、何を指摘したか、お分かりになるでしょうか?
class NotificationJob < ActiveJob::Base queue_as :default def perform(notifiable, user) notifiable.notify(user) end end
こんなことを言ってしまったのです。
オブジェクトを丸ごとシリアライズすると、redis の容量を圧迫し、 シリアライズ・デシリアライズにも時間が掛かるので、 クラス名とidを渡して、job の中で取り出して使って下さい。
この発言には2つ、間違いがありました。
- 1つ目は、
オブジェクトが丸ごとシリアライズされると思っていた
こと。 - 2つ目は、
ActiveJob は クラス名と id を渡す必要がない
ことです。
今回は、ActiveJob では、インスタンスをそのまま渡してもよくなったこと、 そして、GlobalID がどのように使われているかを共有します。
話の流れとしては、下記の3つのシリアライズ方法の比較を行います。
Python memcache はインスタンスごとバイナリダンプ
python
の memcacheclient
は、インスタンスを丸ごとダンプします。
In [1]: class A(object): ...: def __init__(self, a): ...: self.a = a ...: In [2]: a = A(1) In [3]: import memcache In [5]: mc = memcache.Client(['127.0.0.1:11211']) In [7]: mc.set("a", a) Out[7]: True In [8]: mc.get("a") Out[8]: <__main__.A at 0x103efbc10> In [9]: a1 = mc.get("a") In [10]: a1.a Out[10]: 1 In [13]: a == a1 Out[13]: False In [14]: a.__class__ == a1.__class__ Out[14]: True
mc.get
で取り出した時に、Aというクラスのインスタンスとして取り出していることがお分かりになるでしょうか。
インスタンスとしては別ものですが、クラスは同じです。
memcache の set では、
オブジェクトをバイナリダンプするpickle
というモジュールが動いています。
In [6]: mc._val_to_store_info(a, 0) Out[6]: (1, 89, "ccopy_reg\n_reconstructor\np1\n(c__main__\nA\np2\nc__builtin__\nobject\np3\nNtRp4\n(dp5\nS'a'\nI1\nsb.")
12.1. pickle — Python オブジェクトの直列化 — Python 3.4.2 ドキュメント
謎の文字列が格納されていますが、なんとなく読み取れるような? ruby でも marshal を使えば、同じことができると思います。
そしてこれが、一つ目の間違いである、インスタンスを丸ごとダンプしている
と考えた理由でした。
きっと rails も、バイナリダンプしているのだろう、と思い込んでしまったのです。
Resque のシリアライズ
では、Rescue はどうしているでしょうか?
[31] pry(main)> class A [31] pry(main)* def initialize(a) [31] pry(main)* @a = a [31] pry(main)* end [31] pry(main)* end :initialize [32] pry(main)> a = A.new 1 #<A:0x007fa192282b80 @a=1>
A
というクラスを定義し、インスタンス化します。
[33] pry(main)> class Q [33] pry(main)* @queue = :q [33] pry(main)* def self.perform(a) [33] pry(main)* p a [33] pry(main)* end [33] pry(main)* end :perform [34] pry(main)> Resque.enqueue(Q, a) true [35] pry(main)> Resque.keys [ [0] "queue:q", [1] "queues" ] [38] pry(main)> Resque.redis.lrange "queue:q", 0, -1 [ [0] "{\"class\":\"Q\",\"args\":[{\"a\":1}]}" ]
それをQ
というジョブを作成し、Resque に渡します。
このとき、ジョブQ
と、インスタンスa
が redis
に積まれますが、
このとき引数のa
が何のクラスだったのか、という情報は消えるようです。
(resque 1.23 です)
redis に積まれた情報だけでは、復元することができません。
なぜかといえば、そもそもインスタンスをダンプしない方針のようです。
If your jobs were run against marshaled objects, they could potentially be operating on a stale record with out-of-date information.
バイナリダンプしてキューに積んだら、 データが更新されてた時に、古いデータのままで使っちゃうかもしれないでしょ?
という感じでしょうか?
Resque では、クラスとidを渡して、キューの中で取り出すのが標準です。
さてこれが、2つ目の間違い、クラス名とidを渡して、jobの中で呼び出す
という指摘をした原因です。
ActiveJob
も同様だろう、と思い込んでしまったのです。
ActiveJob のシリアライズ
ActiveJobでは、GlobalID
という機能が入っており、明示的なシリアライズが不要です。
マニュアルに明確に記載がありましたので、確認不足すぎて大反省でした。
Active Job の基礎 — Rails ガイド
バイナリダンプでもないけれども、インスタンスをそのまま渡すことができる、 一体これがどのように動いているのでしょうか?
シリアライズの処理は、activejob-4.2.0/lib/active_job/argument.rb
にあります。
def serialize_argument(argument) case argument when *TYPE_WHITELIST argument when GlobalID::Identification { GLOBALID_KEY => argument.to_global_id.to_s } when Array argument.map { |arg| serialize_argument(arg) } when Hash argument.each_with_object({}) do |(key, value), hash| hash[serialize_hash_key(key)] = serialize_argument(value) end else raise SerializationError.new("Unsupported argument type: #{argument.class.name}") end end
引数を一つずつ処理しながら、
- シリアライズできるもの(
TYPE_WHITELIST
) GlobalID::Identification
であるもの- 処理できないもの
に分けています(ArrayとHashに対して再帰するのは、引数の処理として勉強になりますね)。
GlobalID::Identification
そしてそのメソッドである to_global_id.to_s
というのが ActiveRecord を一意に成り立たせているものです。
[1] pry(main)> user = User.first User Load (0.5ms) SELECT "users".* FROM "users" ORDER BY "users"."id" ASC LIMIT 1 => #<User:0x007fd1449c3b88 id: 1, name: "aa", created_at: Sat, 28 Feb 2015 13:12:38 UTC +00:00, updated_at: Sat, 28 Feb 2015 13:12:38 UTC +00:00> [2] pry(main)> user.to_global_id.to_s => "gid://test01/User/1" [3] pry(main)> user = User.new => #<User:0x007fd145d4feb8 id: nil, name: nil, created_at: nil, updated_at: nil> [4] pry(main)> user.to_global_id.to_s URI::InvalidURIError: Expected a URI like gid://app/Person/1234: #<URI::Generic gid://test01/User/>
アプリ名、クラス名、id
で一意になる文字列を生成しています。
id が無ければ、エラーになります。
そんなわけですから、マニュアルには、ActiveModel にミックスイン、とありますが、
Rails 4.2.0
では、GlobalID
というライブラリに変わっており、
ActiveRecord にミックスイン になっていますので、気をつけましょう。
上のコードは、ActiveModel::GlobalIdentificationをミックスインするすべてのクラスで動作します。 このモジュールはActive Modelクラスにデフォルトでミックスインされます。 http://railsguides.jp/active_job_basics.html#globalid
この GlobalID のおかげで、ActiveRecord のインスタンスを渡したとき、 global_id の文字列に変換されて積まれる、ということです。
手動でやっていたことを、ライブラリとして組み込んでしまった感じでしょうか。 また、クラス名と、idを渡すのではなく、1つの gid として渡すのも、センスが良いですね。
ActiveJob の デシリアライズ
デシリアライズを見てみましょう。
GlobalID::Locator.locate(argument)
を使って、
globalid かどうかを確認しています。
def deserialize_argument(argument) case argument when String GlobalID::Locator.locate(argument) || argument when *TYPE_WHITELIST argument when Array argument.map { |arg| deserialize_argument(arg) } when Hash if serialized_global_id?(argument) deserialize_global_id argument else deserialize_hash argument end else raise ArgumentError, "Can only deserialize primitive arguments: #{argument.inspect}" end end
この locate
は、下記の手順でデータを取り出します。
string
が 妥当なgid
かどうか確認gid
があればlocator_for
で、適切なfinder
を取得finder
を使って、データを取得
この finder
は、標準で、ActiveRecordFinder
が用意されています。
よって、実際は、ActiveRecord.find
を使って取り出します。
def locate(gid, options = {}) if gid = GlobalID.parse(gid) locator_for(gid).locate gid if find_allowed?(gid.model_class, options[:only]) end end def locator_for(gid) @locators.fetch(normalize_app(gid.app)) { default_locator } end @locators = {} class ActiveRecordFinder def locate(gid) gid.model_class.find gid.model_id end end mattr_reader(:default_locator) { ActiveRecordFinder.new }
よって、ActiveRecord 以外でも、自前で locator を用意すれば、使うことが可能です。
# Tie a locator to an app. # Useful when different apps collaborate and reference each others' Global IDs. # # The locator can be either a block or a class. # # Using a block: # # GlobalID::Locator.use :foo do |gid| # FooRemote.const_get(gid.model_name).find(gid.model_id) # end # # Using a class: # # GlobalID::Locator.use :bar, BarLocator.new # # class BarLocator # def locate(gid) # @search_client.search name: gid.model_name, id: gid.model_id # end # end
ただ、実装としては、gid.model_class.find gid.model_id
だけですので、
実装したいクラスに、find
メソッドと id
メソッド を用意した方が楽な気がしますね。
まとめ
今回のまとめです。
- キューのためにデータをシリアライズするときは、データのズレを考慮し、バイナリダンプしない
- 永続化されたモデルは、グローバルIDを発行することで、クラスとidを使うよりも、シンプルに扱える
- プルリクを見る時は、謙虚に。しかし臆すること無く。
なお、4.2.0
の時点で、ActiveJob に キーワード引数を使うことはできませんが、
4.2.1
には入るようです。
しばらくは、キーワード引数を使わない実装で、頑張りましょう。
ActiveJob should support passing of keyword arguments to perform method · Issue #18741 · rails/rails
参考
TYPE_WHITELIST はプリミティブ型です
TYPE_WHITELIST = [ NilClass, Fixnum, Float, String, TrueClass, FalseClass, Bignum ]
Resque のダンプは MultiJSON
# Given a Ruby object, returns a string suitable for storage in a # queue. def encode(object) if MultiJson.respond_to?(:dump) && MultiJson.respond_to?(:load) MultiJson.dump object else MultiJson.encode object end end [10] pry(main)> MultiJson.dump(a) "{\"a\":1}" [11] pry(main)> a1 = MultiJson.load(MultiJson.dump(a)) { "a" => 1 } [12] pry(main)> a.class == a1.class false
ActiveJob は自前クラスは入れられない
[1] pry(main)> class AJob < ActiveJob::Base [1] pry(main)* queue_as :default [1] pry(main)* def perform(a) [1] pry(main)* p a [1] pry(main)* end [1] pry(main)* end :perform [2] pry(main)> class A [2] pry(main)* def initialize(a) [2] pry(main)* @a = a [2] pry(main)* end [2] pry(main)* end :initialize [3] pry(main)> a = A.new 1 #<A:0x007ff4d41b0a50 @a=1> [4] pry(main)> AJob.perform_later(a) Enqueued AJob (Job ID: 71481807-dae8-460f-a050-655ab0394bd5) to Resque(default) with arguments: #<A:0x007ff4d41b0a50 @a=1> ActiveJob::SerializationError: Unsupported argument type: A [5] pry(main)> AJob.perform_later({a: 1}) Enqueued AJob (Job ID: f9cf4409-f749-4714-81bf-aa309f400b60) to Resque(default) with arguments: {:a=>1} #<AJob:0x007ff4d2c60128 @arguments=[{:a=>1}], @job_id="f9cf4409-f749-4714-81bf-aa309f400b60", @queue_name="default"> [40] pry(main)> user = User.first User Load (0.5ms) SELECT "users".* FROM "users" ORDER BY "users"."id" ASC LIMIT 1 => #<User:0x007fd142924738 id: 1, name: "aa", created_at: Sat, 28 Feb 2015 13:12:38 UTC +00:00, updated_at: Sat, 28 Feb 2015 13:12:38 UTC +00:00> [41] pry(main)> AJob.perform_later(user) Enqueued AJob (Job ID: 016c46b7-4abe-4cae-9605-84c29572faab) to Inline(default) with arguments: gid://test01/User/1 User Load (1.2ms) SELECT "users".* FROM "users" WHERE "users"."id" = ? LIMIT 1 [["id", 1]] Performing AJob from Inline(default) with arguments: gid://test01/User/1 #<User id: 1, name: "aa", created_at: "2015-02-28 13:12:38", updated_at: "2015-02-28 13:12:38"> Performed AJob from Inline(default) in 0.47ms => #<AJob:0x007fd144f6a6b8 @arguments=[#<User:0x007fd142924738 id: 1, name: "aa", created_at: Sat, 28 Feb 2015 13:12:38 UTC +00:00, updated_at: Sat, 28 Feb 2015 13:12:38 UTC +00:00>], @job_id="016c46b7-4abe-4cae-9605-84c29572faab", @queue_name="default">
参考文献
- Active Job の基礎 — Rails ガイド
- Rails - Active Jobについて - Qiita
- resque/resque at 1-x-stable
- Resqueで色々やって、Redisに何が格納されているのか調べてみた - きたけーの朝は早いブログ
- Redis に保存されてる値を見ようと思った時に覚えておきたい redis コマンド | そんなこと覚えてない
- リスト型 — redis 2.0.3 documentation
- rails/globalid
- rails/activemodel-globalid
- python-memcached/memcache.py at master · linsomniac/python-memcached
- 12.1. pickle — Python オブジェクトの直列化 — Python 3.4.2 ドキュメント