Serverless architectures are fast, cheap, and secure. Detaching the backend of a site opens the door to connect any CMS or data source that supports GraphQL. This tutorial looks at connecting and securing a detached Drupal instance as a data source for a Gridsome build. To save money for this cloud build the backend server will be shut down when not in use using some creative Lambda hooks
The Serverless Architecture for this tutorial:
The primary GraphQL project for Drupal has two versions. The newer version addresses some security issues by forcing you to design your schema. Version 3 will work for this tutorial since this is a fully detached website that will only call the Drupal backend at build, and has its GraphQL endpoints locked down we can use the old version. Add the older version to your Drupal project with composer.
composer require drupal/graphql:"^3.0"
To secure the GraphQL endpoint add Basic Authentication and configure the CORS
settings in Drupal's services.yml
file.
Because this is a static website the only access to the GraphQL endpoint should be from Gridsome at build time, so that endpoint should be locked down with Basic Authentication to password protect access to it.
Restrict access in Apache using the
<LocationMatch>
tag in the Virtual Host configuration, or if you're using Nginx use the
location
statement. It's very important to match the endpoint with a case insensitive
check to protect not only /graphql but variations like /GraphQL. LocationMatch
is case sensitive by default but you can use a regular expression to match any
casing by prepending (?i)
. Also change the default endpoint in
the module settings.
<VirtualHost *:80>
<LocationMatch "(?i)/graphql">
AuthUserFile /var/www/html/.htpasswd
AuthName "Authorization Required"
AuthType Basic
require valid-user
</LocationMatch>
</VirtualHost>
Drupal comes with a file for configuring web services connections. By default
it's inactive, to use it copy the template
from sites/default.services.yml
to sites/services.yml
.
Once the file is copied and active we can update the CORS settings to allow
VueJS/React to connect during build.
In the services file there's a section for CORS configuration, be default it should look like this:
cors.config:
enabled: true
# ...
allowedMethods: ['*']
# ...
allowedOrigins: ['*']
# ...
Make two changes, limit the allowed HTTP methods to POST, GET and OPTIONS and update the allowed origins.
cors.config:
enabled: true
# ...
allowedMethods: ['GET', 'POST', 'OPTIONS']
# ...
allowedOrigins: ['http://localhost:8080', 'https://production.com']
# ...
Add the GraphQL source module to Gridsome.
npm install @gridsome/source-graphql
Once GraphQL is installed configure it in the `gridsome.config.js` file.
module.exports = {
plugins: [
use: '@gridsome/source-graphql',
options: {
url: 'https://drupalbackend.url/graphqlEndpoint,
fieldName: 'drupal',
typeName: 'drupalType',
},
headers: {
Authorization: `Bearer ${process.env.AUTH_TOKEN}`,
},
]
}
A couple of important things are happening in these configuration settings.
.env
file. The AUTH_TOKEN is the Basic Authentication password that was picked earlier in Base64. This header needs to be set for Gridsome to pull data down.fieldName
is being set. When data is pulled down from the backend Gridsome will bring it in under this name so it doesn't conflict with local data.typeName
is being set. This will impact the GraphQL queries, it's added as a prefix to every data type. For example, a Drupal Article Node would be drupalType_NodeArticle
.When a project's backend is completely detached and only required at build it means there is an opportunity to shut the server down to save money. Since AWS on-demand EC2 instances bill by the hour any time the server is off is going to be much cheaper than powered on, you're only paying for the data storage of the hard drive. When content updates/entry is sporadic this can save a ton.
Generally, I like to shut the server down on a schedule. For example, nightly at 8pm. That way if the server is spun up in the day and left on it will power down over night. From a security perspective powering the server off when not in use also provides some marginal additional protection from automated attacks.
If the server is going to be turned off nightly then there needs to be a way to turn it on for content entry. Creating a simple web endpoint with Lambda will provide a URL that will turn the server on, this can be bookmarked and easily shared with the content entry team.
When creating an IAM user it's important to limit access to only what's needed. Since this script only needs to turn the backend server on, that's all it's going to have permission to do. IAM let's you create the permissions with a visual editor or by adding the JSON rule set directly. The only piece of information you need to make the rule is the EC2 instance ID. You can find this listed in EC2 Details tab when you select your instance in the EC2 Dashboard.
To start, create a new IAM role.
On the creation page keep "AWS service" as the Trusted entity type and select "Lambda" under "Common use cases". Hit Next and select the "Create Policy" option upper right.
Clicking the JSON tab should show permssions looking something like this..
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "PowerServerOn",
"Effect": "Allow",
"Action": "ec2:StartInstances",
"Resource": "arn:aws:ec2:*:yourinstance:id/*"
}
]
}
Big difference will be the correct resource ARN. Finish creating and naming the role.
A serverless Lambda function is the cheapest way to add a webhook. The AWS free tier covers millions of innvocations of a function so using it for something meant to be executed once a day is going to be essentially free.
Create a Lambda function for Node and attach the new role with StartInstances
permissions.
const AWS = require('aws-sdk');
exports.handler = async (event, context) => {
var ec2 = new AWS.EC2({ apiVersion: '2016-11-15' })
var params = {
InstanceIds: [
process.env.INSTANCE_ID, // Set in the Lambda function configuration.
]
};
const response = await new Promise((resolve, reject) => {
ec2.startInstances(params, function(err, data) {
if (err) console.log(err, err.stack);
else {
resolve({
statusCode: 200,
headers: {
"Content-type": "text/html"
},
body: waitingPage
});
}
});
});
return response;
};
This script will turn an EC2 instance on if it has permission to. Add an environment variable in the lambda configuration settings. Lambda passes these in under the `process.env` variable. There's no need to install `dotenv`.
Add a single variable INSTANCE_ID with the id of the EC2 instance to restart. Under Actions (top left) publish a new version. Add an HTML endpoint as a trigger and it's ready to go.