Enterprise-Ready Laravel with Openshift
This post was written for the Laravel Blog Contest , see the entries here and vote if you find this interesting!
For small projects, pretty much anything will do to host Laravel. For larger organizations or complex apps spanning multiple services, these problems become common:
- How do you provision clusters of apps dynamically and securely, and in a reproducible, disposable way?
- How do you ensure the runtime environment of your apps are both reproducible yet flexible?
- What is the best way to avoid having to run
npm install
andcomposer install
in production?
The answer is inevitably Kubernetes.
Openshift from Redhat is Kubernetes-as-a-service, with snazzy features and a great GUI and CLI tools — you get the power of Kubernetes without having to worry about maintaining its internals.
This post assumes familiarity with Laravel and Docker, although it should suffice if you have both installed.
The Application
I needed an app that was decently complex to showcase, and after a quick search settled on Laravel Starter Kit. I’ve forked it at https://github.com/chalcedonyt/laravel-starter-kit which is where you’ll find the code I go through in this post.
Signing Up
Go to https://www.openshift.com/learn/get-started/ and sign up for a starter account. Create a project, we’ll call this my-laravel-app
:
To get a taste of what Openshift can do, let’s go ahead and spin a MySQL instance we’ll use later. Click on the Catalog and click on “MySQL”.
When prompted, accept the default values.
This quickly spins up a MySQL instance. Click on “Resources > Secrets” and click on the “mysql” secret. You’ll notice you have a set of secure credentials you can use in your application.
Deploying code to Openshift
The Openshift Catalog ships with a simple Laravel template, but there are some things we can learn by building our own Laravel app on Openshift:
- How to compile our code before deploying it. We don’t want to have to run
npm install
,composer install
, ornpm run production
on a production server. - Control over the runtime (the environment) the code runs in.
- How to come up with a reproducible runtime
- How to gracefully bring up a new version of our code.
Builder and Runtime images
Openshift introduces the idea of a builder and runtime image with a framework called Source-to-Image (S2I). The goal of S2I is to provide a framework which produces a runnable Docker image containing everything you need to serve your app.
A Builder Image contains the environment in which we “compile” the app before passing it on to the next step. This happens in Laravel when we compile Javascript files — our final image doesn’t need NodeJS or npm, it just needs the files produced by npm run production
. When building, it runs its assemble
script. The result is an artifact that is passed to the runtime image.
A Runtime Image contains the environment with software that serves the app. For Laravel, this is typically a PHP7 image with Apache/Nginx installed. When building, it runs its assemble-runtime
script. The result is a runnable image. S2I expects arun
script in the runtime image for Docker CMD
.
S2I files typically live in the s2i
folder. We’ll create these files:
s2i
| bin
| -- assemble
| -- assemble-runtime
| Dockerfile.runtime
| Dockerfile.builder
The Builder Image
Dockerfile.builder
is based on the official NodeJS image and copies the contents of s2i/bin
to $STI_SCRIPTS_PATH
, setting appropriate permissions.
The assemble
script runs npm
, deletes unneeded files, and exports a build artifact.
The Runtime Image
To create a runtime image, we’ll copys2i/bin/assemble-runtime
from the official version, which runs Composer, but add some lines to restore the artifact created by our build step:
The Dockerfile
for our runtime is based on the official PHP7.1 image, and copies our assemble-runtime
file. This is also where you would install any additional PHP extensions if you need them.
IMPORTANT: The base image includes a run
script that runs Apache in the foreground on port 8080. Its document root can be changed with the DOCUMENTROOT
variable, relative to WORKDIR
(i.e. /public
for Laravel apps).
Creating our runnable Docker image
We want to build and tag our builder image and runtime image, then run s2i build
to produce a runnable image. To make this easier, we fall back on a good ol’ Makefile
:
Note thats2i build
accepts either a code repo or a directory to read its code from.
Running make s2i && make runnable-image
will create an image ( my-laravel-app/laravel-starter-kit:latest
) that is a runnable instance of our code, with Javascript compiled and composer dependencies installed. You can run it locally:
docker run -e DOCUMENTROOT=/public -p 8080:8080 my-laravel-app/laravel-starter-kit:latest
All we need now is a database. We can complete our Makefile
by adding code to push our image into the Openshift registry:
To authenticate into the Openshift docker registry and get the registry address,
- Click on your name on the top right of the console, then click “Copy login command”. Paste this into your terminal.
- Click on the button next to your name and click on “About”.
- Copy the value under “Authenticating”, and use that in your terminal to log into the registry. Also use the registry address in your
Makefile
.
Running make publish
should now push your image to Openshift.
IMPORTANT: The project name ( my-laravel-app
) needs to match the Openshift project you created.
Creating an OpenShift template
An Openshift template is a description of the cluster we will create.
Click on “Add to Project > Import YAML/JSON” on the top right of your Openshift Console, paste this snippet in, and click “Create”, then “Continue”.
Let’s stop to break down the template we are about to process:
Parameters
Parameters are configurable variables for our app, which we can use throughout the template.
Other than typical Laravel environment values( APP_KEY
, APP_ENV
, APP_DEBUG
, etc) we also define:
NAME
: What we’ll call our Openshift app — to keep it simple we’ll use this name for stuff that Openshift will create.APP_IMAGE_NAME
andAPP_IMAGE_TAG
: The name and tag of the runnable image we’ve built.- The MySQL instance we created earlier automatically creates an internal service called
mysql
, which we use as theDB_HOST
. APP_URL
is a dummy value we’ll update later.
The DeploymentConfig
A DeploymentConfig contains information about how to deploy and configure our app.
This snippet may look intimidating, but let’s break it down:
replicas
determines how many instances of our app there will be (keep this at 1 for now, as our starter account has strict usage limits)triggers
determine how when our app deploys itself. Here we set it to deploy from 1) A change in configuration (e.g. updating an environment variable), 2) An ImageStream — pushing new images tomy-laravel-app/laravel-starter-kit:latest
automatically triggers a deployment that gracefully replaces the previous code.env
lists the environment variables for our application. Thevalue
of every variable can be one of: 1) A simple string, 2) A template Parameter e.g.${APP_DEBUG}
, or 3) loaded from a Secret.
Using Secrets, we can securely store a reference to the database password and username that were generated earlier!
The Service
A Service is a proxy to the containers deployed by our DeploymentConfig. We’ll connect it to port 8080 (remember that the official PHP7.1 image exposes Apache on port 8080). Remember that deploying MySQL from the catalog earlier automatically created a service called mysql
which could be connected to internally.
Creating the cluster
Hit “Create” to process the template, wait for a bit and you’ll see the app appear!
Click on the pod circle, and the Terminal tab. This gives us access to the application, where we can issue our artisan commands:
php artisan migrate:refresh --seed#https://laravel-starter-kit-docs.darrylfernandez.com/installation/
php artisan storage:link
Creating a Route
Almost there! Now all we need is a public URL. Click on “Create Route” under the Networking section, and hit “Create”.
Wait for a minute and you should see your app for the first time!
Login with the credentials admin@gmail.com / 12345678 and smile in triumph.
Making Laravel stateless
For an app to be scalable across multiple web servers, it needs to be stateless. Laravel ships with a default session driver of file
, which uses (ephemereal) local storage — not the statelessness we want.
//config/sessions.php
...'driver' => env('SESSION_DRIVER', 'file'),
We can add a new environment variable by editing the DeploymentConfig. Click on the link to it:
Click on the “Environment” tab, “Add Value”, and set SESSION_DRIVER
to cookie
. While you’re at it, modify APP_URL
to the dynamically generated URL you got earlier.
After clicking “Save”, you’ll notice Openshift perform a rolling update — a new container is spun up with your runnable image, and gracefully takes the place of the previous one.
Welcome to Enterprise-ready Laravel!
That’s it — we managed to deploy an existing Laravel app to Openshift quite quickly, and made it scale.
By going for a paid account or running the open-source version of Openshift on your own, you’ll be able to do things like quickly scale up the number of pods and assign predefined route URLs in templates.
There are many other great ways to use and supplement Laravel in Openshift:
- With CronJobs and Jobs, you can run queue workers and scheduled tasks off the same runnable images.
- Spin up Redis instances instantly.
- Run your own free object storage with a Minio instance
Credits to my colleagues Alan Burgoyne and Dmitry Gordienko, who pioneered Openshift where I work.