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 4dfb06267c5a7..43903c09928f3 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