diff --git a/cloud/google-cloud-run/Dockerfile b/cloud/google-cloud-run/Dockerfile new file mode 100644 index 00000000..3548f663 --- /dev/null +++ b/cloud/google-cloud-run/Dockerfile @@ -0,0 +1,16 @@ +FROM node:20 as base +WORKDIR /usr/app + +FROM base as build +WORKDIR /usr/app +ENV NODE_ENV=production +ENV CI=true +COPY package.json package-lock.json ./ +RUN npm install --production +COPY . ./ + +FROM node:20 as production +WORKDIR /usr/app +ENV NODE_ENV=production +COPY --from=build /usr/app ./ +CMD ["node", "index.js"] diff --git a/cloud/google-cloud-run/README.md b/cloud/google-cloud-run/README.md new file mode 100644 index 00000000..c6b7b41f --- /dev/null +++ b/cloud/google-cloud-run/README.md @@ -0,0 +1,35 @@ +# Google Cloud Run Example + +This example shows how to deploy a simplified GraphQL API to +[Google Cloud Run](https://cloud.google.com/run). + +## Prerequisites + +Check out the +[Get started](https://cloud.google.com/run/docs/quickstarts/build-and-deploy/deploy-nodejs-service) +guide on Google Cloud to set up the project. + +## Deploy + +```bash +gcloud run deploy example \ + --source . \ + --project=zentered \ + --region=us-central1 \ + --memory=256Mi \ + --platform=managed \ + --allow-unauthenticated \ + --use-http2 \ + --set-env-vars=VERSION=1.0.0 +``` + +You can set environment variables for the Docker container with +`--set-env-vars`, `VERSION` is used as an example here. + +## Next Steps + +- Instead of building from source, you can use + [Cloud Build](https://cloud.google.com/build) for continous deployment from a + Git respository +- Cloud Build can create and push Docker images to + [Artifact Registry](https://cloud.google.com/artifact-registry) diff --git a/cloud/google-cloud-run/app.js b/cloud/google-cloud-run/app.js new file mode 100644 index 00000000..66763e39 --- /dev/null +++ b/cloud/google-cloud-run/app.js @@ -0,0 +1,28 @@ +import Fastify from 'fastify' +import mercurius from 'mercurius' + +function build(opts = {}) { + const fastify = Fastify(opts) + + const schema = ` + type Query { + add(x: Int, y: Int): Int + } +` + + const resolvers = { + Query: { + add: async (_, { x, y }) => x + y + } + } + + fastify.register(mercurius, { + schema, + graphiql: true, + resolvers + }) + + return fastify +} + +export default build diff --git a/cloud/google-cloud-run/config.js b/cloud/google-cloud-run/config.js new file mode 100644 index 00000000..ef822aca --- /dev/null +++ b/cloud/google-cloud-run/config.js @@ -0,0 +1,6 @@ +export const port = process.env.PORT || 3000 +export const debug = process.env.DEBUG === 'true' +export const environment = process.env.NODE_ENV || 'development' +export const production = process.env.NODE_ENV === 'production' +export const development = process.env.NODE_ENV !== 'production' +export const appVersion = process.env.VERSION || '0.0.0' diff --git a/cloud/google-cloud-run/index.js b/cloud/google-cloud-run/index.js new file mode 100644 index 00000000..5f05c08a --- /dev/null +++ b/cloud/google-cloud-run/index.js @@ -0,0 +1,40 @@ +import build from './app.js' +import { development, environment, production, port } from './config.js' + +const host = development ? 'localhost' : '0.0.0.0' + +const start = async () => { + // A custom logger, as Cloud Run does not need a transport configuration + const envToLogger = { + development: { + level: 'debug', + transport: { + target: 'pino-pretty', + options: { + ignore: 'pid,hostname' + } + } + }, + production: true, + test: false + } + + try { + const fastify = await build({ + logger: envToLogger[environment] ?? true, + disableRequestLogging: production, // Cloud Run logs requests, no request logging is needed + http2: production // Enable HTTP2 for production + }) + await fastify.listen({ port: port, host: host }) + fastify.ready(() => { + console.log(`listening on ${JSON.stringify(fastify.server.address())}`) + console.log(fastify.printRoutes()) + }) + } catch (err) { + console.log(err) + // eslint-disable-next-line no-process-exit + process.exit(1) + } +} + +start() diff --git a/cloud/google-cloud-run/package.json b/cloud/google-cloud-run/package.json new file mode 100644 index 00000000..c5e1166c --- /dev/null +++ b/cloud/google-cloud-run/package.json @@ -0,0 +1,20 @@ +{ + "name": "google-cloud-run", + "version": "1.0.0", + "description": "", + "type": "module", + "main": "index.js", + "scripts": { + "start": "node index.js", + "test": "node --test" + }, + "keywords": [], + "author": "Patrick Heneise (https://zentered.co)", + "dependencies": { + "fastify": "^4.17.0", + "mercurius": "^13.0.0" + }, + "devDependencies": { + "pino-pretty": "^10.0.0" + } +} diff --git a/cloud/google-cloud-run/test/graphql.test.js b/cloud/google-cloud-run/test/graphql.test.js new file mode 100644 index 00000000..d01b0a20 --- /dev/null +++ b/cloud/google-cloud-run/test/graphql.test.js @@ -0,0 +1,24 @@ +'use strict' + +import test from 'node:test' +import assert from 'node:assert/strict' +import build from './../app.js' + +test('/gaphql', async (t) => { + const app = build({ logger: false }) + const expected = { data: { add: 2 } } + const query = `{ + add(x:1,y:1) + }` + + const response = await app.inject({ + method: 'POST', + url: '/graphql', + body: { query: query } + }) + const actual = await response.json() + + assert.equal(response.statusCode, 200, 'returns a status code of 200') + assert.deepEqual(actual, expected) + await app.close +})