diff --git a/.aws-infrastructure/.gitignore b/.aws-infrastructure/.gitignore index f60797b..0592e5f 100644 --- a/.aws-infrastructure/.gitignore +++ b/.aws-infrastructure/.gitignore @@ -6,3 +6,4 @@ node_modules # CDK asset staging directory .cdk.staging cdk.out +.cdk/ diff --git a/.aws-infrastructure/Dockerfile b/.aws-infrastructure/Dockerfile index c32ba05..3a36d64 100644 --- a/.aws-infrastructure/Dockerfile +++ b/.aws-infrastructure/Dockerfile @@ -1,6 +1,7 @@ FROM node:13.3.0-alpine ENV HOME=/usr/src/app WORKDIR $HOME -COPY ./ $HOME +COPY ./package.json $HOME/package.json RUN npm install -g aws-cdk@1.18.0 --quiet RUN npm install --quiet +COPY ./ $HOME diff --git a/.aws-infrastructure/bin/CheetSheetInfrastructure.ts b/.aws-infrastructure/bin/CheetSheetInfrastructure.ts index 1c62e82..6680072 100644 --- a/.aws-infrastructure/bin/CheetSheetInfrastructure.ts +++ b/.aws-infrastructure/bin/CheetSheetInfrastructure.ts @@ -2,6 +2,19 @@ import 'source-map-support/register'; import cdk = require('@aws-cdk/core'); import { CheetSheetInfrastructureStack } from '../lib/CheetSheetInfrastructure-stack'; +const ENVIRONMENT = process.env['ENVIRONMENT'] || ''; + +if (ENVIRONMENT === '') { + throw new Error('ENVIRONMENT env variable cannot be blank'); +} const app = new cdk.App(); -new CheetSheetInfrastructureStack(app, 'CheetSheetInfrastructureStack'); +new CheetSheetInfrastructureStack( + app, + `CheetSheetInfrastructureStack-${ENVIRONMENT}`, + { + env: { + 'region': process.env['AWS_DEFAULT_REGION'] + } + } +); diff --git a/.aws-infrastructure/lib/CheetSheetInfrastructure-stack.ts b/.aws-infrastructure/lib/CheetSheetInfrastructure-stack.ts index 551fe14..398339d 100644 --- a/.aws-infrastructure/lib/CheetSheetInfrastructure-stack.ts +++ b/.aws-infrastructure/lib/CheetSheetInfrastructure-stack.ts @@ -1,9 +1,179 @@ import cdk = require('@aws-cdk/core'); +import s3 = require('@aws-cdk/aws-s3'); +import cloudfront = require('@aws-cdk/aws-cloudfront'); +import route53 = require('@aws-cdk/aws-route53'); +import targets = require('@aws-cdk/aws-route53-targets/lib'); +import cognito = require('@aws-cdk/aws-cognito'); +import iam = require('@aws-cdk/aws-iam'); + + +// Read environment variables +const UI_DISTRIBUTION_TYPE = process.env['UI_DISTRIBUTION_TYPE'] || '';; +const AWS_ROUTE53_HOSTED_ZONE_ID = process.env['AWS_ROUTE53_HOSTED_ZONE_ID'] || ''; +const SITE_SUB_DOMAIN = process.env['SITE_DOMAIN'] || ''; +const SITE_DOMAIN = process.env['SITE_SUB_DOMAIN'] || ''; +const AWS_ACM_CERTIFICATE_ARN = process.env['AWS_ACM_CERTIFICATE_ARN'] || ''; +const ENVIRONMENT = process.env['ENVIRONMENT'] || ''; +const CREATE_IAM_POLICIES = process.env['CREATE_IAM_POLICIES'] || 'true'; + +const toBoolean = (value: string | number | boolean): boolean => + [true, 'true', 'True', 'TRUE', '1', 1].includes(value); export class CheetSheetInfrastructureStack extends cdk.Stack { - constructor(scope: cdk.Construct, id: string, props?: cdk.StackProps) { - super(scope, id, props); - // The code that defines your stack goes here - } + siteHostname = `${SITE_SUB_DOMAIN}.${SITE_DOMAIN}`; + siteDomainName = SITE_DOMAIN; + uiDistributionType = UI_DISTRIBUTION_TYPE; + shouldCreateIamPolicies = toBoolean(CREATE_IAM_POLICIES); + + constructor(scope: cdk.Construct, id: string, props?: cdk.StackProps) { + super(scope, id, props); + + // Create these resources to host our API and UI only if on a remote environment + if (ENVIRONMENT !== 'local') { + this.constructWebsiteResources(this.uiDistributionType); + this.constructApiRequiredResources(this.shouldCreateIamPolicies); + } + + // Create our resources for our Authentication Management system + this.constructCognitoResources(); + + // Set up our private bucket that will save our application data (Sheet data is saved here) + this.constructApiRequiredResources(toBoolean(CREATE_IAM_POLICIES)) + + } + + + constructApiRequiredResources(shouldCreateIamPolicies: boolean) { + // Set up our private bucket that will be used to save and track deployment Artifacts for our API + const apiDeploymentBucket = new s3.Bucket(this, 'S3APIDeploymentBucket', { + removalPolicy: cdk.RemovalPolicy.DESTROY, + accessControl: s3.BucketAccessControl.PRIVATE + }); + + // Create IAM Policies to necessary for our Lambda functions in our API + if (shouldCreateIamPolicies) { + const role = new iam.Role(this, 'MyRole', { + assumedBy: new iam.ServicePrincipal('sns.amazonaws.com') + }); + + role.addToPolicy(new iam.PolicyStatement({ + resources: ['*'], + actions: ['lambda:InvokeFunction'] }) + ); + } + } + + /** + * This method constructs all methods needed to setup our authentication + * management in Cognito + */ + constructCognitoResources() { + // Create our Cognito Userpool for tracking users + const userPool = new cognito.UserPool(this, 'CognitoAppUserPool', { + signInType: cognito.SignInType.USERNAME, + autoVerifiedAttributes: [cognito.UserPoolAttribute.EMAIL], + userPoolName: this.siteHostname, + usernameAliasAttributes: [ cognito.UserPoolAttribute.PREFERRED_USERNAME ] + }); + new cognito.CfnUserPoolGroup(this, "CognitoAdminsGroup", { + groupName: 'admin', + userPoolId: userPool.userPoolId, + + }); + + const logoutUrLs = [ + `https://${this.siteHostname}`, + `https://${this.siteHostname}/login`, + `https://${this.siteHostname}/logout`, + `http://${this.siteHostname}`, + `http://${this.siteHostname}/login`, + `http://${this.siteHostname}/logout` + ] + const callbackUrLs = [ + `https://${this.siteHostname}`, + `https://${this.siteHostname}/login`, + `https://${this.siteHostname}/logout`, + `http://${this.siteHostname}`, + `http://${this.siteHostname}/login`, + `http://${this.siteHostname}/logout` + ] + + const userPoolClient = new cognito.CfnUserPoolClient(this, 'CognitoAppUserPoolClient', { + userPoolId: userPool.userPoolId, + explicitAuthFlows: [ cognito.AuthFlow.USER_PASSWORD ], + logoutUrLs: logoutUrLs, + callbackUrLs: callbackUrLs, + allowedOAuthFlows: [ 'implicit', 'code'], + allowedOAuthScopes: [ "email", "openid", "aws.cognito.signin.user.admin", "profile"], + refreshTokenValidity: 30 + }); + + const userPoolDomain = new cognito.CfnUserPoolDomain(this, 'CognitoAppUserPoolDomain', { + userPoolId: userPool.userPoolId, + domain: 'cheet-sheet-dev' + }); + } + + /** + * Create all resources needed to host our UI. There are two options. + * You can directly host from S3, or you can set up a Cloud Front CDN + * if you expect more trafic. + * + * @param targetResourceType - "buket" or "cloudfront" + */ + constructWebsiteResources(targetResourceType = 'bucket') { + if (targetResourceType !== 'bucket' && targetResourceType !== 'cloudfront') + throw new Error('Your UI distribution type must be "bucket" or "cloudfront"'); + + // Set up our public bucket that hosts our frontend + const clientUIAssetsBucket = new s3.Bucket(this, 'S3ClientUIAssetsBucket', { + websiteIndexDocument: 'index.html', + websiteErrorDocument: 'error.html', + bucketName:this.siteHostname, + publicReadAccess: true, + removalPolicy: cdk.RemovalPolicy.DESTROY + }); + + let targetResource: route53.IAliasRecordTarget = new targets.BucketWebsiteTarget(clientUIAssetsBucket); + if (targetResourceType!=='bucket') { + const clientUIWebDistribution = new cloudfront.CloudFrontWebDistribution(this, 'CloudFrontClientUIWebDistribution', { + originConfigs: [ + { + s3OriginSource: { + s3BucketSource: clientUIAssetsBucket + }, + behaviors: [ + { + isDefaultBehavior: true, + allowedMethods: cloudfront.CloudFrontAllowedMethods.ALL, + defaultTtl: cdk.Duration.seconds(60) + }, + ] + } + ], + aliasConfiguration: { + acmCertRef: AWS_ACM_CERTIFICATE_ARN, + names: [ this.siteHostname ], + sslMethod: cloudfront.SSLMethod.SNI, + securityPolicy: cloudfront.SecurityPolicyProtocol.TLS_V1_1_2016 + } + }); + targetResource = new targets.CloudFrontTarget(clientUIWebDistribution); + } + + const zone = route53.HostedZone.fromHostedZoneAttributes(this, 'Zone', { + hostedZoneId: AWS_ROUTE53_HOSTED_ZONE_ID, + zoneName: this.siteDomainName + }); + + const clientUIDNSRecord = new route53.ARecord(this, 'SiteAliasRecord', { + recordName: 'cheet-sheet-dev', + target: route53.AddressRecordTarget.fromAlias( + targetResource + ), + zone + }); + } + } diff --git a/.aws-infrastructure/package.json b/.aws-infrastructure/package.json index a827160..e3450a1 100644 --- a/.aws-infrastructure/package.json +++ b/.aws-infrastructure/package.json @@ -1,5 +1,5 @@ { - "name": ".aws-infrastructure", + "name": "cheet-sheet-infrastructure", "version": "0.1.0", "bin": { "src": "bin/CheetSheetInfrastructure.js" @@ -21,6 +21,14 @@ "typescript": "~3.7.2" }, "dependencies": { + "@aws-cdk/aws-certificatemanager": "^1.18.0", + "@aws-cdk/aws-cloudfront": "^1.18.0", + "@aws-cdk/aws-cognito": "^1.18.0", + "@aws-cdk/aws-iam": "^1.18.0", + "@aws-cdk/aws-route53": "^1.18.0", + "@aws-cdk/aws-route53-targets": "^1.18.0", + "@aws-cdk/aws-s3": "^1.18.0", + "@aws-cdk/aws-s3-deployment": "^1.18.0", "@aws-cdk/core": "^1.18.0", "source-map-support": "^0.5.16" } diff --git a/.env_templates/infrastructure.template.env b/.env_templates/infrastructure.template.env new file mode 100644 index 0000000..87a9aed --- /dev/null +++ b/.env_templates/infrastructure.template.env @@ -0,0 +1,15 @@ +# What unique environment is this infrastructure, ex: local, prod, dev, staging +# If this is set to local, the cdk will only create resources you need to +# run this app locally, not host it to the public. +ENVIRONMENT= +UI_DISTRIBUTION_TYPE= + +AWS_DEFAULT_REGION= +AWS_ACCESS_KEY_ID= +AWS_SECRET_ACCESS_KEY= + +AWS_ROUTE53_HOSTED_ZONE_ID= +SITE_SUB_DOMAIN= +SITE_DOMAIN= + +AWS_ACM_CERTIFICATE_ARN= diff --git a/.env_templates/local.template.env b/.env_templates/local.template.env new file mode 100644 index 0000000..0345135 --- /dev/null +++ b/.env_templates/local.template.env @@ -0,0 +1,27 @@ +# AWS Credentials and regions +AWS_DEFAULT_REGION= +AWS_ACCESS_KEY_ID= +AWS_SECRET_ACCESS_KEY= + +# Tell SAM Local the Path of the API, so it can properly mount volumes +SAM_LOCAL_ABSOLUTE_PATH= + +# Comment this out if your okay with SAM sending data +SAM_CLI_TELEMETRY=0 + +# AWS Lambda Layer ARN, with necessary dependencies for the Python3.7 API +# - pyjwt==1.7.1 +# - cryptography==2.8 +LAMBDA_LAYER= + +# Info for S3 Database which saves sheets +# SHEET_DATA_S3_BUCKET: S3 bucket that stores all sheets +# APP_ADMIN_USER: user who can edit, add and delete public sheets +# PUBLIC_SHEETS_FOLDER: what folder in S3 hosts the public sheets folder +APP_ADMIN_USER= +PUBLIC_SHEETS_FOLDER= +SHEET_DATA_S3_BUCKET= + +# Base Encode JSON Web Tokens for Cognito +COGNITO_ID_JWK_BASE4= +COGNITO_ACCESS_JWK_BASE4= diff --git a/.env_templates/remote.template.env b/.env_templates/remote.template.env new file mode 100644 index 0000000..268b519 --- /dev/null +++ b/.env_templates/remote.template.env @@ -0,0 +1,32 @@ +# AWS Credentials and regions +AWS_DEFAULT_REGION= +AWS_ACCESS_KEY_ID= +AWS_SECRET_ACCESS_KEY= + +# Comment this out if your okay with SAM sending data +SAM_CLI_TELEMETRY=0 + +# AWS Lambda Layer ARN, with necessary dependencies for the Python3.7 API +# - pyjwt==1.7.1 +# - cryptography==2.8 +LAMBDA_LAYER= + +# Info for S3 Database which saves sheets +# SHEET_DATA_S3_BUCKET: S3 bucket that stores all sheets +# APP_ADMIN_USER: user who can edit, add and delete public sheets +# PUBLIC_SHEETS_FOLDER: what folder in S3 hosts the public sheets folder +APP_ADMIN_USER= +PUBLIC_SHEETS_FOLDER= +SHEET_DATA_S3_BUCKET= + +# Base Encode JSON Web Tokens for Cognito +COGNITO_ID_JWK_BASE4= +COGNITO_ACCESS_JWK_BASE4= + +# S3 Bucket For the Client UI +SHEET_DATA_S3_BUCKET= + +# S3 bucket that hosts the client UI +CLIENT_UI_S3_BUCKET= +# S3 bucket that saves deploy Artifacts for the API +API_DEPLOYMENT_S3_BUCKET= diff --git a/.gitignore b/.gitignore index 58723c3..83201a2 100644 --- a/.gitignore +++ b/.gitignore @@ -3,6 +3,7 @@ node_modules package-lock.json *.env +!*.template.env backup.env .ipynb_checkpoints SimpleServer.ipynb diff --git a/docker-compose.deploy.code.yml b/docker-compose.deploy.code.yml index 32899fb..096c280 100644 --- a/docker-compose.deploy.code.yml +++ b/docker-compose.deploy.code.yml @@ -13,6 +13,8 @@ # - API Gateway # - S3 Bucket for UI hosting # - S3 Bucket for App Data Storage +# - IAM Role for a Lambda Function +# Should have permission to update the App Data Storage bucket # # ------------------------------------------------------------------------------ version: '3.7' @@ -22,7 +24,7 @@ services: # -------------------------------------------------------------------------- serverless-angular-client-ui: build: ./client-ui - container_name: serverless-angular-client-ui + container_name: serverless-deploy-angular-client-ui volumes: - ./client-ui:/usr/src/app - /usr/src/app/node_modules @@ -32,12 +34,17 @@ services: # -------------------------------------------------------------------------- serverless-lambda-api: build: ./api - container_name: serverless-lambda-api + container_name: serverless-deploy-lambda-api volumes: - ./api:/app + env_file: + - remote.env environment: - AWS_DEFAULT_REGION=$AWS_DEFAULT_REGION - AWS_ACCESS_KEY_ID=$AWS_ACCESS_KEY_ID - AWS_SECRET_ACCESS_KEY=$AWS_SECRET_ACCESS_KEY - LAMBDA_LAYER:$LAMBDA_LAYER - # command: bash deploy.sh + +networks: + default: + name: cheet-sheet-deploy-code diff --git a/docker-compose.deploy.infrastructure.yml b/docker-compose.deploy.infrastructure.yml index f0f47a7..1d78104 100644 --- a/docker-compose.deploy.infrastructure.yml +++ b/docker-compose.deploy.infrastructure.yml @@ -33,3 +33,7 @@ services: - ./package.json:/usr/src/.aws-infrastructure/package.json - ./.aws-infrastructure:/usr/src/app/ - /usr/src/app/node_modules + +networks: + default: + name: cheet-sheet-deploy-infrastructure diff --git a/docker-compose.deploy.network.yml b/docker-compose.deploy.network.yml deleted file mode 100644 index a61437d..0000000 --- a/docker-compose.deploy.network.yml +++ /dev/null @@ -1,6 +0,0 @@ -# ------------------------------------------------------------------------------ -# NETWORK DOCKER COMPOSE -# -# This Docker Compose is meant to setup the necessary network infrastructure -# for the app. This includes VPC's, Subnets, Roles and Policies. -# ------------------------------------------------------------------------------ diff --git a/docker-compose.yml b/docker-compose.yml index 70ab5ac..3fdeb6f 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -3,6 +3,13 @@ # # This Docker Compose is meant to run the App locally on ones machine. # Its use case is for development. +# +# You need the following AWS resources to run this: +# - Lambda layer with +# pyjwt==1.7.1 +# cryptography==2.8 +# - S3 Bucket for App Data +# - Cognito user pool # ------------------------------------------------------------------------------ version: '3.7' services: @@ -13,7 +20,7 @@ services: build: ./client-ui container_name: serverless-angular-client-ui env_file: - .env + local.env volumes: - ./client-ui:/usr/src/app - /usr/src/app/node_modules @@ -26,7 +33,7 @@ services: build: ./api container_name: serverless-lambda-api env_file: - .env + local.env volumes: - ./api:/app - /var/run/docker.sock:/var/run/docker.sock diff --git a/run_compose.bash b/utils.bash similarity index 100% rename from run_compose.bash rename to utils.bash