Deploy a Laravel application on AWS Elastic Beanstalk and scale it with Memcache
Want to learn how to create a Laravel application on Elastic Beanstalk that is ready to scale? We’ll explore how to set up your Elastic Beanstalk environment, hook it up to a database, deploy your application, and finally how to use Memcache to speed it up.
Memcache is a technology that improves the performance and scalability of web apps and mobile app backends. You should consider using Memcache when your pages are loading too slowly or your app is having scalability issues. Even for small sites, Memcache can make page loads snappy and help future-proof your app.
In this guide we will create a task list application based on the Laravel 5.2 tutorial and scale it with Memcache. The sample app in this guide can be found here.
Prerequisites
Before you complete the steps in this guide, make sure you have all of the following:
- Familiarity with PHP (and ideally some Laravel).
- An AWS account. If you haven’t used AWS before, you can set up an account here.
- The AWS CLI installed and configured on your computer.
- PHP, Composer,
git, and the EB CLI installed on your computer.
Create a Laravel application for Elastic Beanstalk
To start, we create a Laravel skeleton app like so:
$ composer create-project laravel/laravel --prefer-dist laravel_memcache
Installing laravel/laravel (v5.7.15)
- Installing laravel/laravel (v5.7.15): Loading from cache
Created project in laravel_memcache
...
$ cd laravel_memcacheIf you want, you can run the app with php artisan serve and visit it at http://127.0.0.1:8000/. You will see the Laravel skeleton page.
Initialize an Elastic Beanstalk application
Now we need to associate the Laravel skeleton with an Elastic Beanstalk app. Create and configure your EB app with the following steps:
Create an
.ebextensionsfolder and add a01-environment.configfile:Include the required environment variables and PHP config in
.ebextensions/01-environment.config:option_settings: aws:elasticbeanstalk:container:php:phpini: document_root: /public composer_options: --no-dev aws:elasticbeanstalk:application:environment: APP_ENV: production APP_KEY: base64:c61+caO9Dh45ZNcZZ2i//54VgSngKuw1tKxD5sFXjak=You can create your own
APP_KEYwithphp artisan key:generate --show.Create an Elastic Beanstalk repo:
This will set up a new application called
laravel-memcache. Then create an environment to run our application in:Notice that we’re adding a MySQL database to our EB environment. You’ll be prompted for a username and password for the database. You can set them to whatever you like.
Be careful when choosing your password. AWS does not handle symbols very well (! $ @ etc.), and can cause some unexpected behavior. Stick to letters and numbers, and make sure it’s at least eight characters long.
This will create a AWS Relational Database Service (RDS) instance that is associated with this application. When you terminate this application, the database instance will be destroyed as well. If you need a RDS instance that is independent of your Elastic Beanstalk application, create one via the AWS RDS interface.
This configuration process will take about five to fifteen minutes. Go refill your coffee, stretch your legs, and come back later.
Deploy the Laravel skeleton to Elastic Beanstalk
Deploying our skeleton Laravel app can be done with two simple steps:
Initialize a Git repository to commit the skeleton. Note that the Laravel skeleton already comes with a
.gitignorefile.Deploy the Laravel application on EB is easily done by running the
deploycommand:You can now open the application and see if it’s working:
We now have an EB environment with a running Laravel application. The application does not do much though, so let’s implement some task list functionality.
Add task list functionality
Let’s add a task list to the app that enables users to view, add, and delete tasks. To accomplish this, we need to:
- Set up the database
- Create a
Taskmodel - Create the view and controller logic
Set up the MySQL database
Since our EB environment already has a MySQL database initialized, we’ll need to add a way to connect to it through our app.
While you may want to use PostgreSQL, there is currently a bug in the eb cli that prevents you from creating a PostgreSQL instance in us-east-1 . We currently have an open ticket, and if anything changes, we’ll update this post.
To use our database, configure it like so in config/database.php:
<?php
return [
// ...
'default' => env('DB_CONNECTION', 'mysql'),
'connections' => [
// ...
'mysql' => [
'driver' => 'mysql',
'host' => env('RDS_HOSTNAME', '127.0.0.1'),
'port' => env('RDS_PORT', '3306'),
'database' => env('RDS_DB_NAME', ''),
'username' => env('RDS_USERNAME', ''),
'password' => env('RDS_PASSWORD', ''),
'unix_socket' => env('DB_SOCKET', ''),
'charset' => 'utf8mb4',
'collation' => 'utf8mb4_unicode_ci',
'prefix' => '',
'prefix_indexes' => true,
'strict' => true,
'engine' => null,
],
// ...
],
// ...
];:The database config is based on the RDS_* environment variables which are set by Beanstalk automatically whenever a database instance is created.
Local setup
If you want to test your app locally (optional), we recommend using SQLite. To do so, make sure you have php-sqlite installed and configure the SQLite connection in config/database.php:
'sqlite' => [
'driver' => 'sqlite',
'database' => database_path('database.sqlite'),
'prefix' => '',
'foreign_key_constraints' => env('DB_FOREIGN_KEYS', true),
],To use this connection locally, set DB_CONNECTION=sqlite in your app’s .env file.
Issue with MySQL <5.7
At the point of this writing, when you create an Elastic Beanstalk environment with a MySQL database as we did above, it will come with MySQL 5.6. Unfortunately, this version does not work with Laravel out of the box and the running the migrations will result in the following error:
SQLSTATE[42000]: Syntax error or access violation: 1071 Specified key was too
long; max key length is 767 bytes (SQL: alter table `users` add unique
`users_email_unique`(`email`))
To get around this you can either update your MySQL instance or add the following to app/Providers/AppServiceProvider.php:
// ...
use Illuminate\Support\Facades\Schema;
//...
public function boot()
{
Schema::defaultStringLength(191);
}
//...The database is now ready to use. Save the changes with:
Create the Task model
Now we have an empty database. To create and store tasks, we need to do three things:
Create a migration that will create the
taskstable:Tasks should have names, so let’s add
nameto thetaskstable in the newly createddatabase/migrations/<date>_crate_tasks_table.phpfile:Create a
Taskmodel to easily access thetaskstable from our code:This creates an empty
Taskmodel inapp/Task.php. Laravel automatically infers its fields from the migration.Apply the migration to your database:
In order for EB to run the migrations upon deployment, we’ll have to include another config file telling it to do so.
Inside
.ebextensions/02-deploy.configinclude:If you set up SQLite locally, go ahead and create the database and run the migrations (optional):
Finally, commit your changes:
Add a view for the task list
To view the tasks stored in the database, we create a view that lists all tasks. We start with a boilerplate layout:
<!-- resources/views/layouts/app.blade.php -->
<!DOCTYPE html>
<html lang="en">
<head>
<title>MemCachier Laravel Tutorial</title>
<!-- Fonts -->
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/4.4.0/css/font-awesome.min.css"
rel='stylesheet' type='text/css'>
<!-- CSS -->
<link href="{{ elixir('css/app.css') }}" rel="stylesheet">
</head>
<body>
<div class="container">
<nav class="navbar navbar-default">
<!-- Navbar Contents -->
</nav>
</div>
@yield('content')
<!-- JavaScripts -->
<script src="{{ elixir('js/app.js') }}"></script>
</body>
</html>We can now create the task list view as a child view of the above layout:
<!-- resources/views/tasks.blade.php -->
@extends('layouts.app')
@section('content')
<div class="container">
<!-- TODO: New Task Card -->
<!-- Current Tasks -->
@if (count($tasks) > 0)
<div class="card">
<div class="card-body">
<h5 class="card-title">Current Tasks</h5>
<table class="table table-striped">
@foreach ($tasks as $task)
<tr>
<td class="table-text">
<div>{{ $task->name }}</div>
</td>
<td>
<!-- TODO Delete Button -->
</td>
</tr>
@endforeach
</table>
</div>
</div>
@endif
<!-- TODO: Memcache Stats Card -->
</div>
@endsectionIgnore the TODOs for now (we’ll fill them out later). To be able to access this view, connect it to the top route in routes/web.php:
<?php
use App\Task;
// Show Tasks
Route::get('/', function () {
$tasks = Task::orderBy('created_at', 'asc')->get();
return view('tasks', [
'tasks' => $tasks
]);
});If you have a local setup, you can now start a web server with php artisan serve and visit the view at localhost:8000. However, there isn’t much to look at yet because the task list is empty.
Enable task creation
In order for the task list to be more useful, users need to be able to add tasks. Let’s create a card for that:
<!-- resources/views/tasks.blade.php -->
<!-- ... -->
<!-- New Task Card -->
<div class="card">
<div class="card-body">
<h5 class="card-title">New Task</h5>
<!-- Display Validation Errors -->
@include('common.errors')
<!-- New Task Form -->
<form action="{{ url('task') }}" method="POST">
{{ csrf_field() }}
<!-- Task Name -->
<div class="form-group">
<input type="text" name="name" id="task-name" class="form-control"
placeholder="Task Name">
</div>
<!-- Add Task Button -->
<div class="form-group">
<button type="submit" class="btn btn-default">
<i class="fa fa-plus"></i> Add Task
</button>
</div>
</form>
</div>
</div>
<!-- Current Tasks -->
<!-- ... -->Because the task name is provided by the user, we need to make sure the input is valid. In this case, the name must exist, and it must not exceed 255 characters. If the input fails to validate according to these rules, we want to display the following error view:
<!-- resources/views/common/errors.blade.php -->
@if (count($errors) > 0)
<div class="alert alert-danger">
<strong>Whoops! Something went wrong!</strong>
<br><br>
<ul>
@foreach ($errors->all() as $error)
<li>{{ $error }}</li>
@endforeach
</ul>
</div>
@endifLet’s add these new views to routes/web.php:
// ...
use Illuminate\Http\Request;
// Show Tasks
// ...
// Add New Task
Route::post('/task', function (Request $request) {
// Validate input
$validator = Validator::make($request->all(), [
'name' => 'required|max:255',
]);
if ($validator->fails()) {
return redirect('/')
->withInput()
->withErrors($validator);
}
// Create task
$task = new Task;
$task->name = $request->name;
$task->save();
return redirect('/');
});Starting a local web server with php artisan serve and visiting localhost:8000 is now a bit more interesting because you can add tasks.
Enable task deletion
To complete our task list, we also need to be able to remove completed tasks. To delete a task, we add a Delete button to each item in the task list:
<!-- resources/views/tasks.blade.php -->
<!-- ... -->
<!-- Delete Button -->
<form action="{{ url('task/'.$task->id) }}" method="POST">
{{ csrf_field() }}
{{ method_field('DELETE') }}
<button type="submit" class="btn btn-danger">
<i class="fa fa-trash"></i> Delete
</button>
</form>
<!-- ... -->Then we wire this functionality to the appropriate route in routes/web.php:
// ...
// Show Tasks & Add New Task
// ...
// Delete Task
Route::delete('/task/{task}', function (Task $task) {
$task->delete();
return redirect('/');
});Now we can push the changes to Elastic Beanstalk and see the result:
Test the application by adding a few tasks. We now have a functioning task list running on Elasitc Beanstalk. With this complete, we can learn how to improve its performance with Memcache.
If you get a 500 error when you open the application, check the logs. They’re located in the EB console in the side menu labeled Logs. Check your ENV variables to make sure they’re set correctly.
Add caching to Laravel
Memcache is an in-memory, distributed cache. Its primary API consists of two operations: SET(key, value) and GET(key). Memcache is like a hashmap (or dictionary) that is spread across multiple servers, where operations are still performed in constant time.
The most common use for Memcache is to cache expensive database queries and HTML renders so that these expensive operations don’t need to happen over and over again.
Set up Memcache
To use Memcache in Laravel, you first need to provision an actual Memcache cache. You can easily get one for free from MemCachier. MemCachier provides a fast and flexible Memcache compatible with the protocol used by the popular memcached software. So head over to https://www.memcachier.com, sign up for an account, and create a free development cache.
There are three configuration variables you’ll need for your application to be able to connect to your cache: MEMCACHIER_SERVERS, MEMCACHIER_USERNAME, and MEMCACHIER_PASSWORD. You can find these on your cache’s analytics dashboard. You’ll need to add these variables to EB.
We can confirm that they’ve been set by running:
You should see your MemCachier env variables, as well as all the previous env variables we’ve set.
Configure your Memcache
To use Memcache locally you will need to install some dependencies:
- Install the
php-memcachedPECL extention via your OS package manager. - Uncomment
;extension=memcached.soin/etc/php/conf.d/memcached.ini. - Run
php -mto make sure thememcachedmodule is loaded.
On EB, this dependency is already installed and configured.
To set up Memcache in Laravel, we add the following dependency to composer.json:
We then configure the cache in config/cache.php:
//..
'default' => env('CACHE_DRIVER', 'memcached'),
// ...
'memcached' => [
'driver' => 'memcached',
'persistent_id' => 'memcached_pool_id',
'sasl' => [
env('MEMCACHIER_USERNAME'),
env('MEMCACHIER_PASSWORD'),
],
'options' => [
// some nicer default options
// - nicer TCP options
Memcached::OPT_TCP_NODELAY => TRUE,
Memcached::OPT_NO_BLOCK => FALSE,
// - timeouts
Memcached::OPT_CONNECT_TIMEOUT => 2000, // ms
Memcached::OPT_POLL_TIMEOUT => 2000, // ms
Memcached::OPT_RECV_TIMEOUT => 750 * 1000, // us
Memcached::OPT_SEND_TIMEOUT => 750 * 1000, // us
// - better failover
Memcached::OPT_DISTRIBUTION => Memcached::DISTRIBUTION_CONSISTENT,
Memcached::OPT_LIBKETAMA_COMPATIBLE => TRUE,
Memcached::OPT_RETRY_TIMEOUT => 2,
Memcached::OPT_SERVER_FAILURE_LIMIT => 1,
Memcached::OPT_AUTO_EJECT_HOSTS => TRUE,
],
'servers' => array_map(function($s) {
$parts = explode(":", $s);
return [
'host' => $parts[0],
'port' => $parts[1],
'weight' => 100,
];
}, explode(",", env('MEMCACHIER_SERVERS', 'localhost:11211')))
],This configures Laravel’s caching engine with MemCachier, which allows you to use your Memcache in a few different ways:
- Directly access the cache via
get,set,delete, and so on - Cache results of functions with the
rememberForeverfunction - Use Memcache for session storage
- Cache rendered partials
- Cache entire responses
Cache expensive database queries
Memcache is often used to cache the results of expensive database queries. Of course, our simple task list does not have any expensive queries, but let’s assume for this tutorial that fetching all of the tasks from the database is a slow operation.
The rememberForever function makes it easy to add caching to Laravel. You provide two arguments to it:
- A cache key
- A function that queries your database and returns results
The rememberForever function looks up the key in your cache. If the key is present, its corresponding value is returned. Otherwise, the database function you provided is called. Whatever that function returns is then stored in the cache with the corresponding key for future lookups.
This means that the first time you call rememberForever, the expensive database function is called, but every successive call to rememberForever obtains the value from the cache.
Use the rememberForever function to easily add caching to the task view controller in routes/web.php:
// Show Tasks
Route::get('/', function () {
$tasks = Cache::rememberForever('all_tasks', function () {
return Task::orderBy('created_at', 'asc')->get();
});
return view('tasks', [
'tasks' => $tasks
]);
});As you might have noticed, we now have a problem if we add or remove a task. Because rememberForever fetches the task list from the cache, any changes to the database won’t be reflected in the task list. For this reason, whenever we change the tasks in the database, we need to invalidate the cache:
// Add New Task
Route::post('/task', function (Request $request) {
// ...
$task->save();
Cache::forget('all_tasks');
return redirect('/');
});
// Delete Task
Route::delete('/task/{task}', function (Task $task) {
$task->delete();
Cache::forget('all_tasks');
return redirect('/');
});View Memcache statistics
To help demystify Memcache caching operations, we can visualize what’s going on under the hood.
First, we obtain stats every time the task list is requested in routes/web.php:
Route::get('/', function () {
// ...
$stats = Cache::getMemcached()->getStats();
return view('tasks', [
'tasks' => $tasks,
'stats' => array_pop($stats)
]);
});Then, we add a card for the stats at the bottom of the task view:
<!-- resources/views/tasks.blade.php -->
<!-- ... -->
<!-- Stats Card -->
<div class="card">
<div class="card-body">
<h5 class="card-title">Stats</h5>
<table class="table table-striped">
<tr>
<td>Set commands</td>
<td>{{ $stats['cmd_set'] }}</td>
</tr>
<tr>
<td>Get hits</td>
<td>{{ $stats['get_hits'] }}</td>
</tr>
<tr>
<td>Get misses</td>
<td>{{ $stats['get_misses'] }}</td>
</tr>
</table>
</div>
</div>Now push the changes to EB and see the how the stats change when you play with the task list:
You can see that the first time you access the page, the Get misses increase by one. This is because the first time rememberForever is called, the task list is not in the cache. The Set commands also increase because the task list is saved to the cache. If you refresh the page, the misses stay the same, but the Get hits increase because the task list is served from the cache.
When you add a new task or delete a task, your misses will increase again because the cache was invalidated.
You can also see the same stats and more on the analytics dashboard of your cache from within your MemCachier account.
Caching rendered partials
With the help of laravel-partialcache, you can cache rendered partials in Laravel. This is similar to fragment caching in Ruby on Rails or snippet caching in Flask. If you have complex partials in your application, it’s a good idea to cache them because rendering HTML can be a CPU-intensive task.
Do not cache partials that include forms with CSRF tokens.
Our example does not include any complex partials, but for the sake of this tutorial, let’s assume that rendering the task name in the task list takes a lot of CPU cycles and slows down our page.
First, we need to add the laravel-partialcache package to our app:
Second, let’s factor out the task name into a partial:
<!-- resources/views/task/name.blade.php -->
<td class="table-text">
<div>{{ $task->name }}</div>
</td>Now we can import and cache this partial in our task view:
<!-- resources/views/tasks.blade.php -->
<!-- ... -->
<table class="table table-striped">
@foreach ($tasks as $task)
<tr>
<!-- Task Name -->
@cache('task.name', ['task' => $task], null, $task->id)
<!-- Delete Button -->
<!-- ... -->This caches each task name partial with the ID as its key. Note that in this example, we never have to invalidate a cached partial because the name of a task can never change. However, if you add the functionality to change the name of a task, you can easily invalidate the cached partial with PartialCache::forget('task.name', $task->id);.
Let’s see the effect of caching the task name partials in our application:
You should now see an additional Get hit for each task in your list.
Using Memcache for session storage
Memcache works well for storing information for short-lived sessions that time out. However, because Memcache is a cache and therefore not persistent, long-lived sessions are better suited to permanent storage options, such as your database.
Changing the session store from a file (default) to memcached can be done easily by setting the SESSION_DRIVER config var:
Caching entire reponses
In Laravel, it’s also easy to cache the entire rendered HTML response by using laravel-responsecache. This is similar to view caching in Ruby on Rails. This package is easy to use and has good documentation in its README. However, we cannot use it in our example because our task list contains forms with CSRF tokens. To use this package with Memcache, you have to set the config var RESPONSE_CACHE_DRIVER to memcached.
Clean up
Once you’re done with this tutorial and don’t want to use it anymore, you can clean up your EB instance by using:
This will clean up all of the AWS resources.
Sascha