1 | <h1 align="center">Serverless</h1>
|
2 |
|
3 | Run serverless applications and REST APIs using your existing Fastify application.
|
4 |
|
5 | ### Contents
|
6 |
|
7 | - [AWS Lambda](#aws-lambda)
|
8 | - [Google Cloud Run](#google-cloud-run)
|
9 | - [Zeit Now](#zeit-now)
|
10 |
|
11 | ### Attention Readers:
|
12 | > Fastify is not designed to run on serverless environments.
|
13 | The Fastify framework is designed to make implementing a traditional HTTP/S server easy.
|
14 | Serverless environments requests differently than a standard HTTP/S server;
|
15 | thus, we cannot guarantee it will work as expected with Fastify.
|
16 | Regardless, based on the examples given in this document,
|
17 | it is possible to use Fastify in a serverless environment.
|
18 | Again, keep in mind that this is not Fastify's intended use case and
|
19 | we do not test for such integration scenarios.
|
20 |
|
21 | ## AWS Lambda
|
22 |
|
23 | The sample provided allows you to easily build serverless web applications/services
|
24 | and RESTful APIs using Fastify on top of AWS Lambda and Amazon API Gateway.
|
25 |
|
26 | *Note: Using [aws-lambda-fastify](https://github.com/fastify/aws-lambda-fastify) is just one possible way.*
|
27 |
|
28 | ### app.js
|
29 |
|
30 | ```js
|
31 | const fastify = require('fastify');
|
32 |
|
33 | function init() {
|
34 | const app = fastify();
|
35 | app.get('/', (request, reply) => reply.send({ hello: 'world' }));
|
36 | return app;
|
37 | }
|
38 |
|
39 | if (require.main === module) {
|
40 | // called directly i.e. "node app"
|
41 | init().listen(3000, (err) => {
|
42 | if (err) console.error(err);
|
43 | console.log('server listening on 3000');
|
44 | });
|
45 | } else {
|
46 | // required as a module => executed on aws lambda
|
47 | module.exports = init;
|
48 | }
|
49 | ```
|
50 |
|
51 | When executed in your lambda function we don't need to listen to a specific port,
|
52 | so we just export the wrapper function `init` in this case.
|
53 | The [`lambda.js`](https://www.fastify.io/docs/latest/Serverless/#lambda-js) file will use this export.
|
54 |
|
55 | When you execute your Fastify application like always,
|
56 | i.e. `node app.js` *(the detection for this could be `require.main === module`)*,
|
57 | you can normally listen to your port, so you can still run your Fastify function locally.
|
58 |
|
59 | ### lambda.js
|
60 |
|
61 | ```js
|
62 | const awsLambdaFastify = require('aws-lambda-fastify')
|
63 | const init = require('./app');
|
64 |
|
65 | const proxy = awsLambdaFastify(init())
|
66 | // or
|
67 | // const proxy = awsLambdaFastify(init(), { binaryMimeTypes: ['application/octet-stream'] })
|
68 |
|
69 | exports.handler = proxy;
|
70 | // or
|
71 | // exports.handler = (event, context, callback) => proxy(event, context, callback);
|
72 | // or
|
73 | // exports.handler = (event, context) => proxy(event, context);
|
74 | // or
|
75 | // exports.handler = async (event, context) => proxy(event, context);
|
76 | ```
|
77 |
|
78 | We just require [aws-lambda-fastify](https://github.com/fastify/aws-lambda-fastify)
|
79 | (make sure you install the dependency `npm i --save aws-lambda-fastify`) and our
|
80 | [`app.js`](https://www.fastify.io/docs/latest/Serverless/#app-js) file and call the
|
81 | exported `awsLambdaFastify` function with the `app` as the only parameter.
|
82 | The resulting `proxy` function has the correct signature to be used as lambda `handler` function.
|
83 | 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).
|
84 |
|
85 | ### Example
|
86 |
|
87 | 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).
|
88 |
|
89 |
|
90 | ### Considerations
|
91 |
|
92 | - API Gateway doesn't support streams yet, so you're not able to handle [streams](https://www.fastify.io/docs/latest/Reply/#streams).
|
93 | - API Gateway has a timeout of 29 seconds, so it's important to provide a reply during this time.
|
94 |
|
95 | ## Google Cloud Run
|
96 |
|
97 | Unlike AWS Lambda or Google Cloud Functions, Google Cloud Run is a serverless **container** environment. It's 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.
|
98 |
|
99 | *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)*.
|
100 |
|
101 | ### Adjust Fastify server
|
102 |
|
103 | In order for Fastify to properly listen for requests within the container, be sure to set the correct port and address:
|
104 |
|
105 | ```js
|
106 | function build() {
|
107 | const fastify = Fastify({ trustProxy: true })
|
108 | return fastify
|
109 | }
|
110 |
|
111 | async function start() {
|
112 | // Google Cloud Run will set this environment variable for you, so
|
113 | // you can also use it to detect if you are running in Cloud Run
|
114 | const IS_GOOGLE_CLOUD_RUN = process.env.K_SERVICE !== undefined
|
115 |
|
116 | // You must listen on the port Cloud Run provides
|
117 | const port = process.env.PORT || 3000
|
118 |
|
119 | // You must listen on all IPV4 addresses in Cloud Run
|
120 | const address = IS_GOOGLE_CLOUD_RUN ? "0.0.0.0" : undefined
|
121 |
|
122 | try {
|
123 | const server = build()
|
124 | const address = await server.listen(port, address)
|
125 | console.log(`Listening on ${address}`)
|
126 | } catch (err) {
|
127 | console.error(err)
|
128 | process.exit(1)
|
129 | }
|
130 | }
|
131 |
|
132 | module.exports = build
|
133 |
|
134 | if (require.main === module) {
|
135 | start()
|
136 | }
|
137 | ```
|
138 |
|
139 | ### Add a Dockerfile
|
140 |
|
141 | 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).
|
142 |
|
143 | ```Dockerfile
|
144 | # Use the official Node.js 10 image.
|
145 | # https://hub.docker.com/_/node
|
146 | FROM node:10
|
147 |
|
148 | # Create and change to the app directory.
|
149 | WORKDIR /usr/src/app
|
150 |
|
151 | # Copy application dependency manifests to the container image.
|
152 | # A wildcard is used to ensure both package.json AND package-lock.json are copied.
|
153 | # Copying this separately prevents re-running npm install on every code change.
|
154 | COPY package*.json ./
|
155 |
|
156 | # Install production dependencies.
|
157 | RUN npm install --only=production
|
158 |
|
159 | # Copy local code to the container image.
|
160 | COPY . .
|
161 |
|
162 | # Run the web service on container startup.
|
163 | CMD [ "npm", "start" ]
|
164 | ```
|
165 |
|
166 | ### Add a .dockerignore
|
167 |
|
168 | To keep build artifacts out of your container (which keeps it small and improves build times), add a `.dockerignore` file like the one below:
|
169 |
|
170 | ```.dockerignore
|
171 | Dockerfile
|
172 | README.md
|
173 | node_modules
|
174 | npm-debug.log
|
175 | ```
|
176 |
|
177 | ### Submit build
|
178 |
|
179 | 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):
|
180 |
|
181 | ```bash
|
182 | gcloud builds submit --tag gcr.io/PROJECT-ID/APP-NAME
|
183 | ```
|
184 |
|
185 | ### Deploy Image
|
186 |
|
187 | After your image has built, you can deploy it with the following command:
|
188 |
|
189 | ```bash
|
190 | gcloud beta run deploy --image gcr.io/PROJECT-ID/APP-NAME --platform managed
|
191 | ```
|
192 |
|
193 | Your app will be accessible from the URL GCP provides.
|
194 |
|
195 | ## Zeit Now
|
196 |
|
197 | [now](https://zeit.co/home) provides zero configuration deployment for
|
198 | Node.js applications. In order to use now, it is as simple as
|
199 | configuring your `now.json` file like the following:
|
200 |
|
201 | ```json
|
202 | {
|
203 | "version": 2,
|
204 | "builds": [
|
205 | {
|
206 | "src": "api/serverless.js",
|
207 | "use": "@now/node",
|
208 | "config": {
|
209 | "helpers": false
|
210 | }
|
211 | }
|
212 | ],
|
213 | "routes": [
|
214 | { "src": "/.*", "dest": "/api/serverless.js"}
|
215 | ]
|
216 | }
|
217 | ```
|
218 |
|
219 | Then, write a `api/serverless.js` like so:
|
220 |
|
221 | ```js
|
222 | 'use strict'
|
223 |
|
224 | const build = require('./index')
|
225 |
|
226 | const app = build()
|
227 |
|
228 | module.exports = async function (req, res) {
|
229 | await app.ready()
|
230 | app.server.emit('request', req, res)
|
231 | }
|
232 | ```
|
233 |
|
234 | And a `api/index.js` file:
|
235 |
|
236 | ```js
|
237 | 'use strict'
|
238 |
|
239 | const fastify = require('fastify')
|
240 |
|
241 | function build () {
|
242 | const app = fastify({
|
243 | logger: true
|
244 | })
|
245 |
|
246 | app.get('/', async (req, res) => {
|
247 | const { name = 'World' } = req.query
|
248 | req.log.info({ name }, 'hello world!')
|
249 | return `Hello ${name}!`
|
250 | })
|
251 |
|
252 | return app
|
253 | }
|
254 |
|
255 | module.exports = build
|
256 | ```
|
257 |
|
258 | Note that you'll need to use Node 10 by setting it in `package.json`:
|
259 |
|
260 | ```js
|
261 | "engines": {
|
262 | "node": "10.x"
|
263 | },
|
264 | ```
|