├── .dockerignore ├── .gitignore ├── Dockerfile.server ├── Makefile ├── README.md ├── build-image-and-push.sh ├── cdk ├── .gitignore ├── .npmignore ├── README.md ├── bin │ └── cdk.ts ├── cdk.context.json ├── cdk.json ├── jest.config.js ├── lib │ └── stacks │ │ ├── next-server-stack.ts │ │ ├── repository-stack.ts │ │ ├── security-group-stack.ts │ │ ├── static-stack.ts │ │ └── vpc-stack.ts ├── package.json ├── test │ └── stacks │ │ ├── __snapshots__ │ │ └── vpc-stack.test.ts.snap │ │ └── vpc-stack.test.ts └── tsconfig.json ├── components ├── Layout.tsx ├── List.tsx ├── ListDetail.tsx └── ListItem.tsx ├── deploy.sh ├── interfaces └── index.ts ├── next-env.d.ts ├── package.json ├── pages ├── about.tsx ├── api │ └── users │ │ ├── [id].ts │ │ └── index.ts ├── index.tsx └── users │ ├── [id].tsx │ └── index.tsx ├── tsconfig.json ├── utils ├── sample-api.ts └── sample-data.ts └── yarn.lock /.dockerignore: -------------------------------------------------------------------------------- 1 | .git 2 | node_modules 3 | .next 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .next 3 | -------------------------------------------------------------------------------- /Dockerfile.server: -------------------------------------------------------------------------------- 1 | # syntax = docker/dockerfile:experimental 2 | 3 | FROM node:12-stretch-slim AS nextjs-on-ecs-server-builder 4 | 5 | USER node 6 | 7 | WORKDIR /app 8 | 9 | COPY --chown=node:node package.json yarn.lock ./ 10 | 11 | RUN mkdir -p /home/node/.cache 12 | RUN --mount=type=cache,target=/home/node/.cache,id=yarn-cache,sharing=private,uid=1000 yarn install --pure-lockfile 13 | 14 | COPY --chown=node:node . ./ 15 | 16 | RUN npm run build 17 | 18 | FROM node:12-stretch-slim AS nextjs-on-ecs-server 19 | 20 | WORKDIR /app 21 | 22 | COPY --from=nextjs-on-ecs-server-builder /app/package.json /app/yarn.lock ./ 23 | RUN --mount=type=cache,target=/home/node/.cache,id=yarn-cache,sharing=private,uid=1000 yarn install --pure-lockfile --production 24 | COPY --from=nextjs-on-ecs-server-builder /app/.next ./.next 25 | 26 | 27 | CMD ["npm", "run", "start"] 28 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | export DOCKER_BUILDKIT=1 2 | 3 | .PHONY: build 4 | build: 5 | docker build --file Dockerfile.server --target nextjs-on-ecs-server --tag nextjs-on-ecs-server:latest --progress plain . 6 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Next.js deployment sample using full CDN architecture (with CloudFront/S3/ECS/AWS CDK) 2 | 3 | ## Technologies 4 | 5 | - AWS 6 | - CloudFront 7 | - S3 8 | - ECR 9 | - ECS 10 | - AWS CDK 11 | 12 | ## Bootstrap 13 | 14 | ``` 15 | $ yarn install 16 | $ yarn workspace cdk run cdk bootstrap 17 | $ yarn workspace cdk run cdk deploy VpcStack SecurityGroupStack RepositoryStack 18 | $ ECR_BASE= ./build-image-and-push.sh 19 | $ yarn workspace cdk run cdk deploy --context application-version=$(git rev-parse --short HEAD) NextServerStack StaticStack 20 | $ ECR_BASE= ./deploy.sh 21 | ``` 22 | 23 | Then, visit your CloudFront URL. 24 | 25 | ## Deploy 26 | 27 | ``` 28 | $ ECR_BASE= ./deploy.sh 29 | ``` 30 | -------------------------------------------------------------------------------- /build-image-and-push.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -ex 4 | 5 | export DOCKER_BUILDKIT=1 6 | 7 | : ${REVISION:="$(git rev-parse --short HEAD)"} 8 | : ${ECR_BASE:=648803025740.dkr.ecr.ap-northeast-1.amazonaws.com} 9 | 10 | docker build --file Dockerfile.server --target nextjs-on-ecs-server --tag "nextjs-on-ecs-server:$REVISION" --progress plain . 11 | 12 | $(aws ecr get-login --no-include-email --region ap-northeast-1) 13 | docker tag "nextjs-on-ecs-server:$REVISION" "$ECR_BASE/nextjs-on-ecs-server:$REVISION" 14 | docker push "$ECR_BASE/nextjs-on-ecs-server:$REVISION" 15 | echo "$ECR_BASE/nextjs-on-ecs-server:$REVISION" 16 | -------------------------------------------------------------------------------- /cdk/.gitignore: -------------------------------------------------------------------------------- 1 | *.js 2 | !jest.config.js 3 | *.d.ts 4 | node_modules 5 | 6 | # CDK asset staging directory 7 | .cdk.staging 8 | cdk.out 9 | -------------------------------------------------------------------------------- /cdk/.npmignore: -------------------------------------------------------------------------------- 1 | *.ts 2 | !*.d.ts 3 | 4 | # CDK asset staging directory 5 | .cdk.staging 6 | cdk.out 7 | -------------------------------------------------------------------------------- /cdk/README.md: -------------------------------------------------------------------------------- 1 | # Welcome to your CDK TypeScript project! 2 | 3 | This is a blank project for TypeScript development with CDK. 4 | 5 | The `cdk.json` file tells the CDK Toolkit how to execute your app. 6 | 7 | ## Useful commands 8 | 9 | * `npm run build` compile typescript to js 10 | * `npm run watch` watch for changes and compile 11 | * `npm run test` perform the jest unit tests 12 | * `cdk deploy` deploy this stack to your default AWS account/region 13 | * `cdk diff` compare deployed stack with current state 14 | * `cdk synth` emits the synthesized CloudFormation template 15 | -------------------------------------------------------------------------------- /cdk/bin/cdk.ts: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | import "source-map-support/register"; 3 | import * as cdk from "@aws-cdk/core"; 4 | import { VpcStack } from "../lib/stacks/vpc-stack"; 5 | import { NextServerStack } from "../lib/stacks/next-server-stack"; 6 | import { SecurityGroupStack } from "../lib/stacks/security-group-stack"; 7 | import { StaticStack } from "../lib/stacks/static-stack"; 8 | import { RepositoryStack } from "../lib/stacks/repository-stack"; 9 | 10 | const app = new cdk.App(); 11 | 12 | const applicationVersion = app.node.tryGetContext("application-version"); 13 | if (applicationVersion === undefined) { 14 | throw new Error(`no application-version found`); 15 | } 16 | 17 | const vpcStack = new VpcStack(app, "VpcStack"); 18 | const securityGroupStack = new SecurityGroupStack(app, "SecurityGroupStack", { 19 | vpc: vpcStack.vpc 20 | }); 21 | const repositoryStack = new RepositoryStack(app, "RepositoryStack", {}); 22 | const nextServerStack = new NextServerStack(app, "NextServerStack", { 23 | vpc: vpcStack.vpc, 24 | nextServerAlbSg: securityGroupStack.nextServerAlbSg, 25 | nextServerEcsSg: securityGroupStack.nextServerEcsSg, 26 | nextServerRepository: repositoryStack.nextServerRepository, 27 | applicationVersion: applicationVersion 28 | }); 29 | const staticStack = new StaticStack(app, "StaticStack", { 30 | nextServerAlb: nextServerStack.nextServerAlb 31 | }); 32 | -------------------------------------------------------------------------------- /cdk/cdk.context.json: -------------------------------------------------------------------------------- 1 | { 2 | "@aws-cdk/core:enableStackNameDuplicates": "true", 3 | "aws-cdk:enableDiffNoFail": "true", 4 | "application-version": "b5a71af" 5 | } 6 | -------------------------------------------------------------------------------- /cdk/cdk.json: -------------------------------------------------------------------------------- 1 | { 2 | "app": "npx ts-node bin/cdk.ts" 3 | } 4 | -------------------------------------------------------------------------------- /cdk/jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | "roots": [ 3 | "/test" 4 | ], 5 | testMatch: [ '**/*.test.ts'], 6 | "transform": { 7 | "^.+\\.tsx?$": "ts-jest" 8 | }, 9 | } 10 | -------------------------------------------------------------------------------- /cdk/lib/stacks/next-server-stack.ts: -------------------------------------------------------------------------------- 1 | import * as cdk from "@aws-cdk/core"; 2 | import * as ec2 from "@aws-cdk/aws-ec2"; 3 | import * as ecs from "@aws-cdk/aws-ecs"; 4 | import * as elb from "@aws-cdk/aws-elasticloadbalancingv2"; 5 | import * as ecr from "@aws-cdk/aws-ecr"; 6 | import * as logs from "@aws-cdk/aws-logs"; 7 | import { Duration } from "@aws-cdk/core"; 8 | 9 | export interface NextServerStackProps { 10 | readonly vpc: ec2.IVpc; 11 | readonly nextServerAlbSg: ec2.ISecurityGroup; 12 | readonly nextServerEcsSg: ec2.ISecurityGroup; 13 | readonly nextServerRepository: ecr.IRepository; 14 | readonly applicationVersion: string; 15 | } 16 | 17 | export class NextServerStack extends cdk.Stack { 18 | public readonly nextServerAlb: elb.IApplicationLoadBalancer; 19 | 20 | constructor( 21 | scope: cdk.Construct, 22 | id: string, 23 | props: NextServerStackProps & cdk.StackProps 24 | ) { 25 | super(scope, id, props); 26 | 27 | const cluster = new ecs.Cluster(this, "nextjs-on-ecs-cluster", { 28 | clusterName: "nextjs-on-ecs-cluster", 29 | vpc: props.vpc 30 | }); 31 | 32 | const nextServerLogGroup = new logs.LogGroup( 33 | this, 34 | "nextjs-on-ecs-server-log-group", 35 | { 36 | logGroupName: "nextjs-on-ecs-server-log-group" 37 | } 38 | ); 39 | 40 | const nextServerTaskDef = new ecs.FargateTaskDefinition( 41 | this, 42 | "nextjs-on-ecs-server-taskdef", 43 | { 44 | family: "nextjs-on-ecs-server-taskdef" 45 | } 46 | ); 47 | const nextServerContainer = nextServerTaskDef.addContainer( 48 | "nextjs-on-ecs-server-container", 49 | { 50 | image: ecs.ContainerImage.fromEcrRepository( 51 | props.nextServerRepository, 52 | props.applicationVersion 53 | ), 54 | logging: new ecs.AwsLogDriver({ 55 | logGroup: nextServerLogGroup, 56 | streamPrefix: "server" 57 | }) 58 | } 59 | ); 60 | nextServerContainer.addPortMappings({ 61 | containerPort: 3000 62 | }); 63 | 64 | const nextServerService = new ecs.FargateService( 65 | this, 66 | "nextjs-on-ecs-server-service", 67 | { 68 | serviceName: "nextjs-on-ecs-server-service", 69 | cluster: cluster, 70 | taskDefinition: nextServerTaskDef, 71 | desiredCount: 1, 72 | securityGroup: props.nextServerEcsSg, 73 | vpcSubnets: { 74 | subnetType: ec2.SubnetType.PUBLIC 75 | }, 76 | assignPublicIp: true 77 | } 78 | ); 79 | 80 | const targetGroup = new elb.ApplicationTargetGroup( 81 | this, 82 | "nextjs-on-ecs-server-target", 83 | { 84 | targetGroupName: "nextjs-on-ecs-server-target", 85 | targetType: elb.TargetType.IP, 86 | port: 80, 87 | vpc: props.vpc, 88 | healthCheck: { 89 | healthyThresholdCount: 5, 90 | interval: Duration.seconds(5), 91 | timeout: Duration.seconds(3) 92 | }, 93 | deregistrationDelay: Duration.seconds(30) 94 | } 95 | ); 96 | targetGroup.addTarget(nextServerService); 97 | 98 | this.nextServerAlb = new elb.ApplicationLoadBalancer( 99 | this, 100 | "nextjs-on-ecs-server-alb", 101 | { 102 | loadBalancerName: "nextjs-on-ecs-server-alb", 103 | vpc: props.vpc, 104 | internetFacing: true, 105 | securityGroup: props.nextServerAlbSg 106 | } 107 | ); 108 | const listener = this.nextServerAlb.addListener( 109 | "nextjs-on-ecs-server-alb-listener", 110 | { 111 | port: 80, 112 | open: true 113 | } 114 | ); 115 | listener.addTargetGroups("nextjs-on-ecs-server-target-default", { 116 | targetGroups: [targetGroup] 117 | }); 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /cdk/lib/stacks/repository-stack.ts: -------------------------------------------------------------------------------- 1 | import * as cdk from "@aws-cdk/core"; 2 | import * as ecr from "@aws-cdk/aws-ecr"; 3 | 4 | export class RepositoryStack extends cdk.Stack { 5 | public readonly nextServerRepository: ecr.IRepository; 6 | 7 | constructor(scope: cdk.Construct, id: string, props: cdk.StackProps) { 8 | super(scope, id, props); 9 | 10 | this.nextServerRepository = new ecr.Repository( 11 | this, 12 | "nextjs-on-ecs-server-ecr", 13 | { 14 | repositoryName: "nextjs-on-ecs-server" 15 | } 16 | ); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /cdk/lib/stacks/security-group-stack.ts: -------------------------------------------------------------------------------- 1 | import * as cdk from "@aws-cdk/core"; 2 | import * as ec2 from "@aws-cdk/aws-ec2"; 3 | 4 | export interface SecurityGroupStackProps { 5 | readonly vpc: ec2.IVpc; 6 | } 7 | export class SecurityGroupStack extends cdk.Stack { 8 | public readonly nextServerAlbSg: ec2.SecurityGroup; 9 | public readonly nextServerEcsSg: ec2.SecurityGroup; 10 | 11 | constructor( 12 | scope: cdk.Construct, 13 | id: string, 14 | props: SecurityGroupStackProps & cdk.StackProps 15 | ) { 16 | super(scope, id, props); 17 | 18 | // NextのサーバのコンテナにつながるALBのセキュリティグループ 19 | this.nextServerAlbSg = new ec2.SecurityGroup(this, "next-server-alb-sg", { 20 | vpc: props.vpc, 21 | securityGroupName: "next-server-alb-sg" 22 | }); 23 | this.nextServerAlbSg.addIngressRule(ec2.Peer.anyIpv4(), ec2.Port.tcp(80)); 24 | 25 | this.nextServerEcsSg = new ec2.SecurityGroup(this, "next-server-ecs-sg", { 26 | vpc: props.vpc, 27 | securityGroupName: "next-server-ecs-sg" 28 | }); 29 | this.nextServerEcsSg.addIngressRule( 30 | ec2.Peer.ipv4(props.vpc.vpcCidrBlock), 31 | ec2.Port.tcp(3000) 32 | ); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /cdk/lib/stacks/static-stack.ts: -------------------------------------------------------------------------------- 1 | import * as cdk from "@aws-cdk/core"; 2 | import * as s3 from "@aws-cdk/aws-s3"; 3 | import * as cloudfront from "@aws-cdk/aws-cloudfront"; 4 | import * as iam from "@aws-cdk/aws-iam"; 5 | import * as elb from "@aws-cdk/aws-elasticloadbalancingv2"; 6 | import { Duration } from "@aws-cdk/core"; 7 | 8 | interface StaticStackProps { 9 | nextServerAlb: elb.IApplicationLoadBalancer; 10 | } 11 | export class StaticStack extends cdk.Stack { 12 | public readonly staticBucket: s3.Bucket; 13 | 14 | constructor( 15 | scope: cdk.Construct, 16 | id: string, 17 | props: cdk.StackProps & StaticStackProps 18 | ) { 19 | super(scope, id, props); 20 | 21 | this.staticBucket = new s3.Bucket(this, "nextjs-on-ecs-static-bucket", { 22 | bucketName: "nextjs-on-ecs-static-bucket", 23 | versioned: false, 24 | removalPolicy: cdk.RemovalPolicy.DESTROY 25 | }); 26 | 27 | // CloudFront で設定する オリジンアクセスアイデンティティ を作成する 28 | const oai = new cloudfront.OriginAccessIdentity( 29 | this, 30 | "nextjs-on-ecs-cloudfront-oai", 31 | { 32 | comment: "s3 access." 33 | } 34 | ); 35 | 36 | // CloudFront -> staticBucketへのアクセス許可 37 | this.staticBucket.grantRead(oai); 38 | 39 | const distribution = new cloudfront.CloudFrontWebDistribution( 40 | this, 41 | "nextjs-on-ecs-cloudfront", 42 | { 43 | defaultRootObject: "", 44 | viewerProtocolPolicy: cloudfront.ViewerProtocolPolicy.ALLOW_ALL, 45 | originConfigs: [ 46 | { 47 | s3OriginSource: { 48 | s3BucketSource: this.staticBucket, 49 | originAccessIdentity: oai 50 | }, 51 | behaviors: [ 52 | { 53 | pathPattern: "/_next/static/*", 54 | compress: true 55 | } 56 | ] 57 | }, 58 | { 59 | customOriginSource: { 60 | domainName: props.nextServerAlb.loadBalancerDnsName, 61 | originProtocolPolicy: cloudfront.OriginProtocolPolicy.HTTP_ONLY 62 | }, 63 | behaviors: [ 64 | { 65 | isDefaultBehavior: true, 66 | allowedMethods: cloudfront.CloudFrontAllowedMethods.ALL, 67 | forwardedValues: { 68 | queryString: true, 69 | cookies: { 70 | forward: "all" 71 | } 72 | }, 73 | maxTtl: Duration.seconds(0), 74 | minTtl: Duration.seconds(0), 75 | defaultTtl: Duration.seconds(0) 76 | } 77 | ] 78 | } 79 | ] 80 | } 81 | ); 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /cdk/lib/stacks/vpc-stack.ts: -------------------------------------------------------------------------------- 1 | import * as cdk from "@aws-cdk/core"; 2 | import * as ec2 from "@aws-cdk/aws-ec2"; 3 | 4 | export class VpcStack extends cdk.Stack { 5 | public readonly vpc: ec2.Vpc; 6 | 7 | // public/private subnet一つずつのVPCを作成する 8 | constructor(scope: cdk.Construct, id: string, props?: cdk.StackProps) { 9 | super(scope, id, props); 10 | 11 | this.vpc = new ec2.Vpc(this, "VPC", { 12 | cidr: "10.0.0.0/16", 13 | maxAzs: 2, 14 | subnetConfiguration: [ 15 | { 16 | cidrMask: 24, 17 | name: "Public", 18 | subnetType: ec2.SubnetType.PUBLIC 19 | } 20 | ] 21 | }); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /cdk/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "cdk", 3 | "version": "0.1.0", 4 | "bin": { 5 | "cdk": "bin/cdk.js" 6 | }, 7 | "scripts": { 8 | "build": "tsc", 9 | "watch": "tsc -w", 10 | "test": "jest", 11 | "cdk": "cdk" 12 | }, 13 | "devDependencies": { 14 | "@aws-cdk/assert": "^1.23.0", 15 | "@types/jest": "^25.1.2", 16 | "@types/node": "13.7.0", 17 | "jest": "^25.1.0", 18 | "ts-jest": "^25.2.0", 19 | "aws-cdk": "^1.23.0", 20 | "ts-node": "^8.6.2", 21 | "typescript": "~3.7.5" 22 | }, 23 | "dependencies": { 24 | "@aws-cdk/aws-cloudfront": "^1.23.0", 25 | "@aws-cdk/aws-ec2": "^1.23.0", 26 | "@aws-cdk/aws-ecr": "^1.23.0", 27 | "@aws-cdk/aws-ecs": "^1.23.0", 28 | "@aws-cdk/aws-ecs-patterns": "^1.23.0", 29 | "@aws-cdk/aws-elasticloadbalancingv2": "^1.23.0", 30 | "@aws-cdk/aws-iam": "^1.23.0", 31 | "@aws-cdk/aws-logs": "^1.23.0", 32 | "@aws-cdk/aws-s3": "^1.23.0", 33 | "@aws-cdk/core": "^1.23.0", 34 | "source-map-support": "^0.5.16" 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /cdk/test/stacks/__snapshots__/vpc-stack.test.ts.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`Vpc Stack 1`] = ` 4 | Object { 5 | "Resources": Object { 6 | "VPCB9E5F0B4": Object { 7 | "Properties": Object { 8 | "CidrBlock": "10.0.0.0/16", 9 | "EnableDnsHostnames": true, 10 | "EnableDnsSupport": true, 11 | "InstanceTenancy": "default", 12 | "Tags": Array [ 13 | Object { 14 | "Key": "Name", 15 | "Value": "MyTestVpcStack/VPC", 16 | }, 17 | ], 18 | }, 19 | "Type": "AWS::EC2::VPC", 20 | }, 21 | "VPCIGWB7E252D3": Object { 22 | "Properties": Object { 23 | "Tags": Array [ 24 | Object { 25 | "Key": "Name", 26 | "Value": "MyTestVpcStack/VPC", 27 | }, 28 | ], 29 | }, 30 | "Type": "AWS::EC2::InternetGateway", 31 | }, 32 | "VPCPrivateSubnet1DefaultRouteAE1D6490": Object { 33 | "Properties": Object { 34 | "DestinationCidrBlock": "0.0.0.0/0", 35 | "NatGatewayId": Object { 36 | "Ref": "VPCPublicSubnet1NATGatewayE0556630", 37 | }, 38 | "RouteTableId": Object { 39 | "Ref": "VPCPrivateSubnet1RouteTableBE8A6027", 40 | }, 41 | }, 42 | "Type": "AWS::EC2::Route", 43 | }, 44 | "VPCPrivateSubnet1RouteTableAssociation347902D1": Object { 45 | "Properties": Object { 46 | "RouteTableId": Object { 47 | "Ref": "VPCPrivateSubnet1RouteTableBE8A6027", 48 | }, 49 | "SubnetId": Object { 50 | "Ref": "VPCPrivateSubnet1Subnet8BCA10E0", 51 | }, 52 | }, 53 | "Type": "AWS::EC2::SubnetRouteTableAssociation", 54 | }, 55 | "VPCPrivateSubnet1RouteTableBE8A6027": Object { 56 | "Properties": Object { 57 | "Tags": Array [ 58 | Object { 59 | "Key": "Name", 60 | "Value": "MyTestVpcStack/VPC/PrivateSubnet1", 61 | }, 62 | ], 63 | "VpcId": Object { 64 | "Ref": "VPCB9E5F0B4", 65 | }, 66 | }, 67 | "Type": "AWS::EC2::RouteTable", 68 | }, 69 | "VPCPrivateSubnet1Subnet8BCA10E0": Object { 70 | "Properties": Object { 71 | "AvailabilityZone": Object { 72 | "Fn::Select": Array [ 73 | 0, 74 | Object { 75 | "Fn::GetAZs": "", 76 | }, 77 | ], 78 | }, 79 | "CidrBlock": "10.0.1.0/24", 80 | "MapPublicIpOnLaunch": false, 81 | "Tags": Array [ 82 | Object { 83 | "Key": "Name", 84 | "Value": "MyTestVpcStack/VPC/PrivateSubnet1", 85 | }, 86 | Object { 87 | "Key": "aws-cdk:subnet-name", 88 | "Value": "Private", 89 | }, 90 | Object { 91 | "Key": "aws-cdk:subnet-type", 92 | "Value": "Private", 93 | }, 94 | ], 95 | "VpcId": Object { 96 | "Ref": "VPCB9E5F0B4", 97 | }, 98 | }, 99 | "Type": "AWS::EC2::Subnet", 100 | }, 101 | "VPCPublicSubnet1DefaultRoute91CEF279": Object { 102 | "DependsOn": Array [ 103 | "VPCVPCGW99B986DC", 104 | ], 105 | "Properties": Object { 106 | "DestinationCidrBlock": "0.0.0.0/0", 107 | "GatewayId": Object { 108 | "Ref": "VPCIGWB7E252D3", 109 | }, 110 | "RouteTableId": Object { 111 | "Ref": "VPCPublicSubnet1RouteTableFEE4B781", 112 | }, 113 | }, 114 | "Type": "AWS::EC2::Route", 115 | }, 116 | "VPCPublicSubnet1EIP6AD938E8": Object { 117 | "Properties": Object { 118 | "Domain": "vpc", 119 | "Tags": Array [ 120 | Object { 121 | "Key": "Name", 122 | "Value": "MyTestVpcStack/VPC/PublicSubnet1", 123 | }, 124 | ], 125 | }, 126 | "Type": "AWS::EC2::EIP", 127 | }, 128 | "VPCPublicSubnet1NATGatewayE0556630": Object { 129 | "Properties": Object { 130 | "AllocationId": Object { 131 | "Fn::GetAtt": Array [ 132 | "VPCPublicSubnet1EIP6AD938E8", 133 | "AllocationId", 134 | ], 135 | }, 136 | "SubnetId": Object { 137 | "Ref": "VPCPublicSubnet1SubnetB4246D30", 138 | }, 139 | "Tags": Array [ 140 | Object { 141 | "Key": "Name", 142 | "Value": "MyTestVpcStack/VPC/PublicSubnet1", 143 | }, 144 | ], 145 | }, 146 | "Type": "AWS::EC2::NatGateway", 147 | }, 148 | "VPCPublicSubnet1RouteTableAssociation0B0896DC": Object { 149 | "Properties": Object { 150 | "RouteTableId": Object { 151 | "Ref": "VPCPublicSubnet1RouteTableFEE4B781", 152 | }, 153 | "SubnetId": Object { 154 | "Ref": "VPCPublicSubnet1SubnetB4246D30", 155 | }, 156 | }, 157 | "Type": "AWS::EC2::SubnetRouteTableAssociation", 158 | }, 159 | "VPCPublicSubnet1RouteTableFEE4B781": Object { 160 | "Properties": Object { 161 | "Tags": Array [ 162 | Object { 163 | "Key": "Name", 164 | "Value": "MyTestVpcStack/VPC/PublicSubnet1", 165 | }, 166 | ], 167 | "VpcId": Object { 168 | "Ref": "VPCB9E5F0B4", 169 | }, 170 | }, 171 | "Type": "AWS::EC2::RouteTable", 172 | }, 173 | "VPCPublicSubnet1SubnetB4246D30": Object { 174 | "Properties": Object { 175 | "AvailabilityZone": Object { 176 | "Fn::Select": Array [ 177 | 0, 178 | Object { 179 | "Fn::GetAZs": "", 180 | }, 181 | ], 182 | }, 183 | "CidrBlock": "10.0.0.0/24", 184 | "MapPublicIpOnLaunch": true, 185 | "Tags": Array [ 186 | Object { 187 | "Key": "Name", 188 | "Value": "MyTestVpcStack/VPC/PublicSubnet1", 189 | }, 190 | Object { 191 | "Key": "aws-cdk:subnet-name", 192 | "Value": "Public", 193 | }, 194 | Object { 195 | "Key": "aws-cdk:subnet-type", 196 | "Value": "Public", 197 | }, 198 | ], 199 | "VpcId": Object { 200 | "Ref": "VPCB9E5F0B4", 201 | }, 202 | }, 203 | "Type": "AWS::EC2::Subnet", 204 | }, 205 | "VPCVPCGW99B986DC": Object { 206 | "Properties": Object { 207 | "InternetGatewayId": Object { 208 | "Ref": "VPCIGWB7E252D3", 209 | }, 210 | "VpcId": Object { 211 | "Ref": "VPCB9E5F0B4", 212 | }, 213 | }, 214 | "Type": "AWS::EC2::VPCGatewayAttachment", 215 | }, 216 | }, 217 | } 218 | `; 219 | -------------------------------------------------------------------------------- /cdk/test/stacks/vpc-stack.test.ts: -------------------------------------------------------------------------------- 1 | import { 2 | expect as expectCDK, 3 | matchTemplate, 4 | MatchStyle, 5 | SynthUtils 6 | } from "@aws-cdk/assert"; 7 | import * as cdk from "@aws-cdk/core"; 8 | import Cdk = require("../../lib/stacks/vpc-stack"); 9 | 10 | test("Vpc Stack", () => { 11 | const app = new cdk.App(); 12 | const stack = new Cdk.VpcStack(app, "MyTestVpcStack"); 13 | expect(SynthUtils.toCloudFormation(stack)).toMatchSnapshot(); 14 | }); 15 | -------------------------------------------------------------------------------- /cdk/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target":"ES2018", 4 | "module": "commonjs", 5 | "lib": ["es2018"], 6 | "declaration": true, 7 | "strict": true, 8 | "noImplicitAny": true, 9 | "strictNullChecks": true, 10 | "noImplicitThis": true, 11 | "alwaysStrict": true, 12 | "noUnusedLocals": false, 13 | "noUnusedParameters": false, 14 | "noImplicitReturns": true, 15 | "noFallthroughCasesInSwitch": false, 16 | "inlineSourceMap": true, 17 | "inlineSources": true, 18 | "experimentalDecorators": true, 19 | "strictPropertyInitialization":false, 20 | "typeRoots": ["./node_modules/@types"] 21 | }, 22 | "exclude": ["cdk.out"] 23 | } 24 | -------------------------------------------------------------------------------- /components/Layout.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | import Link from 'next/link' 3 | import Head from 'next/head' 4 | 5 | type Props = { 6 | title?: string 7 | } 8 | 9 | const Layout: React.FunctionComponent = ({ 10 | children, 11 | title = 'This is the default title', 12 | }) => ( 13 |
14 | 15 | {title} 16 | 17 | 18 | 19 |
20 | 33 |
34 | {children} 35 |
36 |
37 | I'm here to stay (Footer) 38 |
39 |
40 | ) 41 | 42 | export default Layout 43 | -------------------------------------------------------------------------------- /components/List.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | import ListItem from './ListItem' 3 | import { User } from '../interfaces' 4 | 5 | type Props = { 6 | items: User[] 7 | } 8 | 9 | const List: React.FunctionComponent = ({ items }) => ( 10 |
    11 | {items.map(item => ( 12 |
  • 13 | 14 |
  • 15 | ))} 16 |
17 | ) 18 | 19 | export default List 20 | -------------------------------------------------------------------------------- /components/ListDetail.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | 3 | import { User } from '../interfaces' 4 | 5 | type ListDetailProps = { 6 | item: User 7 | } 8 | 9 | const ListDetail: React.FunctionComponent = ({ 10 | item: user, 11 | }) => ( 12 |
13 |

Detail for {user.name}

14 |

ID: {user.id}

15 |
16 | ) 17 | 18 | export default ListDetail 19 | -------------------------------------------------------------------------------- /components/ListItem.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | import Link from 'next/link' 3 | 4 | import { User } from '../interfaces' 5 | 6 | type Props = { 7 | data: User 8 | } 9 | 10 | const ListItem: React.FunctionComponent = ({ data }) => ( 11 | 12 | 13 | {data.id}: {data.name} 14 | 15 | 16 | ) 17 | 18 | export default ListItem 19 | -------------------------------------------------------------------------------- /deploy.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -ex 4 | 5 | : ${REVISION:="$(git rev-parse --short HEAD)"} 6 | export REVISION 7 | export ECR_BASE 8 | 9 | ./build-image-and-push.sh 10 | 11 | # Next.jsで作った静的ファイルをS3に上げる 12 | rm -rf ./dist/ 13 | mkdir ./dist/ 14 | CONTAINER_ID="$(docker create "nextjs-on-ecs-server:$REVISION")" 15 | docker cp ${CONTAINER_ID}:/app/.next ./dist/ 16 | docker rm -v ${CONTAINER_ID} 17 | aws s3 sync ./dist/.next/static s3://nextjs-on-ecs-static-bucket/_next/static 18 | 19 | # Nextのサーバが動くECSを更新 20 | yarn workspace cdk run cdk deploy --context application-version=$REVISION NextServerStack 21 | 22 | # cdk.context.jsonをデプロイしたrevisionに更新しておく 23 | sed -i '' -E "s/\"application-version\": \"[^\"]+\"/\"application-version\": \"$REVISION\"/" ./cdk/cdk.context.json 24 | -------------------------------------------------------------------------------- /interfaces/index.ts: -------------------------------------------------------------------------------- 1 | // You can include shared interfaces/types in a separate file 2 | // and then use them in any component by importing them. For 3 | // example, to import the interface below do: 4 | // 5 | // import User from 'path/to/interfaces'; 6 | 7 | export type User = { 8 | id: number 9 | name: string 10 | } 11 | -------------------------------------------------------------------------------- /next-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "nextjs-on-ecs", 3 | "version": "1.0.0", 4 | "private": true, 5 | "workspaces": [ 6 | "cdk" 7 | ], 8 | "scripts": { 9 | "dev": "next", 10 | "build": "next build", 11 | "start": "next start", 12 | "type-check": "tsc" 13 | }, 14 | "dependencies": { 15 | "isomorphic-unfetch": "3.0.0", 16 | "next": "latest", 17 | "react": "^16.12.0", 18 | "react-dom": "^16.12.0" 19 | }, 20 | "devDependencies": { 21 | "@types/node": "^12.12.21", 22 | "@types/react": "^16.9.16", 23 | "@types/react-dom": "^16.9.4", 24 | "typescript": "3.7.3" 25 | }, 26 | "license": "ISC" 27 | } 28 | -------------------------------------------------------------------------------- /pages/about.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import Link from "next/link"; 3 | import Layout from "../components/Layout"; 4 | 5 | const AboutPage: React.FunctionComponent = () => ( 6 | 7 |

