Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(global-accelerator): referencing Global Accelerator security group #9358

Merged
merged 10 commits into from
Aug 11, 2020
37 changes: 36 additions & 1 deletion packages/@aws-cdk/aws-globalaccelerator/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down Expand Up @@ -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 groups 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));
```
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
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 {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For safety's sake, can you give this class a private constructor() { }?

To make sure it's only used statically, gives us the opportunity to add a constructor later on should we ever need to (otherwise we can't ever make this class a construct anymore without breaking backwards compat).

/**
* 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', {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is clever!

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;
}
}
1 change: 1 addition & 0 deletions packages/@aws-cdk/aws-globalaccelerator/lib/index.ts
Original file line number Diff line number Diff line change
@@ -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';
7 changes: 5 additions & 2 deletions packages/@aws-cdk/aws-globalaccelerator/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -66,20 +66,23 @@
"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",
"cfn2ts": "0.0.0",
"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"
Expand Down
Original file line number Diff line number Diff line change
@@ -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,
}));
});
2 changes: 1 addition & 1 deletion packages/@aws-cdk/aws-globalaccelerator/test/util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand Down