Upgrade to Pro — share decks privately, control downloads, hide ads and more …

JavaScript(TypeScript)で メディアサイトを インフラから構築する方法 /...

JavaScript(TypeScript)で メディアサイトを インフラから構築する方法 / jsconf-jp-2021

Hidetaka Okamoto

November 27, 2021
Tweet

More Decks by Hidetaka Okamoto

Other Decks in Programming

Transcript

  1. About me • Ԭຊलߴ(Okamoto Hidetaka) • JS(TS) developer • React

    / NestJS / AWS CDK • AWS / Stripe / Alexa / Shifter • དྷि͔Β࠶ͼձࣾһ
  2. Talk about… • My Blog ( https://wp-kyoto.net ) • Backend:

    WordPress • Frontend: Next.js (with Ionic) • Infrastructure: AWS (AWS CDK) • Misc: Stripe / AWS Amplify / Algolia
  3. WebαΠτߏஙͰͷΠϯϑϥͷଟ༷Խ(Ұྫ) • αʔόʔ΍ωοτϫʔΫΛ෺ཧ͔Β༻ҙɾηοτΞοϓ • αʔόʔΛआΓͯɺͦͷதʹLAMP / MEAN stackͳͲΛߏ੒ (VPS) •

    ίϯςφΛར༻ͨ͠ɺImmutableͳ؀ڥΛڞ༗ɾσϓϩΠ (GAE/EKS) • ϑϧϚωʔδυͳSaaSΛ࢖ͬͯɺΞϓϦͷΈσϓϩΠ (Netlify / Vercel) • ΞϓϦ΋ϚωʔδυܕͷSaaSͰɺαΠτӡӦʹूத (WP.com/STUDIO)
  4. Πϯϑϥͷίʔυ؅ཧ “Infrastructure as Code (IaC)” • Chef / Ansible /

    terraform / Serverless Framework / Docker / etc… • ʮ͋Δ΂͖ߏ੒ʯΛίʔυͰఆٛ͠ɺVCSͰ؅ཧ͢Δ • ʮͦͷίʔυ͕͋Ε͹ɺ୭Ͱ΋ಉ͡؀ڥ͕࡞ΕΔʯ • ʮԶͷ؀ڥͰ͸ಈ͍͚ͨͲʁʯͷղফ • αʔόʔ૿ڧ΍ೖΕସ͑࣌ͷޮ཰Խͱ࡞ۀϛεͷ༧๷
  5. Serverless Framework (JS Objectܕ) import type { AWS } from

    '@serverless/typescript'; const serverlessConfiguration: AWS = { service: 'aws-nodejs-typescript', frameworkVersion: '*', provider: { name: 'aws', runtime: 'nodejs12.x', }, functions: { hello: { handler: 'handler.hello', events: [ { http: { method: 'get', path: 'hello', } } ] } } } • AWSͷΈରԠ(2021/11) • ܕ৘ใ: @types/serverless • AWS Lambda + API GW
  6. AWS CDK (Classܕ) import * as core from "@aws-cdk/core"; import

    * as apigateway from "@aws-cdk/aws-apigateway"; import * as lambda from "@aws-cdk/aws-lambda"; export class WidgetService extends core.Construct { constructor(scope: core.Construct, id: string) { super(scope, id); const handler = new lambda.Function(this, "WidgetHandler", { runtime: lambda.Runtime.NODEJS_14_X, // So we can use async in widget.js code: lambda.Code.fromAsset("resources"), handler: "main", }); bucket.grantReadWrite(handler); // was: handler.role); const api = new apigateway.RestApi(this, "widgets-api", { restApiName: "Widget Service", description: "This service serves widgets." }); const getWidgetsIntegration = new apigateway.LambdaIntegration(handler, { requestTemplates: { "application/json": '{ "statusCode": "200" }' } }); api.root.addMethod("GET", getWidgetsIntegration); // GET / } } • CloudFormationͷϥούʔ • AWSϦιʔεఆٛΛ TypeScriptͰॻ͚Δ • CloudFormation YAML΁ͷ ม׵Ͱ୤ग़Մೳ
  7. Adapt (JSXܕ) import Adapt, { Group, handle } from "@adpt/core";

    import { UrlRouter } from "@adpt/cloud/http"; import { NodeService, ReactApp } from "@adpt/cloud/nodejs"; import { Postgres } from "@adpt/cloud/postgres"; import { k8sProdStyle, k8sTestStyle, laptopStyle } from "./ styles"; function App() { const pg = handle(); const app = handle(); const api = handle(); return <Group> <UrlRouter port={8080} routes={[ { path: "/api/", endpoint: api }, { path: "/", endpoint: app } ]} /> <ReactApp handle={app} srcDir="../frontend" /> <NodeService handle={api} srcDir="../backend" connectTo={pg} /> <Postgres handle={pg} /> </Group>; } • React JSXͰߏ੒ఆٛ • AWSҎ֎Ͱ΋࢖͑Δ • v0.xͷͨΊɺ prodಋೖ͸৻ॏʹ
  8. JSΛ࢖ͬͨΠϯϑϥఆٛ • Class / Object / JSXͳͲɺ༷ʑͳه๏͕Մೳ • TypeScriptͷܕఆٛΛ׆༻࣮ͯ͠૷͕جຊܗ •

    AWS / GCPͳͲͷαʔϏεಠࣗͷઃఆ஋ఆٛʹTSͷྗ͕࢖͑Δ • ʢ؍ଌൣғͰ͸ʣެࣜͰπʔϧΛϦϦʔε͍ͯ͠ΔAWS͕ڧΊ
  9. AWS CDK import * as core from "@aws-cdk/core"; import *

    as apigateway from "@aws-cdk/aws-apigateway"; import * as lambda from "@aws-cdk/aws-lambda"; export class WidgetService extends core.Construct { constructor(scope: core.Construct, id: string) { super(scope, id); const handler = new lambda.Function(this, "WidgetHandler", { runtime: lambda.Runtime.NODEJS_14_X, // So we can use async in widget.js code: lambda.Code.fromAsset("resources"), handler: "main", }); bucket.grantReadWrite(handler); // was: handler.role); const api = new apigateway.RestApi(this, "widgets-api", { restApiName: "Widget Service", description: "This service serves widgets." }); const getWidgetsIntegration = new apigateway.LambdaIntegration(handler, { requestTemplates: { "application/json": '{ "statusCode": "200" }' } }); api.root.addMethod("GET", getWidgetsIntegration); // GET / } }
  10. CloudFormationΛ TypeScriptͰ هड़͢Δ const bucket = new s3.CfnBucket(this, "MyBucket", {

    bucketName: "MyBucket", corsConfiguration: { corsRules: [{ allowedOrigins: ["*"], allowedMethods: ["GET"] }] } }); const cfnDistribution = new cloudfront.CfnDistribution(this, 'MyCfnDistribution', { distributionConfig: { enabled: false, origins: [{ domainName: 'domainName', id: 'id', // the properties below are optional connectionAttempts: 123, connectionTimeout: 123, originCustomHeaders: [{ headerName: 'headerName', headerValue: 'headerValue', }], originPath: 'originPath', originShield: { enabled: false, originShieldRegion: 'originShieldRegion', }, s3OriginConfig: { originAccessIdentity: 'originAccessIdentity', }, }], … • AWSϦιʔεΛ 1ͭ1ͭఆٛ • 👍 ৄࡉͳઃఆ͕Մೳ • 😖 AWSͷ஌͕ࣝଟ͘ඞཁ
  11. AWS CDKͷ Construct Library Λར༻͢Δ import * as core from

    "@aws-cdk/core"; import * as apigateway from "@aws-cdk/aws-apigateway"; import * as lambda from "@aws-cdk/aws-lambda"; import * as s3 from "@aws-cdk/aws-s3"; export class WidgetService extends core.Construct { constructor(scope: core.Construct, id: string) { super(scope, id); const bucket = new s3.Bucket(this, "WidgetStore"); const handler = new lambda.Function(this, "WidgetHandler", { runtime: lambda.Runtime.NODEJS_14_X, // So we can use async in widget.js code: lambda.Code.fromAsset("resources"), handler: "widgets.main", environment: { BUCKET: bucket.bucketName } }); bucket.grantReadWrite(handler); // was: handler.role); const api = new apigateway.RestApi(this, "widgets-api", { restApiName: "Widget Service", description: "This service serves widgets." }); const getWidgetsIntegration = new apigateway.LambdaIntegration(handler, { requestTemplates: { "application/json": '{ "statusCode": "200" }' } }); api.root.addMethod("GET", getWidgetsIntegration); // GET / } } • CFNϦιʔεΛ ந৅Խͨ͠ϥΠϒϥϦ܈ • 👍 ͋Δఔ౓ఆٛࡁΈͷ ɹߏ੒͕࢖͑Δ • 😖 ઃఆ͕not for meͷ৔߹ ɹ݁ہίʔυΛॻ͘͜ͱʹ
  12. @aws-cdk/aws- lambda-nodejs ͕ศར new lambda.NodejsFunction(this, 'my-handler', { bundling: { commandHooks:

    { beforeBundling(inputDir: string, outputDir: string): string[] { return [ `echo hello > ${inputDir}/a.txt`, ]; }, afterBundling(inputDir: string, outputDir: string): string[] { return [`cp ${inputDir}/b.txt ${outputDir}/txt`]; }, beforeInstall() { return []; }, }, minify: true, // minify code, defaults to false sourceMap: true, // include source map, defaults to false target: 'es2020', loader: { // Use the 'dataurl' loader for '.png' files '.png': 'dataurl', }, define: { // Replace strings during build time 'process.env.API_KEY': JSON.stringify('xxx-xxxx-xxx'), }, logLevel: lambda.LogLevel.SILENT, // defaults to LogLevel.WARNING keepNames: true, // defaults to false tsconfig: 'custom-tsconfig.json', // use custom-tsconfig.json instead of default, … • TypeScriptͷίʔυΛ esbuildͰίϯύΠϧͯ͠ AWS LambdaʹσϓϩΠ • Lambdaͷ࣮૷ͱߏ੒ ྆ํ·ͱΊͯTSͰ؅ཧͰ͖Δ
  13. serverless-nextjs for AWS CDK // Πϯϑϥఆٛ import { NextJSLambdaEdge }

    from "@sls-next/cdk-construct"; const app = new NextJSLambdaEdge(this, "NextJsApp", { serverlessBuildOutDir: "./build", domain: { domainNames: ["wp-kyoto.net"], certificate: Certificate.fromCertificateArn( this, “WPKYOTOACM", "arn:aws:acm:us-east-1:xxxxxx" ), }, }); ==================================== // Next.jsͷϏϧυઃఆ import * as cdk from "@aws-cdk/core"; import { Builder } from "@sls-next/lambda-at-edge"; const builder = new Builder(".", "./build", { args: ["build"] }); builder .build().then(() => { const app = new cdk.App(); new MyStack(app, `MyStack`); }) • NextJSLambdaEdgeͰ Next.jsΛCDNΤοδʹσϓϩΠ • CDKͳͷͰΧελϚΠζ΋༰қ • Next.jsͷ࣮૷ʹखΛग़͞ͳ͍ͷ Ͱɺ؆୯ʹࣙΊΕΔ
  14. CDKͰͷ ΧελϚΠζྫ const app = new NextJSLambdaEdge(…); // Ωϟογϡઃఆ app.nextLambdaCachePolicy

    = new CachePolicy(this, "NextLambdaCache", { queryStringBehavior: CacheQueryStringBehavior.all(), headerBehavior: CacheHeaderBehavior.allowList( "Authorization", “Origin" ), cookieBehavior: { behavior: "all", }, defaultTtl: Duration.seconds(0), maxTtl: Duration.days(365), minTtl: Duration.seconds(0), enableAcceptEncodingBrotli: true, enableAcceptEncodingGzip: true, }); // AWSϦιʔε΁ͷΞΫηεݖ const targets = [app.defaultNextLambda, app.defaultNextLambda]; targets.forEach((target) => { target.addToRolePolicy( new PolicyStatement({ resources: ["*"], actions: ["ssm:GetParameter"], }) ); }); • ΫϥεͷpropsΛ ্ॻ͖ͯ͠ߋ৽ • CDNઃఆΛҰ෦্ॻ͖ • AWSͷΞΫηεݖݶΛ ֦ு
  15. σΟϨΫτϦߏ੒ !"" README.md !"" next-env.d.ts !"" next.config.js !"" tsconfig.json !""

    package.json !"" pages !"" public !"" styles !"" components !"" hooks !"" tests # %"" cdk !"" tsconfig.cdk.json !"" cdk # !"" bin # %"" serverless-nextjs-cdk-example-stack.ts !"" cdk.json !"" jest.cdk.config.js %"" yarn.lock • Next.jsجຊߏ੒ʹ cdkσΟϨΫτϦΛ௥Ճ • CDKελοΫͷTS͸ ผ్tsconfigΛ࡞੒
  16. AWS AmplifyΛ Overwrite͢Δ import { AmplifyS3ResourceTemplate } from '@aws-amplify/cli-extensibility-helper'; export

    function override( resources: AmplifyS3ResourceTemplate ) { resources.s3Bucket.versioningConfiguration = { status: "Enabled" } } • amplify override <resource> • 👍 ΧελϜ͍ͨ͠৔ॴ͚ͩ ɹίʔυͰมߋͰ͖Δ • 😖 શϦιʔεʹ͸ ɹ·ͩະରԠ
  17. AWSͰJSΛ࢖ͬͯΠϯϑϥΛఆٛ͢Δ • REST API։ൃத৺ -> Serverless FW or AWS CDK

    (Nodejs Construct) • ࡉ͔͘ઃఆΛॻ͖͍ͨΘ͚Ͱ͸ແ͍ -> AWS Amplify (+ amplify overrideͰAWS CDKʣ • Next.jsΛ྿Ձ͔ͭServerlessʹӡ༻͍ͨ͠ -> Serverless Next.js • ΄΅CloudFormationΛॻ͘ͷʹ͍ۙ -> AWS CDK
  18. WordPress / GraphQL / Next.js -> Faust.js • WP EngineʢWPϗεςΟϯάձࣾʣ͕ϦϦʔε

    • WordPressͷϑϩϯτΤϯυΛNext.jsʹ͢ΔͨΊͷFW (ݱঢ়) • WordPress؅ཧը໘࿈ܞ • ϓϨϏϡʔػೳ • GraphQLεΩʔϚ΍Auth༻React Hookͷఏڙ etc…
  19. import { GetServerSidePropsContext } from 'next'; import { getNextServerSideProps }

    from '@faustjs/next'; import { client } from 'client'; export default function MyPage() { const { usePosts } = client; return ( <> <h2>Recent Posts</h2> {usePosts()?.nodes.map((post) => ( <li key={post.id}>{post.title()}</li> ))} </> ); } export async function getServerSideProps(context: GetServerSidePropsContext) { return getNextServerSideProps(context, { Page: MyPage, client, }); } WPͷσʔλऔಘ • Faust.jsఏڙͷ Hook΍ؔ਺Λ࢖͏ • GQtyͰGraphQLͷ APIݺͼग़͠ • WPଆͷ࢓༷Λҙࣝͤͣ࢖͑Δ
  20. JavaScript(TS)ͰϝσΟΞαΠτΛ࡞Δ • αʔϏεɾπʔϧͷਐԽʹΑΓɺ JS͚ͩͰ࣮૷ɾఆٛͰ͖ΔྖҬ͕૿͑ͨ • Construct / 3rd party libraryʹΑΓɺ

    ʮத਎Λҙࣝͤͣʹಈ͔ͤΔ؀ڥઃఆʯ΋ೖख͕༰қʹ • ʮ0͔1͔ʯͰ͸ͳ͍ɻ͕ɺ ʮࣗ෼͚ͨͪͩͰ΋Ͱ͖Δํ๏ʯΛ஌͍ͬͯΔ͜ͱ͸ॏཁ
  21. Thanks • SNS: Twitter->@hide__dev GitHub->hideokamoto • Resources • AWS CDK:

    https://aws.amazon.com/jp/cdk/ • Serverless Framework: https://www.serverless.com/framework • Serverless Next.js: https://serverless-nextjs.com/ • Faust.js: https://faustjs.org/