Razzle: Razzle ๋ฐ ์„œ๋ฒ„๋ฆฌ์Šค

์— ๋งŒ๋“  2018๋…„ 05์›” 30์ผ  ยท  30์ฝ”๋ฉ˜ํŠธ  ยท  ์ถœ์ฒ˜: jaredpalmer/razzle

์—ฌ๋ณด์„ธ์š” !
์„œ๋ฒ„๋ฆฌ์Šค์—์„œ razzle ํ”„๋กœ์ ํŠธ(materialUI, Typescript, Apollo & After)๋ฅผ ์‹คํ–‰ํ•˜๋ ค๊ณ  ํ•ฉ๋‹ˆ๋‹ค.
Serverless๋กœ ์ด๊ฒƒ์„ ์ฒ˜๋ฆฌํ•˜๋Š” ๊ฐ€์žฅ ์ข‹์€ ๋ฐฉ๋ฒ•์€ ๋ฌด์—‡์ž…๋‹ˆ๊นŒ?

ํ˜น์‹œ ์•Œ๊ณ  ๊ณ„์‹  ์˜ˆ๊ฐ€ ์žˆ๋‹ค๋ฉด ๊ธฐ๊บผ์ด ์‚ดํŽด๋ณด๊ฒ ์Šต๋‹ˆ๋‹ค. ๊ฐ์‚ฌ ํ•ด์š” !

๊ฐ€์žฅ ์œ ์šฉํ•œ ๋Œ“๊ธ€

์ด๊ฒƒ์ด ๋‚ด๊ฐ€ AWS CDK๋ฅผ ์‚ฌ์šฉ ํ•˜์—ฌ AWS Lambda์— Razzle ์•ฑ์„ ๋ฐฐํฌํ•œ ๋ฐฉ๋ฒ•์ž…๋‹ˆ๋‹ค.

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,
    });
  }
}

๋ชจ๋“  30 ๋Œ“๊ธ€

๋ˆ„๊ตฐ๊ฐ€ ๋ฐฉ๊ธˆ ์ด๊ฒƒ์„ ํ–ˆ๊ณ  ๊ทธ๊ฒƒ์— ๋Œ€ํ•ด ํŠธ์œ—ํ–ˆ์Šต๋‹ˆ๋‹ค. ๋ฉ‹์ง„ ์˜ˆ๊ฐ€ ๋  ๊ฒƒ์ž…๋‹ˆ๋‹ค.

ํŠธ์œ—์„ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค! ๋‹น์‹ ์€ ๊ทธ๊ฒƒ์„ ๊ฐ€์ง€๊ณ  ์žˆ์Šต๋‹ˆ๊นŒ?

๋‚ด ์งน์งน์ด - ๋ฐฉ๊ธˆ index.js (๋ชจ๋“  ํ•ซ๋กœ๋“œ๊ฐ€ ๋ฐœ์ƒํ•˜๋Š” ๊ณณ)๋ฅผ ๋‹ค์Œ์œผ๋กœ ๋Œ€์ฒดํ–ˆ์Šต๋‹ˆ๋‹ค.

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)
}

๊ทธ๋Ÿฐ ๋‹ค์Œ npm run build ๋ฅผ ์‹คํ–‰ํ•˜๊ณ  node_modules/ ๋ฅผ build/ ๋ณต์‚ฌํ•˜๊ณ  node_modules/ ํด๋”์™€ ํ•จ๊ป˜ server.js ์••์ถ•ํ•˜์—ฌ AWS Lambda์— ์—…๋กœ๋“œํ•ฉ๋‹ˆ๋‹ค. public/ ๋ชจ๋“  ํ•ญ๋ชฉ์€ S3์— ์—…๋กœ๋“œ๋˜๊ณ  Cloudfront๋ฅผ ํ†ตํ•ด ์ „์†ก๋ฉ๋‹ˆ๋‹ค.

Lambda -> API ๊ฒŒ์ดํŠธ์›จ์ด ์ธํ”„๋ผ์˜ ๊ฒฝ์šฐ Terraform์„ ์‚ฌ์šฉํ•˜๊ณ  ์ด ๊ฐ€์ด๋“œ๋ฅผ ๋”ฐ๋ž์Šต๋‹ˆ๋‹ค. https://www.terraform.io/docs/providers/aws/guides/serverless-with-aws-lambda-and-api-gateway.html