About

8 |

This is the about page

9 |

10 | 11 | Go home 12 | 13 |

14 |
15 | ); 16 | 17 | export default AboutPage; 18 | -------------------------------------------------------------------------------- /pages/api/users/[id].ts: -------------------------------------------------------------------------------- 1 | import { NextApiRequest, NextApiResponse } from "next"; 2 | import { sampleUserData } from "../../../utils/sample-data"; 3 | 4 | export default (req: NextApiRequest, res: NextApiResponse) => { 5 | try { 6 | const { id } = req.query; 7 | const selected = sampleUserData.find(data => data.id === Number(id)); 8 | 9 | if (!selected) { 10 | throw new Error("Cannot find user"); 11 | } 12 | 13 | res.status(200).json(selected); 14 | } catch (err) { 15 | res.status(404).json({ statusCode: 404, message: err.message }); 16 | } 17 | }; 18 | -------------------------------------------------------------------------------- /pages/api/users/index.ts: -------------------------------------------------------------------------------- 1 | import { NextApiRequest, NextApiResponse } from "next"; 2 | import { sampleUserData } from "../../../utils/sample-data"; 3 | 4 | export default (_: NextApiRequest, res: NextApiResponse) => { 5 | try { 6 | if (!Array.isArray(sampleUserData)) { 7 | throw new Error("Cannot find user data"); 8 | } 9 | 10 | res.status(200).json(sampleUserData); 11 | } catch (err) { 12 | res.status(500).json({ statusCode: 500, message: err.message }); 13 | } 14 | }; 15 | -------------------------------------------------------------------------------- /pages/index.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import Link from "next/link"; 3 | import Layout from "../components/Layout"; 4 | import { NextPage } from "next"; 5 | 6 | const IndexPage: NextPage = () => { 7 | return ( 8 | 9 |

Hello Next.js 👋Updated4

10 |

11 | 12 | About 13 | 14 |

15 |
16 | ); 17 | }; 18 | 19 | export default IndexPage; 20 | -------------------------------------------------------------------------------- /pages/users/[id].tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { NextPageContext } from "next"; 3 | 4 | import { User } from "../../interfaces"; 5 | import Layout from "../../components/Layout"; 6 | import ListDetail from "../../components/ListDetail"; 7 | import { sampleFetchWrapper } from "../../utils/sample-api"; 8 | 9 | type Props = { 10 | item?: User; 11 | errors?: string; 12 | }; 13 | 14 | class InitialPropsDetail extends React.Component { 15 | static getInitialProps = async ({ query }: NextPageContext) => { 16 | const apiBase = process.browser ? "" : "http://localhost:3000"; 17 | try { 18 | const { id } = query; 19 | const item = await sampleFetchWrapper( 20 | `${apiBase}/api/users/${Array.isArray(id) ? id[0] : id}` 21 | ); 22 | return { item }; 23 | } catch (err) { 24 | return { errors: err.message }; 25 | } 26 | }; 27 | 28 | render() { 29 | const { item, errors } = this.props; 30 | 31 | if (errors) { 32 | return ( 33 | 34 |

35 | Error: {errors} 36 |

37 |
38 | ); 39 | } 40 | 41 | return ( 42 | 47 | {item && } 48 | 49 | ); 50 | } 51 | } 52 | 53 | export default InitialPropsDetail; 54 | -------------------------------------------------------------------------------- /pages/users/index.tsx: -------------------------------------------------------------------------------- 1 | import { NextPage } from "next"; 2 | import Link from "next/link"; 3 | 4 | import Layout from "../../components/Layout"; 5 | import List from "../../components/List"; 6 | import { User } from "../../interfaces"; 7 | import { sampleFetchWrapper } from "../../utils/sample-api"; 8 | 9 | type Props = { 10 | items: User[]; 11 | pathname: string; 12 | }; 13 | 14 | const WithInitialProps: NextPage = ({ items, pathname }) => ( 15 | 16 |

Users List

17 |

18 | Example fetching data from inside getInitialProps(). 19 |

20 |

You are currently on: {pathname}

21 | 22 |

23 | 24 | Go home 25 | 26 |

27 |
28 | ); 29 | 30 | WithInitialProps.getInitialProps = async ({ pathname }) => { 31 | // Example for including initial props in a Next.js function component page. 32 | // Don't forget to include the respective types for any props passed into 33 | // the component. 34 | const apiBase = process.browser ? "" : "http://localhost:3000"; 35 | const items: User[] = await sampleFetchWrapper(`${apiBase}/api/users`); 36 | 37 | return { items, pathname }; 38 | }; 39 | 40 | export default WithInitialProps; 41 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "allowJs": true, 4 | "alwaysStrict": true, 5 | "esModuleInterop": true, 6 | "forceConsistentCasingInFileNames": true, 7 | "isolatedModules": true, 8 | "jsx": "preserve", 9 | "lib": ["dom", "es2017"], 10 | "module": "esnext", 11 | "moduleResolution": "node", 12 | "noEmit": true, 13 | "noFallthroughCasesInSwitch": true, 14 | "noUnusedLocals": true, 15 | "noUnusedParameters": true, 16 | "resolveJsonModule": true, 17 | "skipLibCheck": true, 18 | "strict": true, 19 | "target": "esnext" 20 | }, 21 | "exclude": ["node_modules", "cdk/"], 22 | "include": ["**/*.ts", "**/*.tsx"] 23 | } 24 | -------------------------------------------------------------------------------- /utils/sample-api.ts: -------------------------------------------------------------------------------- 1 | import fetch from 'isomorphic-unfetch' 2 | 3 | export async function sampleFetchWrapper( 4 | input: RequestInfo, 5 | init?: RequestInit 6 | ) { 7 | try { 8 | const data = await fetch(input, init).then(res => res.json()) 9 | return data 10 | } catch (err) { 11 | throw new Error(err.message) 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /utils/sample-data.ts: -------------------------------------------------------------------------------- 1 | import { User } from '../interfaces' 2 | 3 | /** Dummy user data. */ 4 | export const sampleUserData: User[] = [ 5 | { id: 101, name: 'Alice' }, 6 | { id: 102, name: 'Bob' }, 7 | { id: 103, name: 'Caroline' }, 8 | { id: 104, name: 'Dave' }, 9 | ] 10 | --------------------------------------------------------------------------------