Top Banner
MULTI-TENANCY AND RAILS House party, slumlord, or mogul? RedDotRubyConf – Singapore 22-Apr-2011
41

Multi-tenancy with Rails

May 11, 2015

Download

Technology

Paul Gallagher

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'?"
Welcome message from author
This document is posted to help you gain knowledge. Please leave a comment to let me know what you think about it! Share it to your friends and learn new things together.
Transcript
Page 1: Multi-tenancy with Rails

MULTI-TENANCY AND RAILS

House party, slumlord, or mogul?

RedDotRubyConf – Singapore 22-Apr-2011

Page 2: Multi-tenancy with Rails

Why do I care about this?The product: 360º engagement between you and your customers

– Unify and socialize support, brand management, product management, feedback & innovation

Launched privately April 1st

Objective is a public SaaS launch within 6 months

– Many customers– Customers of customers– Single, cohesive (rails) application

..hence we’ve been thinking a lot about multi-tenancy ;-)

Paul [email protected]

personal:tardate.comtwitter.com/tardate

Page 3: Multi-tenancy with Rails

A long time ago in a galaxy far,far away….

My first encounter with “multi-tenancy”

– Take a single-tenant on-premise application and implement for 8 related entities (fiduciary requirement for separation)

Sure, no problem!– Physical separation of the app tier– Shared database cluster hosting separate

database instances– Shared identity management service– Custom perl-based provisioning system

OMG the hardware– 4-node db cluster (9 db instances)– 9x2 app tier servers– Another 4 infrastructure service nodes

These days we’d call this virtualization at best, and cloud-washing otherwise!

.. and they say rails can’t scale!

Page 4: Multi-tenancy with Rails

Three Thoughts for Today

Page 5: Multi-tenancy with Rails

A GENERAL MODEL

Page 6: Multi-tenancy with Rails

Multi-tenancy

Simplest definition possible:

a principle in software architecture

where a single instance of the software

serves multiple client organizations

Page 7: Multi-tenancy with Rails

Tenancy Models

Page 8: Multi-tenancy with Rails

Tenancy Models

Social networks,

LBS, Web 2.0

Social networks,

LBS, Web 2.0

MSP/ASP, PaaS, IaaS

MSP/ASP, PaaS, IaaS

Business, Enterprise 2.0, SaaS

Business, Enterprise 2.0, SaaS

The same idea can manifest itself across the continuum – depending how you approach it

Page 9: Multi-tenancy with Rails

Tenancy Models

Social networks,

LBS, Web 2.0

Social networks,

LBS, Web 2.0

MSP/ASP, PaaS, IaaS

MSP/ASP, PaaS, IaaS

Business, Enterprise 2.0, SaaS

Business, Enterprise 2.0, SaaS

Explicit Rails support..

..but our usage increasingly extends here

Hardware/infrastructure

challenge

Hardware/infrastructure

challenge

Application architecture

challenge

Application architecture

challenge

Page 10: Multi-tenancy with Rails

Tenancy Considerations

Page 11: Multi-tenancy with Rails

Key Concern: Data Partitioning

Page 12: Multi-tenancy with Rails

RE-USE / RE-INVENT

Page 13: Multi-tenancy with Rails

Four Data Partioning Techniques..

1.Instance Partitioning2.RBAC Partitioning3.Model Partitioning4.Schema Partitioning

(just some of the many ways to solve this)

Page 14: Multi-tenancy with Rails

Instance ProvisioningPro

– Apps don’t need to be multi-tenant

– Apps don’t even have to be the same

– You can do some neat tricks with git

Con– $caling– Management– Provisioning and spin-

up times The Infrastructure Engineer’s solutionThe Infrastructure Engineer’s solution

Page 15: Multi-tenancy with Rails

RBAC PartitioningConsider tenancy as just RBAC to another degreePro

– Able to address complex shared/private data requirements

Con– Easy to leak data with

bugs and oversights

Perhaps too clever by half?

UserUser

ModelModel

Query scoped by permissions

Query scoped by permissions

The “I just groked CanCan and it’s awesome” solutionThe “I just groked CanCan and it’s awesome” solution

Page 16: Multi-tenancy with Rails

RBAC Partitioninge.g. with CanCan CanCan +

InheritedResources

But beware .. you are still responsible for

– validations– scoping options in

forms (e.g. select lists)

