Skip to content

Commit 40229fb

Browse files
committed
task2: added s3 bucket and cloudfront distribution
1 parent 64ee378 commit 40229fb

File tree

3 files changed

+301
-12
lines changed

3 files changed

+301
-12
lines changed
Lines changed: 151 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,151 @@
1+
'use strict';
2+
3+
const spawnSync = require('child_process').spawnSync;
4+
5+
class ServerlessPlugin {
6+
constructor(serverless, options) {
7+
this.serverless = serverless;
8+
this.options = options;
9+
this.commands = {
10+
syncToS3: {
11+
usage: 'Deploys the `app` directory to your bucket',
12+
lifecycleEvents: [
13+
'sync',
14+
],
15+
},
16+
domainInfo: {
17+
usage: 'Fetches and prints out the deployed CloudFront domain names',
18+
lifecycleEvents: [
19+
'domainInfo',
20+
],
21+
},
22+
invalidateCloudFrontCache: {
23+
usage: 'Invalidates CloudFront cache',
24+
lifecycleEvents: [
25+
'invalidateCache',
26+
],
27+
},
28+
};
29+
30+
this.hooks = {
31+
'syncToS3:sync': this.syncDirectory.bind(this),
32+
'domainInfo:domainInfo': this.domainInfo.bind(this),
33+
'invalidateCloudFrontCache:invalidateCache': this.invalidateCache.bind(
34+
this,
35+
),
36+
};
37+
}
38+
39+
runAwsCommand(args) {
40+
let command = 'aws';
41+
if (this.serverless.variables.service.provider.region) {
42+
command = `${command} --region ${this.serverless.variables.service.provider.region}`;
43+
}
44+
if (this.serverless.variables.service.provider.profile) {
45+
command = `${command} --profile ${this.serverless.variables.service.provider.profile}`;
46+
}
47+
const result = spawnSync(command, args, { shell: true });
48+
const stdout = result.stdout.toString();
49+
const sterr = result.stderr.toString();
50+
if (stdout) {
51+
this.serverless.cli.log(stdout);
52+
}
53+
if (sterr) {
54+
this.serverless.cli.log(sterr);
55+
}
56+
57+
return { stdout, sterr };
58+
}
59+
60+
// syncs the `app` directory to the provided bucket
61+
syncDirectory() {
62+
const s3Bucket = this.serverless.variables.service.custom.s3Bucket;
63+
const buildFolder = this.serverless.variables.service.custom.client.distributionFolder;
64+
const args = [
65+
's3',
66+
'sync',
67+
`${buildFolder}/`,
68+
`s3://${s3Bucket}/`,
69+
'--delete',
70+
];
71+
const { sterr } = this.runAwsCommand(args);
72+
if (!sterr) {
73+
this.serverless.cli.log('Successfully synced to the S3 bucket');
74+
} else {
75+
throw new Error('Failed syncing to the S3 bucket');
76+
}
77+
}
78+
79+
// fetches the domain name from the CloudFront outputs and prints it out
80+
async domainInfo() {
81+
const provider = this.serverless.getProvider('aws');
82+
const stackName = provider.naming.getStackName(this.options.stage);
83+
const result = await provider.request(
84+
'CloudFormation',
85+
'describeStacks',
86+
{ StackName: stackName },
87+
this.options.stage,
88+
this.options.region,
89+
);
90+
91+
const outputs = result.Stacks[0].Outputs;
92+
const output = outputs.find(
93+
entry => entry.OutputKey === 'WebAppCloudFrontDistributionOutput',
94+
);
95+
96+
if (output && output.OutputValue) {
97+
this.serverless.cli.log(`Web App Domain: ${output.OutputValue}`);
98+
return output.OutputValue;
99+
}
100+
101+
this.serverless.cli.log('Web App Domain: Not Found');
102+
const error = new Error('Could not extract Web App Domain');
103+
throw error;
104+
}
105+
106+
async invalidateCache() {
107+
const provider = this.serverless.getProvider('aws');
108+
109+
const domain = await this.domainInfo();
110+
111+
const result = await provider.request(
112+
'CloudFront',
113+
'listDistributions',
114+
{},
115+
this.options.stage,
116+
this.options.region,
117+
);
118+
119+
const distributions = result.DistributionList.Items;
120+
const distribution = distributions.find(
121+
entry => entry.DomainName === domain,
122+
);
123+
124+
if (distribution) {
125+
this.serverless.cli.log(
126+
`Invalidating CloudFront distribution with id: ${distribution.Id}`,
127+
);
128+
const args = [
129+
'cloudfront',
130+
'create-invalidation',
131+
'--distribution-id',
132+
distribution.Id,
133+
'--paths',
134+
'"/*"',
135+
];
136+
const { sterr } = this.runAwsCommand(args);
137+
if (!sterr) {
138+
this.serverless.cli.log('Successfully invalidated CloudFront cache');
139+
} else {
140+
throw new Error('Failed invalidating CloudFront cache');
141+
}
142+
} else {
143+
const message = `Could not find distribution with domain ${domain}`;
144+
const error = new Error(message);
145+
this.serverless.cli.log(message);
146+
throw error;
147+
}
148+
}
149+
}
150+
151+
module.exports = ServerlessPlugin;

