diff --git a/packages/@aws-cdk/aws-globalaccelerator/README.md b/packages/@aws-cdk/aws-globalaccelerator/README.md index ac304fe3ea03e..7d32fc8c72bae 100644 --- a/packages/@aws-cdk/aws-globalaccelerator/README.md +++ b/packages/@aws-cdk/aws-globalaccelerator/README.md @@ -15,7 +15,7 @@ ## Introduction -AWS Global Accelerator is a service that improves the availability and performance of your applications with local or global users. It provides static IP addresses that act as a fixed entry point to your application endpoints in a single or multiple AWS Regions, such as your Application Load Balancers, Network Load Balancers or Amazon EC2 instances. +AWS Global Accelerator (AGA) is a service that improves the availability and performance of your applications with local or global users. It provides static IP addresses that act as a fixed entry point to your application endpoints in a single or multiple AWS Regions, such as your Application Load Balancers, Network Load Balancers or Amazon EC2 instances. This module supports features under [AWS Global Accelerator](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/AWS_GlobalAccelerator.html) that allows users set up resources using the `@aws-cdk/aws-globalaccelerator` module. @@ -93,3 +93,38 @@ endpointGroup.addElasticIpAddress('EipEndpoint', eip); endpointGroup.addEc2Instance('InstanceEndpoint', instances[0]); endpointGroup.addEndpoint('InstanceEndpoint2', instances[1].instanceId); ``` + +## Accelerator Security Groups + +When using certain AGA features (client IP address preservation), AGA creates elastic network interfaces (ENI) in your AWS account which are +associated with a Security Group, and which are reused for all AGAs associated with that VPC. Per the +[best practices](https://docs.aws.amazon.com/global-accelerator/latest/dg/best-practices-aga.html) page, AGA creates a specific security group +called `GlobalAccelerator` for each VPC it has an ENI in. You can use the security group created by AGA as a source group in other security +groups, such as those for EC2 instances or Elastic Load Balancers, in order to implement least-privilege security group rules. + +CloudFormation doesn't support referencing the security group created by AGA. CDK has a library that enables you to reference the AGA security group +for a VPC using an AwsCustomResource. + +``` +const vpc = new Vpc(stack, 'VPC', {}); +const alb = new elbv2.ApplicationLoadBalancer(stack, 'ALB', { vpc, internetFacing: false }); +const accelerator = new ga.Accelerator(stack, 'Accelerator'); +const listener = new ga.Listener(stack, 'Listener', { + accelerator, + portRanges: [ + { + fromPort: 443, + toPort: 443, + }, + ], +}); +const endpointGroup = new ga.EndpointGroup(stack, 'Group', { listener }); +endpointGroup.addLoadBalancer('AlbEndpoint', alb); + +// Remember that there is only one AGA security group per VPC. +// This code will fail at CloudFormation deployment time if you do not have an AGA +const agaSg = ga.AcceleratorSecurityGroup.fromVpc(stack, 'GlobalAcceleratorSG', vpc); + +// Allow connections from the AGA to the ALB +alb.connections.allowFrom(agaSg, Port.tcp(443)); +``` \ No newline at end of file diff --git a/packages/@aws-cdk/aws-globalaccelerator/lib/accelerator-security-group.ts b/packages/@aws-cdk/aws-globalaccelerator/lib/accelerator-security-group.ts new file mode 100644 index 0000000000000..223954e24134e --- /dev/null +++ b/packages/@aws-cdk/aws-globalaccelerator/lib/accelerator-security-group.ts @@ -0,0 +1,70 @@ +import { ISecurityGroup, SecurityGroup, IVpc } from '@aws-cdk/aws-ec2'; +import { Construct } from '@aws-cdk/core'; +import { AwsCustomResource, AwsCustomResourcePolicy, PhysicalResourceId} from '@aws-cdk/custom-resources'; +import { EndpointGroup } from '../lib'; + +/** + * The security group used by a Global Accelerator to send traffic to resources in a VPC. + */ +export class AcceleratorSecurityGroup { + /** + * Lookup the Global Accelerator security group at CloudFormation deployment time. + * + * As of this writing, Global Accelerators (AGA) create a single security group per VPC. AGA security groups are shared + * by all AGAs in an account. Additionally, there is no CloudFormation mechanism to reference the AGA security groups. + * + * This makes creating security group rules which allow traffic from an AGA complicated in CDK. This lookup will identify + * the AGA security group for a given VPC at CloudFormation deployment time, and lets you create rules for traffic from AGA + * to other resources created by CDK. + */ + public static fromVpc(scope: Construct, id: string, vpc: IVpc, endpointGroup: EndpointGroup): ISecurityGroup { + + // The security group name is always 'GlobalAccelerator' + const globalAcceleratorSGName = 'GlobalAccelerator'; + + // How to reference the security group name in the response from EC2 + const ec2ResponseSGIdField = 'SecurityGroups.0.GroupId'; + + // The AWS Custom Resource that make a call to EC2 to get the security group ID, for the given VPC + const lookupAcceleratorSGCustomResource = new AwsCustomResource(scope, id + 'CustomResource', { + onCreate: { + service: 'EC2', + action: 'describeSecurityGroups', + parameters: { + Filters: [ + { + Name: 'group-name', + Values: [ + globalAcceleratorSGName, + ], + }, + { + Name: 'vpc-id', + Values: [ + vpc.vpcId, + ], + }, + ], + }, + // We get back a list of responses, but the list should be of length 0 or 1 + // Getting no response means no resources have been linked to the AGA + physicalResourceId: PhysicalResourceId.fromResponse(ec2ResponseSGIdField), + }, + policy: AwsCustomResourcePolicy.fromSdkCalls({ + resources: AwsCustomResourcePolicy.ANY_RESOURCE, + }), + }); + + // Look up the security group ID + const sg = SecurityGroup.fromSecurityGroupId(scope, + id, + lookupAcceleratorSGCustomResource.getResponseField(ec2ResponseSGIdField)); + // We add a dependency on the endpoint group, guaranteeing that CloudFormation won't + // try and look up the SG before AGA creates it. The SG is created when a VPC resource + // is associated with an AGA + lookupAcceleratorSGCustomResource.node.addDependency(endpointGroup); + return sg; + } + + private constructor() {} +} diff --git a/packages/@aws-cdk/aws-globalaccelerator/lib/index.ts b/packages/@aws-cdk/aws-globalaccelerator/lib/index.ts index ce94d8991fb5d..ff4675e6af2e5 100644 --- a/packages/@aws-cdk/aws-globalaccelerator/lib/index.ts +++ b/packages/@aws-cdk/aws-globalaccelerator/lib/index.ts @@ -1,5 +1,6 @@ // AWS::GlobalAccelerator CloudFormation Resources: export * from './globalaccelerator.generated'; export * from './accelerator'; +export * from './accelerator-security-group'; export * from './listener'; export * from './endpoint-group'; diff --git a/packages/@aws-cdk/aws-globalaccelerator/package.json b/packages/@aws-cdk/aws-globalaccelerator/package.json index fb84dd0d1d5eb..d9fa05dcacc62 100644 --- a/packages/@aws-cdk/aws-globalaccelerator/package.json +++ b/packages/@aws-cdk/aws-globalaccelerator/package.json @@ -66,7 +66,6 @@ "license": "Apache-2.0", "devDependencies": { "@aws-cdk/assert": "0.0.0", - "@aws-cdk/aws-ec2": "0.0.0", "@aws-cdk/aws-elasticloadbalancingv2": "0.0.0", "cdk-integ-tools": "0.0.0", "cdk-build-tools": "0.0.0", @@ -74,12 +73,16 @@ "pkglint": "0.0.0" }, "dependencies": { + "@aws-cdk/aws-ec2": "0.0.0", "@aws-cdk/core": "0.0.0", + "@aws-cdk/custom-resources": "0.0.0", "constructs": "^3.0.2" }, "peerDependencies": { + "@aws-cdk/aws-ec2": "0.0.0", "@aws-cdk/core": "0.0.0", - "constructs": "^3.0.2" + "constructs": "^3.0.2", + "@aws-cdk/custom-resources": "0.0.0" }, "engines": { "node": ">= 10.13.0 <13 || >=13.7.0" diff --git a/packages/@aws-cdk/aws-globalaccelerator/test/globalaccelerator-security-group.test.ts b/packages/@aws-cdk/aws-globalaccelerator/test/globalaccelerator-security-group.test.ts new file mode 100644 index 0000000000000..c3ca726bf344c --- /dev/null +++ b/packages/@aws-cdk/aws-globalaccelerator/test/globalaccelerator-security-group.test.ts @@ -0,0 +1,103 @@ +import { expect, haveResource, ResourcePart } from '@aws-cdk/assert'; +import { Port } from '@aws-cdk/aws-ec2'; +import * as ga from '../lib'; +import { testFixture, testFixtureAlb } from './util'; + +test('custom resource exists', () => { + // GIVEN + const { stack, vpc } = testFixture(); + const accelerator = new ga.Accelerator(stack, 'Accelerator'); + const listener = new ga.Listener(stack, 'Listener', { + accelerator, + portRanges: [ + { + fromPort: 443, + toPort: 443, + }, + ], + }); + const endpointGroup = new ga.EndpointGroup(stack, 'Group', { listener }); + + // WHEN + ga.AcceleratorSecurityGroup.fromVpc(stack, 'GlobalAcceleratorSG', vpc, endpointGroup); + + // THEN + expect(stack).to(haveResource('Custom::AWS', { + Properties: { + ServiceToken: { + 'Fn::GetAtt': [ + 'AWS679f53fac002430cb0da5b7982bd22872D164C4C', + 'Arn', + ], + }, + Create: { + action: 'describeSecurityGroups', + service: 'EC2', + parameters: { + Filters: [ + { + Name: 'group-name', + Values: [ + 'GlobalAccelerator', + ], + }, + { + Name: 'vpc-id', + Values: [ + { + Ref: 'VPCB9E5F0B4', + }, + ], + }, + ], + }, + physicalResourceId: { + responsePath: 'SecurityGroups.0.GroupId', + }, + }, + }, + DependsOn: [ + 'GroupC77FDACD', + ], + }, ResourcePart.CompleteDefinition)); +}); + +test('can create security group rule', () => { + // GIVEN + const { stack, alb, vpc } = testFixtureAlb(); + const accelerator = new ga.Accelerator(stack, 'Accelerator'); + const listener = new ga.Listener(stack, 'Listener', { + accelerator, + portRanges: [ + { + fromPort: 443, + toPort: 443, + }, + ], + }); + const endpointGroup = new ga.EndpointGroup(stack, 'Group', { listener }); + endpointGroup.addLoadBalancer('endpoint', alb); + + // WHEN + const sg = ga.AcceleratorSecurityGroup.fromVpc(stack, 'GlobalAcceleratorSG', vpc, endpointGroup); + alb.connections.allowFrom(sg, Port.tcp(443)); + + // THEN + expect(stack).to(haveResource('AWS::EC2::SecurityGroupIngress', { + IpProtocol: 'tcp', + FromPort: 443, + GroupId: { + 'Fn::GetAtt': [ + 'ALBSecurityGroup8B8624F8', + 'GroupId', + ], + }, + SourceSecurityGroupId: { + 'Fn::GetAtt': [ + 'GlobalAcceleratorSGCustomResourceC1DB5287', + 'SecurityGroups.0.GroupId', + ], + }, + ToPort: 443, + })); +}); \ No newline at end of file diff --git a/packages/@aws-cdk/aws-globalaccelerator/test/util.ts b/packages/@aws-cdk/aws-globalaccelerator/test/util.ts index 03fc491788e21..56e7561565c1a 100644 --- a/packages/@aws-cdk/aws-globalaccelerator/test/util.ts +++ b/packages/@aws-cdk/aws-globalaccelerator/test/util.ts @@ -19,7 +19,7 @@ export function testFixtureAlb() { const { stack, app, vpc } = testFixture(); const alb = new elbv2.ApplicationLoadBalancer(stack, 'ALB', { vpc, internetFacing: true }); - return { stack, app, alb }; + return { stack, app, alb, vpc }; } export function testFixtureNlb() {