jaredpalmer / razzle

✨ Create server-rendered universal JavaScript applications with no configuration
https://razzlejs.org
MIT License
11.1k stars 867 forks source link

Razzle and Serverless #629

Closed romainquellec closed 6 years ago

romainquellec commented 6 years ago

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 !

jaredpalmer commented 6 years ago

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

romainquellec commented 6 years ago

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

rozenmd commented 6 years ago

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

romainquellec commented 6 years ago

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.

romainquellec commented 6 years ago

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)

romainquellec commented 6 years ago

@rozenmd can you share your razzle.config ?

rozenmd commented 6 years ago

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

romainquellec commented 6 years ago

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.

rozenmd commented 6 years ago

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

romainquellec commented 6 years ago

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 !

jaredpalmer commented 6 years ago

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.

rozenmd commented 6 years ago

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

romainquellec commented 6 years ago

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

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

rozenmd commented 6 years ago

@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
  },
}
romainquellec commented 6 years ago

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

rozenmd commented 6 years ago

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

romainquellec commented 6 years ago

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

mbrochh commented 4 years ago

@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 :)

rozenmd commented 4 years ago

@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.

mbrochh commented 4 years ago

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!

rozenmd commented 4 years ago

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:

jgcmarins commented 4 years ago

Did anyone deployed Razzle to AWS lambda?

atulmy commented 4 years ago

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.

jgcmarins commented 4 years ago

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,
    });
  }
}
fivethreeo commented 3 years ago

@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.

fivethreeo commented 3 years ago

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

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

jgcmarins commented 3 years ago

@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/

jgcmarins commented 3 years ago

Is there anything else missing from https://razzle-git-canary.jared.vercel.app/deployment-options/aws ?

fivethreeo commented 3 years ago

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.

fivethreeo commented 3 years ago

Nothing missing otherwise.