This article addresses some of the common challenges you're likely to encounter when configuring and deploying a Laravel application on Google App Engine.
What is Google App Engine?
Google App Engine (GAE) is a fully managed platform for running web and mobile apps in the cloud. Your GAE app runs in Docker containers and benefits from features such as automatic scaling, load balancing, health-checking and monitoring. In short, GAE lets you offload many of your infrastructure concerns.
You don't need to be familiar with Docker in order to use GAE. Google Cloud will build the images for you and orchestrate the necessary infrastructure according to a configuration file you provide. And if you ever decide that the default setup is lacking you are free to customize the runtime by supplying your own Docker image.
To know whether App Engine is suitable for your project check out the official website.
You should also note that GAE apps run in one of two environments: "standard" and "flexible". This article assumes you've chosen the flexible environment which is, as the name implies, more flexible. It's also a more expensive option. Please refer to the official documentation to learn about pricing, capabilities and limitations.
Prerequisites
This article is not a replacement for the official App Engine documentation. If you plan on deploying a production app to Google App Engine you should familiarize yourself with the platform by reading the documentation.
Make sure you've done the following before you continue:
- Create a Google Cloud account.
- Create a Google App Engine project.
- Install the Cloud SDK on your local
machine. This gives you a command-line interface (
gcloud
) to the Google Cloud Platform which is used to deploy and manage your application. - Create a Laravel project locally.
Configuring Your Laravel App
Your GAE application is configured using a file named app.yaml
. This file
serves as the basis for every deployment and should be placed in your project's root
directory. While there are many available configuration options
which you should eventually explore, the app.yaml
for a Laravel project can be
as simple as this:
runtime: php
env: flex
runtime_config:
document_root: public
# Skip ".env", which is for local development
skip_files:
- .env
env_variables:
APP_ENV: 'production'
APP_KEY: 'abc123'
APP_LOG: errorlog
APP_LOG_LEVEL: debug
# ... more environment variables specific to your project
The env_variables
section should contain all your environment variables,
i.e. the variables you usually put in the .env
file. Remember to respect the
YAML format when copying variables from .env.
to app.yaml
: The syntax is
<key>: <val>
.
GAE will make sure all your Composer dependencies are installed (by running
composer install
) before your application is uploaded to the cloud. However,
there are certain custom commands we want to execute immediately after our
dependencies are installed. We can make use of Composer's
post-install-cmd
event which is triggered after composer install
by placing
the following in our composer.json
file:
"scripts": {
// ... existing scripts are removed for brevity ...
"post-install-cmd": [
"chmod -R 755 bootstrap\/cache",
"php artisan config:cache",
"php artisan route:cache"
]
}
The first command makes our cache directory writable. The second creates a cache file for faster configuration loading, while the third creates a route cache for faster route registration. More information about these commands can be found in the Laravel documentation or in this article.
The commands will run each time a new release is deployed.
Connecting to Google Cloud SQL
Google Cloud SQL is a managed database service which supports MySQL, PostgreSQL and SQL Server. If you don't want to install, configure and maintain your own database server (or cluster), Cloud SQL is for you.
To set up a MySQL instance, see the official documentation. Remember to enable the Cloud SQL API as described in the article. Otherwise, connections will fail.
When the instance is up and running, add the database variables to your
appl.yaml
. You also need a new section (beta_settings
) as shown below:
env_variables:
# ... other env variables omitted for brevity ...
DB_CONNECTION: mysql
DB_SOCKET: '/cloudsql/<connectionName>'
DB_PORT: 3306
DB_DATABASE: '<databaseName>'
DB_USERNAME: '<username>'
DB_PASSWORD: '<password>'
# New section:
beta_settings:
cloud_sql_instances: "<connectionName>"
<connectionName>
should consist of three values separated by a colon:
<projectId>:<region>:<instanceName>
.
<projectId>
is the ID of your Google Cloud project. <instanceName>
is the
name of your MySQL instance. Example: /cloudsql/funkyproject:europe-west1:mysql-instance1
.
You can also retrieve the whole string by running the following command:
gcloud sql instances describe <instanceName>
Look for the connectionName
value.
Deployment
Deploying your app is as easy as running the following command:
gcloud app deploy
Behind the scenes GAE will build a Docker image containing your code, install
the dependencies (which will trigger the post-install-cmnd
commands) and
upload the image to the Google App Engine servers. When the container (your app)
starts to run, all traffic is directed to this specific release.
Queues and Workers
Laravel offers a unified API across a variety of different queue backends that allows you to defer time consuming tasks such as sending emails, talking to external APIs and generating reports.
While there are specialized tools that are better suited for queueing jobs (e.g. Beanstalk or Redis), the simplest way to get started is by storing jobs in the database you're already using -- MySQL or PostgreSQL. Using the database driver saves us from having to spin up another virtual machine or launching another App Engine service.
The Laravel documentation will walk you
through the process of creating, dispatching and deferring jobs. In the end we
want to run our worker by executing the artisan queue:work
command.
Luckily Google App Engines lets us provide a Supervisor configuration for our
app. Supervisor is a process monitor that will make sure our artisan queue:work
process never stops running.
Create the file additional-supervisord.conf
:
[program:queue-worker]
process_name=%(program_name)s_%(process_num)02d
command = php %(ENV_APP_DIR)s/artisan queue:work --sleep=3 --tries=3
stdout_logfile = /dev/stdout
stdout_logfile_maxbytes=0
stderr_logfile = /dev/stderr
stderr_logfile_maxbytes=0
user = www-data
autostart = true
autorestart = true
priority = 5
stopwaitsecs = 20
Then (re)deploy your app with gcloud app deploy
and Google App Engine will
make sure Supervisor monitors your worker.
(Note: Running multiple service instances will lead to multiple workers running simultaneously. If your app generates a lot of traffick you should consider using another, more specialized, queue driver.)
Logging and Exception Handling With Stackdriver
Google Stackdriver is a monitoring service for cloud-powered applications. You can use it to log and collect metrics from any part of your infrastructure including Google App Engine. By integrating your Laravel app with Stackdriver you'll be able to view your application logs in the Cloud Console and receive notifications (email and push!) when errors are detected.
In order to log messages to Google Stackdriver you need to install the log client for Google Cloud:
composer require google/cloud-logging google/cloud-error-reporting
Set enable_stackdriver_integration
to true
in the runtime_config
section
of your app.yaml
file:
runtime: php
env: flex
# Insert this:
runtime_config:
enable_stackdriver_integration: true
# ... environment variables ...
Laravel is able to write logs to different channels. While several channels
are provided out of the box we need to create our own custom channel that sends
log entries to Stackdriver. Configure a new channel by adding a new entry to
the channels
array in config/logging.php
:
'channels' => [
'stackdriver' => [
'driver' => 'custom',
'via' => App\Logging\CreateStackdriverLogger::class,
],
],
The name of our channel is "stackdriver" and the via
option points to a
factory class which will be invoked to create a Monolog instance. Monolog is the
underlying logging library used by Laravel, and we need to instantiate it in a
specific way in order to make it write to Stackdriver.
Create the custom Monolog factory in a new file namedCreateStackdriverLogger.php
and place it in app/Logging
:
<?php
namespace App\Logging;
use Monolog\Logger;
use Google\Cloud\Logging\LoggingClient;
use Monolog\Handler\PsrHandler;
class CreateStackdriverLogger
{
public function __invoke(array $config)
{
$logger = LoggingClient::psrBatchLogger('app');
$handler = new PsrHandler($logger);
return new Logger('stackdriver', [$handler]);
}
}
To make sure our new channel is used in production add the
LOG_CHANNEL
key to the env_variables
section of app.yaml
:
env_variables:
LOG_CHANNEL: 'stackdriver'
Laravel will use this environment variable to determine which log channel to
use. If you haven't modified it, config/logging.php
should should have the
following line by default:
'default' => env('LOG_CHANNEL', 'stack'),
That takes care of logging.
Exceptions should also be reported to Stackdriver. In app/Exceptions/Handler.php
add a conditional to check whether the app is running on GAE. If that's the case
we want the Google Cloud library handle exceptions:
// Import this at the top of the file
use Google\Cloud\ErrorReporting\Bootstrap;
// ... code omitted ...
// Change the `report` function
public function report(Exception $exception)
{
if (isset($_SERVER['GAE_SERVICE'])) {
if ($this->shouldReport($exception)) {
Bootstrap::exceptionHandler($exception);
}
} else {
// Standard behavior
parent::report($exception);
}
}
// ...
The standard Laravel exception handler lets you exclude certain exceptions from
being reported by adding them to the $dontReport
array. This is useful if you
don't want to litter your logs with 404 errors and other uninteresting events.
As you can see in the code snippet above, we retain this behavior by checking
shouldReport()
before passing the exception to Google's exception handler.
Stackdriver provides many features and configuration options that can help you monitor your app's health. You should check out the Cloud Console to see if there's any particular feature or option you want to explore further.
Retrieving the Client's IP Address
GAE doesn't populate the variable $_SERVER['REMOTE_ADDR]
with the IP address
of the client that sent the request. This means that the convenient helper
method $request->getClientIp()
won't return anything. However, you can find
the client's IP in the X-Forwarded-For
header which App Engine populates with
a comma-delimited list of proxy IP addresses through which the request has been
routed. The documentation states:
The first IP in this list is generally the IP of the client that created the request. The subsequent IPs provide information about proxy servers that also handled the request before it reached the application server.
With this in mind we can create a middleware that populates REMOTE_ADDR
with
the client's IP address by extracting the first item in X-Forwarded-For
:
<?php
namespace App\Http\Middleware;
use Closure;
class GaeProxyIp
{
/**
* Handle an incoming request.
*
* @param \Illuminate\Http\Request $request
* @param \Closure $next
* @return mixed
*/
public function handle($request, Closure $next)
{
// Google App Engine includes the client's IP as the first item in
// X-Forwarded-For, but nowhere else; REMOTE_ADDR is empty.
if (isset($_SERVER['GAE_SERVICE'])) {
$forwardedFor = array_map('trim', explode(',', $request->header('X-Forwarded-For')));
$request->server->set('REMOTE_ADDR', $_SERVER['REMOTE_ADDR'] = $forwardedFor[0]);
}
return $next($request);
}
}
Remember to add this middleware in the middleware
array in
app/Http/Kernel.php
for it to run on every request.
You can now call $request->getClientIp()
to retrieve your visitor's IP address.
Redirecting HTTP to HTTPS
App Engine can automatically renew SSL/TLS certificates for you. Certificate management is configured in the Cloud Console.
There's no dedicated configuration option in App Engine to redirect HTTP traffic to HTTPS. This must be done at the application level, either using custom NGINX configuration files or a middleware in your Laravel application.
Here's an example middleware:
<?php
namespace App\Http\Middleware;
use Closure;
class GaeRedirectToHttps
{
/**
* Handle an incoming request.
*
* @param \Illuminate\Http\Request $request
* @param \Closure $next
* @return mixed
*/
public function handle($request, Closure $next)
{
if (isset($_SERVER['GAE_SERVICE']) && !$request->secure()) {
return redirect()->secure($request->getRequestUri());
}
return $next($request);
}
}
You should also consider HTTPS Strict Transport Security (HSTS). This is a web policy that helps to protect against downgrade attacks by telling clients that the website should only be accessed through secure HTTPS connections. Websites communicate this policy by sending a HSTS response header:
Strict-Transport-Security: max-age=31536000
The header instructs conforming clients that subsequent communication with the website should only happen over HTTPS. Also, the header should only be sent over secure connections. You should read up on HSTS if you're not familiar with the policy.
Here's a middleware that sets the relevant header on every (secure) response:
<?php
namespace App\Http\Middleware;
use Closure;
class GaeSetHstsHeader
{
/**
* Handle an incoming request.
*
* @param \Illuminate\Http\Request $request
* @param \Closure $next
* @return mixed
*/
public function handle($request, Closure $next)
{
$response = $next($request);
if (isset($_SERVER['GAE_SERVICE']) && $request->secure()) {
$response->header('Strict-Transport-Security', 'max-age=31536000; includeSubDomains');
}
return $response;
}
}
Where to go from here
Depending on your app there are certain additional steps you may want to take:
- Uploading files to Google Cloud Storage
- Running scheduled jobs
- Connecting to Cloud SQL locally