From 6a9e43f0c6f966df4671267eeda21638611dfb1c Mon Sep 17 00:00:00 2001 From: Edison Gustavo Muenz Date: Tue, 14 Feb 2023 04:45:00 +0100 Subject: [PATCH] feat(elbv2): add metrics to INetworkTargetGroup and IApplicationTargetGroup (#23993) This PR follows the same conventions as #23853 By moving the metrics methods to the `INetworkTargetGroup` and `IApplicationTargetGroup` interfaces it allows to create these metrics also for Target Groups that are imported via the `fromTargetGroupAttributes()` method. To create the metrics for Target Groups requires (1) the full name of the Target Group and (2) the full name of the Load Balancer. For (1): it is readily available given that all imported Target Groups need to provide its ARN. For (2), it is an optional value, so the `.metrics` parameter will throw an error if it was not provided. To solve this problem I did: - Introduce a new interface for each TG type: `INetworkTargetGroupMetrics`, `IApplicationTargetGroupMetrics` - Create a concrete implementation for the new interfaces (1 for each): `NetworkTargetGroupMetrics` and `ApplicationTargetGroupMetrics` - Make each concrete implementation of each Load Balancer to also provide a `metrics` field. The concrete implementations of the load balancers are: `ImportedApplicationTargetGroup`, and `ApplicationLoadBalancer` (and the same for the NLB classes). I chose to create a new interface because code can be reused across the 3 concrete implementations of each Load Balancer. I deprecated the `metricXXX()` methods of each load balancer because I think it is cleaner to access metrics through the new `metrics` attribute/interface. There is a small **gotcha** here because the parameter of the `fromTargetGroupAttributes()` method that refers to the LB is: `loadBalancerArns`, which has its documentation as: > A Token representing the list of ARNs for the load balancer routing to this target group I'm not treating this parameter as a collection of ARNs, but as a single ARN. Also, I'm not treating it only as a token, but hardcoded ARNs can also be supplied, which "sort of" violates its interface. This attribute is weird though because Target Groups cannot have multiple Load Balancers as of today, although its documentation doesn't clearly express that is the case. fix: #10850 ---- *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license* --- .../aws-elasticloadbalancingv2/README.md | 38 +- .../lib/alb/application-target-group.ts | 289 ++++++++++++--- .../lib/nlb/network-target-group.ts | 121 +++++- .../lib/shared/util.ts | 18 + .../test/alb/listener.test.ts | 16 +- .../test/alb/target-group.test.ts | 34 ++ .../aws-cdk-elbv2-StackWithLb.assets.json | 4 +- .../aws-cdk-elbv2-StackWithLb.template.json | 50 +++ ...cdk-elbv2-integ-StackUnderTest.assets.json | 4 +- ...k-elbv2-integ-StackUnderTest.template.json | 184 +++++++++ .../manifest.json | 46 ++- .../integ.nlb-lookup.js.snapshot/tree.json | 348 ++++++++++++++++++ .../test/integ.nlb-lookup.ts | 41 +++ .../test/nlb/target-group.test.ts | 42 ++- 14 files changed, 1145 insertions(+), 90 deletions(-) diff --git a/packages/@aws-cdk/aws-elasticloadbalancingv2/README.md b/packages/@aws-cdk/aws-elasticloadbalancingv2/README.md index da7a5034f72c2..cb969ffd08821 100644 --- a/packages/@aws-cdk/aws-elasticloadbalancingv2/README.md +++ b/packages/@aws-cdk/aws-elasticloadbalancingv2/README.md @@ -607,7 +607,9 @@ const listener = elbv2.NetworkListener.fromLookup(this, 'ALBListener', { ## Metrics -You may create metrics for each Load Balancer through the `metrics` attribute: +You may create metrics for Load Balancers and Target Groups through the `metrics` attribute: + +**Load Balancer:** ```ts declare const alb: elbv2.IApplicationLoadBalancer; @@ -615,3 +617,37 @@ declare const alb: elbv2.IApplicationLoadBalancer; const albMetrics: elbv2.IApplicationLoadBalancerMetrics = alb.metrics; const metricConnectionCount: cloudwatch.Metric = albMetrics.activeConnectionCount(); ``` + +**Target Group:** + +```ts +declare const targetGroup: elbv2.IApplicationTargetGroup; + +const targetGroupMetrics: elbv2.IApplicationTargetGroupMetrics = targetGroup.metrics; +const metricHealthyHostCount: cloudwatch.Metric = targetGroupMetrics.healthyHostCount(); +``` + +Metrics are also available to imported resources: + +```ts +declare const stack: Stack; + +const targetGroup = elbv2.ApplicationTargetGroup.fromTargetGroupAttributes(stack, 'MyTargetGroup', { + targetGroupArn: Fn.importValue('TargetGroupArn'), + loadBalancerArns: Fn.importValue('LoadBalancerArn'), +}); + +const targetGroupMetrics: elbv2.IApplicationTargetGroupMetrics = targetGroup.metrics; +``` + +Notice that TargetGroups must be imported by supplying the Load Balancer too, otherwise accessing the `metrics` will +throw an error: + +```ts +declare const stack: Stack; +const targetGroup = elbv2.ApplicationTargetGroup.fromTargetGroupAttributes(stack, 'MyTargetGroup', { + targetGroupArn: Fn.importValue('TargetGroupArn'), +}); + +const targetGroupMetrics: elbv2.IApplicationTargetGroupMetrics = targetGroup.metrics; // throws an Error() +``` diff --git a/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/alb/application-target-group.ts b/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/alb/application-target-group.ts index 4599f289d892a..a842adb3a978d 100644 --- a/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/alb/application-target-group.ts +++ b/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/alb/application-target-group.ts @@ -1,6 +1,6 @@ import * as cloudwatch from '@aws-cdk/aws-cloudwatch'; import * as ec2 from '@aws-cdk/aws-ec2'; -import { Annotations, Duration, Token } from '@aws-cdk/core'; +import { Aws, Annotations, Duration, Token } from '@aws-cdk/core'; import { IConstruct, Construct } from 'constructs'; import { ApplicationELBMetrics } from '../elasticloadbalancingv2-canned-metrics.generated'; import { @@ -9,7 +9,7 @@ import { } from '../shared/base-target-group'; import { ApplicationProtocol, ApplicationProtocolVersion, Protocol, TargetType, TargetGroupLoadBalancingAlgorithmType } from '../shared/enums'; import { ImportedTargetGroupBase } from '../shared/imported'; -import { determineProtocolAndPort } from '../shared/util'; +import { determineProtocolAndPort, parseLoadBalancerFullName, parseTargetGroupFullName } from '../shared/util'; import { IApplicationListener } from './application-listener'; import { HttpCodeTarget } from './application-load-balancer'; @@ -93,6 +93,188 @@ export interface ApplicationTargetGroupProps extends BaseTargetGroupProps { readonly targets?: IApplicationLoadBalancerTarget[]; } +/** + * 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 */ @@ -117,6 +299,7 @@ export class ApplicationTargetGroup extends TargetGroupBase implements IApplicat 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); @@ -165,6 +348,13 @@ export class ApplicationTargetGroup extends TargetGroupBase implements IApplicat } } + 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 */ @@ -262,24 +452,17 @@ export class ApplicationTargetGroup extends TargetGroupBase implements IApplicat * @default Average over 5 minutes */ public metric(metricName: string, props?: cloudwatch.MetricOptions): cloudwatch.Metric { - return new cloudwatch.Metric({ - namespace: 'AWS/ApplicationELB', - metricName, - dimensionsMap: { - TargetGroup: this.targetGroupFullName, - LoadBalancer: this.firstLoadBalancerFullName, - }, - ...props, - }).attachTo(this); + 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.cannedMetric(ApplicationELBMetrics.iPv6RequestCountSum, props); + return this.metrics.ipv6RequestCount(props); } /** @@ -288,33 +471,30 @@ export class ApplicationTargetGroup extends TargetGroupBase implements IApplicat * 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.cannedMetric(ApplicationELBMetrics.requestCountSum, props); + 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.metric('HealthyHostCount', { - statistic: 'Average', - ...props, - }); + 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.metric('UnHealthyHostCount', { - statistic: 'Average', - ...props, - }); + return this.metrics.unhealthyHostCount(props); } /** @@ -323,12 +503,10 @@ export class ApplicationTargetGroup extends TargetGroupBase implements IApplicat * 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.metric(code, { - statistic: 'Sum', - ...props, - }); + return this.metrics.httpCodeTarget(code, props); } /** @@ -337,36 +515,30 @@ export class ApplicationTargetGroup extends TargetGroupBase implements IApplicat * The only valid statistic is Sum. Note that this represents the average not the sum. * * @default Sum over 5 minutes + * @deprecated Use ``ApplicationTargetGroup.metrics.ipv6RequestCount`` instead */ public metricRequestCountPerTarget(props?: cloudwatch.MetricOptions) { - return this.metric('RequestCountPerTarget', { - statistic: 'Sum', - ...props, - }); + 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.metric('TargetConnectionErrorCount', { - statistic: 'Sum', - ...props, - }); + 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.metric('TargetResponseTime', { - statistic: 'Average', - ...props, - }); + return this.metrics.targetResponseTime(props); } /** @@ -375,12 +547,10 @@ export class ApplicationTargetGroup extends TargetGroupBase implements IApplicat * 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.metric('TargetTLSNegotiationErrorCount', { - statistic: 'Sum', - ...props, - }); + return this.metrics.targetTLSNegotiationErrorCount(props); } protected validateTargetGroup(): string[] { @@ -409,18 +579,6 @@ export class ApplicationTargetGroup extends TargetGroupBase implements IApplicat return ret; } - - private cannedMetric( - fn: (dims: { LoadBalancer: string, TargetGroup: string }) => cloudwatch.MetricProps, - props?: cloudwatch.MetricOptions): cloudwatch.Metric { - return new cloudwatch.Metric({ - ...fn({ - LoadBalancer: this.firstLoadBalancerFullName, - TargetGroup: this.targetGroupFullName, - }), - ...props, - }).attachTo(this); - } } /** @@ -442,6 +600,11 @@ interface ConnectableMember { * 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. * @@ -466,6 +629,17 @@ export interface IApplicationTargetGroup extends ITargetGroup { * 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).addWarning('Cannot register listener on imported target group -- security groups might need to be updated manually'); @@ -484,6 +658,15 @@ class ImportedApplicationTargetGroup extends ImportedTargetGroupBase implements } } } + + public get metrics(): IApplicationTargetGroupMetrics { + if (!this._metrics) { + throw new Error( + 'The imported ApplicationTargetGroup needs the associated ApplicationBalancer to be able to provide metrics. ' + + 'Please specify the ARN value when importing it.'); + } + return this._metrics; + } } /** diff --git a/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/nlb/network-target-group.ts b/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/nlb/network-target-group.ts index 4c19fd228e706..32b18a0ab2816 100644 --- a/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/nlb/network-target-group.ts +++ b/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/nlb/network-target-group.ts @@ -7,7 +7,7 @@ import { } from '../shared/base-target-group'; import { Protocol } from '../shared/enums'; import { ImportedTargetGroupBase } from '../shared/imported'; -import { validateNetworkProtocol } from '../shared/util'; +import { parseLoadBalancerFullName, parseTargetGroupFullName, validateNetworkProtocol } from '../shared/util'; import { INetworkListener } from './network-listener'; /** @@ -62,6 +62,70 @@ export interface NetworkTargetGroupProps extends BaseTargetGroupProps { readonly connectionTermination?: boolean; } +/** + * Contains all metrics for a Target Group of a Network Load Balancer. + */ +export interface INetworkTargetGroupMetrics { + /** + * 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 targets that are considered healthy. + * + * @default Average over 5 minutes + */ + healthyHostCount(props?: cloudwatch.MetricOptions): cloudwatch.Metric; + + /** + * The number of targets that are considered unhealthy. + * + * @default Average over 5 minutes + */ + unHealthyHostCount(props?: cloudwatch.MetricOptions): cloudwatch.Metric; +} + +/** + * The metrics for a network load balancer. + */ +class NetworkTargetGroupMetrics implements INetworkTargetGroupMetrics { + 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/NetworkELB', + metricName, + dimensionsMap: { LoadBalancer: this.loadBalancerFullName, TargetGroup: this.targetGroupFullName }, + ...props, + }).attachTo(this.scope); + } + + public healthyHostCount(props?: cloudwatch.MetricOptions): cloudwatch.Metric { + return this.custom('HealthyHostCount', { + statistic: 'Average', + ...props, + }); + } + + public unHealthyHostCount(props?: cloudwatch.MetricOptions) { + return this.custom('UnHealthyHostCount', { + statistic: 'Average', + ...props, + }); + } +} + /** * Define a Network Target Group */ @@ -83,6 +147,7 @@ export class NetworkTargetGroup extends TargetGroupBase implements INetworkTarge } private readonly listeners: INetworkListener[]; + private _metrics?: INetworkTargetGroupMetrics; constructor(scope: Construct, id: string, props: NetworkTargetGroupProps) { const proto = props.protocol || Protocol.TCP; @@ -106,6 +171,14 @@ export class NetworkTargetGroup extends TargetGroupBase implements INetworkTarge this.setAttribute('deregistration_delay.connection_termination.enabled', props.connectionTermination ? 'true' : 'false'); } this.addTarget(...(props.targets || [])); + + } + + public get metrics(): INetworkTargetGroupMetrics { + if (!this._metrics) { + this._metrics = new NetworkTargetGroupMetrics(this, this.targetGroupFullName, this.firstLoadBalancerFullName); + } + return this._metrics; } /** @@ -132,24 +205,20 @@ export class NetworkTargetGroup extends TargetGroupBase implements INetworkTarge * The number of targets that are considered healthy. * * @default Average over 5 minutes + * @deprecated Use ``NetworkTargetGroup.metrics.healthyHostCount`` instead */ public metricHealthyHostCount(props?: cloudwatch.MetricOptions) { - return this.metric('HealthyHostCount', { - statistic: 'Average', - ...props, - }); + return this.metrics.healthyHostCount(props); } /** * The number of targets that are considered unhealthy. * * @default Average over 5 minutes + * @deprecated Use ``NetworkTargetGroup.metrics.healthyHostCount`` instead */ public metricUnHealthyHostCount(props?: cloudwatch.MetricOptions) { - return this.metric('UnHealthyHostCount', { - statistic: 'Average', - ...props, - }); + return this.metrics.unHealthyHostCount(props); } /** @@ -219,21 +288,17 @@ export class NetworkTargetGroup extends TargetGroupBase implements INetworkTarge return ret; } - - private metric(metricName: string, props?: cloudwatch.MetricOptions): cloudwatch.Metric { - return new cloudwatch.Metric({ - namespace: 'AWS/NetworkELB', - metricName, - dimensionsMap: { LoadBalancer: this.firstLoadBalancerFullName, TargetGroup: this.targetGroupFullName }, - ...props, - }).attachTo(this); - } } /** * A network target group */ export interface INetworkTargetGroup extends ITargetGroup { + /** + * All metrics available for this target group. + */ + readonly metrics: INetworkTargetGroupMetrics; + /** * Register a listener that is load balancing to this target group. * @@ -251,6 +316,26 @@ export interface INetworkTargetGroup extends ITargetGroup { * An imported network target group */ class ImportedNetworkTargetGroup extends ImportedTargetGroupBase implements INetworkTargetGroup { + private readonly _metrics?: INetworkTargetGroupMetrics; + + public constructor(scope: Construct, id: string, props: TargetGroupImportProps) { + super(scope, id, props); + if (this.loadBalancerArns != cdk.Aws.NO_VALUE) { + const targetGroupFullName = parseTargetGroupFullName(this.targetGroupArn); + const firstLoadBalancerFullName = parseLoadBalancerFullName(this.loadBalancerArns); + this._metrics = new NetworkTargetGroupMetrics(this, targetGroupFullName, firstLoadBalancerFullName); + } + } + + public get metrics(): INetworkTargetGroupMetrics { + if (!this._metrics) { + throw new Error( + 'The imported NetworkTargetGroup needs the associated NetworkLoadBalancer to be able to provide metrics. ' + + 'Please specify the ARN value when importing it.'); + } + return this._metrics; + } + public registerListener(_listener: INetworkListener) { // Nothing to do, we know nothing of our members } diff --git a/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/shared/util.ts b/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/shared/util.ts index 518e0865161c9..b52d6ba09ea23 100644 --- a/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/shared/util.ts +++ b/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/shared/util.ts @@ -110,3 +110,21 @@ export function parseLoadBalancerFullName(arn: string): string { return resourceName; } } + +/** + * Transforms: + * + * arn:aws:elasticloadbalancing:us-east-1:123456789:targetgroup/my-target-group/da693d633af407a0 + * + * Into: + * + * targetgroup/my-target-group/da693d633af407a0 + */ +export function parseTargetGroupFullName(arn: string): string { + const arnComponents = Arn.split(arn, ArnFormat.NO_RESOURCE_NAME); + const resource = arnComponents.resource; + if (!resource) { + throw new Error(`Provided ARN does not belong to a target group: ${arn}`); + } + return resource; +} diff --git a/packages/@aws-cdk/aws-elasticloadbalancingv2/test/alb/listener.test.ts b/packages/@aws-cdk/aws-elasticloadbalancingv2/test/alb/listener.test.ts index 838897fa5dfe1..025680b8ca1ad 100644 --- a/packages/@aws-cdk/aws-elasticloadbalancingv2/test/alb/listener.test.ts +++ b/packages/@aws-cdk/aws-elasticloadbalancingv2/test/alb/listener.test.ts @@ -645,14 +645,14 @@ describe('tests', () => { // WHEN const metrics = new Array(); - metrics.push(group.metricHttpCodeTarget(elbv2.HttpCodeTarget.TARGET_3XX_COUNT)); - metrics.push(group.metricIpv6RequestCount()); - metrics.push(group.metricUnhealthyHostCount()); - metrics.push(group.metricUnhealthyHostCount()); - metrics.push(group.metricRequestCount()); - metrics.push(group.metricTargetConnectionErrorCount()); - metrics.push(group.metricTargetResponseTime()); - metrics.push(group.metricTargetTLSNegotiationErrorCount()); + metrics.push(group.metrics.httpCodeTarget(elbv2.HttpCodeTarget.TARGET_3XX_COUNT)); + metrics.push(group.metrics.ipv6RequestCount()); + metrics.push(group.metrics.unhealthyHostCount()); + metrics.push(group.metrics.unhealthyHostCount()); + metrics.push(group.metrics.requestCount()); + metrics.push(group.metrics.targetConnectionErrorCount()); + metrics.push(group.metrics.targetResponseTime()); + metrics.push(group.metrics.targetTLSNegotiationErrorCount()); for (const metric of metrics) { expect(metric.namespace).toEqual('AWS/ApplicationELB'); diff --git a/packages/@aws-cdk/aws-elasticloadbalancingv2/test/alb/target-group.test.ts b/packages/@aws-cdk/aws-elasticloadbalancingv2/test/alb/target-group.test.ts index c11ce5a524a7f..33e31eb57e375 100644 --- a/packages/@aws-cdk/aws-elasticloadbalancingv2/test/alb/target-group.test.ts +++ b/packages/@aws-cdk/aws-elasticloadbalancingv2/test/alb/target-group.test.ts @@ -609,4 +609,38 @@ describe('tests', () => { }, }); }); + + test('imported targetGroup has metrics', () => { + // GIVEN + const app = new cdk.App(); + const stack = new cdk.Stack(app, 'Stack'); + + // WHEN + const targetGroup = elbv2.ApplicationTargetGroup.fromTargetGroupAttributes(stack, 'importedTg', { + targetGroupArn: 'arn:aws:elasticloadbalancing:us-west-2:123456789012:targetgroup/my-target-group/50dc6c495c0c9188', + loadBalancerArns: 'arn:aws:elasticloadbalancing:us-west-2:123456789012:loadbalancer/app/my-load-balancer/73e2d6bc24d8a067', + }); + + const metric = targetGroup.metrics.custom('MetricName'); + + // THEN + expect(metric.namespace).toEqual('AWS/ApplicationELB'); + expect(stack.resolve(metric.dimensions)).toEqual({ + LoadBalancer: 'app/my-load-balancer/73e2d6bc24d8a067', + TargetGroup: 'targetgroup/my-target-group/50dc6c495c0c9188', + }); + }); + + test('imported targetGroup without load balancer cannot have metrics', () => { + // GIVEN + const app = new cdk.App(); + const stack = new cdk.Stack(app, 'Stack'); + + // WHEN + const targetGroup = elbv2.ApplicationTargetGroup.fromTargetGroupAttributes(stack, 'importedTg', { + targetGroupArn: 'arn:aws:elasticloadbalancing:us-west-2:123456789012:targetgroup/my-target-group/50dc6c495c0c9188', + }); + + expect(() => targetGroup.metrics.custom('MetricName')).toThrow(); + }); }); diff --git a/packages/@aws-cdk/aws-elasticloadbalancingv2/test/integ.nlb-lookup.js.snapshot/aws-cdk-elbv2-StackWithLb.assets.json b/packages/@aws-cdk/aws-elasticloadbalancingv2/test/integ.nlb-lookup.js.snapshot/aws-cdk-elbv2-StackWithLb.assets.json index e830638335cf4..7422cdfc10c92 100644 --- a/packages/@aws-cdk/aws-elasticloadbalancingv2/test/integ.nlb-lookup.js.snapshot/aws-cdk-elbv2-StackWithLb.assets.json +++ b/packages/@aws-cdk/aws-elasticloadbalancingv2/test/integ.nlb-lookup.js.snapshot/aws-cdk-elbv2-StackWithLb.assets.json @@ -1,7 +1,7 @@ { "version": "29.0.0", "files": { - "e24b7b4b9bebbe70e470dbe2c83f8f69c8338d37b258b8ebec384b51fd61536d": { + "6ad4c46c9b2688341a64b901149113b860270b54a02e718637959ebf672835b5": { "source": { "path": "aws-cdk-elbv2-StackWithLb.template.json", "packaging": "file" @@ -9,7 +9,7 @@ "destinations": { "12345678-test-region": { "bucketName": "cdk-hnb659fds-assets-12345678-test-region", - "objectKey": "e24b7b4b9bebbe70e470dbe2c83f8f69c8338d37b258b8ebec384b51fd61536d.json", + "objectKey": "6ad4c46c9b2688341a64b901149113b860270b54a02e718637959ebf672835b5.json", "region": "test-region", "assumeRoleArn": "arn:${AWS::Partition}:iam::12345678:role/cdk-hnb659fds-file-publishing-role-12345678-test-region" } diff --git a/packages/@aws-cdk/aws-elasticloadbalancingv2/test/integ.nlb-lookup.js.snapshot/aws-cdk-elbv2-StackWithLb.template.json b/packages/@aws-cdk/aws-elasticloadbalancingv2/test/integ.nlb-lookup.js.snapshot/aws-cdk-elbv2-StackWithLb.template.json index 4170f7e23915c..aae90c3d78bbc 100644 --- a/packages/@aws-cdk/aws-elasticloadbalancingv2/test/integ.nlb-lookup.js.snapshot/aws-cdk-elbv2-StackWithLb.template.json +++ b/packages/@aws-cdk/aws-elasticloadbalancingv2/test/integ.nlb-lookup.js.snapshot/aws-cdk-elbv2-StackWithLb.template.json @@ -390,6 +390,40 @@ "VPCPublicSubnet2DefaultRouteB7481BBA", "VPCPublicSubnet2RouteTableAssociation5A808732" ] + }, + "LBListener49E825B4": { + "Type": "AWS::ElasticLoadBalancingV2::Listener", + "Properties": { + "DefaultActions": [ + { + "TargetGroupArn": { + "Ref": "LBListenerTargetGroupGroup07C223BF" + }, + "Type": "forward" + } + ], + "LoadBalancerArn": { + "Ref": "LB8A12904C" + }, + "Port": 443, + "Protocol": "TCP" + } + }, + "LBListenerTargetGroupGroup07C223BF": { + "Type": "AWS::ElasticLoadBalancingV2::TargetGroup", + "Properties": { + "Port": 443, + "Protocol": "TCP", + "Targets": [ + { + "Id": "10.0.1.1" + } + ], + "TargetType": "ip", + "VpcId": { + "Ref": "VPCB9E5F0B4" + } + } } }, "Outputs": { @@ -401,6 +435,14 @@ "Name": "NlbArn" } }, + "TgArn": { + "Value": { + "Ref": "LBListenerTargetGroupGroup07C223BF" + }, + "Export": { + "Name": "TgArn" + } + }, "ExportsOutputRefLB8A12904C1150D6A6": { "Value": { "Ref": "LB8A12904C" @@ -408,6 +450,14 @@ "Export": { "Name": "aws-cdk-elbv2-StackWithLb:ExportsOutputRefLB8A12904C1150D6A6" } + }, + "ExportsOutputRefLBListenerTargetGroupGroup07C223BF73476D0D": { + "Value": { + "Ref": "LBListenerTargetGroupGroup07C223BF" + }, + "Export": { + "Name": "aws-cdk-elbv2-StackWithLb:ExportsOutputRefLBListenerTargetGroupGroup07C223BF73476D0D" + } } }, "Parameters": { diff --git a/packages/@aws-cdk/aws-elasticloadbalancingv2/test/integ.nlb-lookup.js.snapshot/aws-cdk-elbv2-integ-StackUnderTest.assets.json b/packages/@aws-cdk/aws-elasticloadbalancingv2/test/integ.nlb-lookup.js.snapshot/aws-cdk-elbv2-integ-StackUnderTest.assets.json index 37afa2c575793..d20ce25ee4927 100644 --- a/packages/@aws-cdk/aws-elasticloadbalancingv2/test/integ.nlb-lookup.js.snapshot/aws-cdk-elbv2-integ-StackUnderTest.assets.json +++ b/packages/@aws-cdk/aws-elasticloadbalancingv2/test/integ.nlb-lookup.js.snapshot/aws-cdk-elbv2-integ-StackUnderTest.assets.json @@ -1,7 +1,7 @@ { "version": "29.0.0", "files": { - "e42d9c4a114328c16cb773781b2fee3cceeb499294ef3cb4f7a0aeafce947f13": { + "f26cf09b3a4b15b339d4d26f3d761938d15ff95bb2fce098b9559d13f55e6b94": { "source": { "path": "aws-cdk-elbv2-integ-StackUnderTest.template.json", "packaging": "file" @@ -9,7 +9,7 @@ "destinations": { "12345678-test-region": { "bucketName": "cdk-hnb659fds-assets-12345678-test-region", - "objectKey": "e42d9c4a114328c16cb773781b2fee3cceeb499294ef3cb4f7a0aeafce947f13.json", + "objectKey": "f26cf09b3a4b15b339d4d26f3d761938d15ff95bb2fce098b9559d13f55e6b94.json", "region": "test-region", "assumeRoleArn": "arn:${AWS::Partition}:iam::12345678:role/cdk-hnb659fds-file-publishing-role-12345678-test-region" } diff --git a/packages/@aws-cdk/aws-elasticloadbalancingv2/test/integ.nlb-lookup.js.snapshot/aws-cdk-elbv2-integ-StackUnderTest.template.json b/packages/@aws-cdk/aws-elasticloadbalancingv2/test/integ.nlb-lookup.js.snapshot/aws-cdk-elbv2-integ-StackUnderTest.template.json index 0356b00b65d81..5836bb7ada43c 100644 --- a/packages/@aws-cdk/aws-elasticloadbalancingv2/test/integ.nlb-lookup.js.snapshot/aws-cdk-elbv2-integ-StackUnderTest.template.json +++ b/packages/@aws-cdk/aws-elasticloadbalancingv2/test/integ.nlb-lookup.js.snapshot/aws-cdk-elbv2-integ-StackUnderTest.template.json @@ -147,6 +147,190 @@ "Statistic": "Average", "Threshold": 0 } + }, + "TgByHardcodedArnHealthyHostCount433C0E6E": { + "Type": "AWS::CloudWatch::Alarm", + "Properties": { + "ComparisonOperator": "GreaterThanOrEqualToThreshold", + "EvaluationPeriods": 1, + "Dimensions": [ + { + "Name": "LoadBalancer", + "Value": "net/my-load-balancer/50dc6c495c0c9188" + }, + { + "Name": "TargetGroup", + "Value": "targetgroup/my-target-group/50dc6c495c0c9188" + } + ], + "MetricName": "HealthyHostCount", + "Namespace": "AWS/NetworkELB", + "Period": 300, + "Statistic": "Average", + "Threshold": 0 + } + }, + "TgByCfnOutputsFromAnotherStackOutsideCdkHealthyHostCount3DA06734": { + "Type": "AWS::CloudWatch::Alarm", + "Properties": { + "ComparisonOperator": "GreaterThanOrEqualToThreshold", + "EvaluationPeriods": 1, + "Dimensions": [ + { + "Name": "LoadBalancer", + "Value": { + "Fn::Join": [ + "", + [ + { + "Fn::Select": [ + 1, + { + "Fn::Split": [ + "/", + { + "Fn::ImportValue": "NlbArn" + } + ] + } + ] + }, + "/", + { + "Fn::Select": [ + 2, + { + "Fn::Split": [ + "/", + { + "Fn::ImportValue": "NlbArn" + } + ] + } + ] + }, + "/", + { + "Fn::Select": [ + 3, + { + "Fn::Split": [ + "/", + { + "Fn::ImportValue": "NlbArn" + } + ] + } + ] + } + ] + ] + } + }, + { + "Name": "TargetGroup", + "Value": { + "Fn::Select": [ + 5, + { + "Fn::Split": [ + ":", + { + "Fn::ImportValue": "TgArn" + } + ] + } + ] + } + } + ], + "MetricName": "HealthyHostCount", + "Namespace": "AWS/NetworkELB", + "Period": 300, + "Statistic": "Average", + "Threshold": 0 + } + }, + "TgByCfnOutputsFromAnotherStackWithinCdkHealthyHostCountD4851E85": { + "Type": "AWS::CloudWatch::Alarm", + "Properties": { + "ComparisonOperator": "GreaterThanOrEqualToThreshold", + "EvaluationPeriods": 1, + "Dimensions": [ + { + "Name": "LoadBalancer", + "Value": { + "Fn::Join": [ + "", + [ + { + "Fn::Select": [ + 1, + { + "Fn::Split": [ + "/", + { + "Fn::ImportValue": "aws-cdk-elbv2-StackWithLb:ExportsOutputRefLB8A12904C1150D6A6" + } + ] + } + ] + }, + "/", + { + "Fn::Select": [ + 2, + { + "Fn::Split": [ + "/", + { + "Fn::ImportValue": "aws-cdk-elbv2-StackWithLb:ExportsOutputRefLB8A12904C1150D6A6" + } + ] + } + ] + }, + "/", + { + "Fn::Select": [ + 3, + { + "Fn::Split": [ + "/", + { + "Fn::ImportValue": "aws-cdk-elbv2-StackWithLb:ExportsOutputRefLB8A12904C1150D6A6" + } + ] + } + ] + } + ] + ] + } + }, + { + "Name": "TargetGroup", + "Value": { + "Fn::Select": [ + 5, + { + "Fn::Split": [ + ":", + { + "Fn::ImportValue": "aws-cdk-elbv2-StackWithLb:ExportsOutputRefLBListenerTargetGroupGroup07C223BF73476D0D" + } + ] + } + ] + } + } + ], + "MetricName": "HealthyHostCount", + "Namespace": "AWS/NetworkELB", + "Period": 300, + "Statistic": "Average", + "Threshold": 0 + } } }, "Parameters": { diff --git a/packages/@aws-cdk/aws-elasticloadbalancingv2/test/integ.nlb-lookup.js.snapshot/manifest.json b/packages/@aws-cdk/aws-elasticloadbalancingv2/test/integ.nlb-lookup.js.snapshot/manifest.json index ecb92960fabc0..d524bd19d3da1 100644 --- a/packages/@aws-cdk/aws-elasticloadbalancingv2/test/integ.nlb-lookup.js.snapshot/manifest.json +++ b/packages/@aws-cdk/aws-elasticloadbalancingv2/test/integ.nlb-lookup.js.snapshot/manifest.json @@ -17,7 +17,7 @@ "validateOnSynth": false, "assumeRoleArn": "arn:${AWS::Partition}:iam::12345678:role/cdk-hnb659fds-deploy-role-12345678-test-region", "cloudFormationExecutionRoleArn": "arn:${AWS::Partition}:iam::12345678:role/cdk-hnb659fds-cfn-exec-role-12345678-test-region", - "stackTemplateAssetObjectUrl": "s3://cdk-hnb659fds-assets-12345678-test-region/e24b7b4b9bebbe70e470dbe2c83f8f69c8338d37b258b8ebec384b51fd61536d.json", + "stackTemplateAssetObjectUrl": "s3://cdk-hnb659fds-assets-12345678-test-region/6ad4c46c9b2688341a64b901149113b860270b54a02e718637959ebf672835b5.json", "requiresBootstrapStackVersion": 6, "bootstrapStackVersionSsmParameter": "/cdk-bootstrap/hnb659fds/version", "additionalDependencies": [ @@ -177,18 +177,42 @@ "data": "LB8A12904C" } ], + "/aws-cdk-elbv2-StackWithLb/LB/Listener/Resource": [ + { + "type": "aws:cdk:logicalId", + "data": "LBListener49E825B4" + } + ], + "/aws-cdk-elbv2-StackWithLb/LB/Listener/TargetGroupGroup/Resource": [ + { + "type": "aws:cdk:logicalId", + "data": "LBListenerTargetGroupGroup07C223BF" + } + ], "/aws-cdk-elbv2-StackWithLb/NlbArn": [ { "type": "aws:cdk:logicalId", "data": "NlbArn" } ], + "/aws-cdk-elbv2-StackWithLb/TgArn": [ + { + "type": "aws:cdk:logicalId", + "data": "TgArn" + } + ], "/aws-cdk-elbv2-StackWithLb/Exports/Output{\"Ref\":\"LB8A12904C\"}": [ { "type": "aws:cdk:logicalId", "data": "ExportsOutputRefLB8A12904C1150D6A6" } ], + "/aws-cdk-elbv2-StackWithLb/Exports/Output{\"Ref\":\"LBListenerTargetGroupGroup07C223BF\"}": [ + { + "type": "aws:cdk:logicalId", + "data": "ExportsOutputRefLBListenerTargetGroupGroup07C223BF73476D0D" + } + ], "/aws-cdk-elbv2-StackWithLb/BootstrapVersion": [ { "type": "aws:cdk:logicalId", @@ -314,7 +338,7 @@ "validateOnSynth": false, "assumeRoleArn": "arn:${AWS::Partition}:iam::12345678:role/cdk-hnb659fds-deploy-role-12345678-test-region", "cloudFormationExecutionRoleArn": "arn:${AWS::Partition}:iam::12345678:role/cdk-hnb659fds-cfn-exec-role-12345678-test-region", - "stackTemplateAssetObjectUrl": "s3://cdk-hnb659fds-assets-12345678-test-region/e42d9c4a114328c16cb773781b2fee3cceeb499294ef3cb4f7a0aeafce947f13.json", + "stackTemplateAssetObjectUrl": "s3://cdk-hnb659fds-assets-12345678-test-region/f26cf09b3a4b15b339d4d26f3d761938d15ff95bb2fce098b9559d13f55e6b94.json", "requiresBootstrapStackVersion": 6, "bootstrapStackVersionSsmParameter": "/cdk-bootstrap/hnb659fds/version", "additionalDependencies": [ @@ -349,6 +373,24 @@ "data": "NlbByCfnOutputsFromAnotherStackWithinCdkAlarmFlowCountD865DB84" } ], + "/aws-cdk-elbv2-integ-StackUnderTest/TgByHardcodedArn_HealthyHostCount/Resource": [ + { + "type": "aws:cdk:logicalId", + "data": "TgByHardcodedArnHealthyHostCount433C0E6E" + } + ], + "/aws-cdk-elbv2-integ-StackUnderTest/TgByCfnOutputsFromAnotherStackOutsideCdk_HealthyHostCount/Resource": [ + { + "type": "aws:cdk:logicalId", + "data": "TgByCfnOutputsFromAnotherStackOutsideCdkHealthyHostCount3DA06734" + } + ], + "/aws-cdk-elbv2-integ-StackUnderTest/TgByCfnOutputsFromAnotherStackWithinCdk_HealthyHostCount/Resource": [ + { + "type": "aws:cdk:logicalId", + "data": "TgByCfnOutputsFromAnotherStackWithinCdkHealthyHostCountD4851E85" + } + ], "/aws-cdk-elbv2-integ-StackUnderTest/BootstrapVersion": [ { "type": "aws:cdk:logicalId", diff --git a/packages/@aws-cdk/aws-elasticloadbalancingv2/test/integ.nlb-lookup.js.snapshot/tree.json b/packages/@aws-cdk/aws-elasticloadbalancingv2/test/integ.nlb-lookup.js.snapshot/tree.json index 16876b01d13bb..bc368664df903 100644 --- a/packages/@aws-cdk/aws-elasticloadbalancingv2/test/integ.nlb-lookup.js.snapshot/tree.json +++ b/packages/@aws-cdk/aws-elasticloadbalancingv2/test/integ.nlb-lookup.js.snapshot/tree.json @@ -656,6 +656,76 @@ "fqn": "@aws-cdk/aws-elasticloadbalancingv2.CfnLoadBalancer", "version": "0.0.0" } + }, + "Listener": { + "id": "Listener", + "path": "aws-cdk-elbv2-StackWithLb/LB/Listener", + "children": { + "Resource": { + "id": "Resource", + "path": "aws-cdk-elbv2-StackWithLb/LB/Listener/Resource", + "attributes": { + "aws:cdk:cloudformation:type": "AWS::ElasticLoadBalancingV2::Listener", + "aws:cdk:cloudformation:props": { + "defaultActions": [ + { + "type": "forward", + "targetGroupArn": { + "Ref": "LBListenerTargetGroupGroup07C223BF" + } + } + ], + "loadBalancerArn": { + "Ref": "LB8A12904C" + }, + "port": 443, + "protocol": "TCP" + } + }, + "constructInfo": { + "fqn": "@aws-cdk/aws-elasticloadbalancingv2.CfnListener", + "version": "0.0.0" + } + }, + "TargetGroupGroup": { + "id": "TargetGroupGroup", + "path": "aws-cdk-elbv2-StackWithLb/LB/Listener/TargetGroupGroup", + "children": { + "Resource": { + "id": "Resource", + "path": "aws-cdk-elbv2-StackWithLb/LB/Listener/TargetGroupGroup/Resource", + "attributes": { + "aws:cdk:cloudformation:type": "AWS::ElasticLoadBalancingV2::TargetGroup", + "aws:cdk:cloudformation:props": { + "port": 443, + "protocol": "TCP", + "targets": [ + { + "id": "10.0.1.1" + } + ], + "targetType": "ip", + "vpcId": { + "Ref": "VPCB9E5F0B4" + } + } + }, + "constructInfo": { + "fqn": "@aws-cdk/aws-elasticloadbalancingv2.CfnTargetGroup", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "@aws-cdk/aws-elasticloadbalancingv2.NetworkTargetGroup", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "@aws-cdk/aws-elasticloadbalancingv2.NetworkListener", + "version": "0.0.0" + } } }, "constructInfo": { @@ -671,6 +741,14 @@ "version": "0.0.0" } }, + "TgArn": { + "id": "TgArn", + "path": "aws-cdk-elbv2-StackWithLb/TgArn", + "constructInfo": { + "fqn": "@aws-cdk/core.CfnOutput", + "version": "0.0.0" + } + }, "Exports": { "id": "Exports", "path": "aws-cdk-elbv2-StackWithLb/Exports", @@ -682,6 +760,14 @@ "fqn": "@aws-cdk/core.CfnOutput", "version": "0.0.0" } + }, + "Output{\"Ref\":\"LBListenerTargetGroupGroup07C223BF\"}": { + "id": "Output{\"Ref\":\"LBListenerTargetGroupGroup07C223BF\"}", + "path": "aws-cdk-elbv2-StackWithLb/Exports/Output{\"Ref\":\"LBListenerTargetGroupGroup07C223BF\"}", + "constructInfo": { + "fqn": "@aws-cdk/core.CfnOutput", + "version": "0.0.0" + } } }, "constructInfo": { @@ -1019,6 +1105,268 @@ "version": "0.0.0" } }, + "TgByHardcodedArn": { + "id": "TgByHardcodedArn", + "path": "aws-cdk-elbv2-integ-StackUnderTest/TgByHardcodedArn", + "constructInfo": { + "fqn": "constructs.Construct", + "version": "10.1.237" + } + }, + "TgByHardcodedArn_HealthyHostCount": { + "id": "TgByHardcodedArn_HealthyHostCount", + "path": "aws-cdk-elbv2-integ-StackUnderTest/TgByHardcodedArn_HealthyHostCount", + "children": { + "Resource": { + "id": "Resource", + "path": "aws-cdk-elbv2-integ-StackUnderTest/TgByHardcodedArn_HealthyHostCount/Resource", + "attributes": { + "aws:cdk:cloudformation:type": "AWS::CloudWatch::Alarm", + "aws:cdk:cloudformation:props": { + "comparisonOperator": "GreaterThanOrEqualToThreshold", + "evaluationPeriods": 1, + "dimensions": [ + { + "name": "LoadBalancer", + "value": "net/my-load-balancer/50dc6c495c0c9188" + }, + { + "name": "TargetGroup", + "value": "targetgroup/my-target-group/50dc6c495c0c9188" + } + ], + "metricName": "HealthyHostCount", + "namespace": "AWS/NetworkELB", + "period": 300, + "statistic": "Average", + "threshold": 0 + } + }, + "constructInfo": { + "fqn": "@aws-cdk/aws-cloudwatch.CfnAlarm", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "@aws-cdk/aws-cloudwatch.Alarm", + "version": "0.0.0" + } + }, + "TgByCfnOutputsFromAnotherStackOutsideCdk": { + "id": "TgByCfnOutputsFromAnotherStackOutsideCdk", + "path": "aws-cdk-elbv2-integ-StackUnderTest/TgByCfnOutputsFromAnotherStackOutsideCdk", + "constructInfo": { + "fqn": "constructs.Construct", + "version": "10.1.237" + } + }, + "TgByCfnOutputsFromAnotherStackOutsideCdk_HealthyHostCount": { + "id": "TgByCfnOutputsFromAnotherStackOutsideCdk_HealthyHostCount", + "path": "aws-cdk-elbv2-integ-StackUnderTest/TgByCfnOutputsFromAnotherStackOutsideCdk_HealthyHostCount", + "children": { + "Resource": { + "id": "Resource", + "path": "aws-cdk-elbv2-integ-StackUnderTest/TgByCfnOutputsFromAnotherStackOutsideCdk_HealthyHostCount/Resource", + "attributes": { + "aws:cdk:cloudformation:type": "AWS::CloudWatch::Alarm", + "aws:cdk:cloudformation:props": { + "comparisonOperator": "GreaterThanOrEqualToThreshold", + "evaluationPeriods": 1, + "dimensions": [ + { + "name": "LoadBalancer", + "value": { + "Fn::Join": [ + "", + [ + { + "Fn::Select": [ + 1, + { + "Fn::Split": [ + "/", + { + "Fn::ImportValue": "NlbArn" + } + ] + } + ] + }, + "/", + { + "Fn::Select": [ + 2, + { + "Fn::Split": [ + "/", + { + "Fn::ImportValue": "NlbArn" + } + ] + } + ] + }, + "/", + { + "Fn::Select": [ + 3, + { + "Fn::Split": [ + "/", + { + "Fn::ImportValue": "NlbArn" + } + ] + } + ] + } + ] + ] + } + }, + { + "name": "TargetGroup", + "value": { + "Fn::Select": [ + 5, + { + "Fn::Split": [ + ":", + { + "Fn::ImportValue": "TgArn" + } + ] + } + ] + } + } + ], + "metricName": "HealthyHostCount", + "namespace": "AWS/NetworkELB", + "period": 300, + "statistic": "Average", + "threshold": 0 + } + }, + "constructInfo": { + "fqn": "@aws-cdk/aws-cloudwatch.CfnAlarm", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "@aws-cdk/aws-cloudwatch.Alarm", + "version": "0.0.0" + } + }, + "TgByCfnOutputsFromAnotherStackWithinCdk": { + "id": "TgByCfnOutputsFromAnotherStackWithinCdk", + "path": "aws-cdk-elbv2-integ-StackUnderTest/TgByCfnOutputsFromAnotherStackWithinCdk", + "constructInfo": { + "fqn": "constructs.Construct", + "version": "10.1.237" + } + }, + "TgByCfnOutputsFromAnotherStackWithinCdk_HealthyHostCount": { + "id": "TgByCfnOutputsFromAnotherStackWithinCdk_HealthyHostCount", + "path": "aws-cdk-elbv2-integ-StackUnderTest/TgByCfnOutputsFromAnotherStackWithinCdk_HealthyHostCount", + "children": { + "Resource": { + "id": "Resource", + "path": "aws-cdk-elbv2-integ-StackUnderTest/TgByCfnOutputsFromAnotherStackWithinCdk_HealthyHostCount/Resource", + "attributes": { + "aws:cdk:cloudformation:type": "AWS::CloudWatch::Alarm", + "aws:cdk:cloudformation:props": { + "comparisonOperator": "GreaterThanOrEqualToThreshold", + "evaluationPeriods": 1, + "dimensions": [ + { + "name": "LoadBalancer", + "value": { + "Fn::Join": [ + "", + [ + { + "Fn::Select": [ + 1, + { + "Fn::Split": [ + "/", + { + "Fn::ImportValue": "aws-cdk-elbv2-StackWithLb:ExportsOutputRefLB8A12904C1150D6A6" + } + ] + } + ] + }, + "/", + { + "Fn::Select": [ + 2, + { + "Fn::Split": [ + "/", + { + "Fn::ImportValue": "aws-cdk-elbv2-StackWithLb:ExportsOutputRefLB8A12904C1150D6A6" + } + ] + } + ] + }, + "/", + { + "Fn::Select": [ + 3, + { + "Fn::Split": [ + "/", + { + "Fn::ImportValue": "aws-cdk-elbv2-StackWithLb:ExportsOutputRefLB8A12904C1150D6A6" + } + ] + } + ] + } + ] + ] + } + }, + { + "name": "TargetGroup", + "value": { + "Fn::Select": [ + 5, + { + "Fn::Split": [ + ":", + { + "Fn::ImportValue": "aws-cdk-elbv2-StackWithLb:ExportsOutputRefLBListenerTargetGroupGroup07C223BF73476D0D" + } + ] + } + ] + } + } + ], + "metricName": "HealthyHostCount", + "namespace": "AWS/NetworkELB", + "period": 300, + "statistic": "Average", + "threshold": 0 + } + }, + "constructInfo": { + "fqn": "@aws-cdk/aws-cloudwatch.CfnAlarm", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "@aws-cdk/aws-cloudwatch.Alarm", + "version": "0.0.0" + } + }, "BootstrapVersion": { "id": "BootstrapVersion", "path": "aws-cdk-elbv2-integ-StackUnderTest/BootstrapVersion", diff --git a/packages/@aws-cdk/aws-elasticloadbalancingv2/test/integ.nlb-lookup.ts b/packages/@aws-cdk/aws-elasticloadbalancingv2/test/integ.nlb-lookup.ts index ef5a0f7125ada..2bd2bbc8e1866 100644 --- a/packages/@aws-cdk/aws-elasticloadbalancingv2/test/integ.nlb-lookup.ts +++ b/packages/@aws-cdk/aws-elasticloadbalancingv2/test/integ.nlb-lookup.ts @@ -22,10 +22,21 @@ const lb = new elbv2.NetworkLoadBalancer(stackWithLb, 'LB', { internetFacing: true, loadBalancerName: 'my-load-balancer', }); +const listener = lb.addListener('Listener', { + port: 443, +}); +const group = listener.addTargets('TargetGroup', { + port: 443, + targets: [new elbv2.IpTarget('10.0.1.1')], +}); new cdk.CfnOutput(stackWithLb, 'NlbArn', { value: lb.loadBalancerArn, exportName: 'NlbArn', }); +new cdk.CfnOutput(stackWithLb, 'TgArn', { + value: group.targetGroupArn, + exportName: 'TgArn', +}); const stackLookup = new IntegTestCaseStack(app, 'aws-cdk-elbv2-integ-StackUnderTest', { env: { @@ -34,6 +45,7 @@ const stackLookup = new IntegTestCaseStack(app, 'aws-cdk-elbv2-integ-StackUnderT }, }); +// Load Balancer const lbByHardcodedArn = elbv2.NetworkLoadBalancer.fromNetworkLoadBalancerAttributes(stackLookup, 'NlbByHardcodedArn', { loadBalancerArn: 'arn:aws:elasticloadbalancing:us-west-2:123456789012:loadbalancer/network/my-load-balancer/50dc6c495c0c9188', }); @@ -58,6 +70,35 @@ lbByCfnOutputsFromAnotherStackWithinCdk.metrics.activeFlowCount().createAlarm(st threshold: 0, }); +// Target Group + +const tgByHardcodedArn = elbv2.NetworkTargetGroup.fromTargetGroupAttributes(stackLookup, 'TgByHardcodedArn', { + targetGroupArn: 'arn:aws:elasticloadbalancing:us-west-2:123456789012:targetgroup/my-target-group/50dc6c495c0c9188', + loadBalancerArns: 'arn:aws:elasticloadbalancing:us-west-2:123456789012:loadbalancer/net/my-load-balancer/50dc6c495c0c9188', +}); +tgByHardcodedArn.metrics.healthyHostCount().createAlarm(stackLookup, 'TgByHardcodedArn_HealthyHostCount', { + evaluationPeriods: 1, + threshold: 0, +}); + +const tgByCfnOutputsFromAnotherStackOutsideCdk = elbv2.NetworkTargetGroup.fromTargetGroupAttributes(stackLookup, 'TgByCfnOutputsFromAnotherStackOutsideCdk', { + targetGroupArn: cdk.Fn.importValue('TgArn'), + loadBalancerArns: cdk.Fn.importValue('NlbArn'), +}); +tgByCfnOutputsFromAnotherStackOutsideCdk.metrics.healthyHostCount().createAlarm(stackLookup, 'TgByCfnOutputsFromAnotherStackOutsideCdk_HealthyHostCount', { + evaluationPeriods: 1, + threshold: 0, +}); + +const tgByCfnOutputsFromAnotherStackWithinCdk = elbv2.NetworkTargetGroup.fromTargetGroupAttributes(stackLookup, 'TgByCfnOutputsFromAnotherStackWithinCdk', { + targetGroupArn: group.targetGroupArn, + loadBalancerArns: lb.loadBalancerArn, +}); +tgByCfnOutputsFromAnotherStackWithinCdk.metrics.healthyHostCount().createAlarm(stackLookup, 'TgByCfnOutputsFromAnotherStackWithinCdk_HealthyHostCount', { + evaluationPeriods: 1, + threshold: 0, +}); + new integ.IntegTest(app, 'elbv2-integ', { testCases: [stackLookup], enableLookups: true, diff --git a/packages/@aws-cdk/aws-elasticloadbalancingv2/test/nlb/target-group.test.ts b/packages/@aws-cdk/aws-elasticloadbalancingv2/test/nlb/target-group.test.ts index 6d94b4ee4d45e..84a4a0cd2a642 100644 --- a/packages/@aws-cdk/aws-elasticloadbalancingv2/test/nlb/target-group.test.ts +++ b/packages/@aws-cdk/aws-elasticloadbalancingv2/test/nlb/target-group.test.ts @@ -579,8 +579,8 @@ describe('tests', () => { // WHEN const metrics = new Array(); - metrics.push(targetGroup.metricHealthyHostCount()); - metrics.push(targetGroup.metricUnHealthyHostCount()); + metrics.push(targetGroup.metrics.healthyHostCount()); + metrics.push(targetGroup.metrics.unHealthyHostCount()); // THEN @@ -618,8 +618,8 @@ describe('tests', () => { }); // THEN - expect(() => targetGroup.metricHealthyHostCount()).toThrow(/The TargetGroup needs to be attached to a LoadBalancer/); - expect(() => targetGroup.metricUnHealthyHostCount()).toThrow(/The TargetGroup needs to be attached to a LoadBalancer/); + expect(() => targetGroup.metrics.healthyHostCount()).toThrow(/The TargetGroup needs to be attached to a LoadBalancer/); + expect(() => targetGroup.metrics.unHealthyHostCount()).toThrow(/The TargetGroup needs to be attached to a LoadBalancer/); }); test('imported targetGroup has targetGroupName', () => { @@ -682,4 +682,38 @@ describe('tests', () => { }, }); }); + + test('imported targetGroup has metrics', () => { + // GIVEN + const app = new cdk.App(); + const stack = new cdk.Stack(app, 'Stack'); + + // WHEN + const targetGroup = elbv2.NetworkTargetGroup.fromTargetGroupAttributes(stack, 'importedTg', { + targetGroupArn: 'arn:aws:elasticloadbalancing:us-west-2:123456789012:targetgroup/my-target-group/50dc6c495c0c9188', + loadBalancerArns: 'arn:aws:elasticloadbalancing:us-west-2:123456789012:loadbalancer/net/my-load-balancer/73e2d6bc24d8a067', + }); + + const metric = targetGroup.metrics.custom('MetricName'); + + // THEN + expect(metric.namespace).toEqual('AWS/NetworkELB'); + expect(stack.resolve(metric.dimensions)).toEqual({ + LoadBalancer: 'net/my-load-balancer/73e2d6bc24d8a067', + TargetGroup: 'targetgroup/my-target-group/50dc6c495c0c9188', + }); + }); + + test('imported targetGroup without load balancer cannot have metrics', () => { + // GIVEN + const app = new cdk.App(); + const stack = new cdk.Stack(app, 'Stack'); + + // WHEN + const targetGroup = elbv2.NetworkTargetGroup.fromTargetGroupAttributes(stack, 'importedTg', { + targetGroupArn: 'arn:aws:elasticloadbalancing:us-west-2:123456789012:targetgroup/my-target-group/50dc6c495c0c9188', + }); + + expect(() => targetGroup.metrics.custom('MetricName')).toThrow(); + }); });