Razzle: Razzle and Serverless

Created on 30 May 2018  ·  30Comments  ·  Source: jaredpalmer/razzle

Hello !
I'm trying to run a razzle project (materialUI, Typescript, Apollo & After) on serverless.
What is the best way to handle this with Serverless ?

If you know any example out there, I'm happy to take a look. Thanks !

Most helpful comment

This is how I've been deploying Razzle apps to AWS Lambda with AWS CDK:

RazzleStack

import * as CDK from '@aws-cdk/core';
import * as S3 from '@aws-cdk/aws-s3';
import * as S3Deployment from '@aws-cdk/aws-s3-deployment';
import * as Lambda from '@aws-cdk/aws-lambda';
import * as APIGateway from '@aws-cdk/aws-apigateway';
import * as SSM from '@aws-cdk/aws-ssm';
import * as SecretsManager from '@aws-cdk/aws-secretsmanager';

import { ConfigProps, getParam, ModeStack } from '../helpers';

export class RazzleStack extends ModeStack {
  constructor(app: CDK.App, id: string, props: ConfigProps) {
    super(app, id, props);

    /**
     * S3 bucket to the /public folder
     */
    const publicBucketName = `my-razzle-app-bucket-public-files-${this.mode}`;
    const bucketPublicFiles = new S3.Bucket(this, publicBucketName, {
      publicReadAccess: true,
      bucketName: publicBucketName.toLowerCase(),
    });

    /**
     * Store S3 bucket name
     */
    new SSM.StringParameter(this, `MyRazzleAppBucketAssetsName${this.Mode}`, {
      description: `My Razzle App S3 Bucket Name for Assets on ${this.Mode}`,
      parameterName: `/${props.name}/S3/Assets/Name`,
      stringValue: bucketPublicFiles.bucketName,
    });

    /**
     * Store S3 domainName name
     */
    new SSM.StringParameter(this, `MyRazzleAppBucketAssetsDomainName${this.Mode}`, {
      description: `My Razzle App S3 Bucket DomainName for Assets on ${this.Mode}`,
      parameterName: `/${props.name}/S3/Assets/DomainName`,
      stringValue: bucketPublicFiles.bucketDomainName,
    });

    /**
     * Deploy public folder of build to `my-razzle-app-bucket-public-files-${this.mode}` bucket
     */
    new S3Deployment.BucketDeployment(this, `${publicBucketName}-deploy`, {
      sources: [S3Deployment.Source.asset('./build/public')],
      destinationBucket: bucketPublicFiles,
    });

    /**
     * Environment Variables for SSR Function
     */
    const environmentKeys = [
      'NODE_ENV',
      'RAZZLE_GRAPHQL_URL',
      'RAZZLE_MAPBOX_ACCESS_TOKEN',
      'RAZZLE_GOOGLE_RECAPTCHA_SITE',
      'RAZZLE_GA_ID',
    ];

    const environmentSecret = SecretsManager.Secret.fromSecretAttributes(
      this,
      `MyRazzleAppEnvironmentSecret${this.Mode}`,
      {
        secretArn: getParam(this, `MyRazzleAppSecretsArn${this.Mode}`),
      },
    );

    let environment: { [key: string]: string } = {};
    for (const key of environmentKeys) {
      environment[key] = environmentSecret.secretValueFromJson(key).toString();
    }

    /**
     * Razzle SSR Function
     */
    const myRazzleAppSsrFunction = new Lambda.Function(this, `MyRazzleAppSSRFunction${this.Mode}`, {
      description: `Lambda Function that runs My Razzle App SSR on ${this.Mode}`,
      code: Lambda.Code.fromAsset('./build', {
        exclude: ['public', 'static', '*.json'],
      }),
      handler: 'server.handler',
      runtime: Lambda.Runtime.NODEJS_12_X,
      memorySize: 512,
      timeout: CDK.Duration.seconds(5),
      environment,
      tracing: Lambda.Tracing.ACTIVE,
    });

    /**
     * Razzle ApiGateway
     */
    const razzleSsrApiGatewayName = `MyRazzleAppSSRApiGateway${this.Mode}`;
    const api = new APIGateway.RestApi(this, razzleSsrApiGatewayName, {
      description: `ApiGateway that exposes My Razzle App SSR on ${this.Mode}`,
      binaryMediaTypes: ['*/*'],
      endpointTypes: [APIGateway.EndpointType.REGIONAL],
      deployOptions: {
        stageName: this.mode,
      },
    });
    const integration = new APIGateway.LambdaIntegration(myRazzleAppSsrFunction);
    const root = api.root;
    const pathApi = api.root.addResource('{proxy+}');

    root.addMethod('GET', integration);
    pathApi.addMethod('ANY', integration);

    /**
     * Razzle ApiGateway ID
     */
    new SSM.StringParameter(this, `MyRazzleAppAPIGatewayRestId${this.Mode}`, {
      description: `My Razzle App ApiGateway ID on ${this.Mode}`,
      parameterName: `/${props.name}/APIGateway/ApiId`,
      stringValue: api.restApiId,
    });
  }
}