package.json

Lines changed: 25 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -5,34 +5,47 @@
55
"scripts": {
66
"start": "react-scripts start",
77
"build": "react-scripts build",
8+
"client:deploy": "sls client deploy --no-config-change --no-policy-change --no-cors-change",
9+
"client:deploy:nc": "npm run client:deploy -- --no-confirm",
10+
"client:build:deploy": "npm run build && npm run client:deploy",
11+
"client:build:deploy:nc": "npm run build && npm run client:deploy:nc",
12+
"cloudfront:setup": "sls deploy",
13+
"cloudfront:domainInfo": "sls domainInfo",
14+
"cloudfront:invalidateCache": "sls invalidateCloudFrontCache",
15+
"cloudfront:build:deploy": "npm run client:build:deploy && npm run cloudfront:invalidateCache",
16+
"cloudfront:build:deploy:nc": "npm run client:build:deploy:nc && npm run cloudfront:invalidateCache",
17+
"cloudfront:update:build:deploy": "npm run cloudfront:setup && npm run cloudfront:build:deploy",
18+
"cloudfront:update:build:deploy:nc": "npm run cloudfront:setup && npm run cloudfront:build:deploy:nc",
819
"test": "react-scripts test",
920
"eject": "react-scripts eject"
1021
},
1122
"dependencies": {
12-
"axios": "^0.19.2",
13-
"formik": "^2.1.5",
14-
"formik-material-ui": "^2.0.1",
15-
"yup": "^0.29.1",
1623
"@material-ui/core": "^4.11.0",
1724
"@material-ui/icons": "^4.9.1",
25+
"@reduxjs/toolkit": "^1.2.5",
26+
"@types/lodash": "^4.14.158",
1827
"@types/node": "^12.0.0",
1928
"@types/react": "^16.9.43",
2029
"@types/react-dom": "^16.9.8",
30+
"@types/react-redux": "^7.1.7",
2131
"@types/react-router-dom": "^5.1.5",
2232
"@types/yup": "^0.29.3",
33+
"axios": "^0.19.2",
34+
"enzyme": "^3.11.0",
35+
"enzyme-adapter-react-16": "^1.15.2",
36+
"enzyme-to-json": "^3.4.4",
37+
"formik": "^2.1.5",
38+
"formik-material-ui": "^2.0.1",
39+
"lodash": "^4.17.19",
2340
"react": "^16.13.1",
2441
"react-dom": "^16.13.1",
42+
"react-redux": "^7.2.0",
2543
"react-router-dom": "^5.2.0",
2644
"react-scripts": "3.4.1",
45+
"serverless": "^2.29.0",
46+
"serverless-finch": "^2.6.0",
2747
"typescript": "~3.7.2",
28-
"@reduxjs/toolkit": "^1.2.5",
29-
"react-redux": "^7.2.0",
30-
"@types/react-redux": "^7.1.7",
31-
"@types/lodash": "^4.14.158",
32-
"lodash": "^4.17.19",
33-
"enzyme": "^3.11.0",
34-
"enzyme-adapter-react-16": "^1.15.2",
35-
"enzyme-to-json": "^3.4.4"
48+
"yup": "^0.29.1"
3649
},
3750
"devDependencies": {
3851
"@testing-library/jest-dom": "^4.2.4",

serverless.yml

Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
service: node-in-aws-web
2+
3+
frameworkVersion: '2'
4+
5+
provider:
6+
name: aws
7+
runtime: nodejs12.x
8+
# setup profile for AWS CLI.
9+
# profile: node-aws
10+
11+
plugins:
12+
- serverless-finch
13+
- serverless-single-page-app-plugin
14+
15+
custom:
16+
client:
17+
bucketName: node-in-aws-web-bucket
18+
distributionFolder: build
19+
s3BucketName: ${self:custom.client.bucketName}
20+
21+
## Serverless-single-page-app-plugin configuration:
22+
s3LocalPath: ${self:custom.client.distributionFolder}/
23+
24+
resources:
25+
Resources:
26+
## Specifying the S3 Bucket
27+
WebAppS3Bucket:
28+
Type: AWS::S3::Bucket
29+
Properties:
30+
BucketName: ${self:custom.s3BucketName}
31+
AccessControl: PublicRead
32+
WebsiteConfiguration:
33+
IndexDocument: index.html
34+
ErrorDocument: index.html
35+
# VersioningConfiguration:
36+
# Status: Enabled
37+
38+
## Specifying the policies to make sure all files inside the Bucket are avaialble to CloudFront
39+
WebAppS3BucketPolicy:
40+
Type: AWS::S3::BucketPolicy
41+
Properties:
42+
Bucket:
43+
Ref: WebAppS3Bucket
44+
PolicyDocument:
45+
Statement:
46+
- Sid: 'AllowCloudFrontAccessIdentity'
47+
Effect: Allow
48+
Action: s3:GetObject
49+
Resource: arn:aws:s3:::${self:custom.s3BucketName}/*
50+
Principal:
51+
AWS:
52+
Fn::Join:
53+
- ' '
54+
- - 'arn:aws:iam::cloudfront:user/CloudFront Origin Access Identity'
55+
- !Ref OriginAccessIdentity
56+
57+
OriginAccessIdentity:
58+
Type: AWS::CloudFront::CloudFrontOriginAccessIdentity
59+
Properties:
60+
CloudFrontOriginAccessIdentityConfig:
61+
Comment: Access identity between CloudFront and S3 bucket
62+
63+
## Specifying the CloudFront Distribution to server your Web Application
64+
WebAppCloudFrontDistribution:
65+
Type: AWS::CloudFront::Distribution
66+
Properties:
67+
DistributionConfig:
68+
Origins:
69+
- DomainName: ${self:custom.s3BucketName}.s3.amazonaws.com
70+
## An identifier for the origin which must be unique within the distribution
71+
Id: myS3Origin
72+
## In case you don't want to restrict the bucket access use CustomOriginConfig and remove S3OriginConfig
73+
S3OriginConfig:
74+
OriginAccessIdentity: !Sub origin-access-identity/cloudfront/${OriginAccessIdentity}
75+
# CustomOriginConfig:
76+
# HTTPPort: 80
77+
# HTTPSPort: 443
78+
# OriginProtocolPolicy: https-only
79+
Enabled: true
80+
IPV6Enabled: true
81+
HttpVersion: http2
82+
## Uncomment the following section in case you are using a custom domain
83+
# Aliases:
84+
# - mysite.example.com
85+
DefaultRootObject: index.html
86+
## Since the Single Page App is taking care of the routing we need to make sure ever path is served with index.html
87+
## The only exception are files that actually exist e.h. app.js, reset.css
88+
CustomErrorResponses:
89+
- ErrorCode: 404
90+
ResponseCode: 200
91+
ResponsePagePath: /index.html
92+
DefaultCacheBehavior:
93+
AllowedMethods: [ 'GET', 'HEAD', 'OPTIONS' ]
94+
CachedMethods: [ 'GET', 'HEAD', 'OPTIONS' ]
95+
ForwardedValues:
96+
Headers:
97+
- Access-Control-Request-Headers
98+
- Access-Control-Request-Method
99+
- Origin
100+
- Authorization
101+
## Defining if and how the QueryString and Cookies are forwarded to the origin which in this case is S3
102+
QueryString: false
103+
Cookies:
104+
Forward: none
105+
## The origin id defined above
106+
TargetOriginId: myS3Origin
107+
## The protocol that users can use to access the files in the origin. To allow HTTP use `allow-all`
108+
ViewerProtocolPolicy: redirect-to-https
109+
Compress: true
110+
DefaultTTL: 0
111+
## The certificate to use when viewers use HTTPS to request objects.
112+
ViewerCertificate:
113+
CloudFrontDefaultCertificate: 'true'
114+
## Uncomment the following section in case you want to enable logging for CloudFront requests
115+
# Logging:
116+
# IncludeCookies: 'false'
117+
# Bucket: mylogs.s3.amazonaws.com
118+
# Prefix: myprefix
119+
120+
## In order to print out the hosted domain via `serverless info` we need to define the DomainName output for CloudFormation
121+
Outputs:
122+
WebAppS3BucketOutput:
123+
Value: !Ref WebAppS3Bucket
124+
WebAppCloudFrontDistributionOutput:
125+
Value: !GetAtt WebAppCloudFrontDistribution.DomainName

0 commit comments

Comments
 (0)