Skip to content

Commit

Permalink
Merge pull request #1 from EPAM-JS-Competency-center/task-2
Browse files Browse the repository at this point in the history
added s3 bucket and cloudfront distribution
  • Loading branch information
Ilya Valasiuk authored Mar 18, 2021
2 parents 64ee378 + 50d2a7a commit 05ca160
Show file tree
Hide file tree
Showing 3 changed files with 304 additions and 12 deletions.
151 changes: 151 additions & 0 deletions .serverless_plugins/serverless-single-page-app-plugin.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
'use strict';

const spawnSync = require('child_process').spawnSync;

class ServerlessPlugin {
constructor(serverless, options) {
this.serverless = serverless;
this.options = options;
this.commands = {
syncToS3: {
usage: 'Deploys the `app` directory to your bucket',
lifecycleEvents: [
'sync',
],
},
domainInfo: {
usage: 'Fetches and prints out the deployed CloudFront domain names',
lifecycleEvents: [
'domainInfo',
],
},
invalidateCloudFrontCache: {
usage: 'Invalidates CloudFront cache',
lifecycleEvents: [
'invalidateCache',
],
},
};

this.hooks = {
'syncToS3:sync': this.syncDirectory.bind(this),
'domainInfo:domainInfo': this.domainInfo.bind(this),
'invalidateCloudFrontCache:invalidateCache': this.invalidateCache.bind(
this,
),
};
}

runAwsCommand(args) {
let command = 'aws';
if (this.serverless.variables.service.provider.region) {
command = `${command} --region ${this.serverless.variables.service.provider.region}`;
}
if (this.serverless.variables.service.provider.profile) {
command = `${command} --profile ${this.serverless.variables.service.provider.profile}`;
}
const result = spawnSync(command, args, { shell: true });
const stdout = result.stdout.toString();
const sterr = result.stderr.toString();
if (stdout) {
this.serverless.cli.log(stdout);
}
if (sterr) {
this.serverless.cli.log(sterr);
}

return { stdout, sterr };
}

// syncs the `app` directory to the provided bucket
syncDirectory() {
const s3Bucket = this.serverless.variables.service.custom.s3Bucket;
const buildFolder = this.serverless.variables.service.custom.client.distributionFolder;
const args = [
's3',
'sync',
`${buildFolder}/`,
`s3://${s3Bucket}/`,
'--delete',
];
const { sterr } = this.runAwsCommand(args);
if (!sterr) {
this.serverless.cli.log('Successfully synced to the S3 bucket');
} else {
throw new Error('Failed syncing to the S3 bucket');
}
}

// fetches the domain name from the CloudFront outputs and prints it out
async domainInfo() {
const provider = this.serverless.getProvider('aws');
const stackName = provider.naming.getStackName(this.options.stage);
const result = await provider.request(
'CloudFormation',
'describeStacks',
{ StackName: stackName },
this.options.stage,
this.options.region,
);

const outputs = result.Stacks[0].Outputs;
const output = outputs.find(
entry => entry.OutputKey === 'WebAppCloudFrontDistributionOutput',
);

if (output && output.OutputValue) {
this.serverless.cli.log(`Web App Domain: ${output.OutputValue}`);
return output.OutputValue;
}

this.serverless.cli.log('Web App Domain: Not Found');
const error = new Error('Could not extract Web App Domain');
throw error;
}

async invalidateCache() {
const provider = this.serverless.getProvider('aws');

const domain = await this.domainInfo();

const result = await provider.request(
'CloudFront',
'listDistributions',
{},
this.options.stage,
this.options.region,
);

const distributions = result.DistributionList.Items;
const distribution = distributions.find(
entry => entry.DomainName === domain,
);

if (distribution) {
this.serverless.cli.log(
`Invalidating CloudFront distribution with id: ${distribution.Id}`,
);
const args = [
'cloudfront',
'create-invalidation',
'--distribution-id',
distribution.Id,
'--paths',
'"/*"',
];
const { sterr } = this.runAwsCommand(args);
if (!sterr) {
this.serverless.cli.log('Successfully invalidated CloudFront cache');
} else {
throw new Error('Failed invalidating CloudFront cache');
}
} else {
const message = `Could not find distribution with domain ${domain}`;
const error = new Error(message);
this.serverless.cli.log(message);
throw error;
}
}
}

