Slide 1

Slide 1 text

A Practical Road to SaaS' in Python Armin @mitsuhiko Ronacher

Slide 2

Slide 2 text

)J *N"SNJO BOE*EP0QFO4PVSDF MPUTPG1ZUIPOBOE4BB4 'MBTL 4FOUSZ j

Slide 3

Slide 3 text

No content

Slide 4

Slide 4 text

No content

Slide 5

Slide 5 text

No content

Slide 6

Slide 6 text

No content

Slide 7

Slide 7 text

I love Open Source

Slide 8

Slide 8 text

Therefore I love SaaS

Slide 9

Slide 9 text

SaaS

Slide 10

Slide 10 text

Multi Tenant

Slide 11

Slide 11 text

But also … On Premises?

Slide 12

Slide 12 text

Managed Cloud?

Slide 13

Slide 13 text

Python

Slide 14

Slide 14 text

Why Python?

Slide 15

Slide 15 text

Python in 2017

Slide 16

Slide 16 text

Strong Ecosystem

Slide 17

Slide 17 text

Fast Iteration

Slide 18

Slide 18 text

Stable Environment

Slide 19

Slide 19 text

Powerful
 Metaprogramming

Slide 20

Slide 20 text

Fast Interpreter Introspection

Slide 21

Slide 21 text

Quo Vadis?

Slide 22

Slide 22 text

Python 2.7 / 3.6

Slide 23

Slide 23 text

Machine. Learning

Slide 24

Slide 24 text

The Foundation

Slide 25

Slide 25 text

No content

Slide 26

Slide 26 text

No content

Slide 27

Slide 27 text

aiohttp

Slide 28

Slide 28 text

roll your own?

Slide 29

Slide 29 text

Application Architecture

Slide 30

Slide 30 text

Security First

Slide 31

Slide 31 text

patterns are universal examples are Flask + Flask-SQLAlchemy

Slide 32

Slide 32 text

If you only take one thing away from this talk …

Slide 33

Slide 33 text

Context Awareness … or how I learned to love the thread-local bomb

Slide 34

Slide 34 text

Tenant Isolation from flask import g, request def get_tenant_from_request(): auth = validate_auth(request.headers.get('Authorization')) return Tenant.query.get(auth.tenant_id) def get_current_tenant(): rv = getattr(g, 'current_tenant', None) if rv is None: rv = get_tenant_from_request() g.current_tenant = rv return rv

Slide 35

Slide 35 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 36

Slide 36 text

Automatic Tenant Scoping class TenantQuery(db.Query): current_tenant_constrained = True def tenant_unconstrained_unsafe(self): rv = self._clone() rv.current_tenant_constrained = False return rv @db.event.listens_for(TenantQuery, 'before_compile', retval=True) def ensure_tenant_constrainted(query): for desc in query.column_descriptions: if hasattr(desc['type'], 'tenant') and \ query.current_tenant_constrained: query = query.filter_by(tenant=get_current_tenant()) return query

Slide 37

Slide 37 text

Automatic Tenant Scoping from sqlalchemy.ext.declarative import declared_attr class TenantBoundMixin(object): query_class = TenantQuery @declared_attr def tenant_id(cls): return db.Column(db.Integer, db.ForeignKey('tenant.id')) @declared_attr def tenant(cls): return db.relationship(Tenant, uselist=False)

Slide 38

Slide 38 text

Example Use class Project(TenantBoundMixin, db.Model): id = db.Column(db.Integer, primary_key=True) name = db.Column(db.String(100)) status = db.Column(db.Integer) def __repr__(self): return '' % self.name >>> test.Project.query.all() [] >>> test.Project.query.tenant_unconstrained_unsafe().all() [, Project.name='project2', ...]

Slide 39

Slide 39 text

careful about backrefs!

Slide 40

Slide 40 text

Flask-SQLAlchemy lets you set a default query class for all things

Slide 41

Slide 41 text

Uses for Context

Slide 42

