packages/aws-cdk-lib/aws-elasticloadbalancingv2/lib/alb/application-target-group.ts (337 lines of code) (raw):

import { IConstruct, Construct } from 'constructs'; import { IApplicationListener } from './application-listener'; import { HttpCodeTarget } from './application-load-balancer'; import * as cloudwatch from '../../../aws-cloudwatch'; import * as ec2 from '../../../aws-ec2'; import { Aws, Annotations, Duration, Token } from '../../../core'; import { ValidationError } from '../../../core/lib/errors'; import { ApplicationELBMetrics } from '../elasticloadbalancingv2-canned-metrics.generated'; import { BaseTargetGroupProps, ITargetGroup, loadBalancerNameFromListenerArn, LoadBalancerTargetProps, TargetGroupAttributes, TargetGroupBase, TargetGroupImportProps, } from '../shared/base-target-group'; import { ApplicationProtocol, ApplicationProtocolVersion, Protocol, TargetType, TargetGroupLoadBalancingAlgorithmType } from '../shared/enums'; import { ImportedTargetGroupBase } from '../shared/imported'; import { determineProtocolAndPort, parseLoadBalancerFullName, parseTargetGroupFullName } from '../shared/util'; /** * Properties for defining an Application Target Group */ export interface ApplicationTargetGroupProps extends BaseTargetGroupProps { /** * The protocol used for communication with the target. * * This is not applicable for Lambda targets. * * @default - Determined from port if known */ readonly protocol?: ApplicationProtocol; /** * The protocol version to use * * @default ApplicationProtocolVersion.HTTP1 */ readonly protocolVersion?: ApplicationProtocolVersion; /** * The port on which the target receives traffic. * * This is not applicable for Lambda targets. * * @default - Determined from protocol if known */ readonly port?: number; /** * The time period during which the load balancer sends a newly registered * target a linearly increasing share of the traffic to the target group. * * The range is 30-900 seconds (15 minutes). * * @default 0 */ readonly slowStart?: Duration; /** * The stickiness cookie expiration period. * * Setting this value enables load balancer stickiness. * * After this period, the cookie is considered stale. The minimum value is * 1 second and the maximum value is 7 days (604800 seconds). * * @default - Stickiness is disabled */ readonly stickinessCookieDuration?: Duration; /** * The name of an application-based stickiness cookie. * * Names that start with the following prefixes are not allowed: AWSALB, AWSALBAPP, * and AWSALBTG; they're reserved for use by the load balancer. * * Note: `stickinessCookieName` parameter depends on the presence of `stickinessCookieDuration` parameter. * If `stickinessCookieDuration` is not set, `stickinessCookieName` will be omitted. * * @default - If `stickinessCookieDuration` is set, a load-balancer generated cookie is used. Otherwise, no stickiness is defined. * @see https://docs.aws.amazon.com/elasticloadbalancing/latest/application/sticky-sessions.html */ readonly stickinessCookieName?: string; /** * The load balancing algorithm to select targets for routing requests. * * @default TargetGroupLoadBalancingAlgorithmType.ROUND_ROBIN */ readonly loadBalancingAlgorithmType?: TargetGroupLoadBalancingAlgorithmType; /** * The targets to add to this target group. * * Can be `Instance`, `IPAddress`, or any self-registering load balancing * target. If you use either `Instance` or `IPAddress` as targets, all * target must be of the same type. * * @default - No targets. */ readonly targets?: IApplicationLoadBalancerTarget[]; /** * Indicates whether anomaly mitigation is enabled. * * Only available when `loadBalancingAlgorithmType` is `TargetGroupLoadBalancingAlgorithmType.WEIGHTED_RANDOM` * * @default false * * @see https://docs.aws.amazon.com/elasticloadbalancing/latest/application/load-balancer-target-groups.html#automatic-target-weights */ readonly enableAnomalyMitigation?: boolean; } /** * Contains all metrics for a Target Group of a Application Load Balancer. */ export interface IApplicationTargetGroupMetrics { /** * Return the given named metric for this Network Target Group * * @default Average over 5 minutes */ custom(metricName: string, props?: cloudwatch.MetricOptions): cloudwatch.Metric; /** * The number of IPv6 requests received by the target group * * @default Sum over 5 minutes */ ipv6RequestCount(props?: cloudwatch.MetricOptions): cloudwatch.Metric; /** * The number of requests processed over IPv4 and IPv6. * * This count includes only the requests with a response generated by a target of the load balancer. * * @default Sum over 5 minutes */ requestCount(props?: cloudwatch.MetricOptions): cloudwatch.Metric; /** * The number of healthy hosts in the target group * * @default Average over 5 minutes */ healthyHostCount(props?: cloudwatch.MetricOptions): cloudwatch.Metric; /** * The number of unhealthy hosts in the target group * * @default Average over 5 minutes */ unhealthyHostCount(props?: cloudwatch.MetricOptions): cloudwatch.Metric; /** * The number of HTTP 2xx/3xx/4xx/5xx response codes generated by all targets in this target group. * * This does not include any response codes generated by the load balancer. * * @default Sum over 5 minutes */ httpCodeTarget(code: HttpCodeTarget, props?: cloudwatch.MetricOptions): cloudwatch.Metric; /** * The average number of requests received by each target in a target group. * * The only valid statistic is Sum. Note that this represents the average not the sum. * * @default Sum over 5 minutes */ requestCountPerTarget(props?: cloudwatch.MetricOptions): cloudwatch.Metric; /** * The number of connections that were not successfully established between the load balancer and target. * * @default Sum over 5 minutes */ targetConnectionErrorCount(props?: cloudwatch.MetricOptions): cloudwatch.Metric; /** * The time elapsed, in seconds, after the request leaves the load balancer until a response from the target is received. * * @default Average over 5 minutes */ targetResponseTime(props?: cloudwatch.MetricOptions): cloudwatch.Metric; /** * The number of TLS connections initiated by the load balancer that did not establish a session with the target. * * Possible causes include a mismatch of ciphers or protocols. * * @default Sum over 5 minutes */ targetTLSNegotiationErrorCount(props?: cloudwatch.MetricOptions): cloudwatch.Metric; } /** * The metrics for a Application Load Balancer. */ class ApplicationTargetGroupMetrics implements IApplicationTargetGroupMetrics { private readonly scope: Construct; private readonly loadBalancerFullName: string; private readonly targetGroupFullName: string; public constructor(scope: Construct, targetGroupFullName: string, loadBalancerFullName: string) { this.scope = scope; this.targetGroupFullName = targetGroupFullName; this.loadBalancerFullName = loadBalancerFullName; } public custom(metricName: string, props?: cloudwatch.MetricOptions): cloudwatch.Metric { return new cloudwatch.Metric({ namespace: 'AWS/ApplicationELB', metricName, dimensionsMap: { TargetGroup: this.targetGroupFullName, LoadBalancer: this.loadBalancerFullName, }, ...props, }).attachTo(this.scope); } public ipv6RequestCount(props?: cloudwatch.MetricOptions) { return this.cannedMetric(ApplicationELBMetrics.iPv6RequestCountSum, props); } public requestCount(props?: cloudwatch.MetricOptions) { return this.cannedMetric(ApplicationELBMetrics.requestCountSum, props); } public healthyHostCount(props?: cloudwatch.MetricOptions) { return this.custom('HealthyHostCount', { statistic: 'Average', ...props, }); } public unhealthyHostCount(props?: cloudwatch.MetricOptions) { return this.custom('UnHealthyHostCount', { statistic: 'Average', ...props, }); } public httpCodeTarget(code: HttpCodeTarget, props?: cloudwatch.MetricOptions) { return this.custom(code, { statistic: 'Sum', ...props, }); } public requestCountPerTarget(props?: cloudwatch.MetricOptions) { return this.custom('RequestCountPerTarget', { statistic: 'Sum', ...props, }); } public targetConnectionErrorCount(props?: cloudwatch.MetricOptions) { return this.custom('TargetConnectionErrorCount', { statistic: 'Sum', ...props, }); } public targetResponseTime(props?: cloudwatch.MetricOptions) { return this.custom('TargetResponseTime', { statistic: 'Average', ...props, }); } public targetTLSNegotiationErrorCount(props?: cloudwatch.MetricOptions) { return this.custom('TargetTLSNegotiationErrorCount', { statistic: 'Sum', ...props, }); } private cannedMetric( fn: (dims: { LoadBalancer: string; TargetGroup: string }) => cloudwatch.MetricProps, props?: cloudwatch.MetricOptions): cloudwatch.Metric { return new cloudwatch.Metric({ ...fn({ LoadBalancer: this.loadBalancerFullName, TargetGroup: this.targetGroupFullName, }), ...props, }).attachTo(this.scope); } } /** * Define an Application Target Group */ export class ApplicationTargetGroup extends TargetGroupBase implements IApplicationTargetGroup { /** * Import an existing target group */ public static fromTargetGroupAttributes(scope: Construct, id: string, attrs: TargetGroupAttributes): IApplicationTargetGroup { return new ImportedApplicationTargetGroup(scope, id, attrs); } /** * Import an existing target group * * @deprecated Use `fromTargetGroupAttributes` instead */ public static import(scope: Construct, id: string, props: TargetGroupImportProps): IApplicationTargetGroup { return ApplicationTargetGroup.fromTargetGroupAttributes(scope, id, props); } private readonly connectableMembers: ConnectableMember[]; private readonly listeners: IApplicationListener[]; private readonly protocol?: ApplicationProtocol; private readonly port?: number; private _metrics?: IApplicationTargetGroupMetrics; constructor(scope: Construct, id: string, props: ApplicationTargetGroupProps = {}) { const [protocol, port] = determineProtocolAndPort(props.protocol, props.port); const { protocolVersion } = props; super(scope, id, { ...props }, { protocol, protocolVersion, port, }); this.protocol = protocol; this.port = port; // this.targetType is lazy this.node.addValidation({ validate: () => { if (this.targetType === TargetType.LAMBDA && (this.port || this.protocol)) { return ['port/protocol should not be specified for Lambda targets']; } else { return []; } }, }); this.connectableMembers = []; this.listeners = []; if (props) { const isWeightedRandomAlgorithm = !Token.isUnresolved(props.loadBalancingAlgorithmType) && (props.loadBalancingAlgorithmType === TargetGroupLoadBalancingAlgorithmType.WEIGHTED_RANDOM); if (props.slowStart !== undefined) { // 0 is allowed and disables slow start if ((props.slowStart.toSeconds() < 30 && props.slowStart.toSeconds() !== 0) || props.slowStart.toSeconds() > 900) { throw new ValidationError('Slow start duration value must be between 30 and 900 seconds, or 0 to disable slow start.', this); } this.setAttribute('slow_start.duration_seconds', props.slowStart.toSeconds().toString()); if (isWeightedRandomAlgorithm) { throw new ValidationError('The weighted random routing algorithm can not be used with slow start mode.', this); } } if (props.stickinessCookieDuration) { this.enableCookieStickiness(props.stickinessCookieDuration, props.stickinessCookieName); } else { this.setAttribute('stickiness.enabled', 'false'); } if (props.loadBalancingAlgorithmType) { this.setAttribute('load_balancing.algorithm.type', props.loadBalancingAlgorithmType); } this.addTarget(...(props.targets || [])); if (props.enableAnomalyMitigation !== undefined) { if (props.enableAnomalyMitigation && !isWeightedRandomAlgorithm) { throw new ValidationError('Anomaly mitigation is only available when `loadBalancingAlgorithmType` is `TargetGroupLoadBalancingAlgorithmType.WEIGHTED_RANDOM`.', this); } this.setAttribute('load_balancing.algorithm.anomaly_mitigation', props.enableAnomalyMitigation ? 'on' : 'off'); } } } public get metrics(): IApplicationTargetGroupMetrics { if (!this._metrics) { this._metrics = new ApplicationTargetGroupMetrics(this, this.targetGroupFullName, this.firstLoadBalancerFullName); } return this._metrics; } /** * Add a load balancing target to this target group */ public addTarget(...targets: IApplicationLoadBalancerTarget[]) { for (const target of targets) { const result = target.attachToApplicationTargetGroup(this); this.addLoadBalancerTarget(result); } if (this.targetType === TargetType.LAMBDA) { this.setAttribute('stickiness.enabled', undefined); } } /** * Enable sticky routing via a cookie to members of this target group. * * Note: If the `cookieName` parameter is set, application-based stickiness will be applied, * otherwise it defaults to duration-based stickiness attributes (`lb_cookie`). * * @see https://docs.aws.amazon.com/elasticloadbalancing/latest/application/sticky-sessions.html */ public enableCookieStickiness(duration: Duration, cookieName?: string) { if (duration.toSeconds() < 1 || duration.toSeconds() > 604800) { throw new ValidationError('Stickiness cookie duration value must be between 1 second and 7 days (604800 seconds).', this); } if (cookieName !== undefined) { if (!Token.isUnresolved(cookieName) && (cookieName.startsWith('AWSALB') || cookieName.startsWith('AWSALBAPP') || cookieName.startsWith('AWSALBTG'))) { throw new ValidationError('App cookie names that start with the following prefixes are not allowed: AWSALB, AWSALBAPP, and AWSALBTG; they\'re reserved for use by the load balancer.', this); } if (cookieName === '') { throw new ValidationError('App cookie name cannot be an empty string.', this); } } this.setAttribute('stickiness.enabled', 'true'); if (cookieName) { this.setAttribute('stickiness.type', 'app_cookie'); this.setAttribute('stickiness.app_cookie.cookie_name', cookieName); this.setAttribute('stickiness.app_cookie.duration_seconds', duration.toSeconds().toString()); } else { this.setAttribute('stickiness.type', 'lb_cookie'); this.setAttribute('stickiness.lb_cookie.duration_seconds', duration.toSeconds().toString()); } } /** * Register a connectable as a member of this target group. * * Don't call this directly. It will be called by load balancing targets. */ public registerConnectable(connectable: ec2.IConnectable, portRange?: ec2.Port) { portRange = portRange || ec2.Port.tcp(this.defaultPort); // Notify all listeners that we already know about of this new connectable. // Then remember for new listeners that might get added later. this.connectableMembers.push({ connectable, portRange }); for (const listener of this.listeners) { listener.registerConnectable(connectable, portRange); } } /** * Register a listener that is load balancing to this target group. * * Don't call this directly. It will be called by listeners. */ public registerListener(listener: IApplicationListener, associatingConstruct?: IConstruct) { // Notify this listener of all connectables that we know about. // Then remember for new connectables that might get added later. for (const member of this.connectableMembers) { listener.registerConnectable(member.connectable, member.portRange); } this.listeners.push(listener); this.loadBalancerAttachedDependencies.add(associatingConstruct ?? listener); } /** * Full name of first load balancer */ public get firstLoadBalancerFullName(): string { if (this.listeners.length === 0) { throw new ValidationError('The TargetGroup needs to be attached to a LoadBalancer before you can call this method', this); } return loadBalancerNameFromListenerArn(this.listeners[0].listenerArn); } /** * Return the given named metric for this Application Load Balancer Target Group * * Returns the metric for this target group from the point of view of the first * load balancer load balancing to it. If you have multiple load balancers load * sending traffic to the same target group, you will have to override the dimensions * on this metric. * * @default Average over 5 minutes */ public metric(metricName: string, props?: cloudwatch.MetricOptions): cloudwatch.Metric { return this.metrics.custom(metricName, props); } /** * The number of IPv6 requests received by the target group * * @default Sum over 5 minutes * @deprecated Use ``ApplicationTargetGroup.metrics.ipv6RequestCount`` instead */ public metricIpv6RequestCount(props?: cloudwatch.MetricOptions) { return this.metrics.ipv6RequestCount(props); } /** * The number of requests processed over IPv4 and IPv6. * * This count includes only the requests with a response generated by a target of the load balancer. * * @default Sum over 5 minutes * @deprecated Use ``ApplicationTargetGroup.metrics.requestCount`` instead */ public metricRequestCount(props?: cloudwatch.MetricOptions) { return this.metrics.requestCount(props); } /** * The number of healthy hosts in the target group * * @default Average over 5 minutes * @deprecated Use ``ApplicationTargetGroup.metrics.healthyHostCount`` instead */ public metricHealthyHostCount(props?: cloudwatch.MetricOptions) { return this.metrics.healthyHostCount(props); } /** * The number of unhealthy hosts in the target group * * @default Average over 5 minutes * @deprecated Use ``ApplicationTargetGroup.metrics.unhealthyHostCount`` instead */ public metricUnhealthyHostCount(props?: cloudwatch.MetricOptions) { return this.metrics.unhealthyHostCount(props); } /** * The number of HTTP 2xx/3xx/4xx/5xx response codes generated by all targets in this target group. * * This does not include any response codes generated by the load balancer. * * @default Sum over 5 minutes * @deprecated Use ``ApplicationTargetGroup.metrics.httpCodeTarget`` instead */ public metricHttpCodeTarget(code: HttpCodeTarget, props?: cloudwatch.MetricOptions) { return this.metrics.httpCodeTarget(code, props); } /** * The average number of requests received by each target in a target group. * * The only valid statistic is Sum. Note that this represents the average not the sum. * * @default Sum over 5 minutes * @deprecated Use `ApplicationTargetGroup.metrics.requestCountPerTarget` instead */ public metricRequestCountPerTarget(props?: cloudwatch.MetricOptions) { return this.metrics.requestCountPerTarget(props); } /** * The number of connections that were not successfully established between the load balancer and target. * * @default Sum over 5 minutes * @deprecated Use ``ApplicationTargetGroup.metrics.targetConnectionErrorCount`` instead */ public metricTargetConnectionErrorCount(props?: cloudwatch.MetricOptions) { return this.metrics.targetConnectionErrorCount(props); } /** * The time elapsed, in seconds, after the request leaves the load balancer until a response from the target is received. * * @default Average over 5 minutes * @deprecated Use ``ApplicationTargetGroup.metrics.targetResponseTime`` instead */ public metricTargetResponseTime(props?: cloudwatch.MetricOptions) { return this.metrics.targetResponseTime(props); } /** * The number of TLS connections initiated by the load balancer that did not establish a session with the target. * * Possible causes include a mismatch of ciphers or protocols. * * @default Sum over 5 minutes * @deprecated Use ``ApplicationTargetGroup.metrics.tlsNegotiationErrorCount`` instead */ public metricTargetTLSNegotiationErrorCount(props?: cloudwatch.MetricOptions) { return this.metrics.targetTLSNegotiationErrorCount(props); } protected validateTargetGroup(): string[] { const ret = super.validateTargetGroup(); if (this.targetType !== undefined && this.targetType !== TargetType.LAMBDA && (this.protocol === undefined || this.port === undefined)) { ret.push('At least one of \'port\' or \'protocol\' is required for a non-Lambda TargetGroup'); } if (this.healthCheck) { if (this.healthCheck.interval && this.healthCheck.timeout && this.healthCheck.interval.toMilliseconds() <= this.healthCheck.timeout.toMilliseconds()) { ret.push(`Healthcheck interval ${this.healthCheck.interval.toHumanString()} must be greater than the timeout ${this.healthCheck.timeout.toHumanString()}`); } if (this.healthCheck.protocol) { if (!ALB_HEALTH_CHECK_PROTOCOLS.includes(this.healthCheck.protocol)) { ret.push([ `Health check protocol '${this.healthCheck.protocol}' is not supported. `, `Must be one of [${ALB_HEALTH_CHECK_PROTOCOLS.join(', ')}]`, ].join('')); } } } return ret; } } /** * A connectable member of a target group */ interface ConnectableMember { /** * The connectable member */ connectable: ec2.IConnectable; /** * The port (range) the member is listening on */ portRange: ec2.Port; } /** * A Target Group for Application Load Balancers */ export interface IApplicationTargetGroup extends ITargetGroup { /** * All metrics available for this target group. */ readonly metrics: IApplicationTargetGroupMetrics; /** * Register a listener that is load balancing to this target group. * * Don't call this directly. It will be called by listeners. */ registerListener(listener: IApplicationListener, associatingConstruct?: IConstruct): void; /** * Register a connectable as a member of this target group. * * Don't call this directly. It will be called by load balancing targets. */ registerConnectable(connectable: ec2.IConnectable, portRange?: ec2.Port): void; /** * Add a load balancing target to this target group */ addTarget(...targets: IApplicationLoadBalancerTarget[]): void; } /** * An imported application target group */ class ImportedApplicationTargetGroup extends ImportedTargetGroupBase implements IApplicationTargetGroup { private readonly _metrics?: IApplicationTargetGroupMetrics; public constructor(scope: Construct, id: string, props: TargetGroupAttributes) { super(scope, id, props); if (this.loadBalancerArns != Aws.NO_VALUE) { const targetGroupFullName = parseTargetGroupFullName(this.targetGroupArn); const firstLoadBalancerFullName = parseLoadBalancerFullName(this.loadBalancerArns); this._metrics = new ApplicationTargetGroupMetrics(this, targetGroupFullName, firstLoadBalancerFullName); } } public registerListener(_listener: IApplicationListener, _associatingConstruct?: IConstruct) { // Nothing to do, we know nothing of our members Annotations.of(this).addWarningV2('@aws-cdk/aws-elbv2:albTargetGroupCannotRegisterListener', 'Cannot register listener on imported target group -- security groups might need to be updated manually'); } public registerConnectable(_connectable: ec2.IConnectable, _portRange?: ec2.Port | undefined): void { Annotations.of(this).addWarningV2('@aws-cdk/aws-elbv2:albTargetGroupCannotRegisterConnectable', 'Cannot register connectable on imported target group -- security groups might need to be updated manually'); } public addTarget(...targets: IApplicationLoadBalancerTarget[]) { for (const target of targets) { const result = target.attachToApplicationTargetGroup(this); if (result.targetJson !== undefined) { throw new ValidationError('Cannot add a non-self registering target to an imported TargetGroup. Create a new TargetGroup instead.', this); } } } public get metrics(): IApplicationTargetGroupMetrics { if (!this._metrics) { throw new ValidationError( 'The imported ApplicationTargetGroup needs the associated ApplicationBalancer to be able to provide metrics. ' + 'Please specify the ARN value when importing it.', this); } return this._metrics; } } /** * Interface for constructs that can be targets of an application load balancer */ export interface IApplicationLoadBalancerTarget { /** * Attach load-balanced target to a TargetGroup * * May return JSON to directly add to the [Targets] list, or return undefined * if the target will register itself with the load balancer. */ attachToApplicationTargetGroup(targetGroup: IApplicationTargetGroup): LoadBalancerTargetProps; } const ALB_HEALTH_CHECK_PROTOCOLS = [Protocol.HTTP, Protocol.HTTPS];