module.exports = ServerlessPlugin;
37 changes: 25 additions & 12 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,34 +5,47 @@
"scripts": {
"start": "react-scripts start",
"build": "react-scripts build",
"client:deploy": "sls client deploy --no-config-change --no-policy-change --no-cors-change",
"client:deploy:nc": "npm run client:deploy -- --no-confirm",
"client:build:deploy": "npm run build && npm run client:deploy",
"client:build:deploy:nc": "npm run build && npm run client:deploy:nc",
"cloudfront:setup": "sls deploy",
"cloudfront:domainInfo": "sls domainInfo",
"cloudfront:invalidateCache": "sls invalidateCloudFrontCache",
"cloudfront:build:deploy": "npm run client:build:deploy && npm run cloudfront:invalidateCache",
"cloudfront:build:deploy:nc": "npm run client:build:deploy:nc && npm run cloudfront:invalidateCache",
"cloudfront:update:build:deploy": "npm run cloudfront:setup && npm run cloudfront:build:deploy",
"cloudfront:update:build:deploy:nc": "npm run cloudfront:setup && npm run cloudfront:build:deploy:nc",
"test": "react-scripts test",
"eject": "react-scripts eject"
},
"dependencies": {
"axios": "^0.19.2",
"formik": "^2.1.5",
"formik-material-ui": "^2.0.1",
"yup": "^0.29.1",
"@material-ui/core": "^4.11.0",
"@material-ui/icons": "^4.9.1",
"@reduxjs/toolkit": "^1.2.5",
"@types/lodash": "^4.14.158",
"@types/node": "^12.0.0",
"@types/react": "^16.9.43",
"@types/react-dom": "^16.9.8",
"@types/react-redux": "^7.1.7",
"@types/react-router-dom": "^5.1.5",
"@types/yup": "^0.29.3",
"axios": "^0.19.2",
"enzyme": "^3.11.0",
"enzyme-adapter-react-16": "^1.15.2",
"enzyme-to-json": "^3.4.4",
"formik": "^2.1.5",
"formik-material-ui": "^2.0.1",
"lodash": "^4.17.19",
"react": "^16.13.1",
"react-dom": "^16.13.1",
"react-redux": "^7.2.0",
"react-router-dom": "^5.2.0",
"react-scripts": "3.4.1",
"serverless": "^2.29.0",
"serverless-finch": "^2.6.0",
"typescript": "~3.7.2",
"@reduxjs/toolkit": "^1.2.5",
"react-redux": "^7.2.0",
"@types/react-redux": "^7.1.7",
"@types/lodash": "^4.14.158",
"lodash": "^4.17.19",
"enzyme": "^3.11.0",
"enzyme-adapter-react-16": "^1.15.2",
"enzyme-to-json": "^3.4.4"
"yup": "^0.29.1"
},
"devDependencies": {
"@testing-library/jest-dom": "^4.2.4",
Expand Down
128 changes: 128 additions & 0 deletions serverless.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
service: node-in-aws-web

frameworkVersion: '2'

provider:
name: aws
runtime: nodejs12.x
# setup profile for AWS CLI.
# profile: node-aws

plugins:
- serverless-finch
# 'serverless-single-page-app-plugin' is a custom plugin that located .serverless_plugins folder.
# Existing plugin (https://www.npmjs.com/package/serverless-single-page-app-plugin) doesn't have invalidate cache feature that often used during CI/CD events.
# How to build your own plugins: https://www.serverless.com/framework/docs/providers/aws/guide/plugins#service-local-plugin
- serverless-single-page-app-plugin

custom:
client:
bucketName: node-in-aws-web-bucket
distributionFolder: build
s3BucketName: ${self:custom.client.bucketName}

## Serverless-single-page-app-plugin configuration:
s3LocalPath: ${self:custom.client.distributionFolder}/

resources:
Resources:
## Specifying the S3 Bucket
WebAppS3Bucket:
Type: AWS::S3::Bucket
Properties:
BucketName: ${self:custom.s3BucketName}
AccessControl: PublicRead
WebsiteConfiguration:
IndexDocument: index.html
ErrorDocument: index.html
# VersioningConfiguration:
# Status: Enabled

## Specifying the policies to make sure all files inside the Bucket are avaialble to CloudFront
WebAppS3BucketPolicy:
Type: AWS::S3::BucketPolicy
Properties:
Bucket:
Ref: WebAppS3Bucket
PolicyDocument:
Statement:
- Sid: 'AllowCloudFrontAccessIdentity'
Effect: Allow
Action: s3:GetObject
Resource: arn:aws:s3:::${self:custom.s3BucketName}/*
Principal:
AWS:
Fn::Join:
- ' '
- - 'arn:aws:iam::cloudfront:user/CloudFront Origin Access Identity'
- !Ref OriginAccessIdentity

OriginAccessIdentity:
Type: AWS::CloudFront::CloudFrontOriginAccessIdentity
Properties:
CloudFrontOriginAccessIdentityConfig:
Comment: Access identity between CloudFront and S3 bucket

## Specifying the CloudFront Distribution to server your Web Application
WebAppCloudFrontDistribution:
Type: AWS::CloudFront::Distribution
Properties:
DistributionConfig:
Origins:
- DomainName: ${self:custom.s3BucketName}.s3.amazonaws.com
## An identifier for the origin which must be unique within the distribution
Id: myS3Origin
## In case you don't want to restrict the bucket access use CustomOriginConfig and remove S3OriginConfig
S3OriginConfig:
OriginAccessIdentity: !Sub origin-access-identity/cloudfront/${OriginAccessIdentity}
# CustomOriginConfig:
# HTTPPort: 80
# HTTPSPort: 443
# OriginProtocolPolicy: https-only
Enabled: true
IPV6Enabled: true
HttpVersion: http2
## Uncomment the following section in case you are using a custom domain
# Aliases:
# - mysite.example.com
DefaultRootObject: index.html
## Since the Single Page App is taking care of the routing we need to make sure ever path is served with index.html
## The only exception are files that actually exist e.h. app.js, reset.css
CustomErrorResponses:
- ErrorCode: 404
ResponseCode: 200
ResponsePagePath: /index.html
DefaultCacheBehavior:
AllowedMethods: [ 'GET', 'HEAD', 'OPTIONS' ]
CachedMethods: [ 'GET', 'HEAD', 'OPTIONS' ]
ForwardedValues:
Headers:
- Access-Control-Request-Headers
- Access-Control-Request-Method
- Origin
- Authorization
## Defining if and how the QueryString and Cookies are forwarded to the origin which in this case is S3
QueryString: false
Cookies:
Forward: none
## The origin id defined above
TargetOriginId: myS3Origin
## The protocol that users can use to access the files in the origin. To allow HTTP use `allow-all`
ViewerProtocolPolicy: redirect-to-https
Compress: true
DefaultTTL: 0
## The certificate to use when viewers use HTTPS to request objects.
ViewerCertificate:
CloudFrontDefaultCertificate: 'true'
## Uncomment the following section in case you want to enable logging for CloudFront requests
# Logging:
# IncludeCookies: 'false'
# Bucket: mylogs.s3.amazonaws.com
# Prefix: myprefix

## In order to print out the hosted domain via `serverless info` we need to define the DomainName output for CloudFormation
Outputs:
WebAppS3BucketOutput:
Value: !Ref WebAppS3Bucket
WebAppCloudFrontDistributionOutput:
Value: !GetAtt WebAppCloudFrontDistribution.DomainName

0 comments on commit 05ca160

Please sign in to comment.