Skip to content

Commit

Permalink
Use VPCLink with APIGW HTTP-API so ALB can be private
Browse files Browse the repository at this point in the history
  • Loading branch information
chriswilty committed Feb 13, 2024
1 parent 7c3f68c commit c851851
Show file tree
Hide file tree
Showing 5 changed files with 117 additions and 40 deletions.
17 changes: 13 additions & 4 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 } from 'aws-cdk-lib';
import { App, Environment } from 'aws-cdk-lib';
import 'source-map-support/register';

import {
Expand All @@ -12,6 +12,12 @@ import {
} from '../lib';

const app = new App();

const awsEnv = (): Environment => ({
account: process.env.CDK_DEFAULT_ACCOUNT,
region: process.env.CDK_DEFAULT_REGION,
});

const tags = {
owner: appName,
classification: 'unrestricted',
Expand All @@ -24,22 +30,25 @@ const generateStackName = stackName(app);
const generateDescription = resourceDescription(app);

const uiStack = new UiStack(app, generateStackName('ui'), {
tags,
description: generateDescription('UI stack'),
env: awsEnv(),
tags,
});

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

new ApiStack(app, generateStackName('api'), {
tags,
description: generateDescription('API stack'),
env: awsEnv(),
tags,
// userPool: authStack.userPool,
// userPoolClient: authStack.userPoolClient,
// userPoolDomain: authStack.userPoolDomain,
Expand Down
7 changes: 7 additions & 0 deletions cloud/cdk.context.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"availability-zones:account=600982866784:region=eu-west-1": [
"eu-west-1a",
"eu-west-1b",
"eu-west-1c"
]
}
95 changes: 78 additions & 17 deletions cloud/lib/api-stack.ts
Original file line number Diff line number Diff line change
@@ -1,20 +1,19 @@
import { CorsHttpMethod, HttpApi, VpcLink } from 'aws-cdk-lib/aws-apigatewayv2';
import { HttpAlbIntegration } from 'aws-cdk-lib/aws-apigatewayv2-integrations';
//import { UserPool, UserPoolClient, UserPoolDomain } from 'aws-cdk-lib/aws-cognito';
import { Vpc } from 'aws-cdk-lib/aws-ec2';
import { Port, SecurityGroup, Vpc } from 'aws-cdk-lib/aws-ec2';
import { DockerImageAsset } from 'aws-cdk-lib/aws-ecr-assets';
import {
Cluster,
ContainerImage,
Secret as EnvSecret,
} from 'aws-cdk-lib/aws-ecs';
import { Cluster, ContainerImage, PropagatedTagSource, 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 { AuthenticateCognitoAction } from 'aws-cdk-lib/aws-elasticloadbalancingv2-actions';
import { Bucket } from 'aws-cdk-lib/aws-s3';
import { Secret } from 'aws-cdk-lib/aws-secretsmanager';
import { Stack, StackProps } from 'aws-cdk-lib/core';
import { CfnOutput, RemovalPolicy, Stack, StackProps, Tags } from 'aws-cdk-lib/core';
import { Construct } from 'constructs';
import { join } from 'node:path';

import { resourceName } from './resourceNamingUtils';
import { resourceDescription, resourceName } from './resourceNamingUtils';

type ApiStackProps = StackProps & {
// userPool: UserPool;
Expand All @@ -24,12 +23,15 @@ type ApiStackProps = StackProps & {
};

export class ApiStack extends Stack {
//public readonly loadBalancerUrl: string;

constructor(scope: Construct, id: string, props: ApiStackProps) {
super(scope, id, props);
// TODO Enable cognito auth
const { /*userPool, userPoolClient, userPoolDomain,*/ webappUrl } = props;

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

const dockerImageAsset = new DockerImageAsset(
this,
Expand All @@ -41,7 +43,11 @@ 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,
restrictDefaultSecurityGroup: false, // TODO blog about this!
maxAzs: 2,
});
const clusterName = generateResourceName('cluster');
const cluster = new Cluster(this, clusterName, { clusterName, vpc });

Expand All @@ -51,15 +57,16 @@ export class ApiStack extends Stack {
'dev/SpyLogic/ApiKey'
);

// Create a load-balanced Fargate service and make it public
// Create a private, application-load-balanced Fargate service
const containerPort = 3001;
const serviceName = generateResourceName('fargate');
const loadBalancerName = generateResourceName('alb');
const fargateServiceName = generateResourceName('fargate');
const loadBalancerName = generateResourceName('loadbalancer');
const loadBalancerLogName = generateResourceName('loadbalancer-logs');
const fargateService = new ApplicationLoadBalancedFargateService(
this,
serviceName,
fargateServiceName,
{
serviceName,
serviceName: fargateServiceName,
cluster,
cpu: 256, // Default is 256
desiredCount: 1, // Bump this up for prod!
Expand All @@ -84,13 +91,25 @@ export class ApiStack extends Stack {
},
memoryLimitMiB: 512, // Default is 512
loadBalancerName,
publicLoadBalancer: true, // Default is true
publicLoadBalancer: false,
propagateTags: PropagatedTagSource.SERVICE,
}
);
fargateService.targetGroup.configureHealthCheck({
path: '/health',
});
fargateService.loadBalancer.logAccessLogs(
new Bucket(this, loadBalancerLogName, {
bucketName: loadBalancerLogName,
autoDeleteObjects: true,
removalPolicy: RemovalPolicy.DESTROY,
})
);
//this.loadBalancerUrl = `http://${fargateService.loadBalancer.loadBalancerDnsName}`;

// Hook up Cognito to load balancer
// https://stackoverflow.com/q/71124324
// TODO This needs HTTPS and a Route53 domain, so in meantime try VPCLink:
// 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');
Expand All @@ -104,6 +123,48 @@ export class ApiStack extends Stack {
});
*/

fargateService.targetGroup.configureHealthCheck({ path: '/health' });
// 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], // TODO Mention this in blog! ANY does not appear to work...
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',
});
}
}
32 changes: 16 additions & 16 deletions cloud/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 3 additions & 3 deletions cloud/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
},
"scripts": {
"cdk:synth": "cdk synth -q \"*\"",
"cdk:deploy": "cdk deploy --all",
"cdk:deploy": "cdk deploy --app cdk.out --all",
"cdk:destroy": "cdk destroy --all",
"codecheck": "concurrently \"npm run lint:check\" \"npm run format:check\"",
"format": "prettier . --write",
Expand All @@ -21,13 +21,13 @@
"@typescript-eslint/parser": "^6.9.0",
"concurrently": "^8.2.2",
"eslint": "^8.52.0",
"aws-cdk": "^2.104.0",
"aws-cdk": "^2.117.0",
"prettier": "^3.0.3",
"ts-node": "^10.9.1",
"typescript": "^5.2.2"
},
"dependencies": {
"aws-cdk-lib": "^2.104.0",
"aws-cdk-lib": "^2.117.0",
"constructs": "^10.3.0",
"dotenv": "^16.3.1",
"source-map-support": "^0.5.21"
Expand Down

0 comments on commit c851851

Please sign in to comment.