AWS CDK for EC2 bastion host sharing Elastic IP with rest of VPC
Here’s a small CDK stack to set up an EC2 instance as a bastion host inside a VPC, sharing a fixed Elastic IP with other services in the VPC via the NAT Gateway.
A bastion host is useful for connecting to your own services inside the VPC. With this particular setup, you can also use the same bastion host to connect to external services that require a fixed IP address on your side.
In other words, this allows you to forward ports through the VPC to connect to external services which apply an IP access list, such as MongoDB Atlas, using the same IP address as your other services.
import * as cdk from "aws-cdk-lib";
import { Construct } from "constructs";
import * as ec2 from "aws-cdk-lib/aws-ec2";
import fs from "fs";
/**
* VPC, bastion host and fixed Elastic IP for all outbound internet traffic from
* the VPC. This means that external services see the bastion host as connecting
* from the same IP address as other resources in the VPC.
*/
export class BastionStack extends cdk.Stack {
constructor(scope: Construct, id: string, props: cdk.StackProps) {
super(scope, id, props);
// Elastic IP for accessing external services with an IP access list.
const elasticIp = new ec2.CfnEIP(this, "ElasticIP", {
domain: "vpc",
});
// Example of VPC set up. In practice, the VPC is likely to have been set up
// separately to this stack. This is a configuration for reference.
const vpc = new ec2.Vpc(this, "VPC", {
maxAzs: 1,
natGateways: 1,
// NAT Gateway with Elastic IP, so that all outbound internet traffic uses
// the Elastic IP.
natGatewayProvider: ec2.NatProvider.gateway({
eipAllocationIds: [elasticIp.attrAllocationId],
}),
});
// EC2 instance bastion host.
// Note that SSM Session Manager is the only way to connect to this instance.
const bastionHost = new ec2.BastionHostLinux(this, "BastionHost", {
vpc,
subnetSelection: {
// Private subnet for the bastion host so that it uses the NAT Gateway,
// which has the Elastic IP attached.
subnetType: ec2.SubnetType.PRIVATE_WITH_EGRESS,
},
instanceType: ec2.InstanceType.of(
ec2.InstanceClass.T3,
ec2.InstanceSize.MICRO
),
machineImage: ec2.MachineImage.latestAmazonLinux2023(),
});
// Optional: user data script to install packages on bastion host.
bastionHost.instance.addUserData(
fs.readFileSync(
"path/to/ec2-bastion-host-setup.sh",
"utf8"
)
);
// Example: alternative if you want the Elastic IP to apply to the bastion
// host only. The bastion host could then be in a public subnet and have its
// own independent static IP address.
// new ec2.CfnEIPAssociation(this, "ElasticIpAssociation", {
// allocationId: elasticIp.attrAllocationId,
// instanceId: bastionHost.instance.instanceId,
// });
new cdk.CfnOutput(this, "BastionHostInstanceId", {
value: bastionHost.instance.instanceId,
});
new cdk.CfnOutput(this, "ElasticIp", {
value: elasticIp.ref,
});
}
}
Get in touch if you need assistance with AWS at your organisation on a freelance basis.