Maximizing the Use of EC2 Instance Connect Endpoint with CDK
Tags: typescript, node, aws
Published: 2024-05-06 09:13:59

# Introduction

Are you using the EC2 Instance Connect Endpoint (EIC Endpoint)? It's a groundbreaking service that allows you to eliminate bastion hosts for free.

https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/connect-with-ec2-instance-connect-endpoint.html

With CDK, it has become easy to set this up, so let me introduce you to it.

# Spoiler

You can establish administrative communications not only to EC2 but also to RDS (Aurora). It's incredibly convenient, so I highly recommend giving it a try.

# Preparation

While there is no official L2 construct for the EIC Endpoint, there is an L2 available in the community-driven Construct library called [open-constructs](https://github.com/open-constructs/aws-cdk-library). Therefore, we will set up a CDK project, install this, and use it.

```typescript
npx cdk init --language=typescript
npm install @open-constructs/aws-cdk
```

# Usecase

We will place an EC2 instance in a private subnet and try to manage it via SSH through the EIC Endpoint.

![connection to EC2 instance](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/d4wja8gdnpn4nfiz0vh3.png)

## Implementation example with CDK

```typescript
export class TempCdkProjectStack extends cdk.Stack {
  constructor(scope: Construct, id: string, props?: cdk.StackProps) {
    super(scope, id, props);

    // NAT Gateways and Internet Gateways are not necessary. Please remove them as appropriate.
    const vpc = new ec2.Vpc(this, 'VPC')

    // EC2 instance
    const insntance = new ec2.Instance(this, 'Instance', {
      vpc,
      instanceType: ec2.InstanceType.of(ec2.InstanceClass.T2, ec2.InstanceSize.MICRO),
      machineImage: new ec2.AmazonLinuxImage({
        generation: ec2.AmazonLinuxGeneration.AMAZON_LINUX_2023,
      })
    });

    // L2 Construct for EIC Endpoint
    const eicEndpoint = new ocf.aws_ec2.InstanceConnectEndpoint(this, 'InstanceConnectEndpoint', {
      vpc,
    });

    // Opening a hole in the Security Group from EIC Endpoint to EC2 Instance
    eicEndpoint.connections.allowTo(insntance, ec2.Port.tcp(22));

    // Output of Instance ID
    new cdk.CfnOutput(this, 'InstanceId', {
      value: insntance.instanceId,
    });
  }
}
```

## Connection method
After deploying the above CDK code, execute the following command. Please modify the instance ID to the one output during the `cdk deploy` process.

```sh
$ aws ec2-instance-connect ssh --instance-id i-12345example --connection-type eice

The authenticity of host '10.0.0.1 (<no hostip for proxy command>)' can't be established.
ED25519 key fingerprint is SHA256:abcdefg.
This key is not known by any other names.
Are you sure you want to continue connecting (yes/no/[fingerprint])? yes
Warning: Permanently added '10.0.0.1' (ED25519) to the list of known hosts.
   ,     #_
   ~\_  ####_        Amazon Linux 2023
  ~~  \_#####\
  ~~     \###|
  ~~       \#/ ___   https://aws.amazon.com/linux/amazon-linux-2023
   ~~       V~' '->
    ~~~         /
      ~~._.   _/
         _/ _/
       _/m/'
[ec2-user@ip-10-0-0-1 ~]$
```

It connected smoothly! It's fantastic.

## Administrative communication to RDS (Aurora)

Next, we will place Aurora (MySQL) in a private subnet and attempt to manage it via the EIC Endpoint. 
Although the default port for MySQL is 3306, which is not supported by the EIC Endpoint, we plan to circumvent this by changing the port that the DB listens on to 3389, a daring workaround.

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/qw057n7w5ujwbau3ku8w.png)

## Implementation example with CDK

