You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
485 lines
13 KiB
485 lines
13 KiB
3 years ago
|
<h1 align="center">Serverless</h1>
|
||
|
|
||
|
Run serverless applications and REST APIs using your existing Fastify
|
||
|
application. By default, Fastify will not work on your serverless platform of
|
||
|
choice, you will need to make some small changes to fix this. This document
|
||
|
contains a small guide for the most popular serverless providers and how to use
|
||
|
Fastify with them.
|
||
|
|
||
|
#### Should you use Fastify in a serverless platform?
|
||
|
|
||
|
That is up to you! Keep in mind that functions as a service should always use
|
||
|
small and focused functions, but you can also run an entire web application with
|
||
|
them. It is important to remember that the bigger the application the slower the
|
||
|
initial boot will be. The best way to run Fastify applications in serverless
|
||
|
environments is to use platforms like Google Cloud Run, AWS Fargate, and Azure
|
||
|
Container Instances, where the server can handle multiple requests at the same
|
||
|
time and make full use of Fastify's features.
|
||
|
|
||
|
One of the best features of using Fastify in serverless applications is the ease
|
||
|
of development. In your local environment, you will always run the Fastify
|
||
|
application directly without the need for any additional tools, while the same
|
||
|
code will be executed in your serverless platform of choice with an additional
|
||
|
snippet of code.
|
||
|
|
||
|
### Contents
|
||
|
|
||
|
- [AWS Lambda](#aws-lambda)
|
||
|
- [Google Cloud Functions](#google-cloud-functions)
|
||
|
- [Google Cloud Run](#google-cloud-run)
|
||
|
- [Netlify Lambda](#netlify-lambda)
|
||
|
- [Vercel](#vercel)
|
||
|
|
||
|
## AWS Lambda
|
||
|
|
||
|
The sample provided allows you to easily build serverless web
|
||
|
applications/services and RESTful APIs using Fastify on top of AWS Lambda and
|
||
|
Amazon API Gateway.
|
||
|
|
||
|
*Note: Using [aws-lambda-fastify](https://github.com/fastify/aws-lambda-fastify)
|
||
|
is just one possible way.*
|
||
|
|
||
|
### app.js
|
||
|
|
||
|
```js
|
||
|
const fastify = require('fastify');
|
||
|
|
||
|
function init() {
|
||
|
const app = fastify();
|
||
|
app.get('/', (request, reply) => reply.send({ hello: 'world' }));
|
||
|
return app;
|
||
|
}
|
||
|
|
||
|
if (require.main === module) {
|
||
|
// called directly i.e. "node app"
|
||
|
init().listen(3000, (err) => {
|
||
|
if (err) console.error(err);
|
||
|
console.log('server listening on 3000');
|
||
|
});
|
||
|
} else {
|
||
|
// required as a module => executed on aws lambda
|
||
|
module.exports = init;
|
||
|
}
|
||
|
```
|
||
|
|
||
|
When executed in your lambda function we do not need to listen to a specific
|
||
|
port, so we just export the wrapper function `init` in this case. The
|
||
|
[`lambda.js`](#lambdajs) file
|
||
|
will use this export.
|
||
|
|
||
|
When you execute your Fastify application like always, i.e. `node app.js` *(the
|
||
|
detection for this could be `require.main === module`)*, you can normally listen
|
||
|
to your port, so you can still run your Fastify function locally.
|
||
|
|
||
|
### lambda.js
|
||
|
|
||
|
```js
|
||
|
const awsLambdaFastify = require('aws-lambda-fastify')
|
||
|
const init = require('./app');
|
||
|
|
||
|
const proxy = awsLambdaFastify(init())
|
||
|
// or
|
||
|
// const proxy = awsLambdaFastify(init(), { binaryMimeTypes: ['application/octet-stream'] })
|
||
|
|
||
|
exports.handler = proxy;
|
||
|
// or
|
||
|
// exports.handler = (event, context, callback) => proxy(event, context, callback);
|
||
|
// or
|
||
|
// exports.handler = (event, context) => proxy(event, context);
|
||
|
// or
|
||
|
// exports.handler = async (event, context) => proxy(event, context);
|
||
|
```
|
||
|
|
||
|
We just require
|
||
|
[aws-lambda-fastify](https://github.com/fastify/aws-lambda-fastify) (make sure
|
||
|
you install the dependency `npm i --save aws-lambda-fastify`) and our
|
||
|
[`app.js`](#appjs) file and call
|
||
|
the exported `awsLambdaFastify` function with the `app` as the only parameter.
|
||
|
The resulting `proxy` function has the correct signature to be used as a lambda
|
||
|
`handler` function. This way all the incoming events (API Gateway requests) are
|
||
|
passed to the `proxy` function of
|
||
|
[aws-lambda-fastify](https://github.com/fastify/aws-lambda-fastify).
|
||
|
|
||
|
### Example
|
||
|
|
||
|
An example deployable with
|
||
|
[claudia.js](https://claudiajs.com/tutorials/serverless-express.html) can be
|
||
|
found
|
||
|
[here](https://github.com/claudiajs/example-projects/tree/master/fastify-app-lambda).
|
||
|
|
||
|
|
||
|
### Considerations
|
||
|
|
||
|
- API Gateway does not support streams yet, so you are not able to handle
|
||
|
[streams](../Reference/Reply.md#streams).
|
||
|
- API Gateway has a timeout of 29 seconds, so it is important to provide a reply
|
||
|
during this time.
|
||
|
|
||
|
## Google Cloud Functions
|
||
|
|
||
|
### Creation of Fastify instance
|
||
|
```js
|
||
|
const fastify = require("fastify")({
|
||
|
logger: true // you can also define the level passing an object configuration to logger: {level: 'debug'}
|
||
|
});
|
||
|
```
|
||
|
|
||
|
### Add Custom `contentTypeParser` to Fastify instance
|
||
|
|
||
|
As explained [in issue
|
||
|
#946](https://github.com/fastify/fastify/issues/946#issuecomment-766319521),
|
||
|
since the Google Cloud Functions platform parses the body of the request before
|
||
|
it arrives into Fastify instance, troubling the body request in case of `POST`
|
||
|
and `PATCH` methods, you need to add a custom [`Content-Type
|
||
|
Parser`](../Reference/ContentTypeParser.md) to mitigate this behavior.
|
||
|
|
||
|
```js
|
||
|
fastify.addContentTypeParser('application/json', {}, (req, body, done) => {
|
||
|
done(null, body.body);
|
||
|
});
|
||
|
```
|
||
|
|
||
|
### Define your endpoint (examples)
|
||
|
|
||
|
A simple `GET` endpoint:
|
||
|
```js
|
||
|
fastify.get('/', async (request, reply) => {
|
||
|
reply.send({message: 'Hello World!'})
|
||
|
})
|
||
|
```
|
||
|
|
||
|
Or a more complete `POST` endpoint with schema validation:
|
||
|
```js
|
||
|
fastify.route({
|
||
|
method: 'POST',
|
||
|
url: '/hello',
|
||
|
schema: {
|
||
|
body: {
|
||
|
type: 'object',
|
||
|
properties: {
|
||
|
name: { type: 'string'}
|
||
|
},
|
||
|
required: ['name']
|
||
|
},
|
||
|
response: {
|
||
|
200: {
|
||
|
type: 'object',
|
||
|
properties: {
|
||
|
message: {type: 'string'}
|
||
|
}
|
||
|
}
|
||
|
},
|
||
|
},
|
||
|
handler: async (request, reply) => {
|
||
|
const { name } = request.body;
|
||
|
reply.code(200).send({
|
||
|
message: `Hello ${name}!`
|
||
|
})
|
||
|
}
|
||
|
})
|
||
|
```
|
||
|
|
||
|
### Implement and export the function
|
||
|
|
||
|
Final step, implement the function to handle the request and pass it to Fastify
|
||
|
by emitting `request` event to `fastify.server`:
|
||
|
|
||
|
```js
|
||
|
const fastifyFunction = async (request, reply) => {
|
||
|
await fastify.ready();
|
||
|
fastify.server.emit('request', request, reply)
|
||
|
}
|
||
|
|
||
|
export.fastifyFunction = fastifyFunction;
|
||
|
```
|
||
|
|
||
|
### Local test
|
||
|
|
||
|
Install [Google Functions Framework for
|
||
|
Node.js](https://github.com/GoogleCloudPlatform/functions-framework-nodejs).
|
||
|
|
||
|
You can install it globally:
|
||
|
```bash
|
||
|
npm i -g @google-cloud/functions-framework
|
||
|
```
|
||
|
|
||
|
Or as a development library:
|
||
|
```bash
|
||
|
npm i --save-dev @google-cloud/functions-framework
|
||
|
```
|
||
|
|
||
|
Than you can run your function locally with Functions Framework:
|
||
|
```bash
|
||
|
npx @google-cloud/functions-framework --target=fastifyFunction
|
||
|
```
|
||
|
|
||
|
Or add this command to your `package.json` scripts:
|
||
|
```json
|
||
|
"scripts": {
|
||
|
...
|
||
|
"dev": "npx @google-cloud/functions-framework --target=fastifyFunction"
|
||
|
...
|
||
|
}
|
||
|
```
|
||
|
and run it with `npm run dev`.
|
||
|
|
||
|
|
||
|
### Deploy
|
||
|
```bash
|
||
|
gcloud functions deploy fastifyFunction \
|
||
|
--runtime nodejs14 --trigger-http --region $GOOGLE_REGION --allow-unauthenticated
|
||
|
```
|
||
|
|
||
|
#### Read logs
|
||
|
```bash
|
||
|
gcloud functions logs read
|
||
|
```
|
||
|
|
||
|
#### Example request to `/hello` endpoint
|
||
|
```bash
|
||
|
curl -X POST https://$GOOGLE_REGION-$GOOGLE_PROJECT.cloudfunctions.net/me -H "Content-Type: application/json" -d '{ "name": "Fastify" }'
|
||
|
{"message":"Hello Fastify!"}
|
||
|
```
|
||
|
|
||
|
### References
|
||
|
- [Google Cloud Functions - Node.js Quickstart
|
||
|
](https://cloud.google.com/functions/docs/quickstart-nodejs)
|
||
|
|
||
|
## Google Cloud Run
|
||
|
|
||
|
Unlike AWS Lambda or Google Cloud Functions, Google Cloud Run is a serverless
|
||
|
**container** environment. Its primary purpose is to provide an
|
||
|
infrastructure-abstracted environment to run arbitrary containers. As a result,
|
||
|
Fastify can be deployed to Google Cloud Run with little-to-no code changes from
|
||
|
the way you would write your Fastify app normally.
|
||
|
|
||
|
*Follow the steps below to deploy to Google Cloud Run if you are already
|
||
|
familiar with gcloud or just follow their
|
||
|
[quickstart](https://cloud.google.com/run/docs/quickstarts/build-and-deploy)*.
|
||
|
|
||
|
### Adjust Fastify server
|
||
|
|
||
|
In order for Fastify to properly listen for requests within the container, be
|
||
|
sure to set the correct port and address:
|
||
|
|
||
|
```js
|
||
|
function build() {
|
||
|
const fastify = Fastify({ trustProxy: true })
|
||
|
return fastify
|
||
|
}
|
||
|
|
||
|
async function start() {
|
||
|
// Google Cloud Run will set this environment variable for you, so
|
||
|
// you can also use it to detect if you are running in Cloud Run
|
||
|
const IS_GOOGLE_CLOUD_RUN = process.env.K_SERVICE !== undefined
|
||
|
|
||
|
// You must listen on the port Cloud Run provides
|
||
|
const port = process.env.PORT || 3000
|
||
|
|
||
|
// You must listen on all IPV4 addresses in Cloud Run
|
||
|
const address = IS_GOOGLE_CLOUD_RUN ? "0.0.0.0" : undefined
|
||
|
|
||
|
try {
|
||
|
const server = build()
|
||
|
const address = await server.listen(port, address)
|
||
|
console.log(`Listening on ${address}`)
|
||
|
} catch (err) {
|
||
|
console.error(err)
|
||
|
process.exit(1)
|
||
|
}
|
||
|
}
|
||
|
|
||
|
module.exports = build
|
||
|
|
||
|
if (require.main === module) {
|
||
|
start()
|
||
|
}
|
||
|
```
|
||
|
|
||
|
### Add a Dockerfile
|
||
|
|
||
|
You can add any valid `Dockerfile` that packages and runs a Node app. A basic
|
||
|
`Dockerfile` can be found in the official [gcloud
|
||
|
docs](https://github.com/knative/docs/blob/2d654d1fd6311750cc57187a86253c52f273d924/docs/serving/samples/hello-world/helloworld-nodejs/Dockerfile).
|
||
|
|
||
|
```Dockerfile
|
||
|
# Use the official Node.js 10 image.
|
||
|
# https://hub.docker.com/_/node
|
||
|
FROM node:10
|
||
|
|
||
|
# Create and change to the app directory.
|
||
|
WORKDIR /usr/src/app
|
||
|
|
||
|
# Copy application dependency manifests to the container image.
|
||
|
# A wildcard is used to ensure both package.json AND package-lock.json are copied.
|
||
|
# Copying this separately prevents re-running npm install on every code change.
|
||
|
COPY package*.json ./
|
||
|
|
||
|
# Install production dependencies.
|
||
|
RUN npm install --only=production
|
||
|
|
||
|
# Copy local code to the container image.
|
||
|
COPY . .
|
||
|
|
||
|
# Run the web service on container startup.
|
||
|
CMD [ "npm", "start" ]
|
||
|
```
|
||
|
|
||
|
### Add a .dockerignore
|
||
|
|
||
|
To keep build artifacts out of your container (which keeps it small and improves
|
||
|
build times) add a `.dockerignore` file like the one below:
|
||
|
|
||
|
```.dockerignore
|
||
|
Dockerfile
|
||
|
README.md
|
||
|
node_modules
|
||
|
npm-debug.log
|
||
|
```
|
||
|
|
||
|
### Submit build
|
||
|
|
||
|
Next, submit your app to be built into a Docker image by running the following
|
||
|
command (replacing `PROJECT-ID` and `APP-NAME` with your GCP project id and an
|
||
|
app name):
|
||
|
|
||
|
```bash
|
||
|
gcloud builds submit --tag gcr.io/PROJECT-ID/APP-NAME
|
||
|
```
|
||
|
|
||
|
### Deploy Image
|
||
|
|
||
|
After your image has built, you can deploy it with the following command:
|
||
|
|
||
|
```bash
|
||
|
gcloud beta run deploy --image gcr.io/PROJECT-ID/APP-NAME --platform managed
|
||
|
```
|
||
|
|
||
|
Your app will be accessible from the URL GCP provides.
|
||
|
|
||
|
|
||
|
## netlify-lambda
|
||
|
|
||
|
First, please perform all preparation steps related to **AWS Lambda**.
|
||
|
|
||
|
Create a folder called `functions`, then create `server.js` (and your endpoint
|
||
|
path will be `server.js`) inside the `functions` folder.
|
||
|
|
||
|
### functions/server.js
|
||
|
|
||
|
```js
|
||
|
export { handler } from '../lambda.js'; // Change `lambda.js` path to your `lambda.js` path
|
||
|
```
|
||
|
|
||
|
### netlify.toml
|
||
|
|
||
|
```toml
|
||
|
[build]
|
||
|
# This will be run the site build
|
||
|
command = "npm run build:functions"
|
||
|
# This is the directory is publishing to netlify's CDN
|
||
|
# and this is directory of your front of your app
|
||
|
# publish = "build"
|
||
|
# functions build directory
|
||
|
functions = "functions-build" # always appends `-build` folder to your `functions` folder for builds
|
||
|
```
|
||
|
|
||
|
### webpack.config.netlify.js
|
||
|
|
||
|
**Do not forget to add this Webpack config, or else problems may occur**
|
||
|
|
||
|
```js
|
||
|
const nodeExternals = require('webpack-node-externals');
|
||
|
const dotenv = require('dotenv-safe');
|
||
|
const webpack = require('webpack');
|
||
|
|
||
|
const env = process.env.NODE_ENV || 'production';
|
||
|
const dev = env === 'development';
|
||
|
|
||
|
if (dev) {
|
||
|
dotenv.config({ allowEmptyValues: true });
|
||
|
}
|
||
|
|
||
|
module.exports = {
|
||
|
mode: env,
|
||
|
devtool: dev ? 'eval-source-map' : 'none',
|
||
|
externals: [nodeExternals()],
|
||
|
devServer: {
|
||
|
proxy: {
|
||
|
'/.netlify': {
|
||
|
target: 'http://localhost:9000',
|
||
|
pathRewrite: { '^/.netlify/functions': '' }
|
||
|
}
|
||
|
}
|
||
|
},
|
||
|
module: {
|
||
|
rules: []
|
||
|
},
|
||
|
plugins: [
|
||
|
new webpack.DefinePlugin({
|
||
|
'process.env.APP_ROOT_PATH': JSON.stringify('/'),
|
||
|
'process.env.NETLIFY_ENV': true,
|
||
|
'process.env.CONTEXT': env
|
||
|
})
|
||
|
]
|
||
|
};
|
||
|
```
|
||
|
|
||
|
### Scripts
|
||
|
|
||
|
Add this command to your `package.json` *scripts*
|
||
|
|
||
|
```json
|
||
|
"scripts": {
|
||
|
...
|
||
|
"build:functions": "netlify-lambda build functions --config ./webpack.config.netlify.js"
|
||
|
...
|
||
|
}
|
||
|
```
|
||
|
|
||
|
Then it should work fine
|
||
|
|
||
|
|
||
|
## Vercel
|
||
|
|
||
|
[Vercel](https://vercel.com) provides zero-configuration deployment for Node.js
|
||
|
applications. In order to use it now, it is as simple as configuring your
|
||
|
`vercel.json` file like the following:
|
||
|
|
||
|
```json
|
||
|
{
|
||
|
"rewrites": [
|
||
|
{
|
||
|
"source": "/(.*)",
|
||
|
"destination": "/api/serverless.js"
|
||
|
}
|
||
|
]
|
||
|
}
|
||
|
```
|
||
|
|
||
|
Then, write `api/serverless.js` like so:
|
||
|
|
||
|
```js
|
||
|
"use strict";
|
||
|
|
||
|
// Read the .env file.
|
||
|
import * as dotenv from "dotenv";
|
||
|
dotenv.config();
|
||
|
|
||
|
// Require the framework
|
||
|
import Fastify from "fastify";
|
||
|
|
||
|
// Instantiate Fastify with some config
|
||
|
const app = Fastify({
|
||
|
logger: true,
|
||
|
});
|
||
|
|
||
|
// Register your application as a normal plugin.
|
||
|
app.register(import("../src/app"));
|
||
|
|
||
|
export default async (req, res) => {
|
||
|
await app.ready();
|
||
|
app.server.emit('request', req, res);
|
||
|
}
|
||
|
```
|