diff --git a/deploy/aws/.gitignore b/deploy/aws/.gitignore new file mode 100644 index 00000000..305c7fbc --- /dev/null +++ b/deploy/aws/.gitignore @@ -0,0 +1,11 @@ +*.js +!jest.config.js +*.d.ts +node_modules + +# CDK asset staging directory +.cdk.staging +cdk.out + +# Parcel default cache directory +.parcel-cache diff --git a/deploy/aws/.npmignore b/deploy/aws/.npmignore new file mode 100644 index 00000000..c1d6d45d --- /dev/null +++ b/deploy/aws/.npmignore @@ -0,0 +1,6 @@ +*.ts +!*.d.ts + +# CDK asset staging directory +.cdk.staging +cdk.out diff --git a/deploy/aws/README.md b/deploy/aws/README.md new file mode 100644 index 00000000..66a0f888 --- /dev/null +++ b/deploy/aws/README.md @@ -0,0 +1,15 @@ +# This is the deployment Infrastructure as code for use with AWS + +## configuration, please set them as env var + * `LOCALTUNNEL_BASE_DOMAIN` the root domain in route53 + * `LOCALTUNNEL_ROUTE53_ID` the ID of the route53 hosted zone for the domain + * `LOCALTUNNEL_DOMAIN` the domain of the localtunnel server +## After setting the environment variables, simply run: + ```npm i && cdk deploy``` + +## Deployed Architecture +1. This Architecture brings up a ECS fargate cluster and 2 public subnets + +2. It spins up a container that runs using fargate runtype with the upstream docker image + +3. the container image is placed in one of the two private subnets \ No newline at end of file diff --git a/deploy/aws/bin/deploy.ts b/deploy/aws/bin/deploy.ts new file mode 100644 index 00000000..c09c896b --- /dev/null +++ b/deploy/aws/bin/deploy.ts @@ -0,0 +1,8 @@ +#!/usr/bin/env node +import 'source-map-support/register'; +import * as cdk from '@aws-cdk/core'; +import { DeployStack } from '../lib/deploy-stack'; + +let app = new cdk.App(); +// replace them with HostedZoneID in route53, the domain name for the localtunnel, and the hosted zone domain +new DeployStack(app, 'DeployStack', process.env.LOCALTUNNEL_ROUTE53_ID as string , process.env.LOCALTUNNEL_BASE_DOMAIN as string, process.env.LOCALTUNNEL_DOMAIN as string); diff --git a/deploy/aws/cdk.json b/deploy/aws/cdk.json new file mode 100644 index 00000000..ce1a9d49 --- /dev/null +++ b/deploy/aws/cdk.json @@ -0,0 +1,9 @@ +{ + "app": "npx ts-node --prefer-ts-exts bin/deploy.ts", + "context": { + "@aws-cdk/core:enableStackNameDuplicates": "true", + "aws-cdk:enableDiffNoFail": "true", + "@aws-cdk/core:stackRelativeExports": "true", + "@aws-cdk/aws-ecr-assets:dockerIgnoreSupport": true + } +} diff --git a/deploy/aws/jest.config.js b/deploy/aws/jest.config.js new file mode 100644 index 00000000..772f9749 --- /dev/null +++ b/deploy/aws/jest.config.js @@ -0,0 +1,7 @@ +module.exports = { + roots: ['/test'], + testMatch: ['**/*.test.ts'], + transform: { + '^.+\\.tsx?$': 'ts-jest' + } +}; diff --git a/deploy/aws/lib/deploy-stack.ts b/deploy/aws/lib/deploy-stack.ts new file mode 100644 index 00000000..6977c800 --- /dev/null +++ b/deploy/aws/lib/deploy-stack.ts @@ -0,0 +1,77 @@ +import * as cdk from '@aws-cdk/core'; + +// import dependencies for ECS and building a VPC +import * as ec2 from "@aws-cdk/aws-ec2"; +import * as ecs from "@aws-cdk/aws-ecs"; +import * as ecs_patterns from "@aws-cdk/aws-ecs-patterns"; + +// tls certificate with ACM +import * as route53 from '@aws-cdk/aws-route53'; +import * as acm from '@aws-cdk/aws-certificatemanager'; +import { FargateTaskDefinition } from '@aws-cdk/aws-ecs'; +export class DeployStack extends cdk.Stack { + constructor(scope: cdk.Construct, id: string, hostedZoneID: string, zoneName: string, domainName: string, props?: cdk.StackProps) { + super(scope, id, props); + // create vpc with 3 subnets + const vpc = new ec2.Vpc(this, "LocalTunnelVPC", { + maxAzs: 3, + natGateways: 0 + }) + // create the ecs cluster + const cluster = new ecs.Cluster(this, "LocalTunnelCluster", { + vpc: vpc + }) + // create acm cert + const DNSZone = route53.HostedZone.fromHostedZoneAttributes(this, 'HostedZone', { + zoneName: domainName, + hostedZoneId: hostedZoneID + }) + // add wildcard CNAME + new route53.CnameRecord(this, 'CnameRecordWildcard', { + zone: DNSZone, + recordName: "*", + domainName: domainName + }) + const cert = new acm.Certificate(this, 'Cert', { + domainName: domainName, + subjectAlternativeNames: ["*."+domainName], + validation: acm.CertificateValidation.fromDns(DNSZone) + }) + + + // task definition + let FgTask = new FargateTaskDefinition(this, "LocaltunnelDefinition", { + cpu: 256, + memoryLimitMiB: 512, + }) + FgTask.addContainer("localtunnel", { + image: ecs.ContainerImage.fromRegistry("defunctzombie/localtunnel-server:latest"), + cpu: 128, + entryPoint: ["node", "-r", "esm", "./bin/server", "--domain", domainName], + + }).addPortMappings({ + containerPort: 80 + }) + + // create LBed Fargate service + let localtunnelsvc = new ecs_patterns.ApplicationLoadBalancedFargateService(this, "LocalTunnelService", { + cluster: cluster, + cpu: 512, + desiredCount: 1, + taskDefinition: FgTask, + memoryLimitMiB: 2048, + publicLoadBalancer: true, + certificate: cert, + redirectHTTP: true, + recordType: ecs_patterns.ApplicationLoadBalancedServiceRecordType.ALIAS, + listenerPort: 443, + domainName: domainName, + domainZone: DNSZone, + assignPublicIp: true + }) + // set health route + localtunnelsvc.targetGroup.configureHealthCheck({ + path: "/api/status" + }) + } +} diff --git a/deploy/aws/package.json b/deploy/aws/package.json new file mode 100644 index 00000000..2a6e618f --- /dev/null +++ b/deploy/aws/package.json @@ -0,0 +1,32 @@ +{ + "name": "deploy", + "version": "0.1.0", + "bin": { + "deploy": "bin/deploy.js" + }, + "scripts": { + "build": "tsc", + "watch": "tsc -w", + "test": "jest", + "cdk": "cdk" + }, + "devDependencies": { + "@aws-cdk/assert": "1.75.0", + "@types/jest": "^26.0.10", + "@types/node": "10.17.27", + "jest": "^26.4.2", + "ts-jest": "^26.2.0", + "aws-cdk": "1.75.0", + "ts-node": "^9.0.0", + "typescript": "~3.9.7" + }, + "dependencies": { + "@aws-cdk/aws-certificatemanager": "^1.91.0", + "@aws-cdk/aws-ec2": "^1.91.0", + "@aws-cdk/aws-ecs": "^1.91.0", + "@aws-cdk/aws-ecs-patterns": "^1.91.0", + "@aws-cdk/aws-route53": "^1.91.0", + "@aws-cdk/core": "1.75.0", + "source-map-support": "^0.5.16" + } +} diff --git a/deploy/aws/test/deploy.test.ts b/deploy/aws/test/deploy.test.ts new file mode 100644 index 00000000..2200d64e --- /dev/null +++ b/deploy/aws/test/deploy.test.ts @@ -0,0 +1,13 @@ +import { expect as expectCDK, matchTemplate, MatchStyle } from '@aws-cdk/assert'; +import * as cdk from '@aws-cdk/core'; +import * as Deploy from '../lib/deploy-stack'; + +test('Empty Stack', () => { + const app = new cdk.App(); + // WHEN + const stack = new Deploy.DeployStack(app, 'MyTestStack'); + // THEN + expectCDK(stack).to(matchTemplate({ + "Resources": {} + }, MatchStyle.EXACT)) +}); diff --git a/deploy/aws/tsconfig.json b/deploy/aws/tsconfig.json new file mode 100644 index 00000000..ec75123c --- /dev/null +++ b/deploy/aws/tsconfig.json @@ -0,0 +1,23 @@ +{ + "compilerOptions": { + "target": "ES2018", + "module": "commonjs", + "lib": ["es2018"], + "declaration": true, + "strict": true, + "noImplicitAny": true, + "strictNullChecks": true, + "noImplicitThis": true, + "alwaysStrict": true, + "noUnusedLocals": false, + "noUnusedParameters": false, + "noImplicitReturns": true, + "noFallthroughCasesInSwitch": false, + "inlineSourceMap": true, + "inlineSources": true, + "experimentalDecorators": true, + "strictPropertyInitialization": false, + "typeRoots": ["./node_modules/@types"] + }, + "exclude": ["cdk.out"] +}