On Github sclasen / cloudplay-talk
Scott Clasen / @scottclasen / Heroku API Team
We run several production services based on Play! / Scala, since the days of
addSbtPlugin("play" % "sbt-plugin" % "2.0")
This talk summarizes some of the tooling and best practices we have developed.
Many of them also codified in this template app sclasen/cloudplay
Or this library sclasen/play-extras
we try to the extent possible to build Heroku on Heroku
we call this ongoing effort: ephemeralization
we try to the extent possible to build Heroku on Heroku
we call this ongoing effort: ephemeralization
all of our Play! services fall into this category
this talk touches on several of the concepts described in http://12factor.net/
we use some basic tooling whether an app is ephemeralized, or not
Procfile / .env
web: bin/web continuous: bin/continuous
$ forego start web // starts the web process, capture stdout $ forego start // start web and continuous processes, capture stdout on both
FOO=foooo BAR=barrr
$ echo "foo: $FOO bar: $BAR" foo: bar: $ forego run echo "foo: $FOO bar: $BAR" foo: foooo bar: barrr
heroku config:pull -o -a app-dev //pull the config from app-dev and write it to your .env heroku config:push -o -a app-dev //write your .env to the config vars of the app
Pipelines!
Pipelines!
https://devcenter.heroku.com/articles/labs-pipelines
backed by a Play! service as a matter of fact.
Pipelines + Jenkins + Github Heroku and Hipchat plugins
mostly focused on integration testing
standard dev -> staging -> prod setup
dev + staging have app config + test config in their env
4 jobs per service
1: app-master-to-dev
triggered by a github webhook
heroku jenkins plugin: heroku push buildstep
build app and deploy to dev
4 jobs per service
2: app-test-dev
run integration tests from jenkins, hitting dev
heroku config:pull -o -a app-dev
overwrites .env file locally
foreman run play test
4 jobs per service
3: app-promote-dev-staging
heroku pipeline:promote -a app-dev
thats it!
4 jobs per service
4: app-test-staging
same pattern as dev
run integration tests from jenkins, hitting staging
heroku config:pull -o -a app-dev
overwrites .env file locally
foreman run play test
heroku pipeline:promote -a app-staging
thats it!
Batch/Background Processing
Evolutions/Migrations
Credentials Handling/Encryption
How to keep things as simple as possible?
-Dapplication.global FTW
You have all your models, services, logic
you have play akka etc
web: bin/cloudplay -Dapplication.global=Web continuous: bin/cloudplay -Dapplication.global=Continuous
trait ProcessType extends GlobalSettings { ... } object Web extends ProcessType { override def onStart(app: Application) = { log.info("Starting ProcessType: Web") start(app, "web") } } object Continuous extends ProcessType { override def onStart(app: Application) = { log.info("Starting ProcessType: Continuous") start(app, "continuous") processes.ContinuousProcess.start() } }
Ok, great whats in this processes.ContinuousProcess.start()business?
Lets call it an 'actor batch process'
Run multiple instances, using postgres advisory locks where necessary.
Lets take a look at some code!
Evolutions/Migrations
Seldom mentioned Evolution features in play
-DapplyDownEvolutions.database=true
-Devolutions.useLocks=true
* locks require a db that supports `select for update nowait`
Evolutions: provided by play
Migrations: code/sql you write
Hot Compatibility
So here’s the basic principle that allows you to avoid downtime:
any evolution or migration being deployed should be compatible with the code that is already running.
Hot Compatibility
In order to do so, you’ll usually split your deploy process in two steps:
Make the code compatible with the evolution/migration you need to run
Run the evolution, run the migration, and remove any code written specifically for it
How to write/run?
-Dapplication.global FTW again
object Migration extends ProcessType { override def onStart(app: Application) = { log.info("Starting ProcessType: Migration") start(app, "migration") processes.MigrationProcess.start() } } //add this to procfile migration: bin/cloudplay -Dapplication.global=Migration
Run the migration with: `heroku run migration`
Lets look at the code for processes.MigrationProcess.start()
Credentials Handling/Encryption
Generally two types of credentials in an app.
User credentials/passwords that you dont need to know, only verify.
Other credentials that you need to know in unencrypted form.
User's Passwords
use bcrypt
or the util methods in sclasen/play-extras
which use bcrypt
User's Passwords
CredentialsService.hashPassword
CredentialsService.checkPasswordAgainstHash
Credentials you need in unencrypted form
database passwords, aws keys, api keys, etc
Credentials you need in unencrypted form
If an attacker has both your code and runtime config, you are basically hosed.
So, make it such that if the attacker has only one or the other, you are safe
Encrypting a credential
> sbt console //outputs omitted scala> import com.heroku.play.api.libs.security._ scala> val secret = "the secret password" scala> val secretKey = CredentialsService.generateKey scala> val keyMaskThatGoesInYourCodebase = CredentialsService.generateKey // add to application.conf secret.key.mask=keyMaskThatGoesInYourCodebase scala> val maskedSecretKeyThatGoesInYourENV = CredentialsService.maskKey(secretKey, keyMaskThatGoesInYourCodebase) // add to env heroku config:set SECRET_KEY=maskedSecretKeyThatGoesInYourENV scala> val encryptedSecretThatGoesInYourENV = CredentialsService.doEncryptCredential(secret, secretKey) // add to env heroku config:set SECRET=encryptedSecretThatGoesInYourENV
Decrypting a credential
CredentialsService.decryptCredential takes the names of 2 env vars and 1 property from applicaiton.conf
> forego run sbt console //outputs omitted scala> import com.heroku.play.api.libs.security._ scala> val secret = CredentialsService.decryptCredential("SECRET", "SECRET_KEY", "secret.key.mask")
* there is also a doDecryptCredential form for encrypted values that arent/cant be stored in the env.
Logs as Event Streams
Request-Ids
Splunk, Librato
Actor Based Logback Appender
Factor #11 from 12 Factors
At scale you want to optimize more for machine readability
We do this at heroku with `logfmt`
Factor #11 from 12 Factors
At scale you want to optimize more for machine readability
We do this at heroku with `logfmt`
logfmt http://godoc.org/github.com/kr/logfmt
ident_byte = any byte greater than ' ', excluding '=' and '"' string_byte = any byte excluding '"' and '\' garbage = !ident_byte ident = ident_byte, { ident byte } key = ident value = ident | '"', { string_byte | '\', '"' }, '"' pair = key, '=', value | key, '=' | key message = { garbage, pair }, garbage
//LOGFMT foo=bar a=14 baz="hello kitty" cool%story=bro f %^asdf //parsed to json { "foo": "bar", "a": 14, "baz": "hello kitty", "cool%story": "bro", "f": true, "%^asdf": true }
Which is easier to process?
ERROR user scott@heroku.com is over the account-rate-limit error app=an-app at=over-account-limit user=scott@heroku.com
https://metrics.librato.com/share/dashboards/k4b5bhm8
images from http://www.flickr.com/creativecommons/by-2.0/