diff --git a/projects/web/index.html b/projects/web/index.html new file mode 100644 index 0000000..1910835 --- /dev/null +++ b/projects/web/index.html @@ -0,0 +1,46 @@ + + + + Simple Web Service + + + +

Simple Web Service

+ Enter something in the box below and hit the button: + +
+ + +
+
+    
+ + + + \ No newline at end of file diff --git a/serverless-webapp/README.md b/serverless-webapp/README.md index fd4ef01..266cc4f 100644 --- a/serverless-webapp/README.md +++ b/serverless-webapp/README.md @@ -11,14 +11,14 @@ This [CDK Pattern](https://cdkpatterns.com/patterns/) to make deploying a Dynami /modernapps/serverless-webapp ├── README.md ├── bin -| └── sserverless-webapp.ts +| └── serverless-webapp.ts ├── cdk.json ├── jest.config.js ├── lib -| └── sserverless-webapp-stack.ts +| └── serverless-webapp-stack.ts ├── package.json ├── test -| └── sserverless-webapp.test.ts +| └── serverless-webapp.test.ts └── tsconfig.json ``` diff --git a/serverless-webapp/bin/serverless-webapp.ts b/serverless-webapp/bin/serverless-webapp.ts index d279a53..451ec17 100644 --- a/serverless-webapp/bin/serverless-webapp.ts +++ b/serverless-webapp/bin/serverless-webapp.ts @@ -2,6 +2,14 @@ import 'source-map-support/register'; import * as cdk from '@aws-cdk/core'; import { ServerlessWebappStack } from '../lib/serverless-webapp-stack'; +import { applicationMetaData } from '../config/config'; const app = new cdk.App(); -new ServerlessWebappStack(app, 'ServerlessWebappStack'); + +new ServerlessWebappStack(app, 'ServerlessWebappStack', { + domainName: applicationMetaData.domainName, + // FIXME #3 + // certificate: applicationMetaData.certificate, + // table: db.table, + // monitoring +}); \ No newline at end of file diff --git a/serverless-webapp/config/config.ts b/serverless-webapp/config/config.ts new file mode 100755 index 0000000..7a6808c --- /dev/null +++ b/serverless-webapp/config/config.ts @@ -0,0 +1,8 @@ +/** Configuration file */ +export const applicationMetaData = { + account: "${AWS_ACCOUNT}", + region: "${AWS_REGION}", + + /** Primary Route53 Domain */ + domainName: "web.job4u.vn", +} \ No newline at end of file diff --git a/serverless-webapp/lib/monitoring-stack.ts b/serverless-webapp/lib/monitoring-stack.ts new file mode 100644 index 0000000..ac0094f --- /dev/null +++ b/serverless-webapp/lib/monitoring-stack.ts @@ -0,0 +1,30 @@ +import * as cdk from '@aws-cdk/core'; +import * as cloudwatch from '@aws-cdk/aws-cloudwatch'; + +/** + * Interface that downstream stacks expect to monitoring subsystem. + */ +export interface IMonitoring { + addGraphs(title: string, ...widgets: cloudwatch.IWidget[]): void; +} + +/** + * Class with monitoring facilities. + */ +export class MonitoringStack extends cdk.Stack implements IMonitoring { + private dashboard: cloudwatch.Dashboard; + + constructor(scope: cdk.Construct, id: string, props?: cdk.StackProps) { + super(scope, id, props); + + this.dashboard = new cloudwatch.Dashboard(this, 'Serverless-Webapp Dashboard'); + } + + public addGraphs(title: string, ...widgets: cloudwatch.IWidget[]): void { + this.dashboard.addWidgets(new cloudwatch.TextWidget({ + markdown: `# ${title}`, + })); + this.dashboard.addWidgets(...widgets); + } + +} \ No newline at end of file diff --git a/serverless-webapp/lib/serverless-webapp-stack.ts b/serverless-webapp/lib/serverless-webapp-stack.ts index 6f0cc09..1c768aa 100644 --- a/serverless-webapp/lib/serverless-webapp-stack.ts +++ b/serverless-webapp/lib/serverless-webapp-stack.ts @@ -1,9 +1,137 @@ import * as cdk from '@aws-cdk/core'; +import * as apigateway from '@aws-cdk/aws-apigateway'; +import * as certmgr from '@aws-cdk/aws-certificatemanager'; +import * as dynamodb from '@aws-cdk/aws-dynamodb'; +import * as cloudwatch from '@aws-cdk/aws-cloudwatch'; +import * as cloudfront from '@aws-cdk/aws-cloudfront'; +import * as s3 from '@aws-cdk/aws-s3'; +import * as s3deploy from '@aws-cdk/aws-s3-deployment'; +import * as origins from '@aws-cdk/aws-cloudfront-origins'; +import * as lambda from '@aws-cdk/aws-lambda'; +import { IMonitoring } from './monitoring-stack'; + +/** ServerlessWebapp StackProps */ +export interface WebAppStackProps extends cdk.StackProps { + + /** + * Domain name for the CloudFront distribution + * (Requires 'certificate' to be set) + * @default - Automatically generated domain name under CloudFront domain + */ + readonly domainName?: string; + + /** + * Certificate for the CloudFront distribution + * (Requires 'domainName' to be set) + * @default - Automatically generated domain name under CloudFront domain + */ + readonly certificate?: certmgr.ICertificate; + + /** + * Table to use as backing store for the Lambda Function + */ + // readonly table: dynamodb.ITable; + + /** + * Where to add metrics + */ + // readonly monitoring: IMonitoring; +} + +/** + * Serverless Web-App Stack. + */ export class ServerlessWebappStack extends cdk.Stack { - constructor(scope: cdk.Construct, id: string, props?: cdk.StackProps) { + constructor(scope: cdk.Construct, id: string, props: WebAppStackProps) { super(scope, id, props); // The code that defines your stack goes here + + /** Step 1. Route53-Domain & ACM-Certificate */ + if (!!props.domainName !== !!props.certificate) { + throw new Error('Supply either both or neither of \'domainName\' and \'certificate\''); + } + + /** + * FIXME #4 + * + * Step 2. Lambda & API-Gateway + */ + // const func = new lambda.Function(this, 'Lambda', { + // runtime: lambda.Runtime.NODEJS_14_X, + // handler: 'index.handler', + // code: lambda.Code.fromAsset(`${__dirname}/../build/src/lambda-handlers/lambda-handlers`), + // environment: { + // TABLE_ARN: props.table.tableArn + // }, + // timeout: cdk.Duration.seconds(10), + // }); + + // props.table.grantReadWriteData(func); + + // const apiGateway = new apigateway.LambdaRestApi(this, 'API-Gateway', { + // handler: func, + // endpointTypes: [apigateway.EndpointType.REGIONAL], + // }); + + /** + * Step 3. S3 bucket & Cloudfront distribution + */ + + /** 3.1. S3 bucket to hold the Website with a CloudFront distribution */ + // const bucket = new s3.Bucket(this, 'Bucket', { + // removalPolicy: cdk.RemovalPolicy.DESTROY, + // }); + // const distribution = new cloudfront.Distribution(this, 'Dist', { + // defaultBehavior: { origin: new origins.S3Origin(bucket) }, + // additionalBehaviors: { + // '/api/*': { + // origin: new origins.HttpOrigin(`${apiGateway.restApiId}.execute-api.${this.region}.amazonaws.com`, { + // originPath: `/${apiGateway.deploymentStage.stageName}`, + // }), + // allowedMethods: cloudfront.AllowedMethods.ALLOW_ALL, + // cachePolicy: cloudfront.CachePolicy.CACHING_DISABLED, + // }, + // }, + // defaultRootObject: 'index.html', + // domainNames: props.domainName ? [props.domainName] : undefined, + // certificate: props.certificate, + // }); + + /** 3.2. Upload assets to the S3 bucket */ + // new s3deploy.BucketDeployment(this, 'Deploy', { + // destinationBucket: bucket, + // sources: [s3deploy.Source.asset(`${__dirname}/../../projects/web`)], + // distribution, + // prune: false, + // }); + + /** + * FIXME #5 + * Monitoring: 99% of the requests should be faster than given latency. + * E301: latency 3 seconds + */ + // props.monitoring.addGraphs('Application', + // new cloudwatch.GraphWidget({ + // title: 'P99 Latencies', + // left: [ + // apiGateway.metricLatency({ statistic: 'P99' }), + // apiGateway.metricIntegrationLatency({ statistic: 'P99' }), + // ], + // }), + + // new cloudwatch.GraphWidget({ + // title: 'Counts vs errors', + // left: [ + // apiGateway.metricCount(), + // ], + // right: [ + // apiGateway.metricClientError(), + // apiGateway.metricServerError(), + // ], + // }), + // ); + } -} +} \ No newline at end of file diff --git a/serverless-webapp/package-lock.json b/serverless-webapp/package-lock.json index c5f5b2c..e486437 100644 --- a/serverless-webapp/package-lock.json +++ b/serverless-webapp/package-lock.json @@ -172,6 +172,19 @@ "constructs": "^3.2.0" } }, + "@aws-cdk/aws-cloudfront-origins": { + "version": "1.91.0", + "resolved": "https://registry.npmjs.org/@aws-cdk/aws-cloudfront-origins/-/aws-cloudfront-origins-1.91.0.tgz", + "integrity": "sha512-vayZT70N71dneXrYemVE39HJqXJHR5NGYO0SrrhhapOFuG/GA1+EUdhXVPg+DrNtSSx3Z6Hog1cQlqLFkqOg+A==", + "requires": { + "@aws-cdk/aws-cloudfront": "1.91.0", + "@aws-cdk/aws-elasticloadbalancingv2": "1.91.0", + "@aws-cdk/aws-iam": "1.91.0", + "@aws-cdk/aws-s3": "1.91.0", + "@aws-cdk/core": "1.91.0", + "constructs": "^3.2.0" + } + }, "@aws-cdk/aws-cloudwatch": { "version": "1.91.0", "resolved": "https://registry.npmjs.org/@aws-cdk/aws-cloudwatch/-/aws-cloudwatch-1.91.0.tgz", diff --git a/serverless-webapp/package.json b/serverless-webapp/package.json index eb0d81a..cbe80d2 100644 --- a/serverless-webapp/package.json +++ b/serverless-webapp/package.json @@ -21,6 +21,7 @@ "typescript": "~3.9.7" }, "dependencies": { + "@aws-cdk/aws-cloudfront-origins": "^1.91.0", "@aws-cdk/aws-codepipeline": "^1.91.0", "@aws-cdk/aws-dynamodb": "^1.91.0", "@aws-cdk/aws-lambda": "^1.91.0", diff --git a/serverless-webapp/src/lambda-handlers/healthcheck/index.ts b/serverless-webapp/src/lambda-handlers/healthcheck/index.ts new file mode 100644 index 0000000..7e94b8f --- /dev/null +++ b/serverless-webapp/src/lambda-handlers/healthcheck/index.ts @@ -0,0 +1,14 @@ +/** + * FIXME #4 + * 1. Health Check + * 2. Version + */ +export async function handler(event: any, _context: any) { + return { + statusCode: 200, + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify(event, undefined, 2), + }; + } \ No newline at end of file diff --git a/serverless-webapp/test/serverless-webapp.test.ts b/serverless-webapp/test/serverless-webapp.test.ts index 532a8e5..a9ee9f2 100644 --- a/serverless-webapp/test/serverless-webapp.test.ts +++ b/serverless-webapp/test/serverless-webapp.test.ts @@ -1,11 +1,18 @@ import { expect as expectCDK, matchTemplate, MatchStyle } from '@aws-cdk/assert'; import * as cdk from '@aws-cdk/core'; +import { applicationMetaData } from '../config/config'; import * as ServerlessWebapp from '../lib/serverless-webapp-stack'; test('Empty Stack', () => { const app = new cdk.App(); // WHEN - const stack = new ServerlessWebapp.ServerlessWebappStack(app, 'MyTestStack'); + const stack = new ServerlessWebapp.ServerlessWebappStack(app, 'MyTestStack', { + domainName: applicationMetaData.domainName, + // FIXME #3 + // certificate: applicationMetaData.certificate, + // table: db.table, + // monitoring + }); // THEN expectCDK(stack).to(matchTemplate({ "Resources": {}