Slide 42 text

Current User

Slide 43

Slide 43 text

User from Auth def load_user_from_request(): user_id = session.get('user_id') if user_id is not None: return User.query.get(user_id) return None def get_current_user(): rv = getattr(g, 'current_user', None) if rv is None: rv = g.current_user = load_user_from_request() return rv

Slide 44

Slide 44 text

User Access Scope Restrictions

Slide 45

Slide 45 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 46

Slide 46 text

Audit Logs

Slide 47

Slide 47 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'] = request.remote_addr user = get_current_user() if user is not None: data['user'] = User db.session.add(LogMessage(**data))

Slide 48

Slide 48 text

i18n / l10n

Slide 49

Slide 49 text

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

Slide 50

Slide 50 text

Design as you go

Slide 51

Slide 51 text

Build first, then evolve

Slide 52

Slide 52 text

Sentry is still non sharded Postgres

Slide 53

Slide 53 text

Python helps with Prototype to Production

Slide 54

Slide 54 text

Operating Python

Slide 55

Slide 55 text

CPython: Refcounting PyPy: GC

Slide 56

Slide 56 text

sys._getframe()

Slide 57

Slide 57 text

No content

Slide 58

Slide 58 text

No content

Slide 59

Slide 59 text

Process and Data

Slide 60

Slide 60 text

deploy in seconds be unable to screw up and if you do: instant rollbacks

Slide 61

Slide 61 text

commit review integration deploy

Slide 62

Slide 62 text

requires good test coverage requires good local setup makes it easier for newcomers

Slide 63

Slide 63 text

lint on commit!

Slide 64

Slide 64 text

flake8 & custom linters

Slide 65

Slide 65 text

master is stable

Slide 66

Slide 66 text

AVOID DOWNTIME (how to)

Slide 67

Slide 67 text

bidirectional compatibility

Slide 68

Slide 68 text

My Opinion: Invest into Fast Iteration rather than Scalability

Slide 69

Slide 69 text

Duck-Typing helps Here

Slide 70

Slide 70 text

Quick Release Cycles

Slide 71

Slide 71 text

large systems are organisms

Slide 72

Slide 72 text

not all things run the same code at the same time

Slide 73

Slide 73 text

break up features feature flag them

Slide 74

Slide 74 text

Make Prod & Dev Look Alike

Slide 75

Slide 75 text

On Prem?

Slide 76

Slide 76 text

two release cycles hourly SaaS six-week on-prem

Slide 77

Slide 77 text

Consider shipping WIP feature flag IT AWAY

Slide 78

Slide 78 text

Feature Class class Feature(object): def __init__(self, key, scope, enable_chance=None, default=False): self.key = key self.scope = scope self.enable_chance = enable_chance self.default = default def evaluate(self): scope = self.scope(self) value = load_feature_flag_from_db(self.key, scope) if value is not None: return value if self.enable_chance: if hash_value(scope) / float(MAX_HASH) > self.enable_chance: return True return self.default

Slide 79

Slide 79 text

Random Features def ip_scope(feature): if request: return 'ip:%s' % request.remote_addr NEW_SIGN_IN_FLOW = Feature( key='new-sign-in-flow', scope=ip_scope, enable_chance=0.9, allow_overrides='admin', default=False, )

Slide 80

Slide 80 text

User Features def new_dashboard_default(): tenant = get_current_tenant() if tenant.creation_date > datetime(2017, 1, 1): return True return False NEW_DASHBOARD = Feature( key='new-dashboard', scope=user_scope, allow_overrides='user', default=new_dashboard_default, )

Slide 81

Slide 81 text

Testing Features if is_enabled(NEW_DASHBOARD): ... • Cache • Prefetch • Easier Grepping

Slide 82

Slide 82 text

Mastering Deployments

Slide 83

Slide 83 text

Build Wheels

Slide 84

Slide 84 text

Docker Images then follow up with

Slide 85

Slide 85 text

QA &