DEV Community

kazuho cryer-shinozuka
kazuho cryer-shinozuka

Posted on

Maximizing the Use of EC2 Instance Connect Endpoint with CDK

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. Therefore, we will set up a CDK project, install this, and use it.

npx cdk init --language=typescript
npm install @open-constructs/aws-cdk
Enter fullscreen mode Exit fullscreen mode

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

Implementation example with CDK

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,
    });
  }
}
Enter fullscreen mode Exit fullscreen mode

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.

$ 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 ~]$
Enter fullscreen mode Exit fullscreen mode

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

Implementation example with CDK

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,
    });
  }
}
Enter fullscreen mode Exit fullscreen mode

Connection method

First, obtain the private IP address of the database.

$ 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
Enter fullscreen mode Exit fullscreen mode

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)
$ 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.
Enter fullscreen mode Exit fullscreen mode

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

Image description

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 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!

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!

Top comments (3)

Collapse
 
wesleycheek profile image
Wesley Cheek

Wooow thank you for this great article on this super niche application! Made my day!

For users of dBeaver, check out this guide

I made this nifty Taskfile command to automate opening up the tunnel!

  localRDSConnection:
    desc: Opens a websocket that dBeaver can use to connect to my RDS database
    env:
      RDS_ENDPOINT: MY_RDS_ENDPOINT.rds.amazonaws.com
      VPC_ENDPOINT_ID: eice-MY_INSTANCE_CONNECT_ENDPOINT
      REMOTE_PORT: 3389
      LOCAL_PORT: 3306
    vars:
      IP_RDS:
        sh: "nslookup $RDS_ENDPOINT | awk -F': ' '/^Address: / { print $2 }'"
    cmds:
      - aws ec2-instance-connect open-tunnel
        --instance-connect-endpoint-id $VPC_ENDPOINT_ID
        --private-ip-address {{.IP_RDS}}
        --remote-port $REMOTE_PORT
        --local-port $LOCAL_PORT
Enter fullscreen mode Exit fullscreen mode
Collapse
 
wesleycheek profile image
Wesley Cheek • Edited

If the nslookup isn't working for you, as it wasn't for me today for some reason, you need to use the Console to get the IP_RDS value for the RDS instance private IP: serverfault.com/a/1110424

You can also ping the endpoint to find the private IP.

Here's the new script, where I need to get IP_RDS manually.

  localRDSConnection:
    desc: Opens a websocket that dBeaver can use to connect to my RDS database
    env:
      RDS_ENDPOINT: MY_RDS_ENDPOINT.rds.amazonaws.com # No longer being used..
      VPC_ENDPOINT_ID: eice-MY_INSTANCE_CONNECT_ENDPOINT
      REMOTE_PORT: 3389
      LOCAL_PORT: 3306
      IP_RDS: XX.0.0.XXX
    # vars:
    #   IP_RDS:
    #     sh: "nslookup $RDS_ENDPOINT | awk -F': ' '/^Address: / { print $2 }'"
    cmds:
      - echo {{.IP_RDS}}
      - aws ec2-instance-connect open-tunnel
        --instance-connect-endpoint-id $VPC_ENDPOINT_ID
        --private-ip-address $IP_RDS
        --remote-port $REMOTE_PORT
        --local-port $LOCAL_PORT
Enter fullscreen mode Exit fullscreen mode
Collapse
 
kazuho profile image
kazuho cryer-shinozuka

Thank you for your great example!! I often use dBeaver and I want to use this script😁