class Ability include CanCan::Ability def initialize(user) … # include nil so we can handle :new can :manage, Project, :tenant_id => [user.tenant_id, nil] … endend

# then we can..Project.accessible_by(Ability.new(current_user))

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)

Page 17: Multi-tenancy with Rails

Model PartitioningProbably the most common patternPro

– No special deployment or infra requirements

Con– Relies on application

query logic– Does not inherently

prevent data leakage– Scaling with

performance

tenant_id (or similar) present in every table .. and every query

id tenant_id name …

id tenant_id name …

users

projects

The Software Architect’s solution

The Software Architect’s solution

Page 18: Multi-tenancy with Rails

Model Partitioninggithub.com/wireframe/multitenant

– Handles model aspects well– Released gem (0.2.0)– WIP controller support etc

github.com/penguincoder/acts_as_restricted_subdomain – Comprehensive, but internals need overhaul for Rails 3

github.com/mconnell/multi_tenant – Partial implementation only

Software as a Service Rails Kit– railskits.com/saas/– Model partitioning: single-database scoped access– Not free, but it solves other problems like recurring billing

and payment gateway integration

Page 19: Multi-tenancy with Rails

gem install multitenantUses dynamic default scopes to partition model access to Multitenant.current_tenant

NB: belongs_to_tenant (as used in released gem 0.2.0) renamed to belongs_to_multitenant in current master

class Account < ActiveRecord::Base has_many :users has_many :projectsendclass User < ActiveRecord::Base belongs_to :account belongs_to_tenant :accountendclass Project < ActiveRecord::Base belongs_to :account belongs_to_tenant :accountend

def belongs_to_tenant(association = :tenant) include DynamicDefaultScoping […] default_scope :scoped_to_tenant, lambda { return {} unless Multitenant.current_tenant where({reflection.primary_key_name => Multitenant.current_tenant.id}) }end

I think I’d prefer this to return where('1=0')

I think I’d prefer this to return where('1=0')

Page 20: Multi-tenancy with Rails

gem install multitenantPro

– Transparent enforcement unless you bypass e.g. Model.unscoped

Con– Work-in-progress– You must figure out

how you want to set the current tenant

– Defaults to unscoped if tenant not set

– Doesn’t address validation requirements

# get a useruser = User.find(1) => #<User id: 1, name: "a1-u1", account_id: 1 ..>

# set the current tenantMultitenant.current_tenant = user.account => #<Account id: 1, name: "one” ..>

# now model access is scopedProject.count => 2

# but we can bypass if we do so explicitlyProject.unscoped.count => 4

# also, scoping bypassed if tenant not setMultitenant.current_tenant = nilProject.count => 4

Page 21: Multi-tenancy with Rails

gem install multitenant

Scoping validations needs your attention – it is not automatic!

class Project < ActiveRecord::Base belongs_to :account belongs_to_tenant :account validates_uniqueness_of :name, :scope => :account_idend

Page 22: Multi-tenancy with Rails

gem install multitenant

class User < ActiveRecord::Base belongs_to :account belongs_to_tenant :account belongs_to :projectend

= simple_form_for [user] do |f| = f.error_messages = f.input :name = f.association :project

user = User.find(1) => #<User id: 1, name: "a1-u1", account_id: 1 ..>Account.find(2).projects.first.id => 3

# set the current tenantMultitenant.current_tenant = user.accountProject.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 associationuser.project=> nil # but the invalid key is persisteduser.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?

Page 23: Multi-tenancy with Rails

gem install multitenant

Necessary to validate associations to ensure only accessible values are persisted

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, "is invalid" unless Project.where(:id => value).present? endend

Page 24: Multi-tenancy with Rails

Schema PartitioningNamespace access by schemaPro

– Complete segregation– Can share selected

tables (e.g. users)

Con– Database-specific– Non-std migration

See Guy Naor’s definitive presentation:confreaks.net/videos/111-aac2009-writing-multi-tenant-applications-in-rails

ModelModel

AR::ConnectionAR::Connection

PUBLICPUBLIC

TENANT1TENANT1

TENANT2TENANT2

TENANTnTENANTn

SET search_path TO TENANT2,PUBLIC;

The “Now I’m using a real database let’s see what it

can do” solution

The “Now I’m using a real database let’s see what it

can do” solution

Page 25: Multi-tenancy with Rails

Schema Partitioning

UserUser

QueriesQueries

AccountAccount CustomerCustomer ProjectProject