All 30 comments

Someone just did this and tweeted about it. Would be a cool exampel

Can't find the tweet ! Do you have it ?

Was my tweet - I just replaced index.js (where all the hotloading happens) with

import awsServerlessExpress from 'aws-serverless-express'
import app from './server'
const binaryMimeTypes = [
  'application/octet-stream',
  'font/eot',
  'font/opentype',
  'font/otf',
  'image/jpeg',
  'image/png',
  'image/svg+xml',
]
const server = awsServerlessExpress.createServer(app, null, binaryMimeTypes)

export const handler = (event, context, callback) => {
  awsServerlessExpress.proxy(server, event, context)
}

You then run npm run build, copy node_modules/ to build/, zip server.js together with the node_modules/ folder, and upload that to AWS Lambda. Everything in public/ then gets uploaded to S3 and sent across via Cloudfront.

For Lambda -> API gateway infrastructure I used Terraform and followed this guide: https://www.terraform.io/docs/providers/aws/guides/serverless-with-aws-lambda-and-api-gateway.html

Still not perfect though - getting this error on load from the client-side bundle (https://static.jobsok.io/):

image


Update:
I shit you not, deleting node_modules & reinstalling gave me:

File sizes after gzip:

  210.38 KB (+8 B)  build/static/js/bundle.1a960add.js
  1.96 KB           build/static/css/bundle.5865fae5.css

With no code changes - made the client-side bundle functional again

My handler.js (or index.js) is the same.
Then, I build everything with webpack (project + node_modules). (You just need to modify razzle.config.js for that)

I'm working with serverless, not terraform. Basically the same. Just need to import relative package for the handler (For some reason, webpack doesnt pack this one, maybe bcause its on a different file / path, will fix later).

Serverless upload all this, and that's it. You can see the project here : https://github.com/CuistotduCoin/cuistot

It's far from perfect, and wip.

By the way, @jaredpalmer, can you help me on the razzle/webpack config ?
I want to include everything needed on the bundle, (node_modules included) with best performance. Any examples/tips ? (With Typescript config)

@rozenmd can you share your razzle.config ?

@romainquellec mine is empty - no extra config beyond what razzle provides

Well, I dont know why, but I cant access to aws-serverless-express defined in dependencies (maybe more).
And to optimize all that, i want to exclude aws-sdk from final build (it's always available in a lambda function, will see that later though)

I think its comming from externals, but i cant figured it out.

Yep, I had the same issue. My only solution was to zip up the server.js with all of node_modules

I can't on lambda. Official limit size is 50mo.
General question for everyone : How to bundle node_modules with good perf with razzle. Thanks !

We don’t use lambda, but we do bundle all of our node_modules by removing default externals option in razzle.config.js. This is cool because it means the build folder is effectively a build artifact. We then put it onto s3 then our Jenkins task pulls the new folder onto each server and restarts pm2.

@jaredpalmer - any chance you could post a gist with that razzle.config.js?

by removing default externals option in razzle.config.js:

Like that ?
modify(config, { target, dev }) { if (target === 'node' && !dev) { config.externals = []; } return config; }

@romainquellec that works!!

module.exports = {
  modify: (config, { target, dev }, webpack) => {
    // do something to config
    const appConfig = config // stay immutable here

    if (target === 'node' && !dev) {
      appConfig.externals = []
    }

    return appConfig
  },
}

It's working. Good ! 👍 👍
It's fair to add an example ! I'll look at that this weekend. Do you mind participating @rozenmd ?

Unfortunately I won't have free time this weekend @romainquellec but happy to review/collaborate on PRs

I'm closing this. Will submit a PR a soon as possible.

@romainquellec did you ever get around making that pull request? Are you still runing your Razzle project in a serverless environment? I'm just beginning to explore this possibility and stumbled upon this thread :)

@mbrochh Didn't see a pull request in the end, but I'm still using Razzle on https://onlineornot.com for all of my serverless serverside rendering.

I also just found this blog post: https://medium.com/@RozenMD/build-your-own-app-with-react-graphql-and-serverless-architecture-part-1-server-side-rendering-f0d0144ff5f

I guess I will have to dive in headfirst and try to follow that post :)

Thanks for the post!

https://maxrozen.com/2018/08/08/start-your-own-app-with-react-part-1 is probably a better link for the code formatting.

and Thanks! :smile:

Did anyone deployed Razzle to AWS lambda?

I'm also looking for an example for Razzle on AWS lambda. If anyone has done it and happy to share with us, would really appreciate.

This is how I've been deploying Razzle apps to AWS Lambda with AWS CDK:

RazzleStack

import * as CDK from '@aws-cdk/core';
import * as S3 from '@aws-cdk/aws-s3';
import * as S3Deployment from '@aws-cdk/aws-s3-deployment';
import * as Lambda from '@aws-cdk/aws-lambda';
import * as APIGateway from '@aws-cdk/aws-apigateway';
import * as SSM from '@aws-cdk/aws-ssm';
import * as SecretsManager from '@aws-cdk/aws-secretsmanager';

import { ConfigProps, getParam, ModeStack } from '../helpers';

export class RazzleStack extends ModeStack {
  constructor(app: CDK.App, id: string, props: ConfigProps) {
    super(app, id, props);

    /**
     * S3 bucket to the /public folder
     */
    const publicBucketName = `my-razzle-app-bucket-public-files-${this.mode}`;
    const bucketPublicFiles = new S3.Bucket(this, publicBucketName, {
      publicReadAccess: true,
      bucketName: publicBucketName.toLowerCase(),
    });

    /**
     * Store S3 bucket name
     */
    new SSM.StringParameter(this, `MyRazzleAppBucketAssetsName${this.Mode}`, {
      description: `My Razzle App S3 Bucket Name for Assets on ${this.Mode}`,
      parameterName: `/${props.name}/S3/Assets/Name`,
      stringValue: bucketPublicFiles.bucketName,
    });

    /**
     * Store S3 domainName name
     */
    new SSM.StringParameter(this, `MyRazzleAppBucketAssetsDomainName${this.Mode}`, {
      description: `My Razzle App S3 Bucket DomainName for Assets on ${this.Mode}`,
      parameterName: `/${props.name}/S3/Assets/DomainName`,
      stringValue: bucketPublicFiles.bucketDomainName,
    });

    /**
     * Deploy public folder of build to `my-razzle-app-bucket-public-files-${this.mode}` bucket
     */
    new S3Deployment.BucketDeployment(this, `${publicBucketName}-deploy`, {
      sources: [S3Deployment.Source.asset('./build/public')],
      destinationBucket: bucketPublicFiles,
    });

    /**
     * Environment Variables for SSR Function
     */
    const environmentKeys = [
      'NODE_ENV',
      'RAZZLE_GRAPHQL_URL',
      'RAZZLE_MAPBOX_ACCESS_TOKEN',
      'RAZZLE_GOOGLE_RECAPTCHA_SITE',
      'RAZZLE_GA_ID',
    ];

    const environmentSecret = SecretsManager.Secret.fromSecretAttributes(
      this,
      `MyRazzleAppEnvironmentSecret${this.Mode}`,
      {
        secretArn: getParam(this, `MyRazzleAppSecretsArn${this.Mode}`),
      },
    );

    let environment: { [key: string]: string } = {};
    for (const key of environmentKeys) {
      environment[key] = environmentSecret.secretValueFromJson(key).toString();
    }

    /**
     * Razzle SSR Function
     */
    const myRazzleAppSsrFunction = new Lambda.Function(this, `MyRazzleAppSSRFunction${this.Mode}`, {
      description: `Lambda Function that runs My Razzle App SSR on ${this.Mode}`,
      code: Lambda.Code.fromAsset('./build', {
        exclude: ['public', 'static', '*.json'],
      }),
      handler: 'server.handler',
      runtime: Lambda.Runtime.NODEJS_12_X,
      memorySize: 512,
      timeout: CDK.Duration.seconds(5),
      environment,
      tracing: Lambda.Tracing.ACTIVE,
    });

    /**
     * Razzle ApiGateway
     */
    const razzleSsrApiGatewayName = `MyRazzleAppSSRApiGateway${this.Mode}`;
    const api = new APIGateway.RestApi(this, razzleSsrApiGatewayName, {
      description: `ApiGateway that exposes My Razzle App SSR on ${this.Mode}`,
      binaryMediaTypes: ['*/*'],
      endpointTypes: [APIGateway.EndpointType.REGIONAL],
      deployOptions: {
        stageName: this.mode,
      },
    });
    const integration = new APIGateway.LambdaIntegration(myRazzleAppSsrFunction);
    const root = api.root;
    const pathApi = api.root.addResource('{proxy+}');

    root.addMethod('GET', integration);
    pathApi.addMethod('ANY', integration);

    /**
     * Razzle ApiGateway ID
     */
    new SSM.StringParameter(this, `MyRazzleAppAPIGatewayRestId${this.Mode}`, {
      description: `My Razzle App ApiGateway ID on ${this.Mode}`,
      parameterName: `/${props.name}/APIGateway/ApiId`,
      stringValue: api.restApiId,
    });
  }
}

@jgcmarins about that razzletack setup. Could you do a step by step how to use this? I would like to add it to razzle docs.

https://razzle-git-canary.jared.vercel.app/deployment-options/aws

I need some help on this, can anyone give me a hand?

@fivethreeo the step by step would be to setup CDK at an AWS account and than run cdk deploy with some additional flags and it's live \o/

I would like to do the stage with a domain instead of a path. Maybe make a full featured example with rds, key rotation, TypeOrm, typegraphql, Formik and jwt but not as example in razzle itself.

Nothing missing otherwise.

Was this page helpful?
0 / 5 - 0 ratings

Related issues

panbanda picture panbanda  ·  5Comments

dizzyn picture dizzyn  ·  3Comments

jcblw picture jcblw  ·  4Comments

ewolfe picture ewolfe  ·  4Comments

GouthamKD picture GouthamKD  ·  3Comments