Slide 1

Slide 1 text

Pragmatic SaaS Architecture Armin @mitsuhiko Ronacher

Slide 2

Slide 2 text

)J *N"SNJO BOE*EP0QFO4PVSDF MPUTPG1ZUIPOBOE4BB4

Slide 3

Slide 3 text

lucumr.pocoo.org github.com/mitsuhiko twitter.com/mitsuhiko read discover follow

Slide 4

Slide 4 text

No content

Slide 5

Slide 5 text

800°C 36° 2' 0.4662" N 118° 15' 38.7792" W 795°C 789°C 797°C 793°C 805°C 782°C

Slide 6

Slide 6 text

I love SaaS

Slide 7

Slide 7 text

SaaS

Slide 8

Slide 8 text

Multi Tenant

Slide 9

Slide 9 text

Managed Cloud? single tenant

Slide 10

Slide 10 text

But also … On Premises?

Slide 11

Slide 11 text

Diving In

Slide 12

Slide 12 text

patterns are universal (examples are Python)

Slide 13

Slide 13 text

Building Blocks

Slide 14

Slide 14 text

boring is better than fancy

Slide 15

Slide 15 text

Postgres & Redis & AMQP (for example)

Slide 16

Slide 16 text

Foundation

Slide 17

Slide 17 text

Security First

Slide 18

Slide 18 text

If you only take one thing away from this talk …

Slide 19

Slide 19 text

context awareness

Slide 20

Slide 20 text

Tenant Isolation from framework import get_request def get_tenant_from_request(): request = get_request() auth = validate_auth(request.headers.get('Authorization')) return Tenant.query.get(auth.tenant_id) def get_current_tenant(): rv = tls.current_tenant if rv is None: rv = get_tenant_from_request() tls.current_tenant = rv return rv

Slide 21

Slide 21 text

Automatic Tenant Scoping def batch_update_projects(ids, changes): projects = Project.query.filter( Project.id.in_(ids) & Project.status != ProjectStatus.INVISIBLE ) for project in projects: update_project(project, changes) DANGER!

Slide 22

Slide 22 text

Automatic Tenant Scoping class Project(db.Model): id = db.Column(db.Integer, primary_key=True) status = db.Column(db.Integer) tenant_id = db.Column(db.Integer, db.ForeignKey('tenants.id')) tenant = db.relationship(Tenant) @classproperty def query(cls): return db.Query(self).filter( Project.tenant == get_current_tenant() )

Slide 23

Slide 23 text

User Access Scope Restrictions

Slide 24

Slide 24 text

User Scope & Request Scope def get_current_scopes(): current_user = get_current_user() if current_user is None: all_scopes = set(['anonymous']) else: all_scopes = current_user.get_roles() return all_scopes & scopes_from_request_authorization()

Slide 25

Slide 25 text

Audit Logs

Slide 26

Slide 26 text

Log Security Related Actions def log(action, message=None): data = { 'action': action, 'timestamp': datetime.utcnow() } if message is not None: data['message'] = message if request: data['ip'] = get_request().remote_addr user = get_current_user() if user is not None: data['user'] = User db.session.add(LogMessage(**data))

Slide 27

Slide 27 text

i18n & l10n

Slide 28

Slide 28 text

Language from User or Request def get_current_language(): user = get_current_user() if user is not None: return user.language request = get_current_request() if request and request.accept_languages: return request.accept_languages[0] return 'en_US'

Slide 29

Slide 29 text

Monolith → SOA

Slide 30

Slide 30 text

SOA is complex

Slide 31

Slide 31 text

Start Simple Evolve

Slide 32

Slide 32 text

Design as you go

Slide 33

Slide 33 text

Developer Happiness

Slide 34

Slide 34 text

custom linters on commit mitsuhiko at herzog in ~/Development/sentry on git:master+? workon sentry $ git ci -am 'Performance improvements to the data scrubber.' src/sentry/utils/data_scrubber.py:147:1: F401 'unused' imported but unused

Slide 35

Slide 35 text

master is stable

Slide 36

Slide 36 text

institutionalize bidirectional compatibility

Slide 37

Slide 37 text

fast iteration trumps scalability

Slide 38

Slide 38 text

Quick Release Cycles

Slide 39

Slide 39 text

make dev and prod look alike

Slide 40

Slide 40 text

short and small pull requests

Slide 41

Slide 41 text

feature flag new functionality

Slide 42

Slide 42 text

QA &