Skip to content

Commit

Permalink
feat(aws-ec2): construct for EC2 Instance Connect Endpoint (#22)
Browse files Browse the repository at this point in the history
Co-authored-by: Thorsten Hoeger <[email protected]>
  • Loading branch information
badmintoncryer and hoegertn authored May 4, 2024
1 parent cf66ca3 commit f1fafb2
Show file tree
Hide file tree
Showing 17 changed files with 3,073 additions and 1 deletion.
492 changes: 492 additions & 0 deletions API.md

Large diffs are not rendered by default.

78 changes: 78 additions & 0 deletions src/aws-ec2/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
Constructs for the AWS EC2 service

# EC2 Instance Connect Endpoint CDK Construct

## Overview

The `InstanceConnectEndpoint` construct facilitates the creation and management of [EC2 Instance Connect endpoints](https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/connect-with-ec2-instance-connect-endpoint.html)
within AWS CDK applications.

## Usage

Import the necessary classes from AWS CDK and this construct and create a VPC for the endpoint:

```ts
import { App, Stack } from 'aws-cdk-lib';
import * as ec2 from 'aws-cdk-lib/aws-ec2';
import { InstanceConnectEndpoint } from '@open-constructs/aws-cdk/aws-ec2';

const app = new App();
const stack = new Stack(app, 'InstanceConnectEndpointStack');
const vpc = new ec2.Vpc(stack, 'MyVpc');
```

### Basic Example

Here's how you can create an EC2 Instance Connect endpoint and allow connections to an EC2 instance:

```ts
const instance = new ec2.Instance(this, 'Instance', {
vpc,
instanceType: ec2.InstanceType.of(
ec2.InstanceClass.C5,
ec2.InstanceSize.LARGE,
),
machineImage: new ec2.AmazonLinuxImage({
generation: ec2.AmazonLinuxGeneration.AMAZON_LINUX_2023,
}),
});

const endpoint = new InstanceConnectEndpoint(stack, 'MyEndpoint', {
vpc,
});

// Allow SSH connections to the instance
// You can also use the port 3389 for RDP connections
endpoint.connections.allowTo(instance, ec2.Port.tcp(22));
```

### Advanced Example

Creating an endpoint with a custom settings:

```ts
declare const endpointSecurityGroup: ec2.ISecurityGroup;

const endpoint = new InstanceConnectEndpoint(stack, 'MyCustomEndpoint', {
vpc,
securityGroups: [endpointSecurityGroup], // Specify user-defined security groups
preserveClientIp: true, // Whether your client's IP address is preserved as the source
clientToken: 'my-client-token', // Specify client token to ensure the idempotency of the request.
});
```

Import an existing endpoint:

```ts
declare const existingEndpoint: ec2.IInstanceConnectEndpoint;
declare const securityGroups: ec2.ISecurityGroup[];

const existingEndpoint = InstanceConnectEndpoint.fromInstanceConnectEndpointAttributes(
stack,
'MyExistingEndpoint',
{
instanceConnectEndpointId: existingEndpoint.instanceConnectEndpointId,
securityGroups,
},
);
```
1 change: 1 addition & 0 deletions src/aws-ec2/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './instance-connect-endpoint';
144 changes: 144 additions & 0 deletions src/aws-ec2/instance-connect-endpoint.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
import { IResource, Resource, aws_ec2 } from 'aws-cdk-lib';
import { Construct } from 'constructs';

/**
* An EC2 Instance Connect Endpoint.
*/
export interface IInstanceConnectEndpoint extends aws_ec2.IConnectable, IResource {
/**
* The ID of the EC2 Instance Connect Endpoint.
*
* @attribute
*/
readonly instanceConnectEndpointId: string;
}

/**
* Properties for defining an EC2 Instance Connect Endpoint.
*/
export interface InstanceConnectEndpointProps {
/**
* Unique, case-sensitive identifier that you provide to ensure the idempotency of the request.
*
* @see https://docs.aws.amazon.com/ja_jp/AWSCloudFormation/latest/UserGuide/aws-resource-ec2-instanceconnectendpoint.html#cfn-ec2-instanceconnectendpoint-clienttoken
*/
readonly clientToken?: string;

/**
* Indicates whether your client's IP address is preserved as the source.
*
* @see https://docs.aws.amazon.com/ja_jp/AWSCloudFormation/latest/UserGuide/aws-resource-ec2-instanceconnectendpoint.html#cfn-ec2-instanceconnectendpoint-preserveclientip
* @default true
*/
readonly preserveClientIp?: boolean;

/**
* The security groups to associate with the EC2 Instance Connect Endpoint.
*
* @default - a new security group is created
*/
readonly securityGroups?: aws_ec2.ISecurityGroup[];

/**
* The VPC in which the EC2 Instance Connect Endpoint is created.
*/
readonly vpc: aws_ec2.IVpc;
}

/**
* Attributes for importing an EC2 Instance Connect Endpoint.
*/
export interface InstanceConnectEndpointAttributes {
/**
* The ID of the EC2 Instance Connect Endpoint.
*/
readonly instanceConnectEndpointId: string;

/**
* The security groups associated with the EC2 Instance Connect Endpoint.
*/
readonly securityGroups: aws_ec2.ISecurityGroup[];
}

/**
* Represents an EC2 Instance Connect Endpoint construct in AWS CDK.
*
* @example
* declare const securityGroups: aws_ec2.ISecurityGroup[];
* declare const vpc: aws_ec2.IVpc;
*
* const instanceConnectEndpoint = new InstanceConnectEndpoint(
* stack,
* 'InstanceConnectEndpoint',
* {
* clientToken: 'my-client-token',
* preserveClientIp: true,
* securityGroups,
* vpc,
* },
* );
*/
export class InstanceConnectEndpoint extends Resource implements IInstanceConnectEndpoint {

/**
* Import an existing endpoint to the stack from its attributes.
*/
public static fromInstanceConnectEndpointAttributes(
scope: Construct,
id: string,
attrs: InstanceConnectEndpointAttributes,
): IInstanceConnectEndpoint {
class Import extends Resource implements IInstanceConnectEndpoint {
public readonly instanceConnectEndpointId = attrs.instanceConnectEndpointId;
public readonly connections = new aws_ec2.Connections({
securityGroups: attrs.securityGroups,
});
}

return new Import(scope, id);
}

/**
* The ID of the EC2 Instance Connect Endpoint.
*/
public readonly instanceConnectEndpointId: string;

/**
* The connection object associated with the EC2 Instance Connect Endpoint.
*/
public readonly connections: aws_ec2.Connections;

private readonly props: InstanceConnectEndpointProps;
private readonly securityGroups: aws_ec2.ISecurityGroup[];

constructor(scope: Construct, id: string, props: InstanceConnectEndpointProps) {
super(scope, id);
this.props = props;

this.securityGroups = props.securityGroups ?? [this.createSecurityGroup()];

this.connections = new aws_ec2.Connections({
securityGroups: this.securityGroups,
});

const instanceConnectEndpoint = this.createInstanceConnectEndpoint();

this.instanceConnectEndpointId = instanceConnectEndpoint.attrId;
}

protected createInstanceConnectEndpoint(): aws_ec2.CfnInstanceConnectEndpoint {
return new aws_ec2.CfnInstanceConnectEndpoint(this, 'Resource', {
clientToken: this.props.clientToken,
preserveClientIp: this.props.preserveClientIp,
securityGroupIds: this.securityGroups.map(sg => sg.securityGroupId),
subnetId: this.props.vpc.selectSubnets().subnetIds[0],
});
}

protected createSecurityGroup(): aws_ec2.SecurityGroup {
return new aws_ec2.SecurityGroup(this, 'SecurityGroup', {
vpc: this.props.vpc,
});
}
}

3 changes: 2 additions & 1 deletion src/index.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
// Export constructs here
export * as aws_cur from './aws-cur';
export * as aws_cur from './aws-cur';
export * as aws_ec2 from './aws-ec2';
74 changes: 74 additions & 0 deletions test/aws-ec2/instance-connect-endpoint.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
import { App, Stack, aws_ec2 } from 'aws-cdk-lib';
import { Template } from 'aws-cdk-lib/assertions';
import { InstanceConnectEndpoint } from '../../src/aws-ec2';

describe('InstanceConnectEndpoint', () => {
let app: App;
let stack: Stack;

beforeEach(() => {
app = new App();
stack = new Stack(app, 'TestStack');
});

test('default configuration', () => {
new InstanceConnectEndpoint(stack, 'MyInstanceConnectEndpoint', {
vpc: new aws_ec2.Vpc(stack, 'VPC', {
maxAzs: 2,
}),
});

Template.fromStack(stack).hasResourceProperties('AWS::EC2::InstanceConnectEndpoint', {
SecurityGroupIds: [
{ 'Fn::GetAtt': ['MyInstanceConnectEndpointSecurityGroup99B9E814', 'GroupId'] },
],
SubnetId: { Ref: 'VPCPrivateSubnet1Subnet8BCA10E0' },
});
});

test('custom configuration', () => {
const vpc = new aws_ec2.Vpc(stack, 'VPC', {
maxAzs: 2,
});
new InstanceConnectEndpoint(stack, 'MyCustomInstanceConnectEndpoint', {
vpc,
clientToken: 'my-client-token',
preserveClientIp: false,
securityGroups: [
new aws_ec2.SecurityGroup(stack, 'SecurityGroup', {
vpc,
allowAllOutbound: false,
}),
],
});

Template.fromStack(stack).hasResourceProperties('AWS::EC2::InstanceConnectEndpoint', {
ClientToken: 'my-client-token',
PreserveClientIp: false,
SecurityGroupIds: [
{ 'Fn::GetAtt': ['SecurityGroupDD263621', 'GroupId'] },
],
SubnetId: { Ref: 'VPCPrivateSubnet1Subnet8BCA10E0' },
});
});

test('import from attributes', () => {
const vpc = new aws_ec2.Vpc(stack, 'VPC');
const securityGroup = new aws_ec2.SecurityGroup(stack, 'SecurityGroup', {
vpc,
allowAllOutbound: false,
});

const existingEndpoint = InstanceConnectEndpoint.fromInstanceConnectEndpointAttributes(
stack,
'ImportedInstanceConnectEndpoint',
{
instanceConnectEndpointId: 'my-endpoint-id',
securityGroups: [securityGroup],
},
);

expect(existingEndpoint.instanceConnectEndpointId).toEqual('my-endpoint-id');
expect(existingEndpoint.connections.securityGroups).toEqual([securityGroup]);
});
});
49 changes: 49 additions & 0 deletions test/aws-ec2/integ.instance-connect-endpoint.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import { IntegTest } from '@aws-cdk/integ-tests-alpha';
import * as cdk from 'aws-cdk-lib';
import { Construct } from 'constructs';
import * as ocf from '../../src';

class InstanceConnectEndpointStack extends cdk.Stack {
constructor(scope: Construct) {
super(scope, 'InstanceConnectEndpointStack');

const vpc = new cdk.aws_ec2.Vpc(this, 'VPC', {
maxAzs: 2,
});

const instance = new cdk.aws_ec2.Instance(this, 'Instance', {
vpc,
instanceType: cdk.aws_ec2.InstanceType.of(
cdk.aws_ec2.InstanceClass.C5,
cdk.aws_ec2.InstanceSize.LARGE,
),
machineImage: new cdk.aws_ec2.AmazonLinuxImage({
generation: cdk.aws_ec2.AmazonLinuxGeneration.AMAZON_LINUX_2023,
}),
});

const securityGroup = new cdk.aws_ec2.SecurityGroup(this, 'SecurityGroup', {
vpc,
allowAllOutbound: false,
});

const instanceConnectEndpoint = new ocf.aws_ec2.InstanceConnectEndpoint(
this,
'InstanceConnectEndpoint',
{
clientToken: 'my-client-token',
securityGroups: [securityGroup],
preserveClientIp: true,
vpc,
},
);

instanceConnectEndpoint.connections.allowTo(instance, cdk.aws_ec2.Port.tcp(22));
}
}

const app = new cdk.App();
const testCase = new InstanceConnectEndpointStack(app);
new IntegTest(app, 'InstanceConnectEndpoint', {
testCases: [testCase],
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
{
"version": "36.0.0",
"files": {
"21fbb51d7b23f6a6c262b46a9caee79d744a3ac019fd45422d988b96d44b2a22": {
"source": {
"path": "InstanceConnectEndpointDefaultTestDeployAssert284B1FD7.template.json",
"packaging": "file"
},
"destinations": {
"current_account-current_region": {
"bucketName": "cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}",
"objectKey": "21fbb51d7b23f6a6c262b46a9caee79d744a3ac019fd45422d988b96d44b2a22.json",
"assumeRoleArn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-file-publishing-role-${AWS::AccountId}-${AWS::Region}"
}
}
}
},
"dockerImages": {}
}
Loading

0 comments on commit f1fafb2

Please sign in to comment.