-- default search path: ‘$user’,PUBLIC

PUBLICPUBLIC

Page 26: Multi-tenancy with Rails

Schema Partitioning

UserUser

QueriesQueries

AccountAccount CustomerCustomer ProjectProject PUBLICPUBLIC

UserUser AccountAccount CustomerCustomer ProjectProject TENANT1TENANT1

SET search_path TO TENANT1, ‘$user’, PUBLIC;

Page 27: Multi-tenancy with Rails

Schema Partitioning

UserUser

QueriesQueries

AccountAccount CustomerCustomer ProjectProject PUBLICPUBLIC

CustomerCustomer ProjectProject TENANT1TENANT1

SET search_path TO TENANT1, ‘$user’, PUBLIC;

Page 28: Multi-tenancy with Rails

Schema PartitioningIt just works..# default search pathconn=ActiveRecord::Base.connectionconn.schema_search_path=> "\"$user\",public" Project.count=> 4

# but if we change the search pathconn.schema_search_path = ”tenant1,public" Project.count=> 2

public

tenant1

Page 29: Multi-tenancy with Rails

Schema Partitioning

But how do you setup the schemas?– Can’t use rails migrations because Migrator

does not qualify the schema when calling table_exists? (supplied by the postgres adapter)

So the standard answer is– Normal rake db:migrate to the public

schema– Setup any other schemas yourself

Page 30: Multi-tenancy with Rails

Warez: gem install vpdgithub.com/tardate/vpd gemifies the schema partitioning solution

– “experimental”– Enables standard

migrations into a selected schema

To do:– Abstract the solution

for other databases?– Migration syntax to

include/exclude objects for selected schema?

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 endend

Page 31: Multi-tenancy with Rails

LESSONS LEARNT AND TTD

Page 32: Multi-tenancy with Rails

Think about the End-user Experience

My SaaS SiteMy SaaS Site

Tenant 1Tenant 1 Tenant 2Tenant 2 Tenant 3Tenant 3

UsersUsers UsersUsers UsersUsers

Page 33: Multi-tenancy with Rails

Think about the End-user Experience

My SaaS SiteMy SaaS Site

Tenant 1Tenant 1 Tenant 2Tenant 2 Tenant 3Tenant 3

UsersUsers UsersUsers UsersUsers

Less niche => more likely user groups overlap– Should I have “single sign-on”?– Can a user see consolidated data?– How to handle different permissions/profiles?

Page 34: Multi-tenancy with Rails

Disambiguation

Deciding how to establish the relationship between a site visitor and a tenant

– Sub-domain (tenant1.example.net)– URL path (www.example.net/tenant1)– User login (current_user.tenant)

Do you need non-tenant landing pages?

Page 35: Multi-tenancy with Rails

Refactoring towards multi-tenancy

Tenancy fundamentally shifts the goal posts

Adding RBAC or model partitioning to a lovingly crafted single-tenant app built with the best test-first principles..

– Great way to see your tests break en-masse!– Get sucked into the “code is (probably) right

but test is broken” tar pit

Expect to revisit all tests .. or take another approach

Page 36: Multi-tenancy with Rails

TAKEAWAYS

Page 37: Multi-tenancy with Rails

Decide earlyIt pays to decide early if multi-tenancy is known to be required (now/planned)

– Don’t be bullied into avoiding the issue!

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

YAGNI!YAGNI!Do the simplest thing possible!Do the simplest thing possible!

Page 38: Multi-tenancy with Rails

‘Productize’

If you ever hear this in a meeting, or dream about it at night ..

.. then chances are you need to be multi-tenant!

Page 39: Multi-tenancy with Rails

The Good News

Schema partitioning is a good default solution

– Relatively easy to implement– Most of your app can remain tenant-

agnostic– Offers the flexibility to selectively break

tenant boundaries (& hard to do by accident)

– Ideal for migrating single-tenant apps

Page 40: Multi-tenancy with Rails

We need a ‘Rails Way’

Isn’t it about time Rails had an opinionated, out-of-the-box, solution for multi-tenancy?

Perhaps schema partitioning with scoped migrations..?

create_table :widgets, scope => :all do |t| t.string :name t.timestampsendscope :only => :instance do add_column :projects, :private_attrib, :stringendscope :except => :public do add_column :projects, :private_attrib2, :string end

Page 41: Multi-tenancy with Rails

Thanks!