While deploying our applications on an EC2 instance, we often use the approach where we deploy the frontend of our application in the public subnet and the backend of our application in the private subnet. The perk of this approach is that we make our application more secure. The backend logic, which communicates between the frontend and the database, is not accessible to anyone in this way. However, since EC2 instances in a private subnet don’t have a public IP address, we can’t connect to them using SSH. So, to resolve this issue, we use the VPC endpoint.
The basic infrastructure for our application consists of a VPC with a public and a private subnet. We will then create the internet gateway and a NAT gateway in our VPC, and after that, we will configure the routing tables. The internet gateway allows the instances and the resources to communicate over the internet, and the NAT gateway acts as a channel between the resources in the private subnet to communicate over the internet. Then, we will deploy an instance in the private subnet. The instance deployed in the private subnet has no public IP address, so we can’t connect to it using conventional methods. To establish a connection with the private subnet, we need to create a VPC endpoint. We will create an “Instance Connect Endpoint” in the VPC to establish a connection with the EC2 instance. The infrastructure is similar to the illustration below:
Here, we will create the infrastructure using AWS CloudFormation. CloudFormation allows us to deploy infrastructure as code. Let’s construct a CloudFormation template with the required resources.
Parameters are a way to pass dynamic values into the template at runtime. They allow you to customize and modify the behavior of the resources defined in your CloudFormation template without changing it. We’ll use the parameters to pass values where they are required so that they are easy to manage and update.
Parameters:VpcCidrBlock:Type: StringDefault: "10.1.0.0/16"PublicSubnetCidrBlock:Type: StringDefault: "10.1.1.0/24"PrivateSubnetCidrBlock:Type: StringDefault: "10.1.2.0/24"InstanceAMI:Type: 'AWS::SSM::Parameter::Value<AWS::EC2::Image::Id>'Default: '/aws/service/ami-amazon-linux-latest/al2023-ami-kernel-default-x86_64'AllowedValues:- '/aws/service/ami-amazon-linux-latest/al2023-ami-kernel-default-x86_64' # Replace with your actual AMI IDInstanceType:Type: StringDefault: 't2.micro'
Here, we define the VPC CIDR block, public and private subnet CIDR blocks, the instance’s AMI, and the instance type.
We’ll create our own isolated virtual network, which will be associated with the resources.
cfnVPC:Type: AWS::EC2::VPCProperties:CidrBlock: !Ref VpcCidrBlockEnableDnsHostnames: trueEnableDnsSupport: true
Here, we create the VPC with the name cfnVPC
which uses the CIDR block VpcCidrBlock
, defined in the parameters. We enabled the DNS support and hostnames for any public instances that might reside in this network.
We’ll create an internet gateway so that our resources are accessible to the internet traffic. Creating an internet gateway allows the internet traffic to and from the VPC. We’ll also create a NAT gateway that will allow the private instance to communicate over the internet.
InternetGateway:Type: AWS::EC2::InternetGatewayNATGateway:Type: AWS::EC2::NatGatewayProperties:AllocationId: !GetAtt NATGatewayEIP.AllocationIdSubnetId: !Ref PublicSubnetAttachGateway:Type: AWS::EC2::VPCGatewayAttachmentProperties:VpcId:Ref: cfnVPCInternetGatewayId:Ref: InternetGatewayNATGatewayEIP:Type: AWS::EC2::EIP
Here, we create an internet gateway and NAT gateway. The internet gateway is defined on lines 1–2, and the NAT gateway is defined on lines 4–8. We associate the NAT gateway with a public subnet and attach an elastic IP address to the NAT gateway, which is created on lines 18–19. On lines 10–16, we attach the internet gateway to the VPC.
Subnets are essentially smaller segments of the available address space within your VPC, providing an additional layer of control and security for resources in our environment. Here, we’ll create a private and a public subnet.
PublicSubnet:Type: AWS::EC2::SubnetProperties:VpcId: !Ref cfnVPCCidrBlock: !Ref PublicSubnetCidrBlockPrivateSubnet:Type: AWS::EC2::SubnetProperties:VpcId: !Ref cfnVPCCidrBlock: !Ref PrivateSubnetCidrBlock
Here, we create public and private subnets in the cfnVPC
with the CIDR blocks defined in the parameters section.
A route table defines which external IP addresses can be reached from a subnet or internet gateway.
PrivateRouteTable:Type: AWS::EC2::RouteTableProperties:VpcId: !Ref cfnVPCPublicRoute:Type: AWS::EC2::RouteDependsOn: AttachGatewayProperties:RouteTableId: !Ref PublicRouteTableDestinationCidrBlock: "0.0.0.0/0"GatewayId: !Ref InternetGatewayPrivateRoute:Type: AWS::EC2::RouteProperties:RouteTableId: !Ref PrivateRouteTableDestinationCidrBlock: "0.0.0.0/0"NatGatewayId: !Ref NATGateway
Here, we create route tables for public and private subnets. The public route is associated with the internet gateway, directing traffic to 0.0.0.0/0
via the Internet Gateway. The private route is associated with the NAT gateway, directing traffic to 0.0.0.0/0
via the NAT Gateway.
Subnet association refers to the process of linking a subnet to a specific route table in a Virtual Private Cloud (VPC) within Amazon Web Services (AWS). This association determines the routing rules that apply to the traffic originating from the subnet.
PublicSubnetAssociation:Type: AWS::EC2::SubnetRouteTableAssociationProperties:SubnetId: !Ref PublicSubnetRouteTableId: !Ref PublicRouteTablePrivateSubnetAssociation:Type: AWS::EC2::SubnetRouteTableAssociationProperties:SubnetId: !Ref PrivateSubnetRouteTableId: !Ref PrivateRouteTable
Here, we are associating the route tables with the subnets. The PublicSubnetAssociation
resource associates the public subnet with the public route table. The PrivateSubnetAssociation
resource associates the private subnet with the private route table.
A security group functions as a virtual firewall for our instance, managing inbound and outbound traffic. The security group outlined below permits all traffic over port 22 (SSH) and port 80 (HTTP). This security group is necessary for instances in both the private and public subnets.
MySecurityGroup:Type: AWS::EC2::SecurityGroupProperties:GroupDescription: "Default security group for cfnVPC"VpcId: !Ref cfnVPCSecurityGroupIngress:- IpProtocol: tcpFromPort: 22ToPort: 22CidrIp: 0.0.0.0/0
Here, we define the security group with the identifier MySecurityGroup
and on lines 6–10, we are allowing inbound traffic on port 22, which is designated for SSH connections.
An Instance Connect Endpoint is an AWS feature that allows secure and easy access to your Amazon EC2 instances without the need to manage SSH keys or modify security group rules.
EC2Endpoint:Type: AWS::EC2::InstanceConnectEndpointDependsOn: PrivateInstanceProperties:SecurityGroupIds:- !Ref MySecurityGroupSubnetId: !Ref PrivateSubnet
Here, we are creating the EC2 endpoint. On lines 5–6, we associate it with our security group. On line 7, we associate it with the private subnet.
After all the necessary infrastructure has been defined, we can set up our EC2 instance. The EC2 instance configuration is provided below:
PrivateInstance:Type: AWS::EC2::InstanceDependsOn: NATGatewayProperties:ImageId: !Ref InstanceAMIInstanceType: t2.microSecurityGroupIds:- !GetAtt MySecurityGroup.GroupIdBlockDeviceMappings:- DeviceName: "/dev/xvda"Ebs:VolumeType: "gp2"DeleteOnTermination: "false"VolumeSize: "8"SubnetId: !Ref PrivateSubnet
Here, we are setting the instance type to t2.micro
and the AMI we are using is amazon linux 2023
which we defined in the parameter section. On lines 7–8, we attach the security group to the instance. On lines 9–14, we define the block storage associated with the instance. On line 15, we associate the instance with the private subnet.
The CloudFormation template that we are going to deploy is available in the widget below:
AWSTemplateFormatVersion: '2010-09-09'Parameters:VpcCidrBlock:Type: StringDefault: "10.1.0.0/16"PublicSubnetCidrBlock:Type: StringDefault: "10.1.1.0/24"PrivateSubnetCidrBlock:Type: StringDefault: "10.1.2.0/24"InstanceAMI:Type: 'AWS::SSM::Parameter::Value<AWS::EC2::Image::Id>'Default: '/aws/service/ami-amazon-linux-latest/al2023-ami-kernel-default-x86_64'AllowedValues:- '/aws/service/ami-amazon-linux-latest/al2023-ami-kernel-default-x86_64' # Replace with your actual AMI IDInstanceType:Type: StringDefault: 't2.micro'Resources:cfnVPC:Type: AWS::EC2::VPCProperties:CidrBlock: !Ref VpcCidrBlockEnableDnsHostnames: trueEnableDnsSupport: trueInternetGateway:Type: AWS::EC2::InternetGatewayNATGateway:Type: AWS::EC2::NatGatewayProperties:AllocationId: !GetAtt NATGatewayEIP.AllocationIdSubnetId: !Ref PublicSubnetAttachGateway:Type: AWS::EC2::VPCGatewayAttachmentProperties:VpcId:Ref: cfnVPCInternetGatewayId:Ref: InternetGatewayNATGatewayEIP:Type: AWS::EC2::EIPPublicSubnet:Type: AWS::EC2::SubnetProperties:VpcId: !Ref cfnVPCCidrBlock: !Ref PublicSubnetCidrBlockPrivateSubnet:Type: AWS::EC2::SubnetProperties:VpcId: !Ref cfnVPCCidrBlock: !Ref PrivateSubnetCidrBlockPublicRouteTable:Type: AWS::EC2::RouteTableProperties:VpcId: !Ref cfnVPCPrivateRouteTable:Type: AWS::EC2::RouteTableProperties:VpcId: !Ref cfnVPCPublicRoute:Type: AWS::EC2::RouteDependsOn: AttachGatewayProperties:RouteTableId: !Ref PublicRouteTableDestinationCidrBlock: "0.0.0.0/0"GatewayId: !Ref InternetGatewayPrivateRoute:Type: AWS::EC2::RouteProperties:RouteTableId: !Ref PrivateRouteTableDestinationCidrBlock: "0.0.0.0/0"NatGatewayId: !Ref NATGatewayPublicSubnetAssociation:Type: AWS::EC2::SubnetRouteTableAssociationProperties:SubnetId: !Ref PublicSubnetRouteTableId: !Ref PublicRouteTablePrivateSubnetAssociation:Type: AWS::EC2::SubnetRouteTableAssociationProperties:SubnetId: !Ref PrivateSubnetRouteTableId: !Ref PrivateRouteTableMySecurityGroup:Type: AWS::EC2::SecurityGroupProperties:GroupDescription: "Default security group for cfnVPC"VpcId: !Ref cfnVPCSecurityGroupIngress:- IpProtocol: tcpFromPort: 22ToPort: 22CidrIp: 0.0.0.0/0EC2Endpoint:Type: AWS::EC2::InstanceConnectEndpointDependsOn: PrivateInstanceProperties:SecurityGroupIds:- !Ref MySecurityGroupSubnetId: !Ref PrivateSubnetPrivateInstance:Type: AWS::EC2::InstanceDependsOn: NATGatewayProperties:ImageId: !Ref InstanceAMIInstanceType: t2.microSecurityGroupIds:- !GetAtt MySecurityGroup.GroupIdBlockDeviceMappings:- DeviceName: "/dev/xvda"Ebs:VolumeType: "gp2"DeleteOnTermination: "false"VolumeSize: "8"SubnetId: !Ref PrivateSubnet
You can deploy the template by clicking the “Run” button in the “Deploy stack” playground below and pasting the following command in the terminal:
aws cloudformation create-stack --stack-name cfn-template-deploy --template-body file://Cloudformation_Template.yaml --region us-east-1
In this command, cfn-template-deploy
is the name of the template we are deploying and file://CloudformationTemplate.yaml
is the path to the YAML file, which contains the blueprint of the infrastructure. This command will provide you with the stack ID.
To monitor the template, paste the following command in the terminal:
./status.sh
This command executes the shell script, which logs the stack’s status after 30 seconds interval.
Enter your AWS Access_Key_ID
and Secret_Access_Key
in the widget below before running any commands. If you don’t have these keys, follow this Answer to generate them "How to generate AWS access keys."
STACK_NAME="cfn-template-deploy" REGION="us-east-1" while true; do STATUS=$(aws cloudformation describe-stacks --stack-name $STACK_NAME --region $REGION --query "Stacks[0].StackStatus" --output text) if [ "$STATUS" == "CREATE_COMPLETE" ]; then echo "Stack creation complete." break elif [ "$STATUS" == "ROLLBACK_COMPLETE" ]; then echo "Stack creation failed. Check the CloudFormation console for details." break else echo "Current status: $STATUS" sleep 30 # Wait for 30 seconds before checking again fi done
If you already have the infrastructure deployed and you want to create the endpoint using the console, follow the steps listed below:
Search for “VPC” on the AWS Management Console, then click the “VPC” service from the search results.
Click “Endpoints” from the left menu bar and click the “Create endpoint” button.
Set the name for the endpoint in the “Name tag” section, and select “EC2 Instance Connect Endpoint” in the “Service category” section.
Choose the VPC where you intend to deploy the endpoint in the “VPC” section.
In the “Security groups” section, select the endpoint’s security group, and in the “Subnet’’ section, select the private subnet.
The slides below show how we can configure the EC2 Instance Connect Endpoint using the AWS console:
As the instance resides in the private subnet, it is not reachable via the internet or shell. Therefore, we must establish a connection using the console. Follow the steps below to connect to the EC2 instance in the private subnet:
Search for “EC2” on the AWS Management Console, then click the “EC2” service from the search results.
Click “Instances” from the left menu bar and select the instance created in the private subnet.
Click the “Connect” button, and in the “Connection type” section, select the “Connect using EC2 Instance Connect Endpoint” option.
In the “EC2 Instance Connect Endpoint” section, click “Select an endpoint” and select the available endpoint from the list.
Scroll to the bottom and click the “Connect” button.
The slides below show how we can connect to the EC2 instance using the Instance Connect Endpoint:
Free Resources