๊ทธ๋ž˜๋„ ์™„๋ฒฝํ•˜์ง€๋Š” ์•Š์Šต๋‹ˆ๋‹ค. ํด๋ผ์ด์–ธํŠธ ์ธก ๋ฒˆ๋“ค(https://static.jobsok.io/)์—์„œ ๋กœ๋“œ ์‹œ ์ด ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ•ฉ๋‹ˆ๋‹ค.

image


์—…๋ฐ์ดํŠธ:
๋‚˜๋Š” ๋‹น์‹ ์„ ์  ์žฅํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค. node_modules๋ฅผ ์‚ญ์ œํ•˜๊ณ  ๋‹ค์‹œ ์„ค์น˜ํ•˜๋ฉด ๋‹ค์Œ๊ณผ ๊ฐ™์Šต๋‹ˆ๋‹ค.

File sizes after gzip:

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

์ฝ”๋“œ ๋ณ€๊ฒฝ ์—†์ด - ํด๋ผ์ด์–ธํŠธ ์ธก ๋ฒˆ๋“ค์ด ๋‹ค์‹œ ์ž‘๋™ํ•˜๋„๋ก ๋งŒ๋“ค์—ˆ์Šต๋‹ˆ๋‹ค.

๋‚ด handler.js(๋˜๋Š” index.js)๋Š” ๋™์ผํ•ฉ๋‹ˆ๋‹ค.
๊ทธ๋Ÿฐ ๋‹ค์Œ webpack(project + node_modules)์œผ๋กœ ๋ชจ๋“  ๊ฒƒ์„ ๋นŒ๋“œํ•ฉ๋‹ˆ๋‹ค. (์ด๋ฅผ ์œ„ํ•ด razzle.config.js๋ฅผ ์ˆ˜์ •ํ•˜๋ฉด ๋ฉ๋‹ˆ๋‹ค.)

์ €๋Š” Terraform์ด ์•„๋‹Œ ์„œ๋ฒ„๋ฆฌ์Šค๋กœ ์ž‘์—…ํ•˜๊ณ  ์žˆ์Šต๋‹ˆ๋‹ค. ๊ธฐ๋ณธ์ ์œผ๋กœ ๋™์ผํ•ฉ๋‹ˆ๋‹ค. ํ•ธ๋“ค๋Ÿฌ์— ๋Œ€ํ•œ ์ƒ๋Œ€ ํŒจํ‚ค์ง€๋ฅผ ๊ฐ€์ ธ์˜ค๊ธฐ๋งŒ ํ•˜๋ฉด ๋ฉ๋‹ˆ๋‹ค(์–ด๋–ค ์ด์œ ๋กœ webpack์€ ์ด ํŒจํ‚ค์ง€๋ฅผ ํฌ์žฅํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค. ์•„๋งˆ๋„ ๋‹ค๋ฅธ ํŒŒ์ผ/๊ฒฝ๋กœ์— ์žˆ๊ธฐ ๋•Œ๋ฌธ์— ๋‚˜์ค‘์— ์ˆ˜์ •๋  ๊ฒƒ์ž…๋‹ˆ๋‹ค).

์„œ๋ฒ„๋ฆฌ์Šค๋กœ ์ด ๋ชจ๋“  ๊ฒƒ์„ ์—…๋กœ๋“œํ•˜๋ฉด ๋ฉ๋‹ˆ๋‹ค. ํ”„๋กœ์ ํŠธ๋Š” https://github.com/CuistotduCoin/cuistot ์—์„œ ๋ณผ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

์™„๋ฒฝํ•จ๊ณผ๋Š” ๊ฑฐ๋ฆฌ๊ฐ€ ๋ฉ€๋‹ค.

๊ทธ๊ฑด ๊ทธ๋ ‡๊ณ , @jaredpalmer , razzle/webpack ์„ค์ •์— ๋Œ€ํ•ด ์ €๋ฅผ ๋„์™€์ฃผ์‹œ๊ฒ ์Šต๋‹ˆ๊นŒ?
์ตœ๊ณ ์˜ ์„ฑ๋Šฅ์œผ๋กœ ๋ฒˆ๋“ค(node_modules ํฌํ•จ)์— ํ•„์š”ํ•œ ๋ชจ๋“  ๊ฒƒ์„ ํฌํ•จํ•˜๊ณ  ์‹ถ์Šต๋‹ˆ๋‹ค. ์˜ˆ/ํŒ์ด ์žˆ์Šต๋‹ˆ๊นŒ? (Typescript ๊ตฌ์„ฑ ์‚ฌ์šฉ)

@rozenmd razzle.config ๋ฅผ ๊ณต์œ ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๊นŒ?

@romainquellec ๊ด‘์‚ฐ์ด ๋น„์–ด ์žˆ์Šต๋‹ˆ๋‹ค -

๊ธ€์Ž„์š”, ์ด์œ ๋Š” ๋ชจ๋ฅด๊ฒ ์ง€๋งŒ ์ข…์†์„ฑ์— ์ •์˜๋œ aws-serverless-express์— ์•ก์„ธ์Šคํ•  ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค(๋” ๋งŽ์„ ์ˆ˜๋„ ์žˆ์Œ).
๊ทธ๋ฆฌ๊ณ  ์ด ๋ชจ๋“  ๊ฒƒ์„ ์ตœ์ ํ™”ํ•˜๊ธฐ ์œ„ํ•ด ์ตœ์ข… ๋นŒ๋“œ์—์„œ aws-sdk๋ฅผ ์ œ์™ธํ•˜๊ณ  ์‹ถ์Šต๋‹ˆ๋‹ค(ํ•ญ์ƒ ๋žŒ๋‹ค ํ•จ์ˆ˜์—์„œ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ์ง€๋งŒ ๋‚˜์ค‘์— ๋ณผ ์ˆ˜ ์žˆ์Œ)

๋‚˜๋Š” ๊ทธ๊ฒƒ์ด ์™ธ๋ถ€์—์„œ ์˜ค๋Š” ๊ฒƒ์ด๋ผ๊ณ  ์ƒ๊ฐํ•˜์ง€๋งŒ ๋‚˜๋Š” ๊ทธ๊ฒƒ์„ ์•Œ์•„๋‚ผ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค.

์˜ˆ, ๋‚˜๋Š” ๊ฐ™์€ ๋ฌธ์ œ๊ฐ€์žˆ์—ˆ์Šต๋‹ˆ๋‹ค. ๋‚ด ์œ ์ผํ•œ ํ•ด๊ฒฐ์ฑ…์€ ๋ชจ๋“  node_modules๋กœ server.js๋ฅผ ์••์ถ•ํ•˜๋Š” ๊ฒƒ์ด ์—ˆ์Šต๋‹ˆ๋‹ค.

๋‚˜๋Š” ๋žŒ๋‹ค์—์„œ ํ•  ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค. ๊ณต์‹ ์ œํ•œ ํฌ๊ธฐ๋Š” 50mo์ž…๋‹ˆ๋‹ค.
๋ชจ๋‘๋ฅผ ์œ„ํ•œ ์ผ๋ฐ˜์ ์ธ ์งˆ๋ฌธ: node_modules๋ฅผ razzle๊ณผ ํ•จ๊ป˜ ์ข‹์€ ์„ฑ๋Šฅ์œผ๋กœ ๋ฌถ๋Š” ๋ฐฉ๋ฒ•. ๊ฐ์‚ฌ ํ•ด์š” !

์šฐ๋ฆฌ๋Š” ๋žŒ๋‹ค๋ฅผ ์‚ฌ์šฉํ•˜์ง€ ์•Š์ง€๋งŒ razzle.config.js์—์„œ ๊ธฐ๋ณธ ์™ธ๋ถ€ ์˜ต์…˜์„ ์ œ๊ฑฐํ•˜์—ฌ ๋ชจ๋“  node_modules๋ฅผ ๋ฒˆ๋“ค๋กœ ์ œ๊ณตํ•ฉ๋‹ˆ๋‹ค. ์ด๊ฒƒ์€ ๋นŒ๋“œ ํด๋”๊ฐ€ ์‚ฌ์‹ค์ƒ ๋นŒ๋“œ ์•„ํ‹ฐํŒฉํŠธ์ž„์„ ์˜๋ฏธํ•˜๊ธฐ ๋•Œ๋ฌธ์— ๋ฉ‹์ง€๋‹ค. ๊ทธ๋Ÿฐ ๋‹ค์Œ s3์— ๋„ฃ์€ ๋‹ค์Œ Jenkins ์ž‘์—…์ด ์ƒˆ ํด๋”๋ฅผ ๊ฐ ์„œ๋ฒ„๋กœ ๊ฐ€์ ธ์˜ค๊ณ  pm2๋ฅผ ๋‹ค์‹œ ์‹œ์ž‘ํ•ฉ๋‹ˆ๋‹ค.

@jaredpalmer - razzle.config.js ๋กœ ์š”์ ์„ ๊ฒŒ์‹œํ•  ์ˆ˜ ์žˆ๋Š” ๊ธฐํšŒ๊ฐ€ ์žˆ์Šต๋‹ˆ๊นŒ?

razzle.config.js์—์„œ ๊ธฐ๋ณธ ์™ธ๋ถ€ ์˜ต์…˜์„ ์ œ๊ฑฐํ•˜์—ฌ:

๊ทธ๋ ‡๊ฒŒ?
modify(config, { target, dev }) { if (target === 'node' && !dev) { config.externals = []; } return config; }

@romainquelec ์ž‘๋™ํ•ฉ๋‹ˆ๋‹ค!!

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
  },
}

