Music From the heavens

From music streaming monolith to service-oriented, Kubernetes backed loveliness

Matt Gray

  • Freelance consultant and maker of things
  • PHP and GoLang
  • Been working with Now Music for about 4 years

Now Music

  • A cross platform, music streaming app
  • Contains much of the Now back catalogue
  • Also regularly updated chart tracks
  • Integrates directly with music labels - not a white-label rebrand

Tech

  • Back end is Laravel
  • Cloud hosting is AWS
  • Client side apps are native

What's the problem?

  • We built an MVP, fast and cheap
  • We added features over time
  • We should have refactored more thoroughly

Laravel Codebase

API

Services

Website

CMS

Reporting

Web Traffic

Mobile Apps

Analytics

Cron Jobs

CMS Traffic

Audio Content

Reports

Insights

Emails

Notifications

The straw that Broke The Camel's Back

composer require aws/aws-sdk-php "^3.0"
Your requirements could not be resolved to an installable set of packages

Enter K8s (and docker)

Core

Reporting

API

Website

CMS

CQRS Interface

Database

Analytics

Web Traffic

Mobile Apps

Analytics

Cron Jobs

CMS Traffic

Audio Content

Ingestion

Kubernetes

This is a laravel meetup.

Stop bitching about technical debt.

The authentication problem

  • We want to keep service responsibilities separated
  • We want a unified login mechanism
  • We want to use Laravel's built-in auth functionality
  • We only want the Core service to touch the database

Laravel's session based authentication mechanisms make use of a contract called UserProvider.

It's responsible for converting a set of credentials into a User object.

Luckily it's swappable in Laravel's config files.

The built in provider is called DatabaseUserProvider.

public function retrieveByCredentials(array $credentials)
{
    $user = User::where('email', '=', $credentials['email'])
                ->where('password', '=', $credentials['password']);
    return $user;
}

We just had to create our own version which talked to our core service:

public function retrieveByCredentials(array $credentials)
{
    $core = app()->make('CoreCommunicationsContract');
    $user = $core->call('swapCredsForUser', $credentials);
    return $user;
}

Then all we had to do is register the new UserProvider

'providers' => [
    'users' => [
        'driver' => 'core'
    ],
],

And set it as the default in config/auth.php

Auth::provider('core', function ($app, array $config) {
    return app()->make(CoreUserProvider::class);
});

And what about the API service?

We used Passport to handle API auth which uses the same UserProvider mechanism as session based auth.

Copy and paste implementation from the website codebase.

It was that easy?

No.

Passport is hard coded to store oAuth tokens in a database.

Guess they forgot to interface everything.

Ended up having to subclass a hierarchy of about 6 dependent classes just to inject a custom storage mechanism.

environment specific services

  • We have multiple environments in which we need to run our app: local, testing, staging, production
  • Each has different access to external services (cache, secrets management, queues)
  • We can't use an .env file because we're building docker images which have a static filesystem

1. Interface Everything

"Always build to an interface"

Always wrap internal and external components with an interface

  • Allows use of the IoC container for everything
  • Allows components to be easily mocked in tests
  • Allows different implementations to be created and selected at runtime
  • If an external service changes our internal interface remains static. Minimal codebase exposure

Things we can swap the implementation of on a whim:

Analytics, logging, Facebook, Stripe, iTunes, Google Play, Amazon API, audio delivery, dynamic playlist generation....

2. Extend Laravel's Config

Laravel's config system is great.

It can hold anything you like.

Values can be loaded from the environment which coincidentally is the best way to pass information into a docker container when it boots up.

'radio_playlist_algorithm' => env('RADIO_PLAYLIST_ALGORITHM', 'random-library-resources'),

config/app.php

docker-compose.yml

environment:
    - "RADIO_PLAYLIST_ALGORITHM=analytics-backed"

AppServiceProvider.php

$radioAlgoClass = config('app.radio_playlist_algorithm') == 'random-library-resources'
            ? RandomRadioPlaylistAlgorithm::class : AnalyticsBackedAlgorithm::class;

3. Profit

  • We can now swap out components of our codebase when we boot it up.
  • Single codebase but different code in different environments.
  • Using Kubernetes we can do this as a gradual rollout across a cluster, avoiding any service downtime.
  • Hot-swappable code

THanks!

Q's?

@mattgrayisok

Music From The Heavens

By Matt

Music From The Heavens

  • 1,342