From bd9ee01fc4886ea1b355eb8b00fe2660e2082934 Mon Sep 17 00:00:00 2001 From: Rico Huijbers Date: Wed, 26 Sep 2018 09:59:59 +0200 Subject: [PATCH] feat(aws-elasticloadbalancingv2): support for ALB/NLB (#750) BREAKING CHANGE: Adds classes for modeling Application and Network Load Balancers. AutoScalingGroups now implement the interface that makes constructs a load balancing target. The breaking change is that Security Group rule identifiers have been changed in order to make adding rules more reliable. No code changes are necessary but existing deployments may experience unexpected changes. --- build.sh | 25 +- .../aws-autoscaling/lib/auto-scaling-group.ts | 28 +- .../@aws-cdk/aws-autoscaling/package.json | 1 + ....asg-w-classic-loadbalancer.expected.json} | 4 +- ...ts => integ.asg-w-classic-loadbalancer.ts} | 0 .../test/integ.asg-w-elbv2.expected.json | 524 +++++++++++++++++ .../aws-autoscaling/test/integ.asg-w-elbv2.ts | 36 ++ .../test/test.auto-scaling-group.ts | 2 - packages/@aws-cdk/aws-ec2/lib/connections.ts | 40 +- .../aws-ec2/lib/security-group-rule.ts | 31 + .../@aws-cdk/aws-ec2/lib/security-group.ts | 61 +- packages/@aws-cdk/aws-ec2/lib/vpc-ref.ts | 11 + .../@aws-cdk/aws-ec2/test/test.connections.ts | 36 +- .../aws-elasticloadbalancingv2/README.md | 200 ++++++- .../alb/application-listener-certificate.ts | 41 ++ .../lib/alb/application-listener-rule.ts | 145 +++++ .../lib/alb/application-listener.ts | 532 ++++++++++++++++++ .../lib/alb/application-load-balancer.ts | 212 +++++++ .../lib/alb/application-target-group.ts | 197 +++++++ .../aws-elasticloadbalancingv2/lib/index.ts | 16 + .../lib/nlb/network-listener.ts | 202 +++++++ .../lib/nlb/network-load-balancer.ts | 121 ++++ .../lib/nlb/network-target-group.ts | 91 +++ .../lib/shared/base-listener.ts | 42 ++ .../lib/shared/base-load-balancer.ts | 138 +++++ .../lib/shared/base-target-group.ts | 295 ++++++++++ .../lib/shared/enums.ts | 117 ++++ .../lib/shared/imported.ts | 18 + .../lib/shared/load-balancer-targets.ts | 113 ++++ .../lib/shared/util.ts | 69 +++ .../aws-elasticloadbalancingv2/package.json | 7 +- .../test/alb/test.listener.ts | 309 ++++++++++ .../test/alb/test.load-balancer.ts | 127 +++++ .../test/alb/test.security-groups.ts | 244 ++++++++ .../test/helpers.ts | 26 + .../test/integ.alb.expected.json | 430 ++++++++++++++ .../test/integ.alb.ts | 36 ++ .../test/integ.nlb.expected.json | 365 ++++++++++++ .../test/integ.nlb.ts | 31 + .../test/nlb/test.listener.ts | 115 ++++ .../test/nlb/test.load-balancer.ts | 78 +++ .../test/test.elasticloadbalancingv2.ts | 8 - packages/@aws-cdk/aws-lambda/lib/lambda.ts | 2 +- .../aws-rds/test/integ.cluster.expected.json | 2 +- scripts/runtest.js | 16 + 45 files changed, 5092 insertions(+), 52 deletions(-) rename packages/@aws-cdk/aws-autoscaling/test/{integ.asg-w-loadbalancer.expected.json => integ.asg-w-classic-loadbalancer.expected.json} (98%) rename packages/@aws-cdk/aws-autoscaling/test/{integ.asg-w-loadbalancer.ts => integ.asg-w-classic-loadbalancer.ts} (100%) create mode 100644 packages/@aws-cdk/aws-autoscaling/test/integ.asg-w-elbv2.expected.json create mode 100644 packages/@aws-cdk/aws-autoscaling/test/integ.asg-w-elbv2.ts create mode 100644 packages/@aws-cdk/aws-elasticloadbalancingv2/lib/alb/application-listener-certificate.ts create mode 100644 packages/@aws-cdk/aws-elasticloadbalancingv2/lib/alb/application-listener-rule.ts create mode 100644 packages/@aws-cdk/aws-elasticloadbalancingv2/lib/alb/application-listener.ts create mode 100644 packages/@aws-cdk/aws-elasticloadbalancingv2/lib/alb/application-load-balancer.ts create mode 100644 packages/@aws-cdk/aws-elasticloadbalancingv2/lib/alb/application-target-group.ts create mode 100644 packages/@aws-cdk/aws-elasticloadbalancingv2/lib/nlb/network-listener.ts create mode 100644 packages/@aws-cdk/aws-elasticloadbalancingv2/lib/nlb/network-load-balancer.ts create mode 100644 packages/@aws-cdk/aws-elasticloadbalancingv2/lib/nlb/network-target-group.ts create mode 100644 packages/@aws-cdk/aws-elasticloadbalancingv2/lib/shared/base-listener.ts create mode 100644 packages/@aws-cdk/aws-elasticloadbalancingv2/lib/shared/base-load-balancer.ts create mode 100644 packages/@aws-cdk/aws-elasticloadbalancingv2/lib/shared/base-target-group.ts create mode 100644 packages/@aws-cdk/aws-elasticloadbalancingv2/lib/shared/enums.ts create mode 100644 packages/@aws-cdk/aws-elasticloadbalancingv2/lib/shared/imported.ts create mode 100644 packages/@aws-cdk/aws-elasticloadbalancingv2/lib/shared/load-balancer-targets.ts create mode 100644 packages/@aws-cdk/aws-elasticloadbalancingv2/lib/shared/util.ts create mode 100644 packages/@aws-cdk/aws-elasticloadbalancingv2/test/alb/test.listener.ts create mode 100644 packages/@aws-cdk/aws-elasticloadbalancingv2/test/alb/test.load-balancer.ts create mode 100644 packages/@aws-cdk/aws-elasticloadbalancingv2/test/alb/test.security-groups.ts create mode 100644 packages/@aws-cdk/aws-elasticloadbalancingv2/test/helpers.ts create mode 100644 packages/@aws-cdk/aws-elasticloadbalancingv2/test/integ.alb.expected.json create mode 100644 packages/@aws-cdk/aws-elasticloadbalancingv2/test/integ.alb.ts create mode 100644 packages/@aws-cdk/aws-elasticloadbalancingv2/test/integ.nlb.expected.json create mode 100644 packages/@aws-cdk/aws-elasticloadbalancingv2/test/integ.nlb.ts create mode 100644 packages/@aws-cdk/aws-elasticloadbalancingv2/test/nlb/test.listener.ts create mode 100644 packages/@aws-cdk/aws-elasticloadbalancingv2/test/nlb/test.load-balancer.ts delete mode 100644 packages/@aws-cdk/aws-elasticloadbalancingv2/test/test.elasticloadbalancingv2.ts diff --git a/build.sh b/build.sh index 28997e424f743..5141e93f1fbe8 100755 --- a/build.sh +++ b/build.sh @@ -1,6 +1,27 @@ #!/bin/bash set -euo pipefail +bail="--no-bail" +while [[ "${1:-}" != "" ]]; do + case $1 in + -h|--help) + echo "Usage: build.sh [--bail|-b] [--force|-f]" + exit 1 + ;; + -b|--bail) + bail="--bail" + ;; + -f|--force) + export CDK_BUILD="--force" + ;; + *) + echo "Unrecognized parameter: $1" + exit 1 + ;; + esac + shift +done + if [ ! -d node_modules ]; then /bin/bash ./install.sh fi @@ -24,10 +45,10 @@ trap "rm -rf $MERKLE_BUILD_CACHE" EXIT echo "=============================================================================================" echo "building..." -time lerna run --no-bail --stream build || fail +time lerna run $bail --stream build || fail echo "=============================================================================================" echo "testing..." -lerna run --no-bail --stream test || fail +lerna run $bail --stream test || fail touch $BUILD_INDICATOR diff --git a/packages/@aws-cdk/aws-autoscaling/lib/auto-scaling-group.ts b/packages/@aws-cdk/aws-autoscaling/lib/auto-scaling-group.ts index 7b40984d8425c..62b2e924a31db 100644 --- a/packages/@aws-cdk/aws-autoscaling/lib/auto-scaling-group.ts +++ b/packages/@aws-cdk/aws-autoscaling/lib/auto-scaling-group.ts @@ -1,5 +1,6 @@ import ec2 = require('@aws-cdk/aws-ec2'); import elb = require('@aws-cdk/aws-elasticloadbalancing'); +import elbv2 = require('@aws-cdk/aws-elasticloadbalancingv2'); import iam = require('@aws-cdk/aws-iam'); import sns = require('@aws-cdk/aws-sns'); import cdk = require('@aws-cdk/cdk'); @@ -136,7 +137,8 @@ export interface AutoScalingGroupProps { * * The ASG spans all availability zones. */ -export class AutoScalingGroup extends cdk.Construct implements elb.ILoadBalancerTarget, ec2.IConnectable { +export class AutoScalingGroup extends cdk.Construct implements elb.ILoadBalancerTarget, ec2.IConnectable, + elbv2.IApplicationLoadBalancerTarget, elbv2.INetworkLoadBalancerTarget { /** * The type of OS instances of this fleet are running. */ @@ -157,6 +159,7 @@ export class AutoScalingGroup extends cdk.Construct implements elb.ILoadBalancer private readonly securityGroup: ec2.SecurityGroupRef; private readonly securityGroups: ec2.SecurityGroupRef[] = []; private readonly loadBalancerNames: string[] = []; + private readonly targetGroupArns: string[] = []; constructor(parent: cdk.Construct, name: string, props: AutoScalingGroupProps) { super(parent, name); @@ -206,7 +209,8 @@ export class AutoScalingGroup extends cdk.Construct implements elb.ILoadBalancer maxSize: maxSize.toString(), desiredCapacity: desiredCapacity.toString(), launchConfigurationName: launchConfig.ref, - loadBalancerNames: new cdk.Token(() => this.loadBalancerNames), + loadBalancerNames: new cdk.Token(() => this.loadBalancerNames.length > 0 ? this.loadBalancerNames : undefined), + targetGroupArns: new cdk.Token(() => this.targetGroupArns.length > 0 ? this.targetGroupArns : undefined), }; if (props.notificationsTopic) { @@ -241,10 +245,30 @@ export class AutoScalingGroup extends cdk.Construct implements elb.ILoadBalancer this.securityGroups.push(securityGroup); } + /** + * Attach to a classic load balancer + */ public attachToClassicLB(loadBalancer: elb.LoadBalancer): void { this.loadBalancerNames.push(loadBalancer.loadBalancerName); } + /** + * Attach to ELBv2 Application Target Group + */ + public attachToApplicationTargetGroup(targetGroup: elbv2.ApplicationTargetGroup): elbv2.LoadBalancerTargetProps { + this.targetGroupArns.push(targetGroup.targetGroupArn); + targetGroup.registerConnectable(this); + return { targetType: elbv2.TargetType.SelfRegistering }; + } + + /** + * Attach to ELBv2 Application Target Group + */ + public attachToNetworkTargetGroup(targetGroup: elbv2.NetworkTargetGroup): elbv2.LoadBalancerTargetProps { + this.targetGroupArns.push(targetGroup.targetGroupArn); + return { targetType: elbv2.TargetType.SelfRegistering }; + } + /** * Add command to the startup script of fleet instances. * The command must be in the scripting language supported by the fleet's OS (i.e. Linux/Windows). diff --git a/packages/@aws-cdk/aws-autoscaling/package.json b/packages/@aws-cdk/aws-autoscaling/package.json index fcc1738f9b7fe..de7b30cf73538 100644 --- a/packages/@aws-cdk/aws-autoscaling/package.json +++ b/packages/@aws-cdk/aws-autoscaling/package.json @@ -61,6 +61,7 @@ "dependencies": { "@aws-cdk/aws-ec2": "^0.9.2", "@aws-cdk/aws-elasticloadbalancing": "^0.9.2", + "@aws-cdk/aws-elasticloadbalancingv2": "^0.9.2", "@aws-cdk/aws-iam": "^0.9.2", "@aws-cdk/aws-sns": "^0.9.2", "@aws-cdk/cdk": "^0.9.2" diff --git a/packages/@aws-cdk/aws-autoscaling/test/integ.asg-w-loadbalancer.expected.json b/packages/@aws-cdk/aws-autoscaling/test/integ.asg-w-classic-loadbalancer.expected.json similarity index 98% rename from packages/@aws-cdk/aws-autoscaling/test/integ.asg-w-loadbalancer.expected.json rename to packages/@aws-cdk/aws-autoscaling/test/integ.asg-w-classic-loadbalancer.expected.json index 848931edf69db..c92cd2c79f071 100644 --- a/packages/@aws-cdk/aws-autoscaling/test/integ.asg-w-loadbalancer.expected.json +++ b/packages/@aws-cdk/aws-autoscaling/test/integ.asg-w-classic-loadbalancer.expected.json @@ -458,7 +458,7 @@ } } }, - "FleetInstanceSecurityGroupPort80LBtofleetDC12B17A": { + "FleetInstanceSecurityGroupfromawscdkec2integLBSecurityGroupDEF4F99A8025E910CB": { "Type": "AWS::EC2::SecurityGroupIngress", "Properties": { "IpProtocol": "tcp", @@ -581,7 +581,7 @@ } } }, - "LBSecurityGroupPort80LBtofleet0986F2E8": { + "LBSecurityGrouptoawscdkec2integFleetInstanceSecurityGroupB03BE84D80B371C596": { "Type": "AWS::EC2::SecurityGroupEgress", "Properties": { "GroupId": { diff --git a/packages/@aws-cdk/aws-autoscaling/test/integ.asg-w-loadbalancer.ts b/packages/@aws-cdk/aws-autoscaling/test/integ.asg-w-classic-loadbalancer.ts similarity index 100% rename from packages/@aws-cdk/aws-autoscaling/test/integ.asg-w-loadbalancer.ts rename to packages/@aws-cdk/aws-autoscaling/test/integ.asg-w-classic-loadbalancer.ts diff --git a/packages/@aws-cdk/aws-autoscaling/test/integ.asg-w-elbv2.expected.json b/packages/@aws-cdk/aws-autoscaling/test/integ.asg-w-elbv2.expected.json new file mode 100644 index 0000000000000..7e8084da884f9 --- /dev/null +++ b/packages/@aws-cdk/aws-autoscaling/test/integ.asg-w-elbv2.expected.json @@ -0,0 +1,524 @@ +{ + "Resources": { + "VPCB9E5F0B4": { + "Type": "AWS::EC2::VPC", + "Properties": { + "CidrBlock": "10.0.0.0/16", + "EnableDnsHostnames": true, + "EnableDnsSupport": true, + "InstanceTenancy": "default", + "Tags": [ + { + "Key": "Name", + "Value": "aws-cdk-ec2-integ/VPC" + } + ] + } + }, + "VPCPublicSubnet1SubnetB4246D30": { + "Type": "AWS::EC2::Subnet", + "Properties": { + "CidrBlock": "10.0.0.0/18", + "VpcId": { + "Ref": "VPCB9E5F0B4" + }, + "AvailabilityZone": "test-region-1a", + "MapPublicIpOnLaunch": true, + "Tags": [ + { + "Key": "Name", + "Value": "aws-cdk-ec2-integ/VPC/PublicSubnet1" + } + ] + } + }, + "VPCPublicSubnet1RouteTableFEE4B781": { + "Type": "AWS::EC2::RouteTable", + "Properties": { + "VpcId": { + "Ref": "VPCB9E5F0B4" + }, + "Tags": [ + { + "Key": "Name", + "Value": "aws-cdk-ec2-integ/VPC/PublicSubnet1" + } + ] + } + }, + "VPCPublicSubnet1RouteTableAssociatioin249B4093": { + "Type": "AWS::EC2::SubnetRouteTableAssociation", + "Properties": { + "RouteTableId": { + "Ref": "VPCPublicSubnet1RouteTableFEE4B781" + }, + "SubnetId": { + "Ref": "VPCPublicSubnet1SubnetB4246D30" + } + } + }, + "VPCPublicSubnet1EIP6AD938E8": { + "Type": "AWS::EC2::EIP", + "Properties": { + "Domain": "vpc" + } + }, + "VPCPublicSubnet1NATGatewayE0556630": { + "Type": "AWS::EC2::NatGateway", + "Properties": { + "AllocationId": { + "Fn::GetAtt": [ + "VPCPublicSubnet1EIP6AD938E8", + "AllocationId" + ] + }, + "SubnetId": { + "Ref": "VPCPublicSubnet1SubnetB4246D30" + }, + "Tags": [ + { + "Key": "Name", + "Value": "aws-cdk-ec2-integ/VPC/PublicSubnet1" + } + ] + } + }, + "VPCPublicSubnet1DefaultRoute91CEF279": { + "Type": "AWS::EC2::Route", + "Properties": { + "RouteTableId": { + "Ref": "VPCPublicSubnet1RouteTableFEE4B781" + }, + "DestinationCidrBlock": "0.0.0.0/0", + "GatewayId": { + "Ref": "VPCIGWB7E252D3" + } + } + }, + "VPCPublicSubnet2Subnet74179F39": { + "Type": "AWS::EC2::Subnet", + "Properties": { + "CidrBlock": "10.0.64.0/18", + "VpcId": { + "Ref": "VPCB9E5F0B4" + }, + "AvailabilityZone": "test-region-1b", + "MapPublicIpOnLaunch": true, + "Tags": [ + { + "Key": "Name", + "Value": "aws-cdk-ec2-integ/VPC/PublicSubnet2" + } + ] + } + }, + "VPCPublicSubnet2RouteTable6F1A15F1": { + "Type": "AWS::EC2::RouteTable", + "Properties": { + "VpcId": { + "Ref": "VPCB9E5F0B4" + }, + "Tags": [ + { + "Key": "Name", + "Value": "aws-cdk-ec2-integ/VPC/PublicSubnet2" + } + ] + } + }, + "VPCPublicSubnet2RouteTableAssociatioin766225D7": { + "Type": "AWS::EC2::SubnetRouteTableAssociation", + "Properties": { + "RouteTableId": { + "Ref": "VPCPublicSubnet2RouteTable6F1A15F1" + }, + "SubnetId": { + "Ref": "VPCPublicSubnet2Subnet74179F39" + } + } + }, + "VPCPublicSubnet2EIP4947BC00": { + "Type": "AWS::EC2::EIP", + "Properties": { + "Domain": "vpc" + } + }, + "VPCPublicSubnet2NATGateway3C070193": { + "Type": "AWS::EC2::NatGateway", + "Properties": { + "AllocationId": { + "Fn::GetAtt": [ + "VPCPublicSubnet2EIP4947BC00", + "AllocationId" + ] + }, + "SubnetId": { + "Ref": "VPCPublicSubnet2Subnet74179F39" + }, + "Tags": [ + { + "Key": "Name", + "Value": "aws-cdk-ec2-integ/VPC/PublicSubnet2" + } + ] + } + }, + "VPCPublicSubnet2DefaultRouteB7481BBA": { + "Type": "AWS::EC2::Route", + "Properties": { + "RouteTableId": { + "Ref": "VPCPublicSubnet2RouteTable6F1A15F1" + }, + "DestinationCidrBlock": "0.0.0.0/0", + "GatewayId": { + "Ref": "VPCIGWB7E252D3" + } + } + }, + "VPCPrivateSubnet1Subnet8BCA10E0": { + "Type": "AWS::EC2::Subnet", + "Properties": { + "CidrBlock": "10.0.128.0/18", + "VpcId": { + "Ref": "VPCB9E5F0B4" + }, + "AvailabilityZone": "test-region-1a", + "MapPublicIpOnLaunch": false, + "Tags": [ + { + "Key": "Name", + "Value": "aws-cdk-ec2-integ/VPC/PrivateSubnet1" + } + ] + } + }, + "VPCPrivateSubnet1RouteTableBE8A6027": { + "Type": "AWS::EC2::RouteTable", + "Properties": { + "VpcId": { + "Ref": "VPCB9E5F0B4" + }, + "Tags": [ + { + "Key": "Name", + "Value": "aws-cdk-ec2-integ/VPC/PrivateSubnet1" + } + ] + } + }, + "VPCPrivateSubnet1RouteTableAssociatioin77F7CA18": { + "Type": "AWS::EC2::SubnetRouteTableAssociation", + "Properties": { + "RouteTableId": { + "Ref": "VPCPrivateSubnet1RouteTableBE8A6027" + }, + "SubnetId": { + "Ref": "VPCPrivateSubnet1Subnet8BCA10E0" + } + } + }, + "VPCPrivateSubnet1DefaultRouteAE1D6490": { + "Type": "AWS::EC2::Route", + "Properties": { + "RouteTableId": { + "Ref": "VPCPrivateSubnet1RouteTableBE8A6027" + }, + "DestinationCidrBlock": "0.0.0.0/0", + "NatGatewayId": { + "Ref": "VPCPublicSubnet1NATGatewayE0556630" + } + } + }, + "VPCPrivateSubnet2SubnetCFCDAA7A": { + "Type": "AWS::EC2::Subnet", + "Properties": { + "CidrBlock": "10.0.192.0/18", + "VpcId": { + "Ref": "VPCB9E5F0B4" + }, + "AvailabilityZone": "test-region-1b", + "MapPublicIpOnLaunch": false, + "Tags": [ + { + "Key": "Name", + "Value": "aws-cdk-ec2-integ/VPC/PrivateSubnet2" + } + ] + } + }, + "VPCPrivateSubnet2RouteTable0A19E10E": { + "Type": "AWS::EC2::RouteTable", + "Properties": { + "VpcId": { + "Ref": "VPCB9E5F0B4" + }, + "Tags": [ + { + "Key": "Name", + "Value": "aws-cdk-ec2-integ/VPC/PrivateSubnet2" + } + ] + } + }, + "VPCPrivateSubnet2RouteTableAssociatioinC31995B4": { + "Type": "AWS::EC2::SubnetRouteTableAssociation", + "Properties": { + "RouteTableId": { + "Ref": "VPCPrivateSubnet2RouteTable0A19E10E" + }, + "SubnetId": { + "Ref": "VPCPrivateSubnet2SubnetCFCDAA7A" + } + } + }, + "VPCPrivateSubnet2DefaultRouteF4F5CFD2": { + "Type": "AWS::EC2::Route", + "Properties": { + "RouteTableId": { + "Ref": "VPCPrivateSubnet2RouteTable0A19E10E" + }, + "DestinationCidrBlock": "0.0.0.0/0", + "NatGatewayId": { + "Ref": "VPCPublicSubnet2NATGateway3C070193" + } + } + }, + "VPCIGWB7E252D3": { + "Type": "AWS::EC2::InternetGateway", + "Properties": { + "Tags": [ + { + "Key": "Name", + "Value": "aws-cdk-ec2-integ/VPC" + } + ] + } + }, + "VPCVPCGW99B986DC": { + "Type": "AWS::EC2::VPCGatewayAttachment", + "Properties": { + "VpcId": { + "Ref": "VPCB9E5F0B4" + }, + "InternetGatewayId": { + "Ref": "VPCIGWB7E252D3" + } + } + }, + "FleetInstanceSecurityGroupA8C3D7AD": { + "Type": "AWS::EC2::SecurityGroup", + "Properties": { + "GroupDescription": "aws-cdk-ec2-integ/Fleet/InstanceSecurityGroup", + "SecurityGroupEgress": [ + { + "CidrIp": "0.0.0.0/0", + "Description": "Outbound traffic allowed by default", + "FromPort": -1, + "IpProtocol": "-1", + "ToPort": -1 + } + ], + "SecurityGroupIngress": [], + "VpcId": { + "Ref": "VPCB9E5F0B4" + } + } + }, + "FleetInstanceSecurityGroupfromawscdkec2integLBSecurityGroupDEF4F99A8025E910CB": { + "Type": "AWS::EC2::SecurityGroupIngress", + "Properties": { + "IpProtocol": "tcp", + "Description": "Load balancer to target", + "FromPort": 80, + "GroupId": { + "Fn::GetAtt": [ + "FleetInstanceSecurityGroupA8C3D7AD", + "GroupId" + ] + }, + "SourceSecurityGroupId": { + "Fn::GetAtt": [ + "LBSecurityGroup8A41EA2B", + "GroupId" + ] + }, + "ToPort": 80 + } + }, + "FleetInstanceRoleA605DB82": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "ec2.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + } + } + }, + "FleetInstanceProfileC6192A66": { + "Type": "AWS::IAM::InstanceProfile", + "Properties": { + "Roles": [ + { + "Ref": "FleetInstanceRoleA605DB82" + } + ] + } + }, + "FleetLaunchConfig59F79D36": { + "Type": "AWS::AutoScaling::LaunchConfiguration", + "Properties": { + "ImageId": "ami-1234", + "InstanceType": "t2.micro", + "IamInstanceProfile": { + "Ref": "FleetInstanceProfileC6192A66" + }, + "SecurityGroups": [ + { + "Fn::GetAtt": [ + "FleetInstanceSecurityGroupA8C3D7AD", + "GroupId" + ] + } + ], + "UserData": { + "Fn::Base64": "#!/bin/bash\n" + } + }, + "DependsOn": [ + "FleetInstanceRoleA605DB82" + ] + }, + "FleetASG3971DFE5": { + "Type": "AWS::AutoScaling::AutoScalingGroup", + "Properties": { + "MaxSize": "1", + "MinSize": "1", + "DesiredCapacity": "1", + "LaunchConfigurationName": { + "Ref": "FleetLaunchConfig59F79D36" + }, + "TargetGroupARNs": [ + { + "Ref": "LBListenerTargetGroupF04FCF6D" + } + ], + "VPCZoneIdentifier": [ + { + "Ref": "VPCPrivateSubnet1Subnet8BCA10E0" + }, + { + "Ref": "VPCPrivateSubnet2SubnetCFCDAA7A" + } + ] + }, + "UpdatePolicy": { + "AutoScalingScheduledAction": { + "IgnoreUnmodifiedGroupSizeProperties": true + } + } + }, + "LB8A12904C": { + "Type": "AWS::ElasticLoadBalancingV2::LoadBalancer", + "Properties": { + "LoadBalancerAttributes": [], + "Scheme": "internet-facing", + "SecurityGroups": [ + { + "Fn::GetAtt": [ + "LBSecurityGroup8A41EA2B", + "GroupId" + ] + } + ], + "Subnets": [ + { + "Ref": "VPCPublicSubnet1SubnetB4246D30" + }, + { + "Ref": "VPCPublicSubnet2Subnet74179F39" + } + ], + "Type": "application" + } + }, + "LBSecurityGroup8A41EA2B": { + "Type": "AWS::EC2::SecurityGroup", + "Properties": { + "GroupDescription": "Automatically created Security Group for ELB awscdkec2integLB366431B5", + "SecurityGroupEgress": [], + "SecurityGroupIngress": [ + { + "CidrIp": "0.0.0.0/0", + "Description": "Open to the world", + "FromPort": 80, + "IpProtocol": "tcp", + "ToPort": 80 + } + ], + "VpcId": { + "Ref": "VPCB9E5F0B4" + } + } + }, + "LBSecurityGrouptoawscdkec2integFleetInstanceSecurityGroupB03BE84D80B371C596": { + "Type": "AWS::EC2::SecurityGroupEgress", + "Properties": { + "GroupId": { + "Fn::GetAtt": [ + "LBSecurityGroup8A41EA2B", + "GroupId" + ] + }, + "IpProtocol": "tcp", + "Description": "Load balancer to target", + "DestinationSecurityGroupId": { + "Fn::GetAtt": [ + "FleetInstanceSecurityGroupA8C3D7AD", + "GroupId" + ] + }, + "FromPort": 80, + "ToPort": 80 + } + }, + "LBListener49E825B4": { + "Type": "AWS::ElasticLoadBalancingV2::Listener", + "Properties": { + "DefaultActions": [ + { + "TargetGroupArn": { + "Ref": "LBListenerTargetGroupF04FCF6D" + }, + "Type": "forward" + } + ], + "LoadBalancerArn": { + "Ref": "LB8A12904C" + }, + "Port": 80, + "Protocol": "HTTP", + "Certificates": [] + } + }, + "LBListenerTargetGroupF04FCF6D": { + "Type": "AWS::ElasticLoadBalancingV2::TargetGroup", + "Properties": { + "Port": 80, + "Protocol": "HTTP", + "VpcId": { + "Ref": "VPCB9E5F0B4" + }, + "TargetGroupAttributes": [], + "Targets": [] + } + } + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-autoscaling/test/integ.asg-w-elbv2.ts b/packages/@aws-cdk/aws-autoscaling/test/integ.asg-w-elbv2.ts new file mode 100644 index 0000000000000..8f2d2d657b7f4 --- /dev/null +++ b/packages/@aws-cdk/aws-autoscaling/test/integ.asg-w-elbv2.ts @@ -0,0 +1,36 @@ +#!/usr/bin/env node +import ec2 = require('@aws-cdk/aws-ec2'); +import elbv2 = require('@aws-cdk/aws-elasticloadbalancingv2'); +import cdk = require('@aws-cdk/cdk'); +import autoscaling = require('../lib'); + +const app = new cdk.App(process.argv); +const stack = new cdk.Stack(app, 'aws-cdk-ec2-integ'); + +const vpc = new ec2.VpcNetwork(stack, 'VPC', { + maxAZs: 2 +}); + +const asg = new autoscaling.AutoScalingGroup(stack, 'Fleet', { + vpc, + instanceType: new ec2.InstanceTypePair(ec2.InstanceClass.Burstable2, ec2.InstanceSize.Micro), + machineImage: new ec2.AmazonLinuxImage(), +}); + +const lb = new elbv2.ApplicationLoadBalancer(stack, 'LB', { + vpc, + internetFacing: true +}); + +const listener = lb.addListener('Listener', { + port: 80, +}); + +listener.addTargets('Target', { + port: 80, + targets: [asg] +}); + +listener.connections.allowDefaultPortFromAnyIpv4('Open to the world'); + +process.stdout.write(app.run()); \ No newline at end of file diff --git a/packages/@aws-cdk/aws-autoscaling/test/test.auto-scaling-group.ts b/packages/@aws-cdk/aws-autoscaling/test/test.auto-scaling-group.ts index 1f5964128aedb..124ef0e85386d 100644 --- a/packages/@aws-cdk/aws-autoscaling/test/test.auto-scaling-group.ts +++ b/packages/@aws-cdk/aws-autoscaling/test/test.auto-scaling-group.ts @@ -99,7 +99,6 @@ export = { "LaunchConfigurationName": { "Ref": "MyFleetLaunchConfig5D7F9801" }, - "LoadBalancerNames": [], "MaxSize": "1", "MinSize": "1", "VPCZoneIdentifier": [ @@ -229,7 +228,6 @@ export = { LaunchConfigurationName: { Ref: "MyFleetLaunchConfig5D7F9801" }, - LoadBalancerNames: [], MaxSize: "1", MinSize: "1", VPCZoneIdentifier: [ diff --git a/packages/@aws-cdk/aws-ec2/lib/connections.ts b/packages/@aws-cdk/aws-ec2/lib/connections.ts index 38a518c5f73eb..b49d8d5c7a134 100644 --- a/packages/@aws-cdk/aws-ec2/lib/connections.ts +++ b/packages/@aws-cdk/aws-ec2/lib/connections.ts @@ -69,9 +69,15 @@ export class Connections { */ public readonly securityGroup?: SecurityGroupRef; - private readonly securityGroupRule: ISecurityGroupRule; + /** + * The rule that defines how to represent this peer in a security group + */ + public readonly securityGroupRule: ISecurityGroupRule; - private readonly defaultPortRange?: IPortRange; + /** + * The default port configured for this connection peer, if available + */ + public readonly defaultPortRange?: IPortRange; constructor(props: ConnectionsProps) { if (!props.securityGroupRule && !props.securityGroup) { @@ -86,7 +92,7 @@ export class Connections { /** * Allow connections to the peer on the given port */ - public allowTo(other: IConnectable, portRange: IPortRange, description: string) { + public allowTo(other: IConnectable, portRange: IPortRange, description?: string) { if (this.securityGroup) { this.securityGroup.addEgressRule(other.connections.securityGroupRule, portRange, description); } @@ -99,7 +105,7 @@ export class Connections { /** * Allow connections from the peer on the given port */ - public allowFrom(other: IConnectable, portRange: IPortRange, description: string) { + public allowFrom(other: IConnectable, portRange: IPortRange, description?: string) { if (this.securityGroup) { this.securityGroup.addIngressRule(other.connections.securityGroupRule, portRange, description); } @@ -111,7 +117,7 @@ export class Connections { /** * Allow hosts inside the security group to connect to each other on the given port */ - public allowInternally(portRange: IPortRange, description: string) { + public allowInternally(portRange: IPortRange, description?: string) { if (this.securityGroup) { this.securityGroup.addIngressRule(this.securityGroupRule, portRange, description); } @@ -120,14 +126,14 @@ export class Connections { /** * Allow to all IPv4 ranges */ - public allowToAnyIPv4(portRange: IPortRange, description: string) { + public allowToAnyIPv4(portRange: IPortRange, description?: string) { this.allowTo(new AnyIPv4(), portRange, description); } /** * Allow from any IPv4 ranges */ - public allowFromAnyIPv4(portRange: IPortRange, description: string) { + public allowFromAnyIPv4(portRange: IPortRange, description?: string) { this.allowFrom(new AnyIPv4(), portRange, description); } @@ -136,7 +142,7 @@ export class Connections { * * Even if the peer has a default port, we will always use our default port. */ - public allowDefaultPortFrom(other: IConnectable, description: string) { + public allowDefaultPortFrom(other: IConnectable, description?: string) { if (!this.defaultPortRange) { throw new Error('Cannot call allowDefaultPortFrom(): this resource has no default port'); } @@ -146,7 +152,7 @@ export class Connections { /** * Allow hosts inside the security group to connect to each other */ - public allowDefaultPortInternally(description: string) { + public allowDefaultPortInternally(description?: string) { if (!this.defaultPortRange) { throw new Error('Cannot call allowDefaultPortInternally(): this resource has no default port'); } @@ -156,7 +162,7 @@ export class Connections { /** * Allow default connections from all IPv4 ranges */ - public allowDefaultPortFromAnyIpv4(description: string) { + public allowDefaultPortFromAnyIpv4(description?: string) { if (!this.defaultPortRange) { throw new Error('Cannot call allowDefaultPortFromAnyIpv4(): this resource has no default port'); } @@ -166,11 +172,23 @@ export class Connections { /** * Allow connections to the security group on their default port */ - public allowToDefaultPort(other: IConnectable, description: string) { + public allowToDefaultPort(other: IConnectable, description?: string) { if (other.connections.defaultPortRange === undefined) { throw new Error('Cannot call alloToDefaultPort(): other resource has no default port'); } this.allowTo(other, other.connections.defaultPortRange, description); } + + /** + * Allow connections from the peer on our default port + * + * Even if the peer has a default port, we will always use our default port. + */ + public allowDefaultPortTo(other: IConnectable, description?: string) { + if (!this.defaultPortRange) { + throw new Error('Cannot call allowDefaultPortTo(): this resource has no default port'); + } + this.allowTo(other, this.defaultPortRange, description); + } } \ No newline at end of file diff --git a/packages/@aws-cdk/aws-ec2/lib/security-group-rule.ts b/packages/@aws-cdk/aws-ec2/lib/security-group-rule.ts index 9dbf8af85c5a1..ef0755ba60f74 100644 --- a/packages/@aws-cdk/aws-ec2/lib/security-group-rule.ts +++ b/packages/@aws-cdk/aws-ec2/lib/security-group-rule.ts @@ -9,6 +9,11 @@ export interface ISecurityGroupRule { */ readonly canInlineRule: boolean; + /** + * A unique identifier for this connection peer + */ + readonly uniqueId: string; + /** * Produce the ingress rule JSON for the given connection */ @@ -26,8 +31,10 @@ export interface ISecurityGroupRule { export class CidrIPv4 implements ISecurityGroupRule, IConnectable { public readonly canInlineRule = true; public readonly connections: Connections = new Connections({ securityGroupRule: this }); + public readonly uniqueId: string; constructor(private readonly cidrIp: string) { + this.uniqueId = cidrIp; } /** @@ -59,8 +66,10 @@ export class AnyIPv4 extends CidrIPv4 { export class CidrIPv6 implements ISecurityGroupRule, IConnectable { public readonly canInlineRule = true; public readonly connections: Connections = new Connections({ securityGroupRule: this }); + public readonly uniqueId: string; constructor(private readonly cidrIpv6: string) { + this.uniqueId = cidrIpv6; } /** @@ -98,8 +107,10 @@ export class AnyIPv6 extends CidrIPv6 { export class PrefixList implements ISecurityGroupRule, IConnectable { public readonly canInlineRule = true; public readonly connections: Connections = new Connections({ securityGroupRule: this }); + public readonly uniqueId: string; constructor(private readonly prefixListId: string) { + this.uniqueId = prefixListId; } public toIngressRuleJSON(): any { @@ -153,6 +164,10 @@ export class TcpPort implements IPortRange { toPort: this.port }; } + + public toString() { + return `${this.port}`; + } } /** @@ -171,6 +186,10 @@ export class TcpPortFromAttribute implements IPortRange { toPort: this.port }; } + + public toString() { + return '{IndirectPort}'; + } } /** @@ -189,6 +208,10 @@ export class TcpPortRange implements IPortRange { toPort: this.endPort }; } + + public toString() { + return `${this.startPort}-${this.endPort}`; + } } /** @@ -204,6 +227,10 @@ export class TcpAllPorts implements IPortRange { toPort: 65535 }; } + + public toString() { + return 'ALL PORTS'; + } } /** @@ -219,4 +246,8 @@ export class AllConnections implements IPortRange { toPort: -1, }; } + + public toString() { + return 'ALL TRAFFIC'; + } } \ No newline at end of file diff --git a/packages/@aws-cdk/aws-ec2/lib/security-group.ts b/packages/@aws-cdk/aws-ec2/lib/security-group.ts index 07d23af92f456..b4df710d1eda5 100644 --- a/packages/@aws-cdk/aws-ec2/lib/security-group.ts +++ b/packages/@aws-cdk/aws-ec2/lib/security-group.ts @@ -2,7 +2,6 @@ import { Construct, Output, Token } from '@aws-cdk/cdk'; import { Connections, IConnectable } from './connections'; import { cloudformation } from './ec2.generated'; import { IPortRange, ISecurityGroupRule } from './security-group-rule'; -import { slugify } from './util'; import { VpcNetworkRef } from './vpc-ref'; export interface SecurityGroupRefProps { @@ -32,22 +31,40 @@ export abstract class SecurityGroupRef extends Construct implements ISecurityGro */ public readonly defaultPortRange?: IPortRange; - public addIngressRule(peer: ISecurityGroupRule, connection: IPortRange, description: string) { - new cloudformation.SecurityGroupIngressResource(this, slugify(description), { - groupId: this.securityGroupId, - ...peer.toIngressRuleJSON(), - ...connection.toRuleJSON(), - description - }); + public addIngressRule(peer: ISecurityGroupRule, connection: IPortRange, description?: string) { + let id = `from ${peer.uniqueId}:${connection}`; + if (description === undefined) { + description = id; + } + id = id.replace('/', '_'); + + // Skip duplicates + if (this.tryFindChild(id) === undefined) { + new cloudformation.SecurityGroupIngressResource(this, id, { + groupId: this.securityGroupId, + ...peer.toIngressRuleJSON(), + ...connection.toRuleJSON(), + description + }); + } } - public addEgressRule(peer: ISecurityGroupRule, connection: IPortRange, description: string) { - new cloudformation.SecurityGroupEgressResource(this, slugify(description), { - groupId: this.securityGroupId, - ...peer.toEgressRuleJSON(), - ...connection.toRuleJSON(), - description - }); + public addEgressRule(peer: ISecurityGroupRule, connection: IPortRange, description?: string) { + let id = `to ${peer.uniqueId}:${connection}`; + if (description === undefined) { + description = id; + } + id = id.replace('/', '_'); + + // Skip duplicates + if (this.tryFindChild(id) === undefined) { + new cloudformation.SecurityGroupEgressResource(this, id, { + groupId: this.securityGroupId, + ...peer.toEgressRuleJSON(), + ...connection.toRuleJSON(), + description + }); + } } public toIngressRuleJSON(): any { @@ -139,12 +156,16 @@ export class SecurityGroup extends SecurityGroupRef { this.vpcId = this.securityGroup.securityGroupVpcId; } - public addIngressRule(peer: ISecurityGroupRule, connection: IPortRange, description: string) { + public addIngressRule(peer: ISecurityGroupRule, connection: IPortRange, description?: string) { if (!peer.canInlineRule || !connection.canInlineRule) { super.addIngressRule(peer, connection, description); return; } + if (description === undefined) { + description = `from ${peer.uniqueId}:${connection}`; + } + this.addDirectIngressRule({ ...peer.toIngressRuleJSON(), ...connection.toRuleJSON(), @@ -152,14 +173,18 @@ export class SecurityGroup extends SecurityGroupRef { }); } - public addEgressRule(peer: ISecurityGroupRule, connection: IPortRange, description: string) { + public addEgressRule(peer: ISecurityGroupRule, connection: IPortRange, description?: string) { if (!peer.canInlineRule || !connection.canInlineRule) { super.addEgressRule(peer, connection, description); return; } + if (description === undefined) { + description = `from ${peer.uniqueId}:${connection}`; + } + this.addDirectEgressRule({ - ...peer.toIngressRuleJSON(), + ...peer.toEgressRuleJSON(), ...connection.toRuleJSON(), description }); diff --git a/packages/@aws-cdk/aws-ec2/lib/vpc-ref.ts b/packages/@aws-cdk/aws-ec2/lib/vpc-ref.ts index b05f07dbb9f7a..5e688c51251cf 100644 --- a/packages/@aws-cdk/aws-ec2/lib/vpc-ref.ts +++ b/packages/@aws-cdk/aws-ec2/lib/vpc-ref.ts @@ -158,6 +158,17 @@ export abstract class VpcNetworkRef extends Construct implements IDependable { isolatedSubnetNames: iso.names, }; } + + /** + * Return whether the given subnet is one of this VPC's public subnets. + * + * The subnet must literally be one of the subnet object obtained from + * this VPC. A subnet that merely represents the same subnet will + * never return true. + */ + public isPublicSubnet(subnet: VpcSubnetRef) { + return this.publicSubnets.indexOf(subnet) > -1; + } } /** diff --git a/packages/@aws-cdk/aws-ec2/test/test.connections.ts b/packages/@aws-cdk/aws-ec2/test/test.connections.ts index aedb7330c1b6b..32ed4f1b8506b 100644 --- a/packages/@aws-cdk/aws-ec2/test/test.connections.ts +++ b/packages/@aws-cdk/aws-ec2/test/test.connections.ts @@ -1,7 +1,8 @@ import { expect, haveResource } from '@aws-cdk/assert'; import { Stack } from '@aws-cdk/cdk'; import { Test } from 'nodeunit'; -import { Connections, IConnectable, SecurityGroup, SecurityGroupRef, TcpAllPorts, TcpPort, VpcNetwork } from '../lib'; +import { AllConnections, AnyIPv4, AnyIPv6, Connections, IConnectable, PrefixList, SecurityGroup, SecurityGroupRef, + TcpAllPorts, TcpPort, TcpPortFromAttribute, TcpPortRange, VpcNetwork } from '../lib'; export = { 'peering between two security groups does not recursive infinitely'(test: Test) { @@ -54,6 +55,39 @@ export = { ToPort: 65535 })); + test.done(); + }, + + 'peer between all types of peers and port range types'(test: Test) { + // GIVEN + const stack = new Stack(undefined, 'TestStack', { env: { account: '12345678', region: 'dummy' }}); + const vpc = new VpcNetwork(stack, 'VPC'); + const sg = new SecurityGroup(stack, 'SG', { vpc }); + + const peers = [ + new SecurityGroup(stack, 'PeerGroup', { vpc }), + new AnyIPv4(), + new AnyIPv6(), + new PrefixList('pl-012345'), + ]; + + const ports = [ + new TcpPort(1234), + new TcpPortFromAttribute("port!"), + new TcpAllPorts(), + new TcpPortRange(80, 90), + new AllConnections() + ]; + + // WHEN + for (const peer of peers) { + for (const port of ports) { + sg.connections.allowTo(peer, port); + } + } + + // THEN -- no crash + test.done(); } }; diff --git a/packages/@aws-cdk/aws-elasticloadbalancingv2/README.md b/packages/@aws-cdk/aws-elasticloadbalancingv2/README.md index 236ac2628a36c..b7110d5ca5875 100644 --- a/packages/@aws-cdk/aws-elasticloadbalancingv2/README.md +++ b/packages/@aws-cdk/aws-elasticloadbalancingv2/README.md @@ -1,2 +1,198 @@ -## The CDK Construct Library for AWS Elastic Load Balancing (V2) -This module is part of the [AWS Cloud Development Kit](https://github.com/awslabs/aws-cdk) project. +## AWS Application and Network Load Balancing Construct Library + +The `@aws-cdk/aws-elasticloadbalancingv2` package provides constructs for +configuring application and network load balancers. + +For more information, see the AWS documentation for +[Application Load Balancers](https://docs.aws.amazon.com/elasticloadbalancing/latest/application/introduction.html) +and [Network Load Balancers](https://docs.aws.amazon.com/elasticloadbalancing/latest/network/introduction.html). + +### Defining an Application Load Balancer + +You define an application load balancer by creating an instance of +`ApplicationLoadBalancer`, adding a Listener to the load balancer +and adding Targets to the Listener: + +```ts +import ec2 = require('@aws-cdk/aws-ec2'); +import elbv2 = require('@aws-cdk/aws-elasticloadbalancingv2'); +import autoscaling = require('@aws-cdk/aws-autoscaling'); + +// ... + +const vpc = new ec2.VpcNetwork(...); + +// Create the load balancer in a VPC. 'internetFacing' is 'false' +// by default, which creates an internal load balancer. +const lb = new elbv2.ApplicationLoadBalancer(this, 'LB', { + vpc, + internetFacing: true +}); + +// Add a listener and open up the load balancer's security group +// to the world. 'open' is the default, set this to 'false' +// and use `listener.connections` if you want to be selective +// about who can access the listener. +const listener = lb.addListener('Listener', { + port: 80, + open: true, +}); + +// Create an AutoScaling group and add it as a load balancing +// target to the listener. +const asg = new autoscaling.AutoScalingGroup(...); +listener.addTargets('ApplicationFleet', { + port: 8080, + targets: [asg] +}); +``` + +The security groups of the load balancer and the target are automatically +updated to allow the network traffic. + +#### Conditions + +It's possible to route traffic to targets based on conditions in the incoming +HTTP request. Path- and host-based conditions are supported. For example, +the following will route requests to the indicated AutoScalingGroup +only if the requested host in the request is `example.com`: + +```ts +listener.addTargets('Example.Com Fleet', { + priority: 10, + hostHeader: 'example.com', + port: 8080, + targets: [asg] +}); +``` + +`priority` is a required field when you add targets with conditions. The lowest +number wins. + +Every listener must have at least one target without conditions. + +### Defining a Network Load Balancer + +Network Load Balancers are defined in a similar way to Application Load +Balancers: + +```ts +import ec2 = require('@aws-cdk/aws-ec2'); +import elbv2 = require('@aws-cdk/aws-elasticloadbalancingv2'); +import autoscaling = require('@aws-cdk/aws-autoscaling'); + +// Create the load balancer in a VPC. 'internetFacing' is 'false' +// by default, which creates an internal load balancer. +const lb = new elbv2.NetworkLoadBalancer(stack, 'LB', { + vpc, + internetFacing: true +}); + +// Add a listener on a particular port. +const listener = lb.addListener('Listener', { + port: 443, +}); + +// Add targets on a particular port. +listener.addTargets('AppFleet', { + port: 443, + targets: [asg] +}); +``` + +One thing to keep in mind is that network load balancers do not have security +groups, and no automatic security group configuration is done for you. You will +have to configure the security groups of the target yourself to allow traffic by +clients and/or load balancer instances, depending on your target types. See +[Target Groups for your Network Load +Balancers](https://docs.aws.amazon.com/elasticloadbalancing/latest/network/load-balancer-target-groups.html) +and [Register targets with your Target +Group](https://docs.aws.amazon.com/elasticloadbalancing/latest/network/target-group-register-targets.html) +for more information. + +### Targets and Target Groups + +Application and Network Load Balancers organize load balancing targets in Target +Groups. If you add your balancing targets (such as AutoScalingGroups, ECS +services or individual instances) to your listener directly, the appropriate +`TargetGroup` will be automatically created for you. + +If you need more control over the Target Groups created, create an instance of +`ApplicationTargetGroup` or `NetworkTargetGroup`, add the members you desire, +and add it to the listener by calling `addTargetGroups` instead of `addTargets`. + +`addTargets()` will always return the Target Group it just created for you: + +```ts +const group = listener.addTargets('AppFleet', { + port: 443, + targets: [asg1], +}); + +group.addTarget(asg2); +``` + +### Configuring Health Checks + +Health checks are configured upon creation of a target group: + +```ts +listener.addTargets('AppFleet', { + port: 8080, + targets: [asg], + healthCheck: { + path: '/ping', + intervalSecs: 60, + } +}); +``` + +The health check can also be configured after creation by calling +`configureHealthCheck()` on the created object. + +No attempts are made to configure security groups for the port you're +configuring a health check for, but if the health check is on the same port +you're routing traffic to, the security group already allows the traffic. +If not, you will have to configure the security groups appropriately: + +```ts +listener.addTargets('AppFleet', { + port: 8080, + targets: [asg], + healthCheck: { + port: 8088, + } +}); + +listener.connections.allowFrom(lb, new TcpPort(8088)); +``` + +### Protocol for Load Balancer Targets + +Constructs that want to be a load balancer target should implement +`IApplicationLoadBalancerTarget` and/or `INetworkLoadBalancerTarget`, and +provide an implementation for the function `attachToXxxTargetGroup()`, which can +call functions on the load balancer and should return metadata about the +load balancing target: + +```ts +public attachToApplicationTargetGroup(targetGroup: ApplicationTargetGroup): LoadBalancerTargetProps { + targetGroup.registerConnectable(...); + return { + targetType: TargetType.Instance | TargetType.Ip | TargetType.SelfRegistering, + targetJson: { id: ..., port: ... }, + }; +} +``` + +`targetType` should be one of `Instance` or `Ip` if the target can be directly +added to the target group, or `SelfRegistering` if the target will register new +instances with the load balancer at some later point. + +If the `targetType` is `Instance` or `Ip`, `targetJson` should contain the `id` +of the target (either instance ID or IP address depending on the type) and +optionally a `port` or `availabilityZone` override. + +Application load balancer targets can call `registerConnectable()` on the +target group to register themselves for addition to the load balancer's security +group rules. diff --git a/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/alb/application-listener-certificate.ts b/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/alb/application-listener-certificate.ts new file mode 100644 index 0000000000000..408462dc30d51 --- /dev/null +++ b/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/alb/application-listener-certificate.ts @@ -0,0 +1,41 @@ +import cdk = require('@aws-cdk/cdk'); +import { cloudformation } from '../elasticloadbalancingv2.generated'; +import { IApplicationListener } from './application-listener'; + +/** + * Properties for adding a set of certificates to a listener + */ +export interface ApplicationListenerCertificateProps { + /** + * The listener to attach the rule to + */ + listener: IApplicationListener; + + /** + * ARNs of certificates to attach + * + * Duplicates are not allowed. + */ + certificateArns: string[]; +} + +/** + * Add certificates to a listener + */ +export class ApplicationListenerCertificate extends cdk.Construct implements cdk.IDependable { + /** + * The elements of this resou rce to add ordering dependencies on + */ + public readonly dependencyElements: cdk.IDependable[] = []; + + constructor(parent: cdk.Construct, id: string, props: ApplicationListenerCertificateProps) { + super(parent, id); + + const resource = new cloudformation.ListenerCertificateResource(this, 'Resource', { + listenerArn: props.listener.listenerArn, + certificates: props.certificateArns.map(certificateArn => ({ certificateArn })), + }); + + this.dependencyElements.push(resource); + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/alb/application-listener-rule.ts b/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/alb/application-listener-rule.ts new file mode 100644 index 0000000000000..547c2696c43ba --- /dev/null +++ b/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/alb/application-listener-rule.ts @@ -0,0 +1,145 @@ +import cdk = require('@aws-cdk/cdk'); +import { cloudformation } from '../elasticloadbalancingv2.generated'; +import { IApplicationListener } from './application-listener'; +import { IApplicationTargetGroup } from './application-target-group'; + +/** + * Basic properties for defining a rule on a listener + */ +export interface BaseApplicationListenerRuleProps { + /** + * Priority of the rule + * + * The rule with the lowest priority will be used for every request. + * + * Priorities must be unique. + */ + priority: number; + + /** + * Target groups to forward requests to + */ + targetGroups?: IApplicationTargetGroup[]; + + /** + * Rule applies if the requested host matches the indicated host + * + * May contain up to three '*' wildcards. + * + * @see https://docs.aws.amazon.com/elasticloadbalancing/latest/application/load-balancer-listeners.html#host-conditions + * + * @default No host condition + */ + hostHeader?: string; + + /** + * Rule applies if the requested path matches the given path pattern + * + * May contain up to three '*' wildcards. + * + * @see https://docs.aws.amazon.com/elasticloadbalancing/latest/application/load-balancer-listeners.html#path-conditions + * + * @default No path condition + */ + pathPattern?: string; +} + +/** + * Properties for defining a listener rule + */ +export interface ApplicationListenerRuleProps extends BaseApplicationListenerRuleProps { + /** + * The listener to attach the rule to + */ + listener: IApplicationListener; +} + +/** + * Define a new listener rule + */ +export class ApplicationListenerRule extends cdk.Construct implements cdk.IDependable { + /** + * The ARN of this rule + */ + public readonly listenerRuleArn: string; + + /** + * The elements of this rule to add ordering dependencies on + */ + public readonly dependencyElements: cdk.IDependable[] = []; + + private readonly conditions: {[key: string]: string[] | undefined} = {}; + + private readonly actions: any[] = []; + private readonly listener: IApplicationListener; + + constructor(parent: cdk.Construct, id: string, props: ApplicationListenerRuleProps) { + super(parent, id); + + if (!props.hostHeader && !props.pathPattern) { + throw new Error(`At least one of 'hostHeader' or 'pathPattern' is required when defining a load balancing rule.`); + } + + this.listener = props.listener; + + const resource = new cloudformation.ListenerRuleResource(this, 'Resource', { + listenerArn: props.listener.listenerArn, + priority: props.priority, + conditions: new cdk.Token(() => this.renderConditions()), + actions: new cdk.Token(() => this.actions), + }); + + if (props.hostHeader) { + this.setCondition('host-header', [props.hostHeader]); + } + if (props.pathPattern) { + this.setCondition('path-pattern', [props.pathPattern]); + } + + (props.targetGroups || []).forEach(this.addTargetGroup.bind(this)); + + this.dependencyElements.push(resource); + this.listenerRuleArn = resource.ref; + } + + /** + * Add a non-standard condition to this rule + */ + public setCondition(field: string, values: string[] | undefined) { + this.conditions[field] = values; + } + + /** + * Validate the rule + */ + public validate() { + if (this.actions.length === 0) { + return ['Listener rule needs at least one action']; + } + return []; + } + + /** + * Add a TargetGroup to load balance to + */ + public addTargetGroup(targetGroup: IApplicationTargetGroup) { + this.actions.push({ + targetGroupArn: targetGroup.targetGroupArn, + type: 'forward' + }); + targetGroup.registerListener(this.listener); + } + + /** + * Render the conditions for this rule + */ + private renderConditions() { + const ret = []; + for (const [field, values] of Object.entries(this.conditions)) { + if (values !== undefined) { + ret.push({ field, values }); + } + } + return ret; + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/alb/application-listener.ts b/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/alb/application-listener.ts new file mode 100644 index 0000000000000..5e6e692cc936f --- /dev/null +++ b/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/alb/application-listener.ts @@ -0,0 +1,532 @@ +import ec2 = require('@aws-cdk/aws-ec2'); +import cdk = require('@aws-cdk/cdk'); +import { BaseListener } from '../shared/base-listener'; +import { HealthCheck } from '../shared/base-target-group'; +import { ApplicationProtocol, SslPolicy } from '../shared/enums'; +import { determineProtocolAndPort } from '../shared/util'; +import { ApplicationListenerCertificate } from './application-listener-certificate'; +import { ApplicationListenerRule } from './application-listener-rule'; +import { IApplicationLoadBalancer } from './application-load-balancer'; +import { ApplicationTargetGroup, IApplicationLoadBalancerTarget, IApplicationTargetGroup } from './application-target-group'; + +/** + * Basic properties for an ApplicationListener + */ +export interface BaseApplicationListenerProps { + /** + * The protocol to use + * + * @default Determined from port if known + */ + protocol?: ApplicationProtocol; + + /** + * The port on which the listener listens for requests. + * + * @default Determined from protocol if known + */ + port?: number; + + /** + * The certificates to use on this listener + */ + certificateArns?: string[]; + + /** + * The security policy that defines which ciphers and protocols are supported. + * + * @default the current predefined security policy. + */ + sslPolicy?: SslPolicy; + + /** + * Default target groups to load balance to + * + * @default None + */ + defaultTargetGroups?: IApplicationTargetGroup[]; + + /** + * Allow anyone to connect to this listener + * + * If this is specified, the listener will be opened up to anyone who can reach it. + * For internal load balancers this is anyone in the same VPC. For public load + * balancers, this is anyone on the internet. + * + * If you want to be more selective about who can access this load + * balancer, set this to `false` and use the listener's `connections` + * object to selectively grant access to the listener. + * + * @default true + */ + open?: boolean; +} + +/** + * Properties for defining a standalone ApplicationListener + */ +export interface ApplicationListenerProps extends BaseApplicationListenerProps { + /** + * The load balancer to attach this listener to + */ + loadBalancer: IApplicationLoadBalancer; +} + +/** + * Define an ApplicationListener + */ +export class ApplicationListener extends BaseListener implements IApplicationListener { + /** + * Import an existing listener + */ + public static import(parent: cdk.Construct, id: string, props: ApplicationListenerRefProps): IApplicationListener { + return new ImportedApplicationListener(parent, id, props); + } + + /** + * Manage connections to this ApplicationListener + */ + public readonly connections: ec2.Connections; + + /** + * ARNs of certificates added to this listener + */ + private readonly certificateArns: string[]; + + /** + * Load balancer this listener is associated with + */ + private readonly loadBalancer: IApplicationLoadBalancer; + + /** + * Listener protocol for this listener. + */ + private readonly protocol: ApplicationProtocol; + + /** + * The default port on which this listener is listening + */ + private readonly defaultPort: number; + + constructor(parent: cdk.Construct, id: string, props: ApplicationListenerProps) { + const [protocol, port] = determineProtocolAndPort(props.protocol, props.port); + + super(parent, id, { + loadBalancerArn: props.loadBalancer.loadBalancerArn, + certificates: new cdk.Token(() => this.certificateArns.map(certificateArn => ({ certificateArn }))), + protocol, + port, + sslPolicy: props.sslPolicy, + }); + + this.loadBalancer = props.loadBalancer; + this.protocol = protocol; + this.certificateArns = []; + this.certificateArns.push(...(props.certificateArns || [])); + this.defaultPort = port; + + // This listener edits the securitygroup of the load balancer, + // but adds its own default port. + this.connections = new ec2.Connections({ + securityGroup: props.loadBalancer.connections.securityGroup, + defaultPortRange: new ec2.TcpPort(port), + }); + + (props.defaultTargetGroups || []).forEach(this.addDefaultTargetGroup.bind(this)); + + if (props.open) { + this.connections.allowDefaultPortFrom(new ec2.AnyIPv4(), `Allow from anyone on port ${port}`); + } + } + + /** + * Add one or more certificates to this listener. + */ + public addCertificateArns(_id: string, arns: string[]): void { + this.certificateArns.push(...arns); + } + + /** + * Load balance incoming requests to the given target groups. + * + * It's possible to add conditions to the TargetGroups added in this way. + * At least one TargetGroup must be added without conditions. + */ + public addTargetGroups(id: string, props: AddApplicationTargetGroupsProps): void { + if ((props.hostHeader !== undefined || props.pathPattern !== undefined) !== (props.priority !== undefined)) { + throw new Error(`Setting 'pathPattern' or 'hostHeader' also requires 'priority', and vice versa`); + } + + if (props.priority !== undefined) { + // New rule + // + // TargetGroup.registerListener is called inside ApplicationListenerRule. + new ApplicationListenerRule(this, id + 'Rule', { + listener: this, + hostHeader: props.hostHeader, + pathPattern: props.pathPattern, + priority: props.priority, + targetGroups: props.targetGroups + }); + } else { + // New default target(s) + for (const targetGroup of props.targetGroups) { + this.addDefaultTargetGroup(targetGroup); + } + } + } + + /** + * Load balance incoming requests to the given load balancing targets. + * + * This method implicitly creates an ApplicationTargetGroup for the targets + * involved. + * + * It's possible to add conditions to the targets added in this way. At least + * one set of targets must be added without conditions. + * + * @returns The newly created target group + */ + public addTargets(id: string, props: AddApplicationTargetsProps): ApplicationTargetGroup { + if (!this.loadBalancer.vpc) { + // tslint:disable-next-line:max-line-length + throw new Error('Can only call addTargets() when using a constructed Load Balancer; construct a new TargetGroup and use addTargetGroup'); + } + + const group = new ApplicationTargetGroup(this, id + 'Group', { + deregistrationDelaySec: props.deregistrationDelaySec, + healthCheck: props.healthCheck, + port: props.port, + protocol: props.protocol, + slowStartSec: props.slowStartSec, + stickinessCookieDurationSec: props.stickinessCookieDurationSec, + targetGroupName: props.targetGroupName, + targets: props.targets, + vpc: this.loadBalancer.vpc, + }); + + this.addTargetGroups(id, { + hostHeader: props.hostHeader, + pathPattern: props.pathPattern, + priority: props.priority, + targetGroups: [group], + }); + + return group; + } + + /** + * Register that a connectable that has been added to this load balancer. + * + * Don't call this directly. It is called by ApplicationTargetGroup. + */ + public registerConnectable(connectable: ec2.IConnectable, portRange: ec2.IPortRange): void { + this.connections.allowTo(connectable, portRange, 'Load balancer to target'); + } + + /** + * Validate this listener. + */ + public validate(): string[] { + const errors = super.validate(); + if (this.protocol === ApplicationProtocol.Https && this.certificateArns.length === 0) { + errors.push('HTTPS Listener needs at least one certificate (call addCertificateArns)'); + } + return errors; + } + + /** + * Export this listener + */ + public export(): ApplicationListenerRefProps { + return { + listenerArn: new cdk.Output(this, 'ListenerArn', { value: this.listenerArn }).makeImportValue().toString(), + securityGroupId: this.connections.securityGroup!.export().securityGroupId, + defaultPort: new cdk.Output(this, 'Port', { value: this.defaultPort }).makeImportValue().toString(), + }; + } + + /** + * Add a default TargetGroup + */ + private addDefaultTargetGroup(targetGroup: IApplicationTargetGroup) { + this._addDefaultTargetGroup(targetGroup); + targetGroup.registerListener(this); + } +} + +/** + * Properties to reference an existing listener + */ +export interface IApplicationListener extends ec2.IConnectable { + /** + * ARN of the listener + */ + readonly listenerArn: string; + + /** + * Add one or more certificates to this listener. + */ + addCertificateArns(id: string, arns: string[]): void; + + /** + * Load balance incoming requests to the given target groups. + * + * It's possible to add conditions to the TargetGroups added in this way. + * At least one TargetGroup must be added without conditions. + */ + addTargetGroups(id: string, props: AddApplicationTargetGroupsProps): void; + + /** + * Load balance incoming requests to the given load balancing targets. + * + * This method implicitly creates an ApplicationTargetGroup for the targets + * involved. + * + * It's possible to add conditions to the targets added in this way. At least + * one set of targets must be added without conditions. + * + * @returns The newly created target group + */ + addTargets(id: string, props: AddApplicationTargetsProps): ApplicationTargetGroup; + + /** + * Register that a connectable that has been added to this load balancer. + * + * Don't call this directly. It is called by ApplicationTargetGroup. + */ + registerConnectable(connectable: ec2.IConnectable, portRange: ec2.IPortRange): void; +} + +/** + * Properties to reference an existing listener + */ +export interface ApplicationListenerRefProps { + /** + * ARN of the listener + */ + listenerArn: string; + + /** + * Security group ID of the load balancer this listener is associated with + */ + securityGroupId: string; + + /** + * The default port on which this listener is listening + */ + defaultPort?: string; +} + +class ImportedApplicationListener extends cdk.Construct implements IApplicationListener { + public readonly connections: ec2.Connections; + + /** + * ARN of the listener + */ + public readonly listenerArn: string; + + constructor(parent: cdk.Construct, id: string, props: ApplicationListenerRefProps) { + super(parent, id); + + this.listenerArn = props.listenerArn; + + const defaultPortRange = props.defaultPort !== undefined ? new ec2.TcpPortFromAttribute(props.defaultPort) : undefined; + + this.connections = new ec2.Connections({ + securityGroup: ec2.SecurityGroupRef.import(this, 'SecurityGroup', { securityGroupId: props.securityGroupId }), + defaultPortRange, + }); + } + + /** + * Add one or more certificates to this listener. + */ + public addCertificateArns(id: string, arns: string[]): void { + new ApplicationListenerCertificate(this, id, { + listener: this, + certificateArns: arns + }); + } + + /** + * Load balance incoming requests to the given target groups. + * + * It's possible to add conditions to the TargetGroups added in this way. + * At least one TargetGroup must be added without conditions. + */ + public addTargetGroups(id: string, props: AddApplicationTargetGroupsProps): void { + if ((props.hostHeader !== undefined || props.pathPattern !== undefined) !== (props.priority !== undefined)) { + throw new Error(`Setting 'pathPattern' or 'hostHeader' also requires 'priority', and vice versa`); + } + + if (props.priority !== undefined) { + // New rule + new ApplicationListenerRule(this, id, { + listener: this, + hostHeader: props.hostHeader, + pathPattern: props.pathPattern, + priority: props.priority, + targetGroups: props.targetGroups + }); + } else { + throw new Error('Cannot add default Target Groups to imported ApplicationListener'); + } + } + + /** + * Load balance incoming requests to the given load balancing targets. + * + * This method implicitly creates an ApplicationTargetGroup for the targets + * involved. + * + * It's possible to add conditions to the targets added in this way. At least + * one set of targets must be added without conditions. + * + * @returns The newly created target group + */ + public addTargets(_id: string, _props: AddApplicationTargetsProps): ApplicationTargetGroup { + // tslint:disable-next-line:max-line-length + throw new Error('Can only call addTargets() when using a constructed ApplicationListener; construct a new TargetGroup and use addTargetGroup.'); + } + + /** + * Register that a connectable that has been added to this load balancer. + * + * Don't call this directly. It is called by ApplicationTargetGroup. + */ + public registerConnectable(connectable: ec2.IConnectable, portRange: ec2.IPortRange): void { + this.connections.allowTo(connectable, portRange, 'Load balancer to target'); + } +} + +/** + * Properties for adding a conditional load balancing rule + */ +export interface AddRuleProps { + /** + * Priority of this target group + * + * The rule with the lowest priority will be used for every request. + * If priority is not given, these target groups will be added as + * defaults, and must not have conditions. + * + * Priorities must be unique. + * + * @default Target groups are used as defaults + */ + priority?: number; + + /** + * Rule applies if the requested host matches the indicated host + * + * May contain up to three '*' wildcards. + * + * Requires that priority is set. + * + * @see https://docs.aws.amazon.com/elasticloadbalancing/latest/application/load-balancer-listeners.html#host-conditions + * + * @default No host condition + */ + hostHeader?: string; + + /** + * Rule applies if the requested path matches the given path pattern + * + * May contain up to three '*' wildcards. + * + * Requires that priority is set. + * + * @see https://docs.aws.amazon.com/elasticloadbalancing/latest/application/load-balancer-listeners.html#path-conditions + * + * @default No path condition + */ + pathPattern?: string; +} + +/** + * Properties for adding a new target group to a listener + */ +export interface AddApplicationTargetGroupsProps extends AddRuleProps { + /** + * Target groups to forward requests to + */ + targetGroups: IApplicationTargetGroup[]; +} + +/** + * Properties for adding new targets to a listener + */ +export interface AddApplicationTargetsProps extends AddRuleProps { + /** + * The protocol to use + * + * @default Determined from port if known + */ + protocol?: ApplicationProtocol; + + /** + * The port on which the listener listens for requests. + * + * @default Determined from protocol if known + */ + 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 + */ + slowStartSec?: number; + + /** + * 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 86400 (1 day) + */ + stickinessCookieDurationSec?: number; + + /** + * 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. + */ + targets?: IApplicationLoadBalancerTarget[]; + + /** + * The name of the target group. + * + * This name must be unique per region per account, can have a maximum of + * 32 characters, must contain only alphanumeric characters or hyphens, and + * must not begin or end with a hyphen. + * + * @default Automatically generated + */ + targetGroupName?: string; + + /** + * The amount of time for Elastic Load Balancing to wait before deregistering a target. + * + * The range is 0–3600 seconds. + * + * @default 300 + */ + deregistrationDelaySec?: number; + + /** + * Health check configuration + * + * @default No health check + */ + healthCheck?: HealthCheck; +} diff --git a/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/alb/application-load-balancer.ts b/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/alb/application-load-balancer.ts new file mode 100644 index 0000000000000..43e64c5b37d4b --- /dev/null +++ b/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/alb/application-load-balancer.ts @@ -0,0 +1,212 @@ +import ec2 = require('@aws-cdk/aws-ec2'); +import s3 = require('@aws-cdk/aws-s3'); +import cdk = require('@aws-cdk/cdk'); +import { BaseLoadBalancer, BaseLoadBalancerProps } from '../shared/base-load-balancer'; +import { IpAddressType } from '../shared/enums'; +import { ApplicationListener, BaseApplicationListenerProps } from './application-listener'; + +/** + * Properties for defining an Application Load Balancer + */ +export interface ApplicationLoadBalancerProps extends BaseLoadBalancerProps { + /** + * Security group to associate with this load balancer + * + * @default A security group is created + */ + securityGroup?: ec2.SecurityGroupRef; + + /** + * The type of IP addresses to use + * + * Only applies to application load balancers. + * + * @default IpAddressType.Ipv4 + */ + ipAddressType?: IpAddressType; + + /** + * Indicates whether HTTP/2 is enabled. + * + * @default true + */ + http2Enabled?: boolean; + + /** + * The load balancer idle timeout, in seconds + * + * @default 60 + */ + idleTimeoutSecs?: number; +} + +/** + * Define an Application Load Balancer + */ +export class ApplicationLoadBalancer extends BaseLoadBalancer implements IApplicationLoadBalancer { + /** + * Import an existing Application Load Balancer + */ + public static import(parent: cdk.Construct, id: string, props: ApplicationLoadBalancerRefProps): IApplicationLoadBalancer { + return new ImportedApplicationLoadBalancer(parent, id, props); + } + + public readonly connections: ec2.Connections; + private readonly securityGroup: ec2.SecurityGroupRef; + + constructor(parent: cdk.Construct, id: string, props: ApplicationLoadBalancerProps) { + super(parent, id, props, { + type: "application", + securityGroups: new cdk.Token(() => [this.securityGroup.securityGroupId]), + ipAddressType: props.ipAddressType, + }); + + this.securityGroup = props.securityGroup || new ec2.SecurityGroup(this, 'SecurityGroup', { + vpc: props.vpc, + description: `Automatically created Security Group for ELB ${this.uniqueId}` + }); + this.connections = new ec2.Connections({ securityGroup: this.securityGroup }); + + if (props.http2Enabled === false) { this.setAttribute('routing.http2.enabled', 'false'); } + if (props.idleTimeoutSecs !== undefined) { this.setAttribute('idle_timeout.timeout_seconds', props.idleTimeoutSecs.toString()); } + } + + /** + * Enable access logging for this load balancer + */ + public logAccessLogs(bucket: s3.BucketRef, prefix?: string) { + this.setAttribute('access_logs.s3.enabled', 'true'); + this.setAttribute('access_logs.s3.bucket', bucket.bucketName.toString()); + this.setAttribute('access_logs.s3.prefix', prefix); + + const stack = cdk.Stack.find(this); + + const region = stack.requireRegion('Enable ELBv2 access logging'); + const account = ELBV2_ACCOUNTS[region]; + if (!account) { + throw new Error(`Cannot enable access logging; don't know ELBv2 account for region ${region}`); + } + + // FIXME: can't use grantPut() here because that only takes IAM objects, not arbitrary principals + bucket.addToResourcePolicy(new cdk.PolicyStatement() + .addPrincipal(new cdk.AccountPrincipal(account)) + .addAction('s3:PutObject') + .addResource(bucket.arnForObjects(prefix || '', '*'))); + } + + /** + * Add a new listener to this load balancer + */ + public addListener(id: string, props: BaseApplicationListenerProps): ApplicationListener { + return new ApplicationListener(this, id, { + loadBalancer: this, + ...props + }); + } + + /** + * Export this load balancer + */ + public export(): ApplicationLoadBalancerRefProps { + return { + loadBalancerArn: new cdk.Output(this, 'LoadBalancerArn', { value: this.loadBalancerArn }).makeImportValue().toString(), + securityGroupId: this.securityGroup.export().securityGroupId, + }; + } +} + +/** + * An application load balancer + */ +export interface IApplicationLoadBalancer extends ec2.IConnectable { + /** + * The ARN of this load balancer + */ + readonly loadBalancerArn: string; + + /** + * The VPC this load balancer has been created in (if available) + */ + readonly vpc?: ec2.VpcNetworkRef; + + /** + * Add a new listener to this load balancer + */ + addListener(id: string, props: BaseApplicationListenerProps): ApplicationListener; +} + +/** + * Properties to reference an existing load balancer + */ +export interface ApplicationLoadBalancerRefProps { + /** + * ARN of the load balancer + */ + loadBalancerArn: string; + + /** + * ID of the load balancer's security group + */ + securityGroupId: string; +} + +// https://docs.aws.amazon.com/elasticloadbalancing/latest/application/load-balancer-access-logs.html#access-logging-bucket-permissions +const ELBV2_ACCOUNTS: {[region: string]: string } = { + 'us-east-1': '127311923021', + 'us-east-2': '033677994240', + 'us-west-1': '027434742980', + 'us-west-2': '797873946194', + 'ca-central-1': '985666609251', + 'eu-central-1': '054676820928', + 'eu-west-1': '156460612806', + 'eu-west-2': '652711504416', + 'eu-west-3': '009996457667', + 'ap-northeast-1': '582318560864', + 'ap-northeast-2': '600734575887', + 'ap-northeast-3': '383597477331', + 'ap-southeast-1': '114774131450', + 'ap-southeast-2': '783225319266', + 'ap-south-1': '718504428378', + 'sa-east-1': '507241528517', + 'us-gov-west-1': '048591011584', + 'cn-north-1': '638102146993', + 'cn-northwest-1': '037604701340', +}; + +/** + * An ApplicationLoadBalancer that has been defined elsewhere + */ +class ImportedApplicationLoadBalancer extends cdk.Construct implements IApplicationLoadBalancer, ec2.IConnectable { + /** + * Manage connections for this load balancer + */ + public readonly connections: ec2.Connections; + + /** + * ARN of the load balancer + */ + public readonly loadBalancerArn: string; + + /** + * VPC of the load balancer + * + * Always undefined. + */ + public readonly vpc?: ec2.VpcNetworkRef; + + constructor(parent: cdk.Construct, id: string, props: ApplicationLoadBalancerRefProps) { + super(parent, id); + + this.loadBalancerArn = props.loadBalancerArn; + this.connections = new ec2.Connections({ + securityGroup: ec2.SecurityGroupRef.import(this, 'SecurityGroup', { securityGroupId: props.securityGroupId }) + }); + } + + public addListener(id: string, props: BaseApplicationListenerProps): ApplicationListener { + return new ApplicationListener(this, id, { + loadBalancer: this, + ...props + }); + } +} \ No newline at end of file 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 new file mode 100644 index 0000000000000..ad39a2322ed56 --- /dev/null +++ b/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/alb/application-target-group.ts @@ -0,0 +1,197 @@ +import ec2 = require('@aws-cdk/aws-ec2'); +import cdk = require('@aws-cdk/cdk'); +import { BaseTargetGroup, BaseTargetGroupProps, ITargetGroup, LoadBalancerTargetProps, TargetGroupRefProps } from '../shared/base-target-group'; +import { ApplicationProtocol } from '../shared/enums'; +import { BaseImportedTargetGroup } from '../shared/imported'; +import { determineProtocolAndPort } from '../shared/util'; +import { IApplicationListener } from './application-listener'; + +/** + * Properties for defining an Application Target Group + */ +export interface ApplicationTargetGroupProps extends BaseTargetGroupProps { + /** + * The protocol to use + * + * @default Determined from port if known + */ + protocol?: ApplicationProtocol; + + /** + * The port on which the listener listens for requests. + * + * @default Determined from protocol if known + */ + 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 + */ + slowStartSec?: number; + + /** + * 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 86400 (1 day) + */ + stickinessCookieDurationSec?: number; + + /** + * 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. + */ + targets?: IApplicationLoadBalancerTarget[]; +} + +/** + * Define an Application Target Group + */ +export class ApplicationTargetGroup extends BaseTargetGroup { + /** + * Import an existing target group + */ + public static import(parent: cdk.Construct, id: string, props: TargetGroupRefProps): IApplicationTargetGroup { + return new ImportedApplicationTargetGroup(parent, id, props); + } + + private readonly connectableMembers: ConnectableMember[]; + private readonly listeners: IApplicationListener[]; + + constructor(parent: cdk.Construct, id: string, props: ApplicationTargetGroupProps) { + const [protocol, port] = determineProtocolAndPort(props.protocol, props.port); + + super(parent, id, props, { + protocol, + port, + }); + + this.connectableMembers = []; + this.listeners = []; + + if (props.slowStartSec !== undefined) { + this.setAttribute('slow_start.duration_seconds', props.slowStartSec.toString()); + } + if (props.stickinessCookieDurationSec !== undefined) { + this.enableCookieStickiness(props.stickinessCookieDurationSec); + } + + this.addTarget(...(props.targets || [])); + } + + /** + * 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); + } + } + + /** + * Enable sticky routing via a cookie to members of this target group + */ + public enableCookieStickiness(durationSec: number) { + this.setAttribute('stickiness.enabled', 'true'); + this.setAttribute('stickiness.type', 'lb_cookie'); + this.setAttribute('stickiness.lb_cookie.duration_seconds', durationSec.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.IPortRange) { + if (portRange === undefined) { + if (cdk.unresolved(this.defaultPort)) { + portRange = new ec2.TcpPortFromAttribute(this.defaultPort); + } else { + portRange = new ec2.TcpPort(parseInt(this.defaultPort, 10)); + } + } + + // 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) { + // 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); + } +} + +/** + * 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.IPortRange; +} + +/** + * A Target Group for Application Load Balancers + */ +export interface IApplicationTargetGroup extends ITargetGroup { + /** + * 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): void; +} + +/** + * An imported application target group + */ +class ImportedApplicationTargetGroup extends BaseImportedTargetGroup implements IApplicationTargetGroup { + public registerListener(_listener: IApplicationListener) { + // Nothing to do, we know nothing of our members + } +} + +/** + * 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: ApplicationTargetGroup): LoadBalancerTargetProps; +} diff --git a/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/index.ts b/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/index.ts index 682c0a808371f..ca5dda9e65c2a 100644 --- a/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/index.ts +++ b/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/index.ts @@ -1,2 +1,18 @@ // AWS::ElasticLoadBalancingV2 CloudFormation Resources: export * from './elasticloadbalancingv2.generated'; + +export * from './alb/application-listener'; +export * from './alb/application-listener-certificate'; +export * from './alb/application-listener-rule'; +export * from './alb/application-load-balancer'; +export * from './alb/application-target-group'; + +export * from './nlb/network-listener'; +export * from './nlb/network-load-balancer'; +export * from './nlb/network-target-group'; + +export * from './shared/base-listener'; +export * from './shared/base-load-balancer'; +export * from './shared/base-target-group'; +export * from './shared/enums'; +export * from './shared/load-balancer-targets'; \ No newline at end of file diff --git a/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/nlb/network-listener.ts b/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/nlb/network-listener.ts new file mode 100644 index 0000000000000..e02a20ad8a943 --- /dev/null +++ b/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/nlb/network-listener.ts @@ -0,0 +1,202 @@ +import cdk = require('@aws-cdk/cdk'); +import { BaseListener } from '../shared/base-listener'; +import { HealthCheck } from '../shared/base-target-group'; +import { Protocol } from '../shared/enums'; +import { INetworkLoadBalancer } from './network-load-balancer'; +import { INetworkLoadBalancerTarget, INetworkTargetGroup, NetworkTargetGroup } from './network-target-group'; + +/** + * Basic properties for a Network Listener + */ +export interface BaseNetworkListenerProps { + /** + * The port on which the listener listens for requests. + */ + port: number; + + /** + * Default target groups to load balance to + * + * @default None + */ + defaultTargetGroups?: INetworkTargetGroup[]; +} + +/** + * Properties for a Network Listener attached to a Load Balancer + */ +export interface NetworkListenerProps extends BaseNetworkListenerProps { + /** + * The load balancer to attach this listener to + */ + loadBalancer: INetworkLoadBalancer; +} + +/** + * Define a Network Listener + */ +export class NetworkListener extends BaseListener implements INetworkListener { + /** + * Import an existing listener + */ + public static import(parent: cdk.Construct, id: string, props: NetworkListenerRefProps): INetworkListener { + return new ImportedNetworkListener(parent, id, props); + } + + /** + * The load balancer this listener is attached to + */ + private readonly loadBalancer: INetworkLoadBalancer; + + constructor(parent: cdk.Construct, id: string, props: NetworkListenerProps) { + super(parent, id, { + loadBalancerArn: props.loadBalancer.loadBalancerArn, + protocol: Protocol.Tcp, + port: props.port, + }); + + this.loadBalancer = props.loadBalancer; + + (props.defaultTargetGroups || []).forEach(this._addDefaultTargetGroup.bind(this)); + } + + /** + * Load balance incoming requests to the given target groups. + */ + public addTargetGroups(_id: string, ...targetGroups: INetworkTargetGroup[]): void { + // New default target(s) + for (const targetGroup of targetGroups) { + this._addDefaultTargetGroup(targetGroup); + } + } + + /** + * Load balance incoming requests to the given load balancing targets. + * + * This method implicitly creates an ApplicationTargetGroup for the targets + * involved. + * + * @returns The newly created target group + */ + public addTargets(id: string, props: AddNetworkTargetsProps): NetworkTargetGroup { + if (!this.loadBalancer.vpc) { + // tslint:disable-next-line:max-line-length + throw new Error('Can only call addTargets() when using a constructed Load Balancer; construct a new TargetGroup and use addTargetGroup'); + } + + const group = new NetworkTargetGroup(this, id + 'Group', { + deregistrationDelaySec: props.deregistrationDelaySec, + healthCheck: props.healthCheck, + port: props.port, + proxyProtocolV2: props.proxyProtocolV2, + targetGroupName: props.targetGroupName, + targets: props.targets, + vpc: this.loadBalancer.vpc, + }); + + this.addTargetGroups(id, group); + + return group; + } + + /** + * Export this listener + */ + public export(): NetworkListenerRefProps { + return { + listenerArn: new cdk.Output(this, 'ListenerArn', { value: this.listenerArn }).makeImportValue().toString() + }; + } + +} + +/** + * Properties to reference an existing listener + */ +export interface INetworkListener { + /** + * ARN of the listener + */ + readonly listenerArn: string; +} + +/** + * Properties to reference an existing listener + */ +export interface NetworkListenerRefProps { + /** + * ARN of the listener + */ + listenerArn: string; +} + +/** + * An imported Network Listener + */ +class ImportedNetworkListener extends cdk.Construct implements INetworkListener { + /** + * ARN of the listener + */ + public readonly listenerArn: string; + + constructor(parent: cdk.Construct, id: string, props: NetworkListenerRefProps) { + super(parent, id); + + this.listenerArn = props.listenerArn; + } +} + +/** + * Properties for adding new network targets to a listener + */ +export interface AddNetworkTargetsProps { + /** + * The port on which the listener listens for requests. + * + * @default Determined from protocol if known + */ + port: number; + + /** + * 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. + */ + targets?: INetworkLoadBalancerTarget[]; + + /** + * The name of the target group. + * + * This name must be unique per region per account, can have a maximum of + * 32 characters, must contain only alphanumeric characters or hyphens, and + * must not begin or end with a hyphen. + * + * @default Automatically generated + */ + targetGroupName?: string; + + /** + * The amount of time for Elastic Load Balancing to wait before deregistering a target. + * + * The range is 0–3600 seconds. + * + * @default 300 + */ + deregistrationDelaySec?: number; + + /** + * Indicates whether Proxy Protocol version 2 is enabled. + * + * @default false + */ + proxyProtocolV2?: boolean; + + /** + * Health check configuration + * + * @default No health check + */ + healthCheck?: HealthCheck; +} diff --git a/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/nlb/network-load-balancer.ts b/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/nlb/network-load-balancer.ts new file mode 100644 index 0000000000000..5611d5920f1b5 --- /dev/null +++ b/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/nlb/network-load-balancer.ts @@ -0,0 +1,121 @@ +import ec2 = require('@aws-cdk/aws-ec2'); +import cdk = require('@aws-cdk/cdk'); +import { BaseLoadBalancer, BaseLoadBalancerProps } from '../shared/base-load-balancer'; +import { BaseNetworkListenerProps, NetworkListener } from './network-listener'; + +/** + * Properties for a network load balancer + */ +export interface NetworkLoadBalancerProps extends BaseLoadBalancerProps { + /** + * Indicates whether cross-zone load balancing is enabled. + * + * @default false + */ + crossZoneEnabled?: boolean; +} + +/** + * Define a new network load balancer + */ +export class NetworkLoadBalancer extends BaseLoadBalancer implements INetworkLoadBalancer { + public static import(parent: cdk.Construct, id: string, props: NetworkLoadBalancerRefProps): INetworkLoadBalancer { + return new ImportedNetworkLoadBalancer(parent, id, props); + } + + constructor(parent: cdk.Construct, id: string, props: NetworkLoadBalancerProps) { + super(parent, id, props, { + type: "network", + }); + + if (props.crossZoneEnabled) { this.setAttribute('load_balancing.cross_zone.enabled', 'true'); } + } + + /** + * Add a listener to this load balancer + * + * @returns The newly created listener + */ + public addListener(id: string, props: BaseNetworkListenerProps): NetworkListener { + return new NetworkListener(this, id, { + loadBalancer: this, + ...props + }); + } + + /** + * Export this load balancer + */ + public export(): NetworkLoadBalancerRefProps { + return { + loadBalancerArn: new cdk.Output(this, 'LoadBalancerArn', { value: this.loadBalancerArn }).makeImportValue().toString() + }; + } +} + +/** + * A network load balancer + */ +export interface INetworkLoadBalancer { + /** + * The ARN of this load balancer + */ + readonly loadBalancerArn: string; + + /** + * The VPC this load balancer has been created in (if available) + */ + readonly vpc?: ec2.VpcNetworkRef; + + /** + * Add a listener to this load balancer + * + * @returns The newly created listener + */ + addListener(id: string, props: BaseNetworkListenerProps): NetworkListener; +} + +/** + * Properties to reference an existing load balancer + */ +export interface NetworkLoadBalancerRefProps { + /** + * ARN of the load balancer + */ + loadBalancerArn: string; +} + +/** + * An imported network load balancer + */ +class ImportedNetworkLoadBalancer extends cdk.Construct implements INetworkLoadBalancer { + /** + * ARN of the load balancer + */ + public readonly loadBalancerArn: string; + + /** + * VPC of the load balancer + * + * Always undefined. + */ + public readonly vpc?: ec2.VpcNetworkRef; + + constructor(parent: cdk.Construct, id: string, props: NetworkLoadBalancerRefProps) { + super(parent, id); + + this.loadBalancerArn = props.loadBalancerArn; + } + + /** + * Add a listener to this load balancer + * + * @returns The newly created listener + */ + public addListener(id: string, props: BaseNetworkListenerProps): NetworkListener { + return new NetworkListener(this, id, { + loadBalancer: this, + ...props + }); + } +} \ No newline at end of file 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 new file mode 100644 index 0000000000000..d03acd72412bb --- /dev/null +++ b/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/nlb/network-target-group.ts @@ -0,0 +1,91 @@ +import cdk = require('@aws-cdk/cdk'); +import { BaseTargetGroup, BaseTargetGroupProps, ITargetGroup, LoadBalancerTargetProps, TargetGroupRefProps } from '../shared/base-target-group'; +import { Protocol } from '../shared/enums'; +import { BaseImportedTargetGroup } from '../shared/imported'; + +/** + * Properties for a new Network Target Group + */ +export interface NetworkTargetGroupProps extends BaseTargetGroupProps { + /** + * The port on which the listener listens for requests. + */ + port: number; + + /** + * Indicates whether Proxy Protocol version 2 is enabled. + * + * @default false + */ + proxyProtocolV2?: boolean; + + /** + * 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. + */ + targets?: INetworkLoadBalancerTarget[]; +} + +/** + * Define a Network Target Group + */ +export class NetworkTargetGroup extends BaseTargetGroup { + /** + * Import an existing listener + */ + public static import(parent: cdk.Construct, id: string, props: TargetGroupRefProps): INetworkTargetGroup { + return new ImportedNetworkTargetGroup(parent, id, props); + } + + constructor(parent: cdk.Construct, id: string, props: NetworkTargetGroupProps) { + super(parent, id, props, { + protocol: Protocol.Tcp, + port: props.port, + }); + + if (props.proxyProtocolV2) { + this.setAttribute('proxy_protocol_v2.enabled', 'true'); + } + + this.addTarget(...(props.targets || [])); + } + + /** + * Add a load balancing target to this target group + */ + public addTarget(...targets: INetworkLoadBalancerTarget[]) { + for (const target of targets) { + const result = target.attachToNetworkTargetGroup(this); + this.addLoadBalancerTarget(result); + } + } +} + +/** + * A network target group + */ +// tslint:disable-next-line:no-empty-interface +export interface INetworkTargetGroup extends ITargetGroup { +} + +/** + * An imported network target group + */ +class ImportedNetworkTargetGroup extends BaseImportedTargetGroup implements INetworkTargetGroup { +} + +/** + * Interface for constructs that can be targets of an network load balancer + */ +export interface INetworkLoadBalancerTarget { + /** + * 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. + */ + attachToNetworkTargetGroup(targetGroup: NetworkTargetGroup): LoadBalancerTargetProps; +} diff --git a/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/shared/base-listener.ts b/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/shared/base-listener.ts new file mode 100644 index 0000000000000..f2a486a05a607 --- /dev/null +++ b/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/shared/base-listener.ts @@ -0,0 +1,42 @@ +import cdk = require('@aws-cdk/cdk'); +import { cloudformation } from '../elasticloadbalancingv2.generated'; +import { ITargetGroup } from './base-target-group'; + +/** + * Base class for listeners + */ +export abstract class BaseListener extends cdk.Construct { + public readonly listenerArn: string; + private readonly defaultActions: any[] = []; + + constructor(parent: cdk.Construct, id: string, additionalProps: any) { + super(parent, id); + + const resource = new cloudformation.ListenerResource(this, 'Resource', { + ...additionalProps, + defaultActions: new cdk.Token(() => this.defaultActions), + }); + + this.listenerArn = resource.ref; + } + + /** + * Validate this listener + */ + public validate(): string[] { + if (this.defaultActions.length === 0) { + return ['Listener needs at least one default target group (call addTargetGroups)']; + } + return []; + } + + /** + * Add a TargetGroup to the list of default actions of this listener + */ + protected _addDefaultTargetGroup(targetGroup: ITargetGroup) { + this.defaultActions.push({ + targetGroupArn: targetGroup.targetGroupArn, + type: 'forward' + }); + } +} diff --git a/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/shared/base-load-balancer.ts b/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/shared/base-load-balancer.ts new file mode 100644 index 0000000000000..195a615204f0c --- /dev/null +++ b/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/shared/base-load-balancer.ts @@ -0,0 +1,138 @@ +import ec2 = require('@aws-cdk/aws-ec2'); +import cdk = require('@aws-cdk/cdk'); +import { cloudformation } from '../elasticloadbalancingv2.generated'; +import { Attributes, ifUndefined, renderAttributes } from './util'; + +/** + * Shared properties of both Application and Network Load Balancers + */ +export interface BaseLoadBalancerProps { + /** + * Name of the load balancer + * + * @default Automatically generated name + */ + loadBalancerName?: string; + + /** + * The VPC network to place the load balancer in + */ + vpc: ec2.VpcNetworkRef; + + /** + * Whether the load balancer has an internet-routable address + * + * @default false + */ + internetFacing?: boolean; + + /** + * Where in the VPC to place the load balancer + * + * @default Public subnets if internetFacing, otherwise private subnets + */ + vpcPlacement?: ec2.VpcPlacementStrategy; + + /** + * Indicates whether deletion protection is enabled. + * + * @default false + */ + deletionProtection?: boolean; +} + +/** + * Base class for both Application and Network Load Balancers + */ +export abstract class BaseLoadBalancer extends cdk.Construct { + /** + * The canonical hosted zone ID of this load balancer + * + * @example Z2P70J7EXAMPLE + */ + public readonly canonicalHostedZoneId: string; + + /** + * The DNS name of this load balancer + * + * @example my-load-balancer-424835706.us-west-2.elb.amazonaws.com + */ + public readonly dnsName: string; + + /** + * The full name of this load balancer + * + * @example app/my-load-balancer/50dc6c495c0c9188 + */ + public readonly fullName: string; + + /** + * The name of this load balancer + * + * @example my-load-balancer + */ + public readonly loadBalancerName: string; + + /** + * The ARN of this load balancer + * + * @example arn:aws:elasticloadbalancing:us-west-2:123456789012:loadbalancer/app/my-internal-load-balancer/50dc6c495c0c9188 + */ + public readonly loadBalancerArn: string; + + /** + * The VPC this load balancer has been created in, if available + * + * If the Load Balancer was imported, the VPC is not available. + */ + public readonly vpc?: ec2.VpcNetworkRef; + + /** + * Attributes set on this load balancer + */ + private readonly attributes: Attributes = {}; + + constructor(parent: cdk.Construct, id: string, baseProps: BaseLoadBalancerProps, additionalProps: any) { + super(parent, id); + + const internetFacing = ifUndefined(baseProps.internetFacing, false); + + const subnets = baseProps.vpc.subnets(ifUndefined(baseProps.vpcPlacement, + { subnetsToUse: internetFacing ? ec2.SubnetType.Public : ec2.SubnetType.Private })); + + this.vpc = baseProps.vpc; + + const resource = new cloudformation.LoadBalancerResource(this, 'Resource', { + loadBalancerName: baseProps.loadBalancerName, + subnets: subnets.map(s => s.subnetId), + scheme: internetFacing ? 'internet-facing' : 'internal', + loadBalancerAttributes: new cdk.Token(() => renderAttributes(this.attributes)), + ...additionalProps + }); + + if (baseProps.deletionProtection) { this.setAttribute('deletion_protection.enabled', 'true'); } + + this.canonicalHostedZoneId = resource.loadBalancerCanonicalHostedZoneId; + this.dnsName = resource.loadBalancerDnsName; + this.fullName = resource.loadBalancerFullName; + this.loadBalancerName = resource.loadBalancerName; + this.loadBalancerArn = resource.ref; + } + + /** + * Set a non-standard attribute on the load balancer + * + * @see https://docs.aws.amazon.com/elasticloadbalancing/latest/application/application-load-balancers.html#load-balancer-attributes + */ + public setAttribute(key: string, value: string | undefined) { + this.attributes[key] = value; + } + + /** + * Remove an attribute from the load balancer + */ + public removeAttribute(key: string) { + this.setAttribute(key, undefined); + } + +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/shared/base-target-group.ts b/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/shared/base-target-group.ts new file mode 100644 index 0000000000000..3cf944ef54f0b --- /dev/null +++ b/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/shared/base-target-group.ts @@ -0,0 +1,295 @@ +import ec2 = require('@aws-cdk/aws-ec2'); +import cdk = require('@aws-cdk/cdk'); +import { cloudformation } from '../elasticloadbalancingv2.generated'; +import { Protocol, TargetType } from './enums'; +import { Attributes, renderAttributes } from './util'; + +/** + * Basic properties of both Application and Network Target Groups + */ +export interface BaseTargetGroupProps { + /** + * The name of the target group. + * + * This name must be unique per region per account, can have a maximum of + * 32 characters, must contain only alphanumeric characters or hyphens, and + * must not begin or end with a hyphen. + * + * @default Automatically generated + */ + targetGroupName?: string; + + /** + * The virtual private cloud (VPC). + */ + vpc: ec2.VpcNetworkRef; + + /** + * The amount of time for Elastic Load Balancing to wait before deregistering a target. + * + * The range is 0–3600 seconds. + * + * @default 300 + */ + deregistrationDelaySec?: number; + + /** + * Health check configuration + * + * @default No health check + */ + healthCheck?: HealthCheck; +} + +/** + * Properties for configuring a health check + */ +export interface HealthCheck { + /** + * The approximate number of seconds between health checks for an individual target. + * + * @default 30 + */ + intervalSecs?: number; + + /** + * The ping path destination where Elastic Load Balancing sends health check requests. + * + * @default / + */ + path?: string; + + /** + * The port that the load balancer uses when performing health checks on the targets. + * + * @default 'traffic-port' + */ + port?: string; + + /** + * The protocol the load balancer uses when performing health checks on targets. + * + * The TCP protocol is supported only if the protocol of the target group + * is TCP. + * + * @default HTTP for ALBs, TCP for NLBs + */ + protocol?: Protocol; + + /** + * The amount of time, in seconds, during which no response from a target means a failed health check. + * + * For Application Load Balancers, the range is 2–60 seconds and the + * default is 5 seconds. For Network Load Balancers, this is 10 seconds for + * TCP and HTTPS health checks and 6 seconds for HTTP health checks. + * + * @default 5 for ALBs, 10 or 6 for NLBs + */ + timeoutSeconds?: number; + + /** + * The number of consecutive health checks successes required before considering an unhealthy target healthy. + * + * For Application Load Balancers, the default is 5. For Network Load Balancers, the default is 3. + * + * @default 5 for ALBs, 3 for NLBs + */ + healthyThresholdCount?: number; + + /** + * The number of consecutive health check failures required before considering a target unhealthy. + * + * For Application Load Balancers, the default is 2. For Network Load + * Balancers, this value must be the same as the healthy threshold count. + * + * @default 2 + */ + unhealthyThresholdCount?: number; + + /** + * HTTP code to use when checking for a successful response from a target. + * + * For Application Load Balancers, you can specify values between 200 and + * 499, and the default value is 200. You can specify multiple values (for + * example, "200,202") or a range of values (for example, "200-299"). + */ + healthyHttpCodes?: string; +} + +/** + * Define the target of a load balancer + */ +export abstract class BaseTargetGroup extends cdk.Construct implements ITargetGroup { + /** + * The ARN of the target group + */ + public readonly targetGroupArn: string; + + /** + * The full name of the target group + */ + public readonly targetGroupFullName: string; + + /** + * The name of the target group + */ + public readonly targetGroupName: string; + + /** + * Health check for the members of this target group + */ + public healthCheck: HealthCheck; + + /** + * Default port configured for members of this target group + */ + protected readonly defaultPort: string; + + /** + * Attributes of this target group + */ + private readonly attributes: Attributes = {}; + + /** + * The JSON objects returned by the directly registered members of this target group + */ + private readonly targetsJson = new Array(); + + /** + * The types of the directly registered members of this target group + */ + private targetType?: TargetType; + + /** + * The target group resource + */ + private readonly resource: cloudformation.TargetGroupResource; + + constructor(parent: cdk.Construct, id: string, baseProps: BaseTargetGroupProps, additionalProps: any) { + super(parent, id); + + if (baseProps.deregistrationDelaySec !== undefined) { + this.setAttribute('deregistration_delay.timeout_seconds', baseProps.deregistrationDelaySec.toString()); + } + + this.healthCheck = baseProps.healthCheck || {}; + + this.resource = new cloudformation.TargetGroupResource(this, 'Resource', { + targetGroupName: baseProps.targetGroupName, + targetGroupAttributes: new cdk.Token(() => renderAttributes(this.attributes)), + targetType: new cdk.Token(() => this.targetType), + targets: new cdk.Token(() => this.targetsJson), + vpcId: baseProps.vpc.vpcId, + + // HEALTH CHECK + healthCheckIntervalSeconds: new cdk.Token(() => this.healthCheck && this.healthCheck.intervalSecs), + healthCheckPath: new cdk.Token(() => this.healthCheck && this.healthCheck.path), + healthCheckPort: new cdk.Token(() => this.healthCheck && this.healthCheck.port), + healthCheckProtocol: new cdk.Token(() => this.healthCheck && this.healthCheck.protocol), + healthCheckTimeoutSeconds: new cdk.Token(() => this.healthCheck && this.healthCheck.timeoutSeconds), + healthyThresholdCount: new cdk.Token(() => this.healthCheck && this.healthCheck.healthyThresholdCount), + matcher: new cdk.Token(() => this.healthCheck && this.healthCheck.healthyHttpCodes !== undefined ? { + httpCode: this.healthCheck.healthyHttpCodes + } : undefined), + + ...additionalProps + }); + + this.targetGroupArn = this.resource.ref; + this.targetGroupFullName = this.resource.targetGroupFullName; + this.targetGroupName = this.resource.targetGroupName; + this.defaultPort = `${additionalProps.port}`; + } + + /** + * Set/replace the target group's health check + */ + public configureHealthCheck(healthCheck: HealthCheck) { + this.healthCheck = healthCheck; + } + + /** + * Set a non-standard attribute on the target group + * + * @see https://docs.aws.amazon.com/elasticloadbalancing/latest/application/load-balancer-target-groups.html#target-group-attributes + */ + public setAttribute(key: string, value: string | undefined) { + this.attributes[key] = value; + } + + /** + * Export this target group + */ + public export(): TargetGroupRefProps { + return { + targetGroupArn: new cdk.Output(this, 'TargetGroupArn', { value: this.targetGroupArn }).makeImportValue().toString(), + defaultPort: new cdk.Output(this, 'Port', { value: this.defaultPort }).makeImportValue().toString(), + }; + } + + /** + * Add a dependency between this target group and the indicated resources + */ + public addDependency(...other: cdk.IDependable[]) { + this.resource.addDependency(...other); + } + + /** + * Register the given load balancing target as part of this group + */ + protected addLoadBalancerTarget(props: LoadBalancerTargetProps) { + if ((props.targetType === TargetType.SelfRegistering) !== (props.targetJson === undefined)) { + throw new Error('Load balancing target should specify targetJson if and only if TargetType is not SelfRegistering'); + } + if (props.targetType !== TargetType.SelfRegistering) { + if (this.targetType !== undefined && this.targetType !== props.targetType) { + throw new Error(`Already have a of type '${this.targetType}', adding '${props.targetType}'; make all targets the same type.`); + } + this.targetType = props.targetType; + } + + if (props.targetJson) { + this.targetsJson.push(props.targetJson); + } + } +} + +/** + * Properties to reference an existing target group + */ +export interface TargetGroupRefProps { + /** + * ARN of the target group + */ + targetGroupArn: string; + + /** + * Port target group is listening on + */ + defaultPort: string; +} + +/** + * A target group + */ +export interface ITargetGroup { + /** + * ARN of the target group + */ + readonly targetGroupArn: string; +} + +/** + * Result of attaching a target to load balancer + */ +export interface LoadBalancerTargetProps { + /** + * What kind of target this is + */ + targetType: TargetType; + + /** + * JSON representing the target's direct addition to the TargetGroup list + */ + targetJson?: any; +} diff --git a/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/shared/enums.ts b/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/shared/enums.ts new file mode 100644 index 0000000000000..4b549051fd660 --- /dev/null +++ b/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/shared/enums.ts @@ -0,0 +1,117 @@ +/** + * What kind of addresses to allocate to the load balancer + */ +export enum IpAddressType { + /** + * Allocate IPv4 addresses + */ + Ipv4 = 'ipv4', + + /** + * Allocate both IPv4 and IPv6 addresses + */ + DualStack = 'dualstack', +} + +/** + * Backend protocol for health checks + */ +export enum Protocol { + /** + * HTTP + */ + Http = 'HTTP', + + /** + * HTTPS + */ + Https = 'HTTPS', + + /** + * TCP + */ + Tcp = 'TCP' +} + +/** + * Load balancing protocol for application load balancers + */ +export enum ApplicationProtocol { + /** + * HTTP + */ + Http = 'HTTP', + + /** + * HTTPS + */ + Https = 'HTTPS' +} + +/** + * Elastic Load Balancing provides the following security policies for Application Load Balancers + * + * We recommend the Recommended policy for general use. You can + * use the ForwardSecrecy policy if you require Forward Secrecy + * (FS). + * + * You can use one of the TLS policies to meet compliance and security + * standards that require disabling certain TLS protocol versions, or to + * support legacy clients that require deprecated ciphers. + * + * @see https://docs.aws.amazon.com/elasticloadbalancing/latest/application/create-https-listener.html + */ +export enum SslPolicy { + /** + * The recommended security policy + */ + Recommended = 'ELBSecurityPolicy-2016-08', + + /** + * Forward secrecy ciphers only + */ + ForwardSecrecy = 'ELBSecurityPolicy-FS-2018-06', + + /** + * TLS1.2 only and no SHA ciphers + */ + TLS12 = 'ELBSecurityPolicy-TLS-1-2-2017-01', + + /** + * TLS1.2 only with all ciphers + */ + TLS12Ext = 'ELBSecurityPolicy-TLS-1-2-Ext-2018-06', + + /** + * TLS1.1 and higher with all ciphers + */ + TLS11 = 'ELBSecurityPolicy-TLS-1-1-2017-01', + + /** + * Support for DES-CBC3-SHA + * + * Do not use this security policy unless you must support a legacy client + * that requires the DES-CBC3-SHA cipher, which is a weak cipher. + */ + Legacy = 'ELBSecurityPolicy-TLS-1-0-2015-04', +} + +/** + * How to interpret the load balancing target identifiers + */ +export enum TargetType { + /** + * Targets identified by instance ID + */ + Instance = 'instance', + + /** + * Targets identified by IP address + */ + Ip = 'ip', + + /** + * A target that will register itself with the target group + */ + SelfRegistering = 'self-registering', +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/shared/imported.ts b/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/shared/imported.ts new file mode 100644 index 0000000000000..570adfef61f6c --- /dev/null +++ b/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/shared/imported.ts @@ -0,0 +1,18 @@ +import cdk = require('@aws-cdk/cdk'); +import { TargetGroupRefProps } from './base-target-group'; + +/** + * Base class for existing target groups + */ +export class BaseImportedTargetGroup extends cdk.Construct { + /** + * ARN of the target group + */ + public readonly targetGroupArn: string; + + constructor(parent: cdk.Construct, id: string, props: TargetGroupRefProps) { + super(parent, id); + + this.targetGroupArn = props.targetGroupArn; + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/shared/load-balancer-targets.ts b/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/shared/load-balancer-targets.ts new file mode 100644 index 0000000000000..47c83b194ae14 --- /dev/null +++ b/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/shared/load-balancer-targets.ts @@ -0,0 +1,113 @@ +import { ApplicationTargetGroup, IApplicationLoadBalancerTarget } from "../alb/application-target-group"; +import { INetworkLoadBalancerTarget, NetworkTargetGroup } from "../nlb/network-target-group"; +import { ITargetGroup, LoadBalancerTargetProps } from "./base-target-group"; +import { TargetType } from "./enums"; + +/** + * An EC2 instance that is the target for load balancing + * + * If you register a target of this type, you are responsible for making + * sure the load balancer's security group can connect to the instance. + */ +export class InstanceTarget implements IApplicationLoadBalancerTarget, INetworkLoadBalancerTarget { + /** + * Create a new Instance target + * + * @param instanceId Instance ID of the instance to register to + * @param port Override the default port for the target group + */ + constructor(private readonly instanceId: string, private readonly port?: number) { + } + + /** + * Register this instance target with a load balancer + * + * Don't call this, it is called automatically when you add the target to a + * load balancer. + */ + public attachToApplicationTargetGroup(targetGroup: ApplicationTargetGroup): LoadBalancerTargetProps { + return this.attach(targetGroup); + } + + /** + * Register this instance target with a load balancer + * + * Don't call this, it is called automatically when you add the target to a + * load balancer. + */ + public attachToNetworkTargetGroup(targetGroup: NetworkTargetGroup): LoadBalancerTargetProps { + return this.attach(targetGroup); + } + + private attach(_targetGroup: ITargetGroup): LoadBalancerTargetProps { + return { + targetType: TargetType.Instance, + targetJson: { id: this.instanceId, port: this.port } + }; + } +} + +/** + * An IP address that is a target for load balancing. + * + * Specify IP addresses from the subnets of the virtual private cloud (VPC) for + * the target group, the RFC 1918 range (10.0.0.0/8, 172.16.0.0/12, and + * 192.168.0.0/16), and the RFC 6598 range (100.64.0.0/10). You can't specify + * publicly routable IP addresses. + * + * If you register a target of this type, you are responsible for making + * sure the load balancer's security group can send packets to the IP address. + */ +export class IpTarget implements IApplicationLoadBalancerTarget, INetworkLoadBalancerTarget { + /** + * Create a new IPAddress target + * + * The availabilityZone parameter determines whether the target receives + * traffic from the load balancer nodes in the specified Availability Zone + * or from all enabled Availability Zones for the load balancer. + * + * This parameter is not supported if the target type of the target group + * is instance. If the IP address is in a subnet of the VPC for the target + * group, the Availability Zone is automatically detected and this + * parameter is optional. If the IP address is outside the VPC, this + * parameter is required. + * + * With an Application Load Balancer, if the IP address is outside the VPC + * for the target group, the only supported value is all. + * + * Default is automatic. + * + * @param ipAddress The IP Address to load balance to + * @param port Override the group's default port + * @param availabilityZone Availability zone to send traffic from + */ + constructor(private readonly ipAddress: string, private readonly port?: number, private readonly availabilityZone?: string) { + } + + /** + * Register this instance target with a load balancer + * + * Don't call this, it is called automatically when you add the target to a + * load balancer. + */ + public attachToApplicationTargetGroup(targetGroup: ApplicationTargetGroup): LoadBalancerTargetProps { + return this.attach(targetGroup); + } + + /** + * Register this instance target with a load balancer + * + * Don't call this, it is called automatically when you add the target to a + * load balancer. + */ + public attachToNetworkTargetGroup(targetGroup: NetworkTargetGroup): LoadBalancerTargetProps { + return this.attach(targetGroup); + } + + private attach(_targetGroup: ITargetGroup): LoadBalancerTargetProps { + return { + targetType: TargetType.Ip, + targetJson: { id: this.ipAddress, port: this.port, availabilityZone: this.availabilityZone } + }; + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/shared/util.ts b/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/shared/util.ts new file mode 100644 index 0000000000000..abe9933de4d3b --- /dev/null +++ b/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/shared/util.ts @@ -0,0 +1,69 @@ +import { ApplicationProtocol } from "./enums"; + +export type Attributes = {[key: string]: string | undefined}; + +/** + * Render an attribute dict to a list of { key, value } pairs + */ +export function renderAttributes(attributes: Attributes) { + const ret: any[] = []; + for (const [key, value] of Object.entries(attributes)) { + if (value !== undefined) { + ret.push({ key, value }); + } + } + return ret; +} + +/** + * Return the appropriate default port for a given protocol + */ +export function defaultPortForProtocol(proto: ApplicationProtocol): number { + switch (proto) { + case ApplicationProtocol.Http: return 80; + case ApplicationProtocol.Https: return 443; + default: + throw new Error(`Unrecognized protocol: ${proto}`); + } +} + +/** + * Return the appropriate default protocol for a given port + */ +export function defaultProtocolForPort(port: number): ApplicationProtocol { + switch (port) { + case 80: + case 8000: + case 8008: + case 8080: + return ApplicationProtocol.Http; + + case 443: + case 8443: + return ApplicationProtocol.Https; + + default: + throw new Error(`Don't know default protocol for port: ${port}; please supply a protocol`); + } +} + +/** + * Given a protocol and a port, try to guess the other one if it's undefined + */ +export function determineProtocolAndPort(protocol: ApplicationProtocol | undefined, port: number | undefined): [ApplicationProtocol, number] { + if (protocol === undefined && port === undefined) { + throw new Error('Supply at least one of protocol and port'); + } + + if (protocol === undefined) { protocol = defaultProtocolForPort(port!); } + if (port === undefined) { port = defaultPortForProtocol(protocol!); } + + return [protocol, port]; +} + +/** + * Helper function to default undefined input props + */ +export function ifUndefined(x: T | undefined, def: T) { + return x !== undefined ? x : def; +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-elasticloadbalancingv2/package.json b/packages/@aws-cdk/aws-elasticloadbalancingv2/package.json index eacf99253fd9c..1886040b02a45 100644 --- a/packages/@aws-cdk/aws-elasticloadbalancingv2/package.json +++ b/packages/@aws-cdk/aws-elasticloadbalancingv2/package.json @@ -55,10 +55,13 @@ "@aws-cdk/assert": "^0.9.2", "cdk-build-tools": "^0.9.2", "cfn2ts": "^0.9.2", - "pkglint": "^0.9.2" + "pkglint": "^0.9.2", + "cdk-integ-tools": "^0.9.2" }, "dependencies": { - "@aws-cdk/cdk": "^0.9.2" + "@aws-cdk/cdk": "^0.9.2", + "@aws-cdk/aws-ec2": "^0.9.2", + "@aws-cdk/aws-s3": "^0.9.2" }, "homepage": "https://github.com/awslabs/aws-cdk" } diff --git a/packages/@aws-cdk/aws-elasticloadbalancingv2/test/alb/test.listener.ts b/packages/@aws-cdk/aws-elasticloadbalancingv2/test/alb/test.listener.ts new file mode 100644 index 0000000000000..5e8578c70d110 --- /dev/null +++ b/packages/@aws-cdk/aws-elasticloadbalancingv2/test/alb/test.listener.ts @@ -0,0 +1,309 @@ +import { expect, haveResource } from '@aws-cdk/assert'; +import ec2 = require('@aws-cdk/aws-ec2'); +import cdk = require('@aws-cdk/cdk'); +import { Test } from 'nodeunit'; +import elbv2 = require('../../lib'); +import { FakeSelfRegisteringTarget } from '../helpers'; + +export = { + 'Listener guesses protocol from port'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + const vpc = new ec2.VpcNetwork(stack, 'Stack'); + const lb = new elbv2.ApplicationLoadBalancer(stack, 'LB', { vpc }); + + // WHEN + lb.addListener('Listener', { + port: 443, + certificateArns: ['bla'], + defaultTargetGroups: [new elbv2.ApplicationTargetGroup(stack, 'Group', { vpc, port: 80 })] + }); + + // THEN + expect(stack).to(haveResource('AWS::ElasticLoadBalancingV2::Listener', { + Protocol: 'HTTPS' + })); + + test.done(); + }, + + 'Listener guesses port from protocol'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + const vpc = new ec2.VpcNetwork(stack, 'Stack'); + const lb = new elbv2.ApplicationLoadBalancer(stack, 'LB', { vpc }); + + // WHEN + lb.addListener('Listener', { + protocol: elbv2.ApplicationProtocol.Http, + defaultTargetGroups: [new elbv2.ApplicationTargetGroup(stack, 'Group', { vpc, port: 80 })] + }); + + // THEN + expect(stack).to(haveResource('AWS::ElasticLoadBalancingV2::Listener', { + Port: 80 + })); + + test.done(); + }, + + 'HTTPS listener requires certificate'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + const vpc = new ec2.VpcNetwork(stack, 'Stack'); + const lb = new elbv2.ApplicationLoadBalancer(stack, 'LB', { vpc }); + + // WHEN + lb.addListener('Listener', { + port: 443, + defaultTargetGroups: [new elbv2.ApplicationTargetGroup(stack, 'Group', { vpc, port: 80 })] + }); + + // THEN + const errors = stack.validateTree(); + test.deepEqual(errors.map(e => e.message), ['HTTPS Listener needs at least one certificate (call addCertificateArns)']); + + test.done(); + }, + + 'Can add target groups with and without conditions'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + const vpc = new ec2.VpcNetwork(stack, 'Stack'); + const lb = new elbv2.ApplicationLoadBalancer(stack, 'LB', { vpc }); + const listener = lb.addListener('Listener', { port: 80 }); + const group = new elbv2.ApplicationTargetGroup(stack, 'TargetGroup', { vpc, port: 80 }); + + // WHEN + listener.addTargetGroups('Default', { + targetGroups: [group] + }); + listener.addTargetGroups('WithPath', { + priority: 10, + pathPattern: '/hello', + targetGroups: [group] + }); + + // THEN + expect(stack).to(haveResource('AWS::ElasticLoadBalancingV2::Listener', { + DefaultActions: [ + { + TargetGroupArn: { Ref: "TargetGroup3D7CD9B8" }, + Type: "forward" + } + ], + })); + expect(stack).to(haveResource('AWS::ElasticLoadBalancingV2::ListenerRule', { + Priority: 10, + Conditions: [ + { + Field: 'path-pattern', + Values: ['/hello'] + } + ], + Actions: [ + { + TargetGroupArn: { Ref: "TargetGroup3D7CD9B8" }, + Type: "forward" + } + ], + })); + + test.done(); + }, + + 'Can implicitly create target groups with and without conditions'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + const vpc = new ec2.VpcNetwork(stack, 'Stack'); + const lb = new elbv2.ApplicationLoadBalancer(stack, 'LB', { vpc }); + const listener = lb.addListener('Listener', { port: 80 }); + + // WHEN + listener.addTargets('Targets', { + port: 80, + targets: [new elbv2.InstanceTarget('i-12345')] + }); + listener.addTargets('WithPath', { + priority: 10, + pathPattern: '/hello', + port: 80, + targets: [new elbv2.InstanceTarget('i-5678')] + }); + + // THEN + expect(stack).to(haveResource('AWS::ElasticLoadBalancingV2::Listener', { + DefaultActions: [ + { + TargetGroupArn: { Ref: "LBListenerTargetsGroup76EF81E8" }, + Type: "forward" + } + ], + })); + expect(stack).to(haveResource('AWS::ElasticLoadBalancingV2::TargetGroup', { + VpcId: { Ref: "Stack8A423254" }, + Port: 80, + Protocol: "HTTP", + Targets: [ + { Id: "i-12345" } + ] + })); + expect(stack).to(haveResource('AWS::ElasticLoadBalancingV2::ListenerRule', { + Actions: [ + { + TargetGroupArn: { Ref: "LBListenerWithPathGroupE889F9E5" }, + Type: "forward" + } + ], + })); + expect(stack).to(haveResource('AWS::ElasticLoadBalancingV2::TargetGroup', { + VpcId: { Ref: "Stack8A423254" }, + Port: 80, + Protocol: "HTTP", + Targets: [ + { Id: "i-5678" } + ] + })); + + test.done(); + }, + + 'Add certificate to constructed listener'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + const vpc = new ec2.VpcNetwork(stack, 'Stack'); + const lb = new elbv2.ApplicationLoadBalancer(stack, 'LB', { vpc }); + const listener = lb.addListener('Listener', { port: 443 }); + + // WHEN + listener.addCertificateArns('Arns', ['cert']); + listener.addTargets('Targets', { port: 8080, targets: [new elbv2.IpTarget('1.2.3.4')] }); + + // THEN + expect(stack).to(haveResource('AWS::ElasticLoadBalancingV2::Listener', { + Certificates: [ + { CertificateArn: "cert" } + ], + })); + + test.done(); + }, + + 'Add certificate to imported listener'(test: Test) { + // GIVEN + const stack1 = new cdk.Stack(); + const vpc = new ec2.VpcNetwork(stack1, 'Stack'); + const lb = new elbv2.ApplicationLoadBalancer(stack1, 'LB', { vpc }); + const listener1 = lb.addListener('Listener', { port: 443 }); + + const stack2 = new cdk.Stack(); + const listener2 = elbv2.ApplicationListener.import(stack2, 'Listener', listener1.export()); + + // WHEN + listener2.addCertificateArns('Arns', ['cert']); + + // THEN + expect(stack2).to(haveResource('AWS::ElasticLoadBalancingV2::ListenerCertificate', { + Certificates: [ + { CertificateArn: "cert" } + ], + })); + + test.done(); + }, + + 'Enable stickiness for targets'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + const vpc = new ec2.VpcNetwork(stack, 'Stack'); + const lb = new elbv2.ApplicationLoadBalancer(stack, 'LB', { vpc }); + const listener = lb.addListener('Listener', { port: 80 }); + + // WHEN + const group = listener.addTargets('Group', { + port: 80, + targets: [new FakeSelfRegisteringTarget(stack, 'Target', vpc)] + }); + group.enableCookieStickiness(3600); + + // THEN + expect(stack).to(haveResource('AWS::ElasticLoadBalancingV2::TargetGroup', { + TargetGroupAttributes: [ + { + Key: "stickiness.enabled", + Value: "true" + }, + { + Key: "stickiness.type", + Value: "lb_cookie" + }, + { + Key: "stickiness.lb_cookie.duration_seconds", + Value: "3600" + } + ] + })); + + test.done(); + }, + + 'Enable health check for targets'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + const vpc = new ec2.VpcNetwork(stack, 'Stack'); + const lb = new elbv2.ApplicationLoadBalancer(stack, 'LB', { vpc }); + const listener = lb.addListener('Listener', { port: 80 }); + + // WHEN + const group = listener.addTargets('Group', { + port: 80, + targets: [new FakeSelfRegisteringTarget(stack, 'Target', vpc)] + }); + group.configureHealthCheck({ + timeoutSeconds: 3600, + intervalSecs: 30, + path: '/test', + }); + + // THEN + expect(stack).to(haveResource('AWS::ElasticLoadBalancingV2::TargetGroup', { + HealthCheckIntervalSeconds: 30, + HealthCheckPath: "/test", + HealthCheckTimeoutSeconds: 3600, + })); + + test.done(); + }, + + 'Can call addTargetGroups on imported listener'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + const vpc = new ec2.VpcNetwork(stack, 'VPC'); + const listener = elbv2.ApplicationListener.import(stack, 'Listener', { + listenerArn: 'ieks', + securityGroupId: 'sg-12345' + }); + const group = new elbv2.ApplicationTargetGroup(stack, 'TargetGroup', { vpc, port: 80 }); + + // WHEN + listener.addTargetGroups('Gruuup', { + priority: 30, + hostHeader: 'example.com', + targetGroups: [group] + }); + + // THEN + expect(stack).to(haveResource('AWS::ElasticLoadBalancingV2::ListenerRule', { + ListenerArn: 'ieks', + Priority: 30, + Actions: [ + { + TargetGroupArn: { Ref: "TargetGroup3D7CD9B8" }, + Type: "forward" + } + ], + })); + + test.done(); + }, +}; \ No newline at end of file diff --git a/packages/@aws-cdk/aws-elasticloadbalancingv2/test/alb/test.load-balancer.ts b/packages/@aws-cdk/aws-elasticloadbalancingv2/test/alb/test.load-balancer.ts new file mode 100644 index 0000000000000..26d4a7e31d434 --- /dev/null +++ b/packages/@aws-cdk/aws-elasticloadbalancingv2/test/alb/test.load-balancer.ts @@ -0,0 +1,127 @@ +import { expect, haveResource } from '@aws-cdk/assert'; +import ec2 = require('@aws-cdk/aws-ec2'); +import s3 = require('@aws-cdk/aws-s3'); +import cdk = require('@aws-cdk/cdk'); +import { Test } from 'nodeunit'; +import elbv2 = require('../../lib'); + +export = { + 'Trivial construction: internet facing'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + const vpc = new ec2.VpcNetwork(stack, 'Stack'); + + // WHEN + new elbv2.ApplicationLoadBalancer(stack, 'LB', { + vpc, + internetFacing: true, + }); + + // THEN + expect(stack).to(haveResource('AWS::ElasticLoadBalancingV2::LoadBalancer', { + Scheme: "internet-facing", + Subnets: [ + { Ref: "StackPublicSubnet1Subnet0AD81D22" }, + { Ref: "StackPublicSubnet2Subnet3C7D2288" }, + { Ref: "StackPublicSubnet3SubnetCC1055D9" } + ], + Type: "application" + })); + + test.done(); + }, + + 'Trivial construction: internal'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + const vpc = new ec2.VpcNetwork(stack, 'Stack'); + + // WHEN + new elbv2.ApplicationLoadBalancer(stack, 'LB', { vpc }); + + // THEN + expect(stack).to(haveResource('AWS::ElasticLoadBalancingV2::LoadBalancer', { + Scheme: "internal", + Subnets: [ + { Ref: "StackPrivateSubnet1Subnet47AC2BC7" }, + { Ref: "StackPrivateSubnet2SubnetA2F8EDD8" }, + { Ref: "StackPrivateSubnet3Subnet28548F2E" } + ], + Type: "application" + })); + + test.done(); + }, + + 'Attributes'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + const vpc = new ec2.VpcNetwork(stack, 'Stack'); + + // WHEN + new elbv2.ApplicationLoadBalancer(stack, 'LB', { + vpc, + deletionProtection: true, + http2Enabled: false, + idleTimeoutSecs: 1000, + }); + + // THEN + expect(stack).to(haveResource('AWS::ElasticLoadBalancingV2::LoadBalancer', { + LoadBalancerAttributes: [ + { + Key: "deletion_protection.enabled", + Value: "true" + }, + { + Key: "routing.http2.enabled", + Value: "false" + }, + { + Key: "idle_timeout.timeout_seconds", + Value: "1000" + } + ] + })); + + test.done(); + }, + + 'Access logging'(test: Test) { + // GIVEN + const stack = new cdk.Stack(undefined, undefined, { env: { region: 'us-east-1' }}); + const vpc = new ec2.VpcNetwork(stack, 'Stack'); + const bucket = new s3.Bucket(stack, 'AccessLoggingBucket'); + const lb = new elbv2.ApplicationLoadBalancer(stack, 'LB', { vpc }); + + // WHEN + lb.logAccessLogs(bucket); + + // THEN + expect(stack).to(haveResource('AWS::ElasticLoadBalancingV2::LoadBalancer', { + LoadBalancerAttributes: [ + { + Key: "access_logs.s3.enabled", + Value: "true" + }, + { + Key: "access_logs.s3.bucket", + Value: { Ref: "AccessLoggingBucketA6D88F29" } + } + ], + })); + expect(stack).to(haveResource('AWS::S3::BucketPolicy', { + PolicyDocument: { + Statement: [ + { + Action: "s3:PutObject", + Principal: { AWS: { "Fn::Join": [ "", [ "arn:", { Ref: "AWS::Partition" }, ":iam::127311923021:root" ] ] } }, + Resource: { "Fn::Join": [ "", [ { "Fn::GetAtt": [ "AccessLoggingBucketA6D88F29", "Arn" ] }, "/", "", "*" ] ] } + } + ] + } + })); + + test.done(); + } +}; \ No newline at end of file diff --git a/packages/@aws-cdk/aws-elasticloadbalancingv2/test/alb/test.security-groups.ts b/packages/@aws-cdk/aws-elasticloadbalancingv2/test/alb/test.security-groups.ts new file mode 100644 index 0000000000000..f4525bfb9667d --- /dev/null +++ b/packages/@aws-cdk/aws-elasticloadbalancingv2/test/alb/test.security-groups.ts @@ -0,0 +1,244 @@ +import { expect, haveResource } from '@aws-cdk/assert'; +import ec2 = require('@aws-cdk/aws-ec2'); +import cdk = require('@aws-cdk/cdk'); +import { Test } from 'nodeunit'; +import elbv2 = require('../../lib'); +import { FakeSelfRegisteringTarget } from '../helpers'; + +export = { + 'security groups are automatically opened bidi for default rule'(test: Test) { + // GIVEN + const fixture = new TestFixture(); + const target = new FakeSelfRegisteringTarget(fixture.stack, 'Target', fixture.vpc); + + // WHEN + fixture.listener.addTargets('TargetGroup', { + port: 8008, + targets: [target] + }); + + // THEN + expectSameStackSGRules(fixture.stack); + + test.done(); + }, + + 'security groups are automatically opened bidi for additional rule'(test: Test) { + // GIVEN + const fixture = new TestFixture(); + const target1 = new FakeSelfRegisteringTarget(fixture.stack, 'DefaultTarget', fixture.vpc); + const target2 = new FakeSelfRegisteringTarget(fixture.stack, 'Target', fixture.vpc); + + // WHEN + fixture.listener.addTargets('TargetGroup1', { + port: 80, + targets: [target1] + }); + + fixture.listener.addTargetGroups('Rule', { + priority: 10, + hostHeader: 'example.com', + targetGroups: [new elbv2.ApplicationTargetGroup(fixture.stack, 'TargetGroup2', { + vpc: fixture.vpc, + port: 8008, + targets: [target2] + })] + }); + + // THEN + expectSameStackSGRules(fixture.stack); + + test.done(); + }, + + 'adding the same targets twice also works'(test: Test) { + // GIVEN + const fixture = new TestFixture(); + const target = new FakeSelfRegisteringTarget(fixture.stack, 'Target', fixture.vpc); + + // WHEN + const group = new elbv2.ApplicationTargetGroup(fixture.stack, 'TargetGroup', { + vpc: fixture.vpc, + port: 8008, + targets: [target] + }); + + fixture.listener.addTargetGroups('Default', { + targetGroups: [group] + }); + fixture.listener.addTargetGroups('WithPath', { + priority: 10, + pathPattern: '/hello', + targetGroups: [group] + }); + + // THEN + expectSameStackSGRules(fixture.stack); + + test.done(); + }, + + 'same result if target is added to group after assigning to listener'(test: Test) { + // GIVEN + const fixture = new TestFixture(); + const group = new elbv2.ApplicationTargetGroup(fixture.stack, 'TargetGroup', { + vpc: fixture.vpc, + port: 8008 + }); + fixture.listener.addTargetGroups('Default', { + targetGroups: [group] + }); + + // WHEN + const target = new FakeSelfRegisteringTarget(fixture.stack, 'Target', fixture.vpc); + group.addTarget(target); + + // THEN + expectSameStackSGRules(fixture.stack); + + test.done(); + }, + + 'SG peering works on exported/imported load balancer'(test: Test) { + // GIVEN + const fixture = new TestFixture(); + const stack2 = new cdk.Stack(); + const vpc2 = new ec2.VpcNetwork(stack2, 'VPC'); + const group = new elbv2.ApplicationTargetGroup(stack2, 'TargetGroup', { + // We're assuming the 2nd VPC is peered to the 1st, or something. + vpc: vpc2, + port: 8008, + targets: [new FakeSelfRegisteringTarget(stack2, 'Target', vpc2)], + }); + + // WHEN + const lb2 = elbv2.ApplicationLoadBalancer.import(stack2, 'LB', fixture.lb.export()); + const listener2 = lb2.addListener('YetAnotherListener', { port: 80 }); + listener2.addTargetGroups('Default', { targetGroups: [group] }); + + // THEN + expectedImportedSGRules(stack2); + + test.done(); + }, + + 'SG peering works on exported/imported listener'(test: Test) { + // GIVEN + const fixture = new TestFixture(); + const stack2 = new cdk.Stack(); + const vpc2 = new ec2.VpcNetwork(stack2, 'VPC'); + const group = new elbv2.ApplicationTargetGroup(stack2, 'TargetGroup', { + // We're assuming the 2nd VPC is peered to the 1st, or something. + vpc: vpc2, + port: 8008, + targets: [new FakeSelfRegisteringTarget(stack2, 'Target', vpc2)], + }); + + // WHEN + const listener2 = elbv2.ApplicationListener.import(stack2, 'YetAnotherListener', fixture.listener.export()); + listener2.addTargetGroups('Default', { + // Must be a non-default target + priority: 10, + hostHeader: 'example.com', + targetGroups: [group] + }); + + // THEN + expectedImportedSGRules(stack2); + + test.done(); + }, + + 'default port peering works on constructed listener'(test: Test) { + // GIVEN + const fixture = new TestFixture(); + fixture.listener.addTargets('Default', { port: 8080, targets: [new elbv2.InstanceTarget('i-12345')] }); + + // WHEN + fixture.listener.connections.allowDefaultPortFromAnyIpv4('Open to the world'); + + // THEN + expect(fixture.stack).to(haveResource('AWS::EC2::SecurityGroup', { + SecurityGroupIngress: [ + { + CidrIp: "0.0.0.0/0", + Description: "Open to the world", + FromPort: 80, + IpProtocol: "tcp", + ToPort: 80 + } + ], + })); + + test.done(); + }, + + 'default port peering works on imported listener'(test: Test) { + // GIVEN + const fixture = new TestFixture(); + fixture.listener.addTargets('Default', { port: 8080, targets: [new elbv2.InstanceTarget('i-12345')] }); + const stack2 = new cdk.Stack(); + + // WHEN + const listener2 = elbv2.ApplicationListener.import(stack2, 'YetAnotherListener', fixture.listener.export()); + listener2.connections.allowDefaultPortFromAnyIpv4('Open to the world'); + + // THEN + expect(stack2).to(haveResource('AWS::EC2::SecurityGroupIngress', { + CidrIp: "0.0.0.0/0", + Description: "Open to the world", + IpProtocol: "tcp", + FromPort: { "Fn::ImportValue": "LBListenerPort7A9266A6" }, + ToPort: { "Fn::ImportValue": "LBListenerPort7A9266A6" }, + GroupId: IMPORTED_LB_SECURITY_GROUP + })); + + test.done(); + }, +}; + +const LB_SECURITY_GROUP = { "Fn::GetAtt": [ "LBSecurityGroup8A41EA2B", "GroupId" ] }; +const IMPORTED_LB_SECURITY_GROUP = { "Fn::ImportValue": "LBSecurityGroupSecurityGroupId0270B565" }; + +function expectSameStackSGRules(stack: cdk.Stack) { + expectSGRules(stack, LB_SECURITY_GROUP); +} + +function expectedImportedSGRules(stack: cdk.Stack) { + expectSGRules(stack, IMPORTED_LB_SECURITY_GROUP); +} + +function expectSGRules(stack: cdk.Stack, lbGroup: any) { + expect(stack).to(haveResource('AWS::EC2::SecurityGroupEgress', { + GroupId: lbGroup, + IpProtocol: "tcp", + Description: "Load balancer to target", + DestinationSecurityGroupId: { "Fn::GetAtt": [ "TargetSGDB98152D", "GroupId" ] }, + FromPort: 8008, + ToPort: 8008 + })); + expect(stack).to(haveResource('AWS::EC2::SecurityGroupIngress', { + IpProtocol: "tcp", + Description: "Load balancer to target", + FromPort: 8008, + GroupId: { "Fn::GetAtt": [ "TargetSGDB98152D", "GroupId" ] }, + SourceSecurityGroupId: lbGroup, + ToPort: 8008 + })); +} + +class TestFixture { + public readonly stack: cdk.Stack; + public readonly vpc: ec2.VpcNetwork; + public readonly lb: elbv2.ApplicationLoadBalancer; + public readonly listener: elbv2.ApplicationListener; + + constructor() { + this.stack = new cdk.Stack(); + this.vpc = new ec2.VpcNetwork(this.stack, 'VPC', { + maxAZs: 2 + }); + this.lb = new elbv2.ApplicationLoadBalancer(this.stack, 'LB', { vpc: this.vpc }); + this.listener = this.lb.addListener('Listener', { port: 80, open: false }); + } +} diff --git a/packages/@aws-cdk/aws-elasticloadbalancingv2/test/helpers.ts b/packages/@aws-cdk/aws-elasticloadbalancingv2/test/helpers.ts new file mode 100644 index 0000000000000..5f818262f9f33 --- /dev/null +++ b/packages/@aws-cdk/aws-elasticloadbalancingv2/test/helpers.ts @@ -0,0 +1,26 @@ +import ec2 = require('@aws-cdk/aws-ec2'); +import cdk = require('@aws-cdk/cdk'); +import elbv2 = require('../lib'); + +export class FakeSelfRegisteringTarget extends cdk.Construct implements elbv2.IApplicationLoadBalancerTarget, elbv2.INetworkLoadBalancerTarget, + ec2.IConnectable { + public readonly securityGroup: ec2.SecurityGroup; + public readonly connections: ec2.Connections; + + constructor(parent: cdk.Construct, id: string, vpc: ec2.VpcNetwork) { + super(parent, id); + this.securityGroup = new ec2.SecurityGroup(this, 'SG', { vpc }); + this.connections = new ec2.Connections({ + securityGroup: this.securityGroup + }); + } + + public attachToApplicationTargetGroup(targetGroup: elbv2.ApplicationTargetGroup): elbv2.LoadBalancerTargetProps { + targetGroup.registerConnectable(this); + return { targetType: elbv2.TargetType.SelfRegistering }; + } + + public attachToNetworkTargetGroup(_targetGroup: elbv2.NetworkTargetGroup): elbv2.LoadBalancerTargetProps { + return { targetType: elbv2.TargetType.SelfRegistering }; + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-elasticloadbalancingv2/test/integ.alb.expected.json b/packages/@aws-cdk/aws-elasticloadbalancingv2/test/integ.alb.expected.json new file mode 100644 index 0000000000000..07eeab7b440b4 --- /dev/null +++ b/packages/@aws-cdk/aws-elasticloadbalancingv2/test/integ.alb.expected.json @@ -0,0 +1,430 @@ +{ + "Resources": { + "VPCB9E5F0B4": { + "Type": "AWS::EC2::VPC", + "Properties": { + "CidrBlock": "10.0.0.0/16", + "EnableDnsHostnames": true, + "EnableDnsSupport": true, + "InstanceTenancy": "default", + "Tags": [ + { + "Key": "Name", + "Value": "aws-cdk-elbv2-integ/VPC" + } + ] + } + }, + "VPCPublicSubnet1SubnetB4246D30": { + "Type": "AWS::EC2::Subnet", + "Properties": { + "CidrBlock": "10.0.0.0/18", + "VpcId": { + "Ref": "VPCB9E5F0B4" + }, + "AvailabilityZone": "test-region-1a", + "MapPublicIpOnLaunch": true, + "Tags": [ + { + "Key": "Name", + "Value": "aws-cdk-elbv2-integ/VPC/PublicSubnet1" + } + ] + } + }, + "VPCPublicSubnet1RouteTableFEE4B781": { + "Type": "AWS::EC2::RouteTable", + "Properties": { + "VpcId": { + "Ref": "VPCB9E5F0B4" + }, + "Tags": [ + { + "Key": "Name", + "Value": "aws-cdk-elbv2-integ/VPC/PublicSubnet1" + } + ] + } + }, + "VPCPublicSubnet1RouteTableAssociatioin249B4093": { + "Type": "AWS::EC2::SubnetRouteTableAssociation", + "Properties": { + "RouteTableId": { + "Ref": "VPCPublicSubnet1RouteTableFEE4B781" + }, + "SubnetId": { + "Ref": "VPCPublicSubnet1SubnetB4246D30" + } + } + }, + "VPCPublicSubnet1EIP6AD938E8": { + "Type": "AWS::EC2::EIP", + "Properties": { + "Domain": "vpc" + } + }, + "VPCPublicSubnet1NATGatewayE0556630": { + "Type": "AWS::EC2::NatGateway", + "Properties": { + "AllocationId": { + "Fn::GetAtt": [ + "VPCPublicSubnet1EIP6AD938E8", + "AllocationId" + ] + }, + "SubnetId": { + "Ref": "VPCPublicSubnet1SubnetB4246D30" + }, + "Tags": [ + { + "Key": "Name", + "Value": "aws-cdk-elbv2-integ/VPC/PublicSubnet1" + } + ] + } + }, + "VPCPublicSubnet1DefaultRoute91CEF279": { + "Type": "AWS::EC2::Route", + "Properties": { + "RouteTableId": { + "Ref": "VPCPublicSubnet1RouteTableFEE4B781" + }, + "DestinationCidrBlock": "0.0.0.0/0", + "GatewayId": { + "Ref": "VPCIGWB7E252D3" + } + } + }, + "VPCPublicSubnet2Subnet74179F39": { + "Type": "AWS::EC2::Subnet", + "Properties": { + "CidrBlock": "10.0.64.0/18", + "VpcId": { + "Ref": "VPCB9E5F0B4" + }, + "AvailabilityZone": "test-region-1b", + "MapPublicIpOnLaunch": true, + "Tags": [ + { + "Key": "Name", + "Value": "aws-cdk-elbv2-integ/VPC/PublicSubnet2" + } + ] + } + }, + "VPCPublicSubnet2RouteTable6F1A15F1": { + "Type": "AWS::EC2::RouteTable", + "Properties": { + "VpcId": { + "Ref": "VPCB9E5F0B4" + }, + "Tags": [ + { + "Key": "Name", + "Value": "aws-cdk-elbv2-integ/VPC/PublicSubnet2" + } + ] + } + }, + "VPCPublicSubnet2RouteTableAssociatioin766225D7": { + "Type": "AWS::EC2::SubnetRouteTableAssociation", + "Properties": { + "RouteTableId": { + "Ref": "VPCPublicSubnet2RouteTable6F1A15F1" + }, + "SubnetId": { + "Ref": "VPCPublicSubnet2Subnet74179F39" + } + } + }, + "VPCPublicSubnet2EIP4947BC00": { + "Type": "AWS::EC2::EIP", + "Properties": { + "Domain": "vpc" + } + }, + "VPCPublicSubnet2NATGateway3C070193": { + "Type": "AWS::EC2::NatGateway", + "Properties": { + "AllocationId": { + "Fn::GetAtt": [ + "VPCPublicSubnet2EIP4947BC00", + "AllocationId" + ] + }, + "SubnetId": { + "Ref": "VPCPublicSubnet2Subnet74179F39" + }, + "Tags": [ + { + "Key": "Name", + "Value": "aws-cdk-elbv2-integ/VPC/PublicSubnet2" + } + ] + } + }, + "VPCPublicSubnet2DefaultRouteB7481BBA": { + "Type": "AWS::EC2::Route", + "Properties": { + "RouteTableId": { + "Ref": "VPCPublicSubnet2RouteTable6F1A15F1" + }, + "DestinationCidrBlock": "0.0.0.0/0", + "GatewayId": { + "Ref": "VPCIGWB7E252D3" + } + } + }, + "VPCPrivateSubnet1Subnet8BCA10E0": { + "Type": "AWS::EC2::Subnet", + "Properties": { + "CidrBlock": "10.0.128.0/18", + "VpcId": { + "Ref": "VPCB9E5F0B4" + }, + "AvailabilityZone": "test-region-1a", + "MapPublicIpOnLaunch": false, + "Tags": [ + { + "Key": "Name", + "Value": "aws-cdk-elbv2-integ/VPC/PrivateSubnet1" + } + ] + } + }, + "VPCPrivateSubnet1RouteTableBE8A6027": { + "Type": "AWS::EC2::RouteTable", + "Properties": { + "VpcId": { + "Ref": "VPCB9E5F0B4" + }, + "Tags": [ + { + "Key": "Name", + "Value": "aws-cdk-elbv2-integ/VPC/PrivateSubnet1" + } + ] + } + }, + "VPCPrivateSubnet1RouteTableAssociatioin77F7CA18": { + "Type": "AWS::EC2::SubnetRouteTableAssociation", + "Properties": { + "RouteTableId": { + "Ref": "VPCPrivateSubnet1RouteTableBE8A6027" + }, + "SubnetId": { + "Ref": "VPCPrivateSubnet1Subnet8BCA10E0" + } + } + }, + "VPCPrivateSubnet1DefaultRouteAE1D6490": { + "Type": "AWS::EC2::Route", + "Properties": { + "RouteTableId": { + "Ref": "VPCPrivateSubnet1RouteTableBE8A6027" + }, + "DestinationCidrBlock": "0.0.0.0/0", + "NatGatewayId": { + "Ref": "VPCPublicSubnet1NATGatewayE0556630" + } + } + }, + "VPCPrivateSubnet2SubnetCFCDAA7A": { + "Type": "AWS::EC2::Subnet", + "Properties": { + "CidrBlock": "10.0.192.0/18", + "VpcId": { + "Ref": "VPCB9E5F0B4" + }, + "AvailabilityZone": "test-region-1b", + "MapPublicIpOnLaunch": false, + "Tags": [ + { + "Key": "Name", + "Value": "aws-cdk-elbv2-integ/VPC/PrivateSubnet2" + } + ] + } + }, + "VPCPrivateSubnet2RouteTable0A19E10E": { + "Type": "AWS::EC2::RouteTable", + "Properties": { + "VpcId": { + "Ref": "VPCB9E5F0B4" + }, + "Tags": [ + { + "Key": "Name", + "Value": "aws-cdk-elbv2-integ/VPC/PrivateSubnet2" + } + ] + } + }, + "VPCPrivateSubnet2RouteTableAssociatioinC31995B4": { + "Type": "AWS::EC2::SubnetRouteTableAssociation", + "Properties": { + "RouteTableId": { + "Ref": "VPCPrivateSubnet2RouteTable0A19E10E" + }, + "SubnetId": { + "Ref": "VPCPrivateSubnet2SubnetCFCDAA7A" + } + } + }, + "VPCPrivateSubnet2DefaultRouteF4F5CFD2": { + "Type": "AWS::EC2::Route", + "Properties": { + "RouteTableId": { + "Ref": "VPCPrivateSubnet2RouteTable0A19E10E" + }, + "DestinationCidrBlock": "0.0.0.0/0", + "NatGatewayId": { + "Ref": "VPCPublicSubnet2NATGateway3C070193" + } + } + }, + "VPCIGWB7E252D3": { + "Type": "AWS::EC2::InternetGateway", + "Properties": { + "Tags": [ + { + "Key": "Name", + "Value": "aws-cdk-elbv2-integ/VPC" + } + ] + } + }, + "VPCVPCGW99B986DC": { + "Type": "AWS::EC2::VPCGatewayAttachment", + "Properties": { + "VpcId": { + "Ref": "VPCB9E5F0B4" + }, + "InternetGatewayId": { + "Ref": "VPCIGWB7E252D3" + } + } + }, + "LB8A12904C": { + "Type": "AWS::ElasticLoadBalancingV2::LoadBalancer", + "Properties": { + "LoadBalancerAttributes": [], + "Scheme": "internet-facing", + "SecurityGroups": [ + { + "Fn::GetAtt": [ + "LBSecurityGroup8A41EA2B", + "GroupId" + ] + } + ], + "Subnets": [ + { + "Ref": "VPCPublicSubnet1SubnetB4246D30" + }, + { + "Ref": "VPCPublicSubnet2Subnet74179F39" + } + ], + "Type": "application" + } + }, + "LBSecurityGroup8A41EA2B": { + "Type": "AWS::EC2::SecurityGroup", + "Properties": { + "GroupDescription": "Automatically created Security Group for ELB awscdkelbv2integLB9950B1E4", + "SecurityGroupEgress": [], + "SecurityGroupIngress": [ + { + "CidrIp": "0.0.0.0/0", + "Description": "Open to the world", + "FromPort": 80, + "IpProtocol": "tcp", + "ToPort": 80 + } + ], + "VpcId": { + "Ref": "VPCB9E5F0B4" + } + } + }, + "LBListener49E825B4": { + "Type": "AWS::ElasticLoadBalancingV2::Listener", + "Properties": { + "DefaultActions": [ + { + "TargetGroupArn": { + "Ref": "LBListenerTargetGroupF04FCF6D" + }, + "Type": "forward" + } + ], + "LoadBalancerArn": { + "Ref": "LB8A12904C" + }, + "Port": 80, + "Protocol": "HTTP", + "Certificates": [] + } + }, + "LBListenerTargetGroupF04FCF6D": { + "Type": "AWS::ElasticLoadBalancingV2::TargetGroup", + "Properties": { + "Port": 80, + "Protocol": "HTTP", + "VpcId": { + "Ref": "VPCB9E5F0B4" + }, + "TargetGroupAttributes": [], + "Targets": [ + { + "Id": "10.0.1.1" + } + ], + "TargetType": "ip" + } + }, + "LBListenerConditionalTargetGroupA75CCCD9": { + "Type": "AWS::ElasticLoadBalancingV2::TargetGroup", + "Properties": { + "Port": 80, + "Protocol": "HTTP", + "VpcId": { + "Ref": "VPCB9E5F0B4" + }, + "TargetGroupAttributes": [], + "Targets": [ + { + "Id": "10.0.1.2" + } + ], + "TargetType": "ip" + } + }, + "LBListenerConditionalTargetRule91FA260F": { + "Type": "AWS::ElasticLoadBalancingV2::ListenerRule", + "Properties": { + "Actions": [ + { + "TargetGroupArn": { + "Ref": "LBListenerConditionalTargetGroupA75CCCD9" + }, + "Type": "forward" + } + ], + "Conditions": [ + { + "Field": "host-header", + "Values": [ + "example.com" + ] + } + ], + "ListenerArn": { + "Ref": "LBListener49E825B4" + }, + "Priority": 10 + } + } + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-elasticloadbalancingv2/test/integ.alb.ts b/packages/@aws-cdk/aws-elasticloadbalancingv2/test/integ.alb.ts new file mode 100644 index 0000000000000..4b10506ac0ea9 --- /dev/null +++ b/packages/@aws-cdk/aws-elasticloadbalancingv2/test/integ.alb.ts @@ -0,0 +1,36 @@ +#!/usr/bin/env node +import ec2 = require('@aws-cdk/aws-ec2'); +import cdk = require('@aws-cdk/cdk'); +import elbv2 = require('../lib'); + +const app = new cdk.App(process.argv); +const stack = new cdk.Stack(app, 'aws-cdk-elbv2-integ'); + +const vpc = new ec2.VpcNetwork(stack, 'VPC', { + maxAZs: 2 +}); + +const lb = new elbv2.ApplicationLoadBalancer(stack, 'LB', { + vpc, + internetFacing: true +}); + +const listener = lb.addListener('Listener', { + port: 80, +}); + +listener.addTargets('Target', { + port: 80, + targets: [new elbv2.IpTarget('10.0.1.1')] +}); + +listener.addTargets('ConditionalTarget', { + priority: 10, + hostHeader: 'example.com', + port: 80, + targets: [new elbv2.IpTarget('10.0.1.2')] +}); + +listener.connections.allowDefaultPortFromAnyIpv4('Open to the world'); + +process.stdout.write(app.run()); diff --git a/packages/@aws-cdk/aws-elasticloadbalancingv2/test/integ.nlb.expected.json b/packages/@aws-cdk/aws-elasticloadbalancingv2/test/integ.nlb.expected.json new file mode 100644 index 0000000000000..6c7d92a1e87fe --- /dev/null +++ b/packages/@aws-cdk/aws-elasticloadbalancingv2/test/integ.nlb.expected.json @@ -0,0 +1,365 @@ +{ + "Resources": { + "VPCB9E5F0B4": { + "Type": "AWS::EC2::VPC", + "Properties": { + "CidrBlock": "10.0.0.0/16", + "EnableDnsHostnames": true, + "EnableDnsSupport": true, + "InstanceTenancy": "default", + "Tags": [ + { + "Key": "Name", + "Value": "aws-cdk-elbv2-integ/VPC" + } + ] + } + }, + "VPCPublicSubnet1SubnetB4246D30": { + "Type": "AWS::EC2::Subnet", + "Properties": { + "CidrBlock": "10.0.0.0/18", + "VpcId": { + "Ref": "VPCB9E5F0B4" + }, + "AvailabilityZone": "test-region-1a", + "MapPublicIpOnLaunch": true, + "Tags": [ + { + "Key": "Name", + "Value": "aws-cdk-elbv2-integ/VPC/PublicSubnet1" + } + ] + } + }, + "VPCPublicSubnet1RouteTableFEE4B781": { + "Type": "AWS::EC2::RouteTable", + "Properties": { + "VpcId": { + "Ref": "VPCB9E5F0B4" + }, + "Tags": [ + { + "Key": "Name", + "Value": "aws-cdk-elbv2-integ/VPC/PublicSubnet1" + } + ] + } + }, + "VPCPublicSubnet1RouteTableAssociatioin249B4093": { + "Type": "AWS::EC2::SubnetRouteTableAssociation", + "Properties": { + "RouteTableId": { + "Ref": "VPCPublicSubnet1RouteTableFEE4B781" + }, + "SubnetId": { + "Ref": "VPCPublicSubnet1SubnetB4246D30" + } + } + }, + "VPCPublicSubnet1EIP6AD938E8": { + "Type": "AWS::EC2::EIP", + "Properties": { + "Domain": "vpc" + } + }, + "VPCPublicSubnet1NATGatewayE0556630": { + "Type": "AWS::EC2::NatGateway", + "Properties": { + "AllocationId": { + "Fn::GetAtt": [ + "VPCPublicSubnet1EIP6AD938E8", + "AllocationId" + ] + }, + "SubnetId": { + "Ref": "VPCPublicSubnet1SubnetB4246D30" + }, + "Tags": [ + { + "Key": "Name", + "Value": "aws-cdk-elbv2-integ/VPC/PublicSubnet1" + } + ] + } + }, + "VPCPublicSubnet1DefaultRoute91CEF279": { + "Type": "AWS::EC2::Route", + "Properties": { + "RouteTableId": { + "Ref": "VPCPublicSubnet1RouteTableFEE4B781" + }, + "DestinationCidrBlock": "0.0.0.0/0", + "GatewayId": { + "Ref": "VPCIGWB7E252D3" + } + } + }, + "VPCPublicSubnet2Subnet74179F39": { + "Type": "AWS::EC2::Subnet", + "Properties": { + "CidrBlock": "10.0.64.0/18", + "VpcId": { + "Ref": "VPCB9E5F0B4" + }, + "AvailabilityZone": "test-region-1b", + "MapPublicIpOnLaunch": true, + "Tags": [ + { + "Key": "Name", + "Value": "aws-cdk-elbv2-integ/VPC/PublicSubnet2" + } + ] + } + }, + "VPCPublicSubnet2RouteTable6F1A15F1": { + "Type": "AWS::EC2::RouteTable", + "Properties": { + "VpcId": { + "Ref": "VPCB9E5F0B4" + }, + "Tags": [ + { + "Key": "Name", + "Value": "aws-cdk-elbv2-integ/VPC/PublicSubnet2" + } + ] + } + }, + "VPCPublicSubnet2RouteTableAssociatioin766225D7": { + "Type": "AWS::EC2::SubnetRouteTableAssociation", + "Properties": { + "RouteTableId": { + "Ref": "VPCPublicSubnet2RouteTable6F1A15F1" + }, + "SubnetId": { + "Ref": "VPCPublicSubnet2Subnet74179F39" + } + } + }, + "VPCPublicSubnet2EIP4947BC00": { + "Type": "AWS::EC2::EIP", + "Properties": { + "Domain": "vpc" + } + }, + "VPCPublicSubnet2NATGateway3C070193": { + "Type": "AWS::EC2::NatGateway", + "Properties": { + "AllocationId": { + "Fn::GetAtt": [ + "VPCPublicSubnet2EIP4947BC00", + "AllocationId" + ] + }, + "SubnetId": { + "Ref": "VPCPublicSubnet2Subnet74179F39" + }, + "Tags": [ + { + "Key": "Name", + "Value": "aws-cdk-elbv2-integ/VPC/PublicSubnet2" + } + ] + } + }, + "VPCPublicSubnet2DefaultRouteB7481BBA": { + "Type": "AWS::EC2::Route", + "Properties": { + "RouteTableId": { + "Ref": "VPCPublicSubnet2RouteTable6F1A15F1" + }, + "DestinationCidrBlock": "0.0.0.0/0", + "GatewayId": { + "Ref": "VPCIGWB7E252D3" + } + } + }, + "VPCPrivateSubnet1Subnet8BCA10E0": { + "Type": "AWS::EC2::Subnet", + "Properties": { + "CidrBlock": "10.0.128.0/18", + "VpcId": { + "Ref": "VPCB9E5F0B4" + }, + "AvailabilityZone": "test-region-1a", + "MapPublicIpOnLaunch": false, + "Tags": [ + { + "Key": "Name", + "Value": "aws-cdk-elbv2-integ/VPC/PrivateSubnet1" + } + ] + } + }, + "VPCPrivateSubnet1RouteTableBE8A6027": { + "Type": "AWS::EC2::RouteTable", + "Properties": { + "VpcId": { + "Ref": "VPCB9E5F0B4" + }, + "Tags": [ + { + "Key": "Name", + "Value": "aws-cdk-elbv2-integ/VPC/PrivateSubnet1" + } + ] + } + }, + "VPCPrivateSubnet1RouteTableAssociatioin77F7CA18": { + "Type": "AWS::EC2::SubnetRouteTableAssociation", + "Properties": { + "RouteTableId": { + "Ref": "VPCPrivateSubnet1RouteTableBE8A6027" + }, + "SubnetId": { + "Ref": "VPCPrivateSubnet1Subnet8BCA10E0" + } + } + }, + "VPCPrivateSubnet1DefaultRouteAE1D6490": { + "Type": "AWS::EC2::Route", + "Properties": { + "RouteTableId": { + "Ref": "VPCPrivateSubnet1RouteTableBE8A6027" + }, + "DestinationCidrBlock": "0.0.0.0/0", + "NatGatewayId": { + "Ref": "VPCPublicSubnet1NATGatewayE0556630" + } + } + }, + "VPCPrivateSubnet2SubnetCFCDAA7A": { + "Type": "AWS::EC2::Subnet", + "Properties": { + "CidrBlock": "10.0.192.0/18", + "VpcId": { + "Ref": "VPCB9E5F0B4" + }, + "AvailabilityZone": "test-region-1b", + "MapPublicIpOnLaunch": false, + "Tags": [ + { + "Key": "Name", + "Value": "aws-cdk-elbv2-integ/VPC/PrivateSubnet2" + } + ] + } + }, + "VPCPrivateSubnet2RouteTable0A19E10E": { + "Type": "AWS::EC2::RouteTable", + "Properties": { + "VpcId": { + "Ref": "VPCB9E5F0B4" + }, + "Tags": [ + { + "Key": "Name", + "Value": "aws-cdk-elbv2-integ/VPC/PrivateSubnet2" + } + ] + } + }, + "VPCPrivateSubnet2RouteTableAssociatioinC31995B4": { + "Type": "AWS::EC2::SubnetRouteTableAssociation", + "Properties": { + "RouteTableId": { + "Ref": "VPCPrivateSubnet2RouteTable0A19E10E" + }, + "SubnetId": { + "Ref": "VPCPrivateSubnet2SubnetCFCDAA7A" + } + } + }, + "VPCPrivateSubnet2DefaultRouteF4F5CFD2": { + "Type": "AWS::EC2::Route", + "Properties": { + "RouteTableId": { + "Ref": "VPCPrivateSubnet2RouteTable0A19E10E" + }, + "DestinationCidrBlock": "0.0.0.0/0", + "NatGatewayId": { + "Ref": "VPCPublicSubnet2NATGateway3C070193" + } + } + }, + "VPCIGWB7E252D3": { + "Type": "AWS::EC2::InternetGateway", + "Properties": { + "Tags": [ + { + "Key": "Name", + "Value": "aws-cdk-elbv2-integ/VPC" + } + ] + } + }, + "VPCVPCGW99B986DC": { + "Type": "AWS::EC2::VPCGatewayAttachment", + "Properties": { + "VpcId": { + "Ref": "VPCB9E5F0B4" + }, + "InternetGatewayId": { + "Ref": "VPCIGWB7E252D3" + } + } + }, + "LB8A12904C": { + "Type": "AWS::ElasticLoadBalancingV2::LoadBalancer", + "Properties": { + "LoadBalancerAttributes": [], + "Scheme": "internet-facing", + "Subnets": [ + { + "Ref": "VPCPublicSubnet1SubnetB4246D30" + }, + { + "Ref": "VPCPublicSubnet2Subnet74179F39" + } + ], + "Type": "network" + } + }, + "LBListener49E825B4": { + "Type": "AWS::ElasticLoadBalancingV2::Listener", + "Properties": { + "DefaultActions": [ + { + "TargetGroupArn": { + "Ref": "LBListenerTargetGroupF04FCF6D" + }, + "Type": "forward" + } + ], + "LoadBalancerArn": { + "Ref": "LB8A12904C" + }, + "Port": 443, + "Protocol": "TCP" + } + }, + "LBListenerTargetGroupF04FCF6D": { + "Type": "AWS::ElasticLoadBalancingV2::TargetGroup", + "Properties": { + "Port": 443, + "Protocol": "TCP", + "VpcId": { + "Ref": "VPCB9E5F0B4" + }, + "TargetGroupAttributes": [], + "Targets": [ + { + "Id": "10.0.1.1" + } + ], + "TargetType": "ip" + }, + "DependsOn": [ + "VPCB9E5F0B4", + "VPCIGWB7E252D3", + "VPCVPCGW99B986DC" + ] + } + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-elasticloadbalancingv2/test/integ.nlb.ts b/packages/@aws-cdk/aws-elasticloadbalancingv2/test/integ.nlb.ts new file mode 100644 index 0000000000000..4f6520f69ccaf --- /dev/null +++ b/packages/@aws-cdk/aws-elasticloadbalancingv2/test/integ.nlb.ts @@ -0,0 +1,31 @@ +#!/usr/bin/env node +import ec2 = require('@aws-cdk/aws-ec2'); +import cdk = require('@aws-cdk/cdk'); +import elbv2 = require('../lib'); + +const app = new cdk.App(process.argv); +const stack = new cdk.Stack(app, 'aws-cdk-elbv2-integ'); + +const vpc = new ec2.VpcNetwork(stack, 'VPC', { + maxAZs: 2 +}); + +const lb = new elbv2.NetworkLoadBalancer(stack, 'LB', { + vpc, + internetFacing: true +}); + +const listener = lb.addListener('Listener', { + port: 443, +}); + +const group = listener.addTargets('Target', { + port: 443, + targets: [new elbv2.IpTarget('10.0.1.1')] +}); + +group.addDependency(vpc); + +// The target's security group must allow being routed by the LB and the clients. + +process.stdout.write(app.run()); diff --git a/packages/@aws-cdk/aws-elasticloadbalancingv2/test/nlb/test.listener.ts b/packages/@aws-cdk/aws-elasticloadbalancingv2/test/nlb/test.listener.ts new file mode 100644 index 0000000000000..23bdae7feafe6 --- /dev/null +++ b/packages/@aws-cdk/aws-elasticloadbalancingv2/test/nlb/test.listener.ts @@ -0,0 +1,115 @@ +import { expect, haveResource } from '@aws-cdk/assert'; +import ec2 = require('@aws-cdk/aws-ec2'); +import cdk = require('@aws-cdk/cdk'); +import { Test } from 'nodeunit'; +import elbv2 = require('../../lib'); +import { FakeSelfRegisteringTarget } from '../helpers'; + +export = { + 'Trivial add listener'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + const vpc = new ec2.VpcNetwork(stack, 'Stack'); + const lb = new elbv2.NetworkLoadBalancer(stack, 'LB', { vpc }); + + // WHEN + lb.addListener('Listener', { + port: 443, + defaultTargetGroups: [new elbv2.NetworkTargetGroup(stack, 'Group', { vpc, port: 80 })] + }); + + // THEN + expect(stack).to(haveResource('AWS::ElasticLoadBalancingV2::Listener', { + Protocol: 'TCP', + Port: 443 + })); + + test.done(); + }, + + 'Can add target groups'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + const vpc = new ec2.VpcNetwork(stack, 'Stack'); + const lb = new elbv2.NetworkLoadBalancer(stack, 'LB', { vpc }); + const listener = lb.addListener('Listener', { port: 443 }); + const group = new elbv2.NetworkTargetGroup(stack, 'TargetGroup', { vpc, port: 80 }); + + // WHEN + listener.addTargetGroups('Default', group); + + // THEN + expect(stack).to(haveResource('AWS::ElasticLoadBalancingV2::Listener', { + DefaultActions: [ + { + TargetGroupArn: { Ref: "TargetGroup3D7CD9B8" }, + Type: "forward" + } + ], + })); + + test.done(); + }, + + 'Can implicitly create target groups'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + const vpc = new ec2.VpcNetwork(stack, 'Stack'); + const lb = new elbv2.NetworkLoadBalancer(stack, 'LB', { vpc }); + const listener = lb.addListener('Listener', { port: 443 }); + + // WHEN + listener.addTargets('Targets', { + port: 80, + targets: [new elbv2.InstanceTarget('i-12345')] + }); + + // THEN + expect(stack).to(haveResource('AWS::ElasticLoadBalancingV2::Listener', { + DefaultActions: [ + { + TargetGroupArn: { Ref: "LBListenerTargetsGroup76EF81E8" }, + Type: "forward" + } + ], + })); + expect(stack).to(haveResource('AWS::ElasticLoadBalancingV2::TargetGroup', { + VpcId: { Ref: "Stack8A423254" }, + Port: 80, + Protocol: "TCP", + Targets: [ + { Id: "i-12345" } + ] + })); + + test.done(); + }, + + 'Enable health check for targets'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + const vpc = new ec2.VpcNetwork(stack, 'Stack'); + const lb = new elbv2.NetworkLoadBalancer(stack, 'LB', { vpc }); + const listener = lb.addListener('Listener', { port: 443 }); + + // WHEN + const group = listener.addTargets('Group', { + port: 80, + targets: [new FakeSelfRegisteringTarget(stack, 'Target', vpc)] + }); + group.configureHealthCheck({ + timeoutSeconds: 3600, + intervalSecs: 30, + path: '/test', + }); + + // THEN + expect(stack).to(haveResource('AWS::ElasticLoadBalancingV2::TargetGroup', { + HealthCheckIntervalSeconds: 30, + HealthCheckPath: "/test", + HealthCheckTimeoutSeconds: 3600, + })); + + test.done(); + }, +}; diff --git a/packages/@aws-cdk/aws-elasticloadbalancingv2/test/nlb/test.load-balancer.ts b/packages/@aws-cdk/aws-elasticloadbalancingv2/test/nlb/test.load-balancer.ts new file mode 100644 index 0000000000000..6431adb03f7f4 --- /dev/null +++ b/packages/@aws-cdk/aws-elasticloadbalancingv2/test/nlb/test.load-balancer.ts @@ -0,0 +1,78 @@ +import { expect, haveResource } from '@aws-cdk/assert'; +import ec2 = require('@aws-cdk/aws-ec2'); +import cdk = require('@aws-cdk/cdk'); +import { Test } from 'nodeunit'; +import elbv2 = require('../../lib'); + +export = { + 'Trivial construction: internet facing'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + const vpc = new ec2.VpcNetwork(stack, 'Stack'); + + // WHEN + new elbv2.NetworkLoadBalancer(stack, 'LB', { + vpc, + internetFacing: true, + }); + + // THEN + expect(stack).to(haveResource('AWS::ElasticLoadBalancingV2::LoadBalancer', { + Scheme: "internet-facing", + Subnets: [ + { Ref: "StackPublicSubnet1Subnet0AD81D22" }, + { Ref: "StackPublicSubnet2Subnet3C7D2288" }, + { Ref: "StackPublicSubnet3SubnetCC1055D9" } + ], + Type: "network" + })); + + test.done(); + }, + + 'Trivial construction: internal'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + const vpc = new ec2.VpcNetwork(stack, 'Stack'); + + // WHEN + new elbv2.NetworkLoadBalancer(stack, 'LB', { vpc }); + + // THEN + expect(stack).to(haveResource('AWS::ElasticLoadBalancingV2::LoadBalancer', { + Scheme: "internal", + Subnets: [ + { Ref: "StackPrivateSubnet1Subnet47AC2BC7" }, + { Ref: "StackPrivateSubnet2SubnetA2F8EDD8" }, + { Ref: "StackPrivateSubnet3Subnet28548F2E" } + ], + Type: "network" + })); + + test.done(); + }, + + 'Attributes'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + const vpc = new ec2.VpcNetwork(stack, 'Stack'); + + // WHEN + new elbv2.NetworkLoadBalancer(stack, 'LB', { + vpc, + crossZoneEnabled: true, + }); + + // THEN + expect(stack).to(haveResource('AWS::ElasticLoadBalancingV2::LoadBalancer', { + LoadBalancerAttributes: [ + { + Key: "load_balancing.cross_zone.enabled", + Value: "true" + } + ] + })); + + test.done(); + }, +}; diff --git a/packages/@aws-cdk/aws-elasticloadbalancingv2/test/test.elasticloadbalancingv2.ts b/packages/@aws-cdk/aws-elasticloadbalancingv2/test/test.elasticloadbalancingv2.ts deleted file mode 100644 index db4c843199541..0000000000000 --- a/packages/@aws-cdk/aws-elasticloadbalancingv2/test/test.elasticloadbalancingv2.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { Test, testCase } from 'nodeunit'; - -exports = testCase({ - notTested(test: Test) { - test.ok(true, 'No tests are specified for this package.'); - test.done(); - } -}); diff --git a/packages/@aws-cdk/aws-lambda/lib/lambda.ts b/packages/@aws-cdk/aws-lambda/lib/lambda.ts index 99f52bc1d2083..a83e139088b19 100644 --- a/packages/@aws-cdk/aws-lambda/lib/lambda.ts +++ b/packages/@aws-cdk/aws-lambda/lib/lambda.ts @@ -328,7 +328,7 @@ export class Function extends FunctionRef { // won't work because the ENIs don't get a Public IP. const subnets = props.vpc.subnets(props.vpcPlacement); for (const subnet of subnets) { - if (props.vpc.publicSubnets.indexOf(subnet) > -1) { + if (props.vpc.isPublicSubnet(subnet)) { throw new Error('Not possible to place Lambda Functions in a Public subnet'); } } diff --git a/packages/@aws-cdk/aws-rds/test/integ.cluster.expected.json b/packages/@aws-cdk/aws-rds/test/integ.cluster.expected.json index 3a3c1b4ae947f..c4e75bcba0011 100644 --- a/packages/@aws-cdk/aws-rds/test/integ.cluster.expected.json +++ b/packages/@aws-cdk/aws-rds/test/integ.cluster.expected.json @@ -340,7 +340,7 @@ } } }, - "DatabaseSecurityGroupOpentotheworld94E9606E": { + "DatabaseSecurityGroupfrom00000IndirectPortF24F2E03": { "Type": "AWS::EC2::SecurityGroupIngress", "Properties": { "IpProtocol": "tcp", diff --git a/scripts/runtest.js b/scripts/runtest.js index d501aca18f22c..3b4c56a253d80 100755 --- a/scripts/runtest.js +++ b/scripts/runtest.js @@ -1,5 +1,21 @@ #!/usr/bin/env node // Helper script to invoke 'nodeunit' on a .ts file. This should involve compilation, it doesn't right now. +// +// Example launch config to use with this script: +// +// { +// "configurations": [ +// { +// "type": "node", +// "request": "launch", +// "name": "Debug Current Unit Test File", +// "program": "${workspaceFolder}/scripts/runtest.js", +// "args": [ +// "${file}" +// ] +// } +// ] +// }⏎ const path = require('path'); // Unfortunately, nodeunit has no programmatic interface. Therefore, the