์ž‘๋™ ์ค‘์ž…๋‹ˆ๋‹ค. ์ข‹์€ ! ๐Ÿ‘ ๐Ÿ‘
์˜ˆ๋ฅผ ์ถ”๊ฐ€ํ•˜๋Š” ๊ฒƒ์ด ์ข‹์Šต๋‹ˆ๋‹ค! ์ด๋ฒˆ ์ฃผ๋ง์— ์‚ดํŽด๋ณด๊ฒ ์Šต๋‹ˆ๋‹ค. @rozenmd์— ์ฐธ์—ฌ

๋ถˆํ–‰ํžˆ๋„ ์ด๋ฒˆ ์ฃผ๋ง @romainquellec์— ์ž์œ  ์‹œ๊ฐ„์ด ์—†์ง€๋งŒ PR์— ๋Œ€ํ•ด ๊ฒ€ํ† /ํ˜‘๋ ฅํ•˜๊ฒŒ ๋˜์–ด ๊ธฐ์ฉ๋‹ˆ๋‹ค.

๋‚˜๋Š” ์ด๊ฒƒ์„ ๋‹ซ๋Š”๋‹ค. ๊ฐ€๋Šฅํ•œ ํ•œ ๋นจ๋ฆฌ PR์„ ์ œ์ถœํ•  ๊ฒƒ์ž…๋‹ˆ๋‹ค.

