Multi-tenancy with Rails


Published on

Presentation from RedDotRubyConf 2011 in Singapore. It explains multi-tenancy and why it is increasingly required for Rails development. Four of the many approaches are covered in some detail (including what resources we have available for re-use) and I end with a naive question (& call to action?) .. "Isn't it about time there was a 'Rails Way'?"

Published in: Technology
  • Be the first to comment

No Downloads
Total views
On SlideShare
From Embeds
Number of Embeds
Embeds 0
No embeds

No notes for slide
  • The lower in the stack you can partition More robust and secure But greater scalability challenges
  • Multi-tenancy with Rails

    1. 1. MULTI-TENANCY AND RAILS <ul><li>House party, slumlord, or mogul? </li></ul>RedDotRubyConf – Singapore 22-Apr-2011
    2. 2. Why do I care about this? <ul><li>The product: 360º engagement between you and your customers </li></ul><ul><ul><li>Unify and socialize support, brand management, product management, feedback & innovation </li></ul></ul><ul><li>Launched privately April 1 st </li></ul><ul><li>Objective is a public SaaS launch within 6 months </li></ul><ul><ul><li>Many customers </li></ul></ul><ul><ul><li>Customers of customers </li></ul></ul><ul><ul><li>Single, cohesive (rails) application </li></ul></ul><ul><li>..hence we’ve been thinking a lot about multi-tenancy ;-) </li></ul>Paul Gallagher [email_address] personal:
    3. 3. A long time ago in a galaxy far, far away…. <ul><li>My first encounter with “multi-tenancy” </li></ul><ul><ul><li>Take a single-tenant on-premise application and implement for 8 related entities (fiduciary requirement for separation) </li></ul></ul><ul><li>Sure, no problem! </li></ul><ul><ul><li>Physical separation of the app tier </li></ul></ul><ul><ul><li>Shared database cluster hosting separate database instances </li></ul></ul><ul><ul><li>Shared identity management service </li></ul></ul><ul><ul><li>Custom perl-based provisioning system </li></ul></ul><ul><li>OMG the hardware </li></ul><ul><ul><li>4-node db cluster (9 db instances) </li></ul></ul><ul><ul><li>9x2 app tier servers </li></ul></ul><ul><ul><li>Another 4 infrastructure service nodes </li></ul></ul><ul><li>These days we’d call this virtualization at best, and cloud-washing otherwise! </li></ul><ul><li>.. and they say rails can’t scale! </li></ul>
    4. 4. Three Thoughts for Today
    6. 6. Multi-tenancy <ul><li>Simplest definition possible: </li></ul><ul><ul><li>a principle in software architecture </li></ul></ul><ul><ul><li>where a single instance of the software </li></ul></ul><ul><ul><li>serves multiple client organizations </li></ul></ul>
    7. 7. Tenancy Models
    8. 8. Tenancy Models Social networks, LBS, Web 2.0 MSP/ASP, PaaS, IaaS Business, Enterprise 2.0, SaaS The same idea can manifest itself across the continuum – depending how you approach it
    9. 9. Tenancy Models Social networks, LBS, Web 2.0 MSP/ASP, PaaS, IaaS Business, Enterprise 2.0, SaaS Hardware/infrastructure challenge Application architecture challenge Explicit Rails support.. ..but our usage increasingly extends here
    10. 10. Tenancy Considerations
    11. 11. Key Concern: Data Partitioning
    12. 12. RE-USE / RE-INVENT
    13. 13. Four Data Partioning Techniques.. <ul><li>Instance Partitioning </li></ul><ul><li>RBAC Partitioning </li></ul><ul><li>Model Partitioning </li></ul><ul><li>Schema Partitioning </li></ul><ul><li>(just some of the many ways to solve this) </li></ul>
    14. 14. Instance Provisioning <ul><li>Pro </li></ul><ul><ul><li>Apps don’t need to be multi-tenant </li></ul></ul><ul><ul><li>Apps don’t even have to be the same </li></ul></ul><ul><ul><li>You can do some neat tricks with git </li></ul></ul><ul><li>Con </li></ul><ul><ul><li>$caling </li></ul></ul><ul><ul><li>Management </li></ul></ul><ul><ul><li>Provisioning and spin-up times </li></ul></ul>The Infrastructure Engineer’s solution
    15. 15. RBAC Partitioning <ul><li>Consider tenancy as just RBAC to another degree </li></ul><ul><li>Pro </li></ul><ul><ul><li>Able to address complex shared/private data requirements </li></ul></ul><ul><li>Con </li></ul><ul><ul><li>Easy to leak data with bugs and oversights </li></ul></ul><ul><li>Perhaps too clever by half? </li></ul>User Model Query scoped by permissions The “I just groked CanCan and it’s awesome” solution
    16. 16. RBAC Partitioning <ul><li>e.g. with CanCan </li></ul><ul><li>CanCan + InheritedResources </li></ul><ul><li>But beware .. you are still responsible for </li></ul><ul><ul><li>validations </li></ul></ul><ul><ul><li>scoping options in forms (e.g. select lists) </li></ul></ul>class Ability include CanCan::Ability def initialize(user) … # include nil so we can handle :new can :manage, Project, :tenant_id => [user.tenant_id, nil] … end end # then we can.. Project.accessible_by( class ProjectsController < InheritedResources::Base prepend_before_filter :authenticate_user! load_and_authorize_resource # that’s all folks! end Proxy data access thru one model (user/tenant)
    17. 17. Model Partitioning <ul><li>Probably the most common pattern </li></ul><ul><li>Pro </li></ul><ul><ul><li>No special deployment or infra requirements </li></ul></ul><ul><li>Con </li></ul><ul><ul><li>Relies on application query logic </li></ul></ul><ul><ul><li>Does not inherently prevent data leakage </li></ul></ul><ul><ul><li>Scaling with performance </li></ul></ul><ul><li>tenant_id (or similar) present in every table .. and every query </li></ul>users projects The Software Architect’s solution id tenant_id name … id tenant_id name …
    18. 18. Model Partitioning <ul><li> </li></ul><ul><ul><li>Handles model aspects well </li></ul></ul><ul><ul><li>Released gem (0.2.0) </li></ul></ul><ul><ul><li>WIP controller support etc </li></ul></ul><ul><li> </li></ul><ul><ul><li>Comprehensive, but internals need overhaul for Rails 3 </li></ul></ul><ul><li> </li></ul><ul><ul><li>Partial implementation only </li></ul></ul><ul><li>Software as a Service Rails Kit </li></ul><ul><ul><li> </li></ul></ul><ul><ul><li>Model partitioning: single-database scoped access </li></ul></ul><ul><ul><li>Not free, but it solves other problems like recurring billing and payment gateway integration </li></ul></ul>
    19. 19. gem install multitenant <ul><li>Uses dynamic default scopes to partition model access to Multitenant.current_tenant </li></ul><ul><li>NB: belongs_to_tenant (as used in released gem 0.2.0) renamed to belongs_to_multitenant in current master </li></ul>class Account < ActiveRecord::Base has_many :users has_many :projects end class User < ActiveRecord::Base belongs_to :account belongs_to_tenant :account end class Project < ActiveRecord::Base belongs_to :account belongs_to_tenant :account end def belongs_to_tenant(association = :tenant) include DynamicDefaultScoping […] default_scope :scoped_to_tenant, lambda { return {} unless Multitenant.current_tenant where({reflection.primary_key_name =>}) } end I think I’d prefer this to return where('1=0')
    20. 20. gem install multitenant <ul><li>Pro </li></ul><ul><ul><li>Transparent enforcement unless you bypass e.g. Model.unscoped </li></ul></ul><ul><li>Con </li></ul><ul><ul><li>Work-in-progress </li></ul></ul><ul><ul><li>You must figure out how you want to set the current tenant </li></ul></ul><ul><ul><li>Defaults to unscoped if tenant not set </li></ul></ul><ul><ul><li>Doesn’t address validation requirements </li></ul></ul># get a user user = User.find(1) => #<User id: 1, name: &quot;a1-u1&quot;, account_id: 1 ..> # set the current tenant Multitenant.current_tenant = user.account => #<Account id: 1, name: &quot;one” ..> # now model access is scoped Project.count => 2 # but we can bypass if we do so explicitly Project.unscoped.count => 4 # also, scoping bypassed if tenant not set Multitenant.current_tenant = nil Project.count => 4
    21. 21. gem install multitenant <ul><li>Scoping validations needs your attention – it is not automatic! </li></ul>class Project < ActiveRecord::Base belongs_to :account belongs_to_tenant :account validates_uniqueness_of :name, :scope => :account_id end
    22. 22. gem install multitenant class User < ActiveRecord::Base belongs_to :account belongs_to_tenant :account belongs_to :project end = simple_form_for [user] do |f| = f.error_messages = f.input :name = f.association :project user = User.find(1) => #<User id: 1, name: &quot;a1-u1&quot;, account_id: 1 ..> Account.find(2) => 3 # set the current tenant Multitenant.current_tenant = user.account Project.where(:id => 3).present? => false # but we can still assign an inaccessible project: user.update_attributes(:project_id => 3).valid? => true # we can’t access the association user.project => nil # but the invalid key is persisted user.project_id => 3 Since all tenant-related models are scoped, our app will display valid options to the user But what if someone sneaks in with some form-injection?
    23. 23. gem install multitenant <ul><li>Necessary to validate associations to ensure only accessible values are persisted </li></ul>class User < ActiveRecord::Base belongs_to :account belongs_to_tenant :account belongs_to :project validates_each :project_id do |record,attr,value| record.errors.add attr, &quot;is invalid&quot; unless Project.where(:id => value).present? end end
    24. 24. Schema Partitioning <ul><li>Namespace access by schema </li></ul><ul><li>Pro </li></ul><ul><ul><li>Complete segregation </li></ul></ul><ul><ul><li>Can share selected tables (e.g. users) </li></ul></ul><ul><li>Con </li></ul><ul><ul><li>Database-specific </li></ul></ul><ul><ul><li>Non-std migration </li></ul></ul>See Guy Naor’s definitive presentation: Model AR::Connection PUBLIC TENANT1 TENANT2 TENANT n SET search_path TO TENANT2,PUBLIC; The “Now I’m using a real database let’s see what it can do” solution
    25. 25. Schema Partitioning User Queries Account Customer Project -- default search path: ‘$user’,PUBLIC PUBLIC
    26. 26. Schema Partitioning User Queries Account Customer Project PUBLIC User Account Customer Project TENANT1 SET search_path TO TENANT1, ‘$user’, PUBLIC;
    27. 27. Schema Partitioning User Queries Account Customer Project PUBLIC Customer Project TENANT1 SET search_path TO TENANT1, ‘$user’, PUBLIC;
    28. 28. Schema Partitioning <ul><li>It just works.. </li></ul># default search path conn=ActiveRecord::Base.connection conn.schema_search_path => &quot;&quot;$user&quot;,public&quot; Project.count => 4 # but if we change the search path conn.schema_search_path = ”tenant1,public&quot; Project.count => 2 public tenant1
    29. 29. Schema Partitioning <ul><li>But how do you setup the schemas? </li></ul><ul><ul><li>Can’t use rails migrations because Migrator does not qualify the schema when calling table_exists? (supplied by the postgres adapter) </li></ul></ul><ul><li>So the standard answer is </li></ul><ul><ul><li>Normal rake db:migrate to the public schema </li></ul></ul><ul><ul><li>Setup any other schemas yourself </li></ul></ul>
    30. 30. Warez: gem install vpd <ul><li> </li></ul><ul><li>gemifies the schema partitioning solution </li></ul><ul><ul><li>“ experimental” </li></ul></ul><ul><ul><li>Enables standard migrations into a selected schema </li></ul></ul><ul><li>To do: </li></ul><ul><ul><li>Abstract the solution for other databases? </li></ul></ul><ul><ul><li>Migration syntax to include/exclude objects for selected schema? </li></ul></ul>gem 'vpd' class ApplicationController < ActionController::Base protect_from_forgery before_filter :set_tenant def set_tenant schema_to_use = request.subdomains.last if schema_to_use.present? # now activate the schema (and make sure # it is migrated up to date) Vpd.activate(schema_to_use,true) else # ensure we are running with the default Vpd.activate_default end end end
    32. 32. Think about the End-user Experience My SaaS Site Tenant 1 Tenant 2 Tenant 3 Users Users Users
    33. 33. Think about the End-user Experience My SaaS Site Tenant 1 Tenant 2 Tenant 3 Users Users Users <ul><li>Less niche => more likely user groups overlap </li></ul><ul><ul><li>Should I have “single sign-on”? </li></ul></ul><ul><ul><li>Can a user see consolidated data? </li></ul></ul><ul><ul><li>How to handle different permissions/profiles? </li></ul></ul>
    34. 34. Disambiguation <ul><li>Deciding how to establish the relationship between a site visitor and a tenant </li></ul><ul><ul><li>Sub-domain ( </li></ul></ul><ul><ul><li>URL path ( </li></ul></ul><ul><ul><li>User login (current_user.tenant) </li></ul></ul><ul><li>Do you need non-tenant landing pages? </li></ul>
    35. 35. Refactoring towards multi-tenancy <ul><li>Tenancy fundamentally shifts the goal posts </li></ul><ul><li>Adding RBAC or model partitioning to a lovingly crafted single-tenant app built with the best test-first principles.. </li></ul><ul><ul><li>Great way to see your tests break en-masse! </li></ul></ul><ul><ul><li>Get sucked into the “code is (probably) right but test is broken” tar pit </li></ul></ul><ul><li>Expect to revisit all tests .. or take another approach </li></ul>
    36. 36. TAKEAWAYS
    37. 37. Decide early <ul><li>It pays to decide early if multi-tenancy is known to be required (now/planned) </li></ul><ul><ul><li>Don’t be bullied into avoiding the issue! </li></ul></ul><ul><li>Selecting a specific multi-tenant architecture from the start can save a whole lot of pain, minimize rework, and avoid coding yourself into a corner </li></ul>YAGNI! Do the simplest thing possible!
    38. 38. ‘ Productize’ <ul><li>If you ever hear this in a meeting, or dream about it at night .. </li></ul><ul><li>.. then chances are you need to be multi-tenant! </li></ul>
    39. 39. The Good News <ul><li>Schema partitioning is a good default solution </li></ul><ul><ul><li>Relatively easy to implement </li></ul></ul><ul><ul><li>Most of your app can remain tenant-agnostic </li></ul></ul><ul><ul><li>Offers the flexibility to selectively break tenant boundaries (& hard to do by accident) </li></ul></ul><ul><ul><li>Ideal for migrating single-tenant apps </li></ul></ul>
    40. 40. We need a ‘Rails Way’ <ul><li>Isn’t it about time Rails had an opinionated, out-of-the-box, solution for multi-tenancy? </li></ul><ul><li>Perhaps schema partitioning with scoped migrations..? </li></ul>create_table :widgets, scope => :all do |t| t.string :name t.timestamps end scope :only => :instance do add_column :projects, :private_attrib, :string end scope :except => :public do add_column :projects, :private_attrib2, :string end
    41. 41. <ul><li>Thanks! </li></ul>