```typescript
export class TempCdkProjectStack extends cdk.Stack {
  constructor(scope: Construct, id: string, props?: cdk.StackProps) {
    super(scope, id, props);

    const vpc = new ec2.Vpc(this, 'VPC')

    // Aurora cluster
    const auroraCluster = new rds.DatabaseCluster(this, 'Aurora', {
      engine: rds.DatabaseClusterEngine.auroraMysql({
        version: rds.AuroraMysqlEngineVersion.VER_3_06_0,
      }),
      // 【Important】 Change port to 3389 to use EIC Endpoint
      port: 3389,
      // Please use rds.Credentials.fromGeneratedSecret('hogeuser')
      credentials: rds.Credentials.fromPassword('admin', cdk.SecretValue.unsafePlainText('testPass')),
      vpc,
      writer: rds.ClusterInstance.serverlessV2('writer'),
    });

    const eicEndpoint = new ocf.aws_ec2.InstanceConnectEndpoint(this, 'InstanceConnectEndpoint', {
      vpc,
    });

    // Opening a hole in the Security Group from EIC Endpoint to EC2 Instance
    eicEndpoint.connections.allowTo(auroraCluster, ec2.Port.tcp(3389));

    // DB Endpoint (Use to get private IP)
    new cdk.CfnOutput(this, 'AuroraEndpoint', {
      value: auroraCluster.clusterEndpoint.hostname,
    });

    new cdk.CfnOutput(this, 'EicEndpointId', {
      value: eicEndpoint.instanceConnectEndpointId,
    });
  }
}
```

## Connection method
First, obtain the private IP address of the database.

```sh
$ nslookup hogehoge.cluster-cvekcubvryhp.ap-northeast-1.rds.amazonaws.com
Server:         8.8.8.8
Address:        8.8.8.8#53

Non-authoritative answer:
 hogehoge.cluster-cvekcubvryhp.ap-northeast-1.rds.amazonaws.com   canonical name =  hogehoge.cluster-cvekcubvryhp.ap-northeast-1.rds.amazonaws.com.
Name:    hogehoge.cluster-cvekcubvryhp.ap-northeast-1.rds.amazonaws.com
Address: 10.0.198.63
```

Next, set up an SSH tunnel. Please specify the parameters as follows:

|Parameter|value|
|-|-|
|--private-ip-address|Private IP of the DB|
|--instance-connect-endpoint-id|EIC Endpoint ID|
|--local-port|Local listening port (Anything is OK)|
|--remote-port|DB listening port (3389)|

```sh
$ aws ec2-instance-connect open-tunnel --instance-connect-endpoint-id eice-hogehoge --private-ip-address 10.0.198.63 --local-port 3306 --remote-port 3389
Listening for connections on port 3306.
```

The tunnel has been successfully established! With the above command still running, try connecting to localhost:3306 using any suitable DB client tool.

This time, we'll connect using Sequel Ace. The credentials are as specified in the CDK: username: admin, password: testPass.

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/4ogzlgi2hb0i0vyqlhs9.png)

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ira0o2p8d2mrb6oxnpxg.png)

Great to hear it worked! It’s fantastic, isn’t it?

This is something you can't do with Session Manager, so I think it's a major point of differentiation.

# Precautions for use

## Permissions when accessing via EIC Endpoint
Users need specific permissions for access via the EIC Endpoint. Please refer to [the official documentation](https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/permissions-for-ec2-instance-connect-endpoint.html#iam-OpenTunnel) for precise information.

By leveraging this, you can grant access permissions to specific users only. Thanks to IAM, authorization management becomes a breeze!

## Is database access included in the use cases for EIC Endpoint?

I'm not sure. It's probably a gray area.

Initially, when the EIC Endpoint service was released, all ports were open, but soon after, all ports except 22 and 3389 were closed. Therefore, I believe AWS wants us to use it only for SSH and RDP.

Currently, the EIC Endpoint only determines whether to allow or deny communication based on port numbers (Layer 4). I don't think they go as far as inspecting the payload at Layer 7 to make these decisions, but is it technically possible to do so...?? I'd appreciate it if someone with more expertise could clarify...

So, while it works, please use it at your own risk.

## Can you also access other VPC resources?

It should work as long as you have the private IP and port number within the VPC determined.

## At last

Actually, I created [a PR for this L2 construct](https://github.com/open-constructs/aws-cdk-library/pull/22)!

There are only two L2 constructs in open-constructs so far, so there are endless opportunities for contribution waiting. I encourage everyone to give it a try!