@romainquellec ๊ทธ ํ’€ ๋ฆฌํ€˜์ŠคํŠธ๋ฅผ ํ•ด๋ณธ ์ ์ด ์žˆ์Šต๋‹ˆ๊นŒ? ์—ฌ์ „ํžˆ ์„œ๋ฒ„๋ฆฌ์Šค ํ™˜๊ฒฝ์—์„œ Razzle ํ”„๋กœ์ ํŠธ๋ฅผ ์‹คํ–‰ํ•˜๊ณ  ์žˆ์Šต๋‹ˆ๊นŒ? ๋‚˜๋Š”์ด ๊ฐ€๋Šฅ์„ฑ์„ ํƒ๊ตฌํ•˜๊ธฐ ์‹œ์ž‘ํ–ˆ๊ณ ์ด ์Šค๋ ˆ๋“œ๋ฅผ ์šฐ์—ฐํžˆ ๋ฐœ๊ฒฌํ–ˆ์Šต๋‹ˆ๋‹ค. :)

@mbrochh ๊ฒฐ๊ตญ ํ’€ ๋ฆฌํ€˜์ŠคํŠธ๋ฅผ ๋ณด์ง€ ๋ชปํ–ˆ์ง€๋งŒ ์ €๋Š” ์—ฌ์ „ํžˆ ๋ชจ๋“  ์„œ๋ฒ„๋ฆฌ์Šค ์„œ๋ฒ„์‚ฌ์ด๋“œ ๋ Œ๋”๋ง์„ ์œ„ํ•ด https://onlineornot.com ์—์„œ Razzle์„ ์‚ฌ์šฉํ•˜๊ณ 

๋‚˜๋Š” ๋˜ํ•œ ์ด ๋ธ”๋กœ๊ทธ ๊ฒŒ์‹œ๋ฌผ์„ ๋ฐœ๊ฒฌํ–ˆ์Šต๋‹ˆ๋‹ค: https://medium.com/@RozenMD/build -your-own-app-with-react-graphql-and-serverless-architecture-part-1-server-side-rendering-f0d0144ff5f

๋‚˜๋Š” ๋จธ๋ฆฌ ์šฐ์„ ์œผ๋กœ ๋‹ค์ด๋น™ํ•˜๊ณ  ๊ทธ ๊ฒŒ์‹œ๋ฌผ์„ ๋”ฐ๋ผ๊ฐ€๋ ค๊ณ  ๋…ธ๋ ฅํ•ด์•ผ ํ•  ๊ฒƒ ๊ฐ™์•„์š” :)

๊ฒŒ์‹œ๋ฌผ์— ๊ฐ์‚ฌ๋“œ๋ฆฝ๋‹ˆ๋‹ค!

