Skip to content

Commit

Permalink
806, 809: Route 53 and Certificates for spylogic.ai (#860)
Browse files Browse the repository at this point in the history
* Add DNS and certificate to cloudfront and fargate
  • Loading branch information
chriswilty committed Apr 29, 2024
1 parent 36c6a48 commit 39c2d11
Show file tree
Hide file tree
Showing 10 changed files with 315 additions and 190 deletions.
3 changes: 1 addition & 2 deletions backend/src/server/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,10 @@ import queryTypes from 'query-types';

import { importMetaUrl } from './importMetaUtils';
import nonSessionRoutes from './nonSessionRoutes';
import { usingForwardedHeader } from './proxySetup';
import sessionRoutes from './sessionRoutes';
import uiRoutes from './uiRoutes';

const app = usingForwardedHeader(express())
const app = express()
.use(express.json())
.use(queryTypes.middleware());

Expand Down
7 changes: 7 additions & 0 deletions cloud/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,3 +48,10 @@ npm install
# run the bootstrap command
npx cdk bootstrap --custom-permissions-boundary cdk-developer-policy
```

Unless your default region is `us-east-1`, you will also need to bootstrap that region, as certificates for CloudFront
currently need to be deployed into that region:

```
npx cdk bootstrap --custom-permissions-boundary cdk-developer-policy aws://YOUR_ACCOUNT_NUMBER/us-east-1
```
42 changes: 36 additions & 6 deletions cloud/bin/cloud.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
#!/usr/bin/env node
import { App, Environment } from 'aws-cdk-lib';
import { App, Environment } from 'aws-cdk-lib/core';
import 'source-map-support/register';

import {
Expand All @@ -9,10 +9,16 @@ import {
stackName,
ApiStack,
AuthStack,
CertificateStack,
HostedZoneStack,
UiStack,
} from '../lib';

const app = new App();
const generateStackName = stackName(app);
const generateDescription = resourceDescription(app);

/* Common stack resources */

const env: Environment = {
account: process.env.CDK_DEFAULT_ACCOUNT,
Expand All @@ -23,31 +29,55 @@ const tags = {
owner: appName,
classification: 'unrestricted',
'environment-type': environmentName(app),
'keep-alive': '8-6-without-weekends',
'keep-alive': '9-5-without-weekends',
};

const generateStackName = stackName(app);
const generateDescription = resourceDescription(app);
/* Stack constructs */

const hostedZoneStack = new HostedZoneStack(
app,
generateStackName('hosted-zone'),
{
description: generateDescription('Hosted Zone stack'),
env,
tags,
}
);

const certificateStack = new CertificateStack(
app,
generateStackName('certificate'),
{
description: generateDescription('Certificate stack'),
env,
tags,
hostedZone: hostedZoneStack.hostedZone,
}
);

const uiStack = new UiStack(app, generateStackName('ui'), {
description: generateDescription('UI stack'),
env,
tags,
certificate: certificateStack.cloudFrontCert,
hostedZone: hostedZoneStack.hostedZone,
});

/*const authStack = */ new AuthStack(app, generateStackName('auth'), {
description: generateDescription('Auth stack'),
env,
tags,
webappUrl: uiStack.cloudfrontUrl,
webappUrl: uiStack.cloudFrontUrl,
});

new ApiStack(app, generateStackName('api'), {
description: generateDescription('API stack'),
env,
tags,
certificate: certificateStack.loadBalancerCert,
hostedZone: hostedZoneStack.hostedZone,
// userPool: authStack.userPool,
// userPoolClient: authStack.userPoolClient,
// userPoolDomain: authStack.userPoolDomain,
webappUrl: uiStack.cloudfrontUrl,
webappUrl: uiStack.cloudFrontUrl,
});
3 changes: 2 additions & 1 deletion cloud/cdk.context.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,6 @@
"eu-north-1a",
"eu-north-1b",
"eu-north-1c"
]
],
"ami:account=992382568770:filters.image-type.0=machine:filters.name.0=amzn-ami-vpc-nat-*:filters.state.0=available:owners.0=amazon:region=eu-north-1": "ami-072517490bf2cf3a3"
}
205 changes: 32 additions & 173 deletions cloud/lib/api-stack.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,12 @@
import { CorsHttpMethod, HttpApi, VpcLink } from 'aws-cdk-lib/aws-apigatewayv2';
import { HttpAlbIntegration } from 'aws-cdk-lib/aws-apigatewayv2-integrations';
import { ICertificate } from 'aws-cdk-lib/aws-certificatemanager';
//import { UserPool, UserPoolClient, UserPoolDomain } from 'aws-cdk-lib/aws-cognito';
import { Port, SecurityGroup, Vpc } from 'aws-cdk-lib/aws-ec2';
import {
InstanceClass,
InstanceSize,
InstanceType,
NatInstanceProvider,
Vpc,
} from 'aws-cdk-lib/aws-ec2';
import { DockerImageAsset } from 'aws-cdk-lib/aws-ecr-assets';
import {
Cluster,
Expand All @@ -10,59 +15,34 @@ import {
Secret as EnvSecret,
} from 'aws-cdk-lib/aws-ecs';
import { ApplicationLoadBalancedFargateService } from 'aws-cdk-lib/aws-ecs-patterns';
//import { ListenerAction } from 'aws-cdk-lib/aws-elasticloadbalancingv2';
//import { ListenerAction, ListenerCondition } from 'aws-cdk-lib/aws-elasticloadbalancingv2';
//import { AuthenticateCognitoAction } from 'aws-cdk-lib/aws-elasticloadbalancingv2-actions';
import {
Effect,
PolicyStatement,
Role,
ServicePrincipal,
} from 'aws-cdk-lib/aws-iam';
import { Runtime } from 'aws-cdk-lib/aws-lambda';
import { NodejsFunction } from 'aws-cdk-lib/aws-lambda-nodejs';
import { IHostedZone } from 'aws-cdk-lib/aws-route53';
import { Bucket } from 'aws-cdk-lib/aws-s3';
import {
CronOptionsWithTimezone,
Group,
Schedule,
ScheduleExpression,
ScheduleTargetInput,
} from '@aws-cdk/aws-scheduler-alpha';
import { LambdaInvoke } from '@aws-cdk/aws-scheduler-targets-alpha';
import { Secret } from 'aws-cdk-lib/aws-secretsmanager';
import {
CfnOutput,
RemovalPolicy,
Stack,
StackProps,
Tags,
TimeZone,
} from 'aws-cdk-lib/core';
import { RemovalPolicy, Stack, StackProps } from 'aws-cdk-lib/core';
import { Construct } from 'constructs';
import { join } from 'node:path';

import {
appName,
resourceDescription,
resourceName,
} from './resourceNamingUtils';
import type { ServiceEventLambda } from './startStopServiceLambda';
import { appName, resourceName } from './resourceNamingUtils';

type ApiStackProps = StackProps & {
// userPool: UserPool;
// userPoolClient: UserPoolClient;
// userPoolDomain: UserPoolDomain;
certificate: ICertificate;
hostedZone: IHostedZone;
webappUrl: string;
};

export class ApiStack extends Stack {
constructor(scope: Construct, id: string, props: ApiStackProps) {
super(scope, id, props);
// TODO Enable cognito/JWT authorization
const { /*userPool, userPoolClient, userPoolDomain,*/ webappUrl } = props;

const { certificate, hostedZone, webappUrl } = props;
const domainName = `api.${hostedZone.zoneName}`;

const generateResourceName = resourceName(scope);
const generateResourceDescription = resourceDescription(scope);

const dockerImageAsset = new DockerImageAsset(
this,
Expand All @@ -74,7 +54,13 @@ export class ApiStack extends Stack {

// Default AZs is all in region, but for environment-agnostic stack, max is 2!
const vpcName = generateResourceName('vpc');
const vpc = new Vpc(this, vpcName, { vpcName, maxAzs: 2 });
const vpc = new Vpc(this, vpcName, {
vpcName,
maxAzs: 2,
natGatewayProvider: NatInstanceProvider.instance({
instanceType: InstanceType.of(InstanceClass.T3, InstanceSize.MICRO),
}),
});
const clusterName = generateResourceName('cluster');
const cluster = new Cluster(this, clusterName, { clusterName, vpc });

Expand All @@ -95,8 +81,8 @@ export class ApiStack extends Stack {
{
serviceName: fargateServiceName,
cluster,
cpu: 256, // Default is 256
desiredCount: 1, // Bump this up for prod!
cpu: 256,
desiredCount: 1,
taskImageOptions: {
image: ContainerImage.fromDockerImageAsset(dockerImageAsset),
containerPort,
Expand All @@ -117,10 +103,11 @@ export class ApiStack extends Stack {
),
},
},
memoryLimitMiB: 512, // Default is 512
memoryLimitMiB: 512,
loadBalancerName,
openListener: false,
publicLoadBalancer: false,
certificate,
domainName,
domainZone: hostedZone,
propagateTags: PropagatedTagSource.SERVICE,
}
);
Expand All @@ -135,84 +122,8 @@ export class ApiStack extends Stack {
})
);

// Lambda to bring fargate service up or down
const startStopFunctionName = generateResourceName('fargate-switch');
const startStopServiceFunction = new NodejsFunction(
this,
startStopFunctionName,
{
functionName: startStopFunctionName,
description: generateResourceDescription(
'Fargate Service start/stop function'
),
runtime: Runtime.NODEJS_18_X,
handler: 'handler',
entry: join(__dirname, './startStopServiceLambda.ts'),
bundling: {
minify: true,
},
environment: {
CLUSTER_NAME: cluster.clusterName,
SERVICE_NAME: fargateService.service.serviceName,
},
}
);
startStopServiceFunction.addToRolePolicy(
new PolicyStatement({
effect: Effect.ALLOW,
actions: ['ecs:DescribeServices', 'ecs:UpdateService'],
resources: [fargateService.service.serviceArn],
})
);

// Schedule fargate service up at start of day, down at end
const schedulerRoleName = generateResourceName('scheduler-role');
const schedulerRole = new Role(this, schedulerRoleName, {
roleName: schedulerRoleName,
assumedBy: new ServicePrincipal('scheduler.amazonaws.com'),
});
const lambdaTarget = (operation: ServiceEventLambda['operation']) =>
new LambdaInvoke(startStopServiceFunction, {
input: ScheduleTargetInput.fromObject({ operation }),
role: schedulerRole,
});
const cronDef: CronOptionsWithTimezone = {
weekDay: 'MON-FRI',
minute: '0',
timeZone: TimeZone.EUROPE_LONDON,
};
const scheduleGroupName = generateResourceName('fargate-scheduler-group');
const scheduleGroup = new Group(this, scheduleGroupName, {
groupName: scheduleGroupName,
removalPolicy: RemovalPolicy.DESTROY,
});
const serverUpScheduleName = generateResourceName('server-up');
new Schedule(this, serverUpScheduleName, {
scheduleName: serverUpScheduleName,
description: generateResourceDescription('Scheduled server-up event'),
target: lambdaTarget('start'),
group: scheduleGroup,
schedule: ScheduleExpression.cron({
...cronDef,
hour: '9',
}),
});
const serverDownScheduleName = generateResourceName('server-down');
new Schedule(this, serverDownScheduleName, {
scheduleName: serverDownScheduleName,
description: generateResourceDescription('Scheduled server-down event'),
target: lambdaTarget('stop'),
group: scheduleGroup,
schedule: ScheduleExpression.cron({
...cronDef,
hour: '17',
}),
});

// Hook up Cognito to load balancer
// TODO Hook up Cognito to load balancer!
// https://stackoverflow.com/q/71124324
// TODO Needs HTTPS and a Route53 domain, so for now we're using APIGateway and VPCLink:
// https://repost.aws/knowledge-center/api-gateway-alb-integration
/*
const authActionName = generateResourceName('alb-auth');
fargateService.listener.addAction(authActionName, {
Expand All @@ -222,60 +133,8 @@ export class ApiStack extends Stack {
userPoolDomain,
next: ListenerAction.forward([fargateService.targetGroup]),
}),
conditions: [ListenerCondition.hostHeaders([domainName])],
});
*/

// Create an HTTP APIGateway with a VPCLink integrated with our load balancer
const securityGroupName = generateResourceName('vpclink-sg');
const vpcLinkSecurityGroup = new SecurityGroup(this, securityGroupName, {
vpc,
securityGroupName,
allowAllOutbound: false,
});
vpcLinkSecurityGroup.connections.allowFromAnyIpv4(
Port.tcp(80),
'APIGW to VPCLink'
);
vpcLinkSecurityGroup.connections.allowTo(
fargateService.loadBalancer,
Port.tcp(80),
'VPCLink to ALB'
);

const vpcLinkName = generateResourceName('vpclink');
const vpcLink = new VpcLink(this, vpcLinkName, {
vpc,
vpcLinkName,
securityGroups: [vpcLinkSecurityGroup],
});
Object.entries(props.tags ?? {}).forEach(([key, value]) => {
Tags.of(vpcLink).add(key, value);
});

const apiName = generateResourceName('api');
const api = new HttpApi(this, apiName, {
apiName,
description: generateResourceDescription('API'),
corsPreflight: {
allowOrigins: [webappUrl],
allowMethods: [CorsHttpMethod.ANY],
allowHeaders: ['Content-Type', 'Authorization'],
allowCredentials: true,
},
});
api.addRoutes({
path: '/{proxy+}',
integration: new HttpAlbIntegration(
generateResourceName('api-integration'),
fargateService.loadBalancer.listeners[0],
{ vpcLink }
),
});

new CfnOutput(this, 'APIGatewayURL', {
value:
api.defaultStage?.url ??
'FATAL ERROR: Gateway does not have a default stage',
});
}
}
Loading

0 comments on commit 39c2d11

Please sign in to comment.