caccl-deploy
Version:
A cli tool for managing ECS/Fargate app deployments
219 lines (194 loc) • 6.75 kB
text/typescript
import {
aws_cloudwatch as cloudwatch,
aws_ec2 as ec2,
aws_ecs as ecs,
aws_elasticloadbalancingv2 as elb,
aws_s3 as s3,
CfnOutput,
Duration,
Stack,
} from 'aws-cdk-lib';
import { Construct } from 'constructs';
export type LoadBalancerSecurityGoups = {
primary?: ec2.SecurityGroup;
misc?: ec2.SecurityGroup;
};
export interface CacclLoadBalancerExtraOptions {
healthCheckPath?: string;
targetDeregistrationDelay?: number;
}
export interface CacclLoadBalancerProps {
vpc: ec2.Vpc;
securityGroups: LoadBalancerSecurityGoups;
certificateArn: string;
loadBalancerTarget: ecs.IEcsLoadBalancerTarget;
albLogBucketName?: string;
extraOptions?: CacclLoadBalancerExtraOptions;
targetDeregistrationDelay?: number; // in seconds
}
export class CacclLoadBalancer extends Construct {
loadBalancer: elb.ApplicationLoadBalancer;
httpsListener: elb.ApplicationListener;
metrics: { [key: string]: cloudwatch.Metric };
alarms: cloudwatch.Alarm[];
constructor(scope: Construct, id: string, props: CacclLoadBalancerProps) {
super(scope, id);
const {
vpc,
securityGroups,
certificateArn,
loadBalancerTarget,
albLogBucketName,
// includes targetDeregistrationDelay & healthCheckPath which are applied to the ApplicationTargetGroup below
extraOptions,
} = props;
const targetDeregistrationDelay =
extraOptions?.targetDeregistrationDelay ?? 30;
const healthCheckPath = extraOptions?.healthCheckPath ?? '/';
this.loadBalancer = new elb.ApplicationLoadBalancer(this, 'LoadBalancer', {
vpc,
securityGroup: securityGroups.primary,
internetFacing: true,
});
if (securityGroups.misc) {
this.loadBalancer.addSecurityGroup(securityGroups.misc);
}
if (albLogBucketName !== undefined) {
const bucket = s3.Bucket.fromBucketName(
this,
'AlbLogBucket',
albLogBucketName,
);
const objPrefix = Stack.of(this).stackName;
this.loadBalancer.logAccessLogs(bucket, objPrefix);
}
new elb.CfnListener(this, 'HttpRedirect', {
loadBalancerArn: this.loadBalancer.loadBalancerArn,
protocol: elb.ApplicationProtocol.HTTP,
port: 80,
defaultActions: [
{
type: 'redirect',
redirectConfig: {
statusCode: 'HTTP_301',
port: '443',
protocol: 'HTTPS',
host: '#{host}',
path: '/#{path}',
query: '#{query}',
},
},
],
});
const httpsListener = new elb.ApplicationListener(this, 'HttpsListener', {
loadBalancer: this.loadBalancer,
certificates: [{ certificateArn }],
port: 443,
protocol: elb.ApplicationProtocol.HTTPS,
/**
* if we don't make this false the listener construct will add rules
* to our security group that we don't want/need
*/
open: false,
});
const atgProps = {
vpc,
port: 443,
protocol: elb.ApplicationProtocol.HTTPS,
// setting this duration value enables the lb stickiness; 1 day is the default
stickinessCookieDuration: Duration.seconds(86400),
targetType: elb.TargetType.IP,
targets: [loadBalancerTarget],
deregistrationDelay: Duration.seconds(targetDeregistrationDelay),
healthCheck: {
// allow a redirect to indicate service is operational
healthyHttpCodes: '200,302',
},
};
if (healthCheckPath !== undefined && healthCheckPath !== '/') {
// this seems like a bonkers way to accomplish inserting an additional k/v pair
// into a nested object, but eslint complained about every other approach
atgProps.healthCheck = {
...atgProps.healthCheck,
...{ path: healthCheckPath },
};
}
const appTargetGroup = new elb.ApplicationTargetGroup(
this,
'TargetGroup',
atgProps,
);
httpsListener.addTargetGroups('AppTargetGroup', {
targetGroups: [appTargetGroup],
});
this.metrics = {
RequestCount: this.loadBalancer.metricRequestCount(),
NewConnectionCount: this.loadBalancer.metricNewConnectionCount(),
ActiveConnectionCount: this.loadBalancer.metricActiveConnectionCount(),
TargetResponseTime: this.loadBalancer
.metricTargetResponseTime({
period: Duration.minutes(1),
unit: cloudwatch.Unit.MILLISECONDS,
statistic: 'avg',
})
.with({ period: Duration.minutes(1) }),
RejectedConnectionCount: this.loadBalancer
.metricRejectedConnectionCount({
period: Duration.minutes(1),
statistic: 'sum',
})
.with({ period: Duration.minutes(1) }),
UnHealthyHostCount: appTargetGroup
.metricUnhealthyHostCount({
period: Duration.minutes(1),
statistic: 'sum',
})
.with({ period: Duration.minutes(1) }),
};
this.alarms = [
new cloudwatch.Alarm(this, 'TargetResponseTimeAlarm', {
metric: this.metrics.TargetResponseTime,
threshold: 1,
evaluationPeriods: 3,
treatMissingData: cloudwatch.TreatMissingData.IGNORE,
alarmDescription: `${
Stack.of(this).stackName
} load balancer target response time (TargetResponseTime)`,
}),
new cloudwatch.Alarm(this, 'RejectedConnectionsAlarm', {
metric: this.metrics.RejectedConnectionCount,
threshold: 1,
evaluationPeriods: 1,
treatMissingData: cloudwatch.TreatMissingData.IGNORE,
alarmDescription: `${
Stack.of(this).stackName
} load balancer rejected connections (RejectedConnectionCount)`,
}),
new cloudwatch.Alarm(this, 'UnhealthHostAlarm', {
metric: this.metrics.UnHealthyHostCount,
threshold: 1,
evaluationPeriods: 3,
treatMissingData: cloudwatch.TreatMissingData.IGNORE,
alarmDescription: `${
Stack.of(this).stackName
} target group unhealthy host count (UnHealthyHostCount)`,
}),
];
new CfnOutput(this, 'LoadBalancerHostname', {
exportName: `${Stack.of(this).stackName}-load-balancer-hostname`,
value: this.loadBalancer.loadBalancerDnsName,
});
if (securityGroups.primary) {
new CfnOutput(this, 'LoadBalancerPrimarySecurityGroup', {
exportName: `${Stack.of(this).stackName}-primary-security-group`,
value: securityGroups.primary.securityGroupId,
});
}
if (securityGroups.misc) {
new CfnOutput(this, 'LoadBalancerMiscSecurityGroup', {
exportName: `${Stack.of(this).stackName}-misc-security-group`,
value: securityGroups.misc.securityGroupId,
});
}
}
}