https://maxrozen.com/2018/08/08/start-your-own-app-with-react-part-1 ์ด ์ฝ”๋“œ ํ˜•์‹ ์ง€์ •์— ๋Œ€ํ•œ ๋” ๋‚˜์€ ๋งํฌ์ผ ๊ฒƒ์ž…๋‹ˆ๋‹ค.

๊ทธ๋ฆฌ๊ณ  ๊ณ ๋งˆ์›Œ! :์›ƒ๋‹ค:

Razzle์„ AWS ๋žŒ๋‹ค์— ๋ฐฐํฌํ•œ ์‚ฌ๋žŒ์ด ์žˆ์Šต๋‹ˆ๊นŒ?

๋˜ํ•œ AWS ๋žŒ๋‹ค์—์„œ Razzle์— ๋Œ€ํ•œ ์˜ˆ๋ฅผ ์ฐพ๊ณ  ์žˆ์Šต๋‹ˆ๋‹ค. ๋ˆ„๊ตฐ๊ฐ€๊ฐ€ ๊ทธ๊ฒƒ์„ํ–ˆ๊ณ  ์šฐ๋ฆฌ์™€ ๊ณต์œ ํ•˜๊ฒŒ๋œ๋‹ค๋ฉด ์ •๋ง ๊ฐ์‚ฌํ•˜๊ฒ ์Šต๋‹ˆ๋‹ค.

์ด๊ฒƒ์ด ๋‚ด๊ฐ€ AWS CDK๋ฅผ ์‚ฌ์šฉ ํ•˜์—ฌ AWS Lambda์— Razzle ์•ฑ์„ ๋ฐฐํฌํ•œ ๋ฐฉ๋ฒ•์ž…๋‹ˆ๋‹ค.

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์˜ razzletack ์„ค์ •์— ๋Œ€ํ•ด ์„ค๋ช…ํ•ฉ๋‹ˆ๋‹ค. ์ด๊ฒƒ์„ ์‚ฌ์šฉํ•˜๋Š” ๋ฐฉ๋ฒ•์„ ๋‹จ๊ณ„๋ณ„๋กœ ์ˆ˜ํ–‰ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๊นŒ? razzle ๋ฌธ์„œ์— ์ถ”๊ฐ€ํ•˜๊ณ  ์‹ถ์Šต๋‹ˆ๋‹ค.

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

๋„์›€์ด ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค. ๋ˆ„๊ฐ€ ๋„์™€์ค„ ์ˆ˜ ์žˆ์Šต๋‹ˆ๊นŒ?

@fivethreeo ๋Š” ๋‹จ๊ณ„๋ณ„๋กœ AWS ๊ณ„์ •์—์„œ CDK๋ฅผ ์„ค์ •ํ•˜๊ณ  ๋ช‡ ๊ฐ€์ง€ ์ถ”๊ฐ€ ํ”Œ๋ž˜๊ทธ๋กœ cdk deploy ๋ฅผ ์‹คํ–‰ํ•˜๋Š” ๊ฒƒ๋ณด๋‹ค ๋ผ์ด๋ธŒ \o/

https://razzle-git-canary.jared.vercel.app/deployment-options/aws ์—์„œ ๋ˆ„๋ฝ๋œ ๊ฒƒ์ด ์žˆ์Šต๋‹ˆ๊นŒ?
?

๊ฒฝ๋กœ๊ฐ€ ์•„๋‹Œ ๋„๋ฉ”์ธ์œผ๋กœ ๋ฌด๋Œ€๋ฅผ ๋งŒ๋“ค๊ณ  ์‹ถ์Šต๋‹ˆ๋‹ค. rds, ํ‚ค ํšŒ์ „, TypeOrm, typegraphql, Formik ๋ฐ jwt๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ์™„์ „ํ•œ ๊ธฐ๋Šฅ์˜ ์˜ˆ๋ฅผ ๋งŒ๋“ค ์ˆ˜ ์žˆ์ง€๋งŒ razzle ์ž์ฒด์˜ ์˜ˆ๋Š” ์•„๋‹™๋‹ˆ๋‹ค.

๋‹ฌ๋ฆฌ ๋น ์ง„ ๊ฒƒ์€ ์—†์Šต๋‹ˆ๋‹ค.

์ด ํŽ˜์ด์ง€๊ฐ€ ๋„์›€์ด ๋˜์—ˆ๋‚˜์š”?
0 / 5 - 0 ๋“ฑ๊ธ‰