Use CFN custom resource to obtain NLB private address and set it as the source of the security group
Unlike ALB, security groups cannot be attached to NLB.
Network Load Balancers do not have associated security groups.
Register targets with your target group
From this specification, the security group for the EC2 instance to be associated with the NLB must be defined with IP address based rules.
In this case, we will use a CloudFormation custom resource to obtain the private address of the NLB and specify it as the source of the security group.
Environment
Create an NLB.
Specify an Auto Scaling group in private subnets as the target group.
Place the target group across two AZs.
Associate the Auto Scaling group with the group and create a modern Amazon Linux 2-based EC2 instance inside.
The EC2 instance will have Apache installed and will act as a web server.
To install Apache, access the yum repository for Amazon Linux 2 built on an S3 bucket.
Create a VPC endpoint for S3 to access this repository.
Define a Lambda function and configure this function as a CloudFormation custom resource.
The function’s action is to retrieve the private address associated with the NLB.
The runtime for the function is Python 3.8.
CloudFormation template files
The above configuration is built with CloudFormation.
The CloudFormation templates are located at the following URL
https://github.com/awstut-an-r/awstut-fa/tree/main/093
Explanation of key points of the template files
This page focuses on how to use CloudFormation custom resources to obtain private addresses for NLB and designate it as the source of the security group.
For more information on the basics of CloudFormation custom resources, please refer to the following page
For information on how to attach a resource in a private subnet to an ELB (ALB), please refer to the following page
For information on how to run yum on Amazon Linux instances in a private subnet, please refer to the following page
For information on Auto Scaling without scaling policy, please refer to the following page
Disabling Client IP Preservation
Resources:
NLBTargetGroup:
Type: AWS::ElasticLoadBalancingV2::TargetGroup
Properties:
Name: !Sub "${Prefix}-NLBTargetGroup"
Protocol: TCP
Port: !Ref HTTPPort
TargetGroupAttributes:
- Key: preserve_client_ip.enabled
Value: false
VpcId: !Ref VPC
Code language: YAML (yaml)
The point is the NLB target group attribute.
In this case, we will disable the client IP address preservation.
Network Load Balancers can preserve the source IP address of clients when routing requests to backend targets. When you disable client IP preservation, the private IP address of the Network Load Balancer becomes the client IP address for all incoming traffic.
Client IP preservation
When this setting is enabled, the public address used by the client is the source and cannot be specified as the source of the security group.
This is because the source address of the client is unspecified.
Therefore, disable this setting and use the NLB’s private address as the source.
Getting NLB Private Address in CloudFormation Custom Resource
Custom Resource
Resources:
CustomResource:
Type: Custom::CustomResource
Properties:
ServiceToken: !GetAtt Function.Arn
Code language: YAML (yaml)
Set the ARN of the resource to be used for the backend action to the ServiceToken property.
In this case, the Lambda function described below will be used for the action, so set the ARN for this function.
Lambda function
Resources:
Function:
Type: AWS::Lambda::Function
Properties:
Code:
ZipFile: |
import boto3
import cfnresponse
import os
nlb_loadbalancer_name = os.environ['NLB_LOADBALANCER_NAME']
filter_value = '*{nlb}*'.format(nlb=nlb_loadbalancer_name)
client = boto3.client('ec2')
CREATE = 'Create'
UPDATE = 'Update'
response_data = {}
def lambda_handler(event, context):
try:
if event['RequestType'] == CREATE or event['RequestType'] == UPDATE:
response = client.describe_network_interfaces(
Filters=[
{
'Name':'description',
'Values':[
filter_value
]
}
]
)
private_addresses = [interface['PrivateIpAddress'] for interface in response['NetworkInterfaces']]
response_data['PrivateAddresses'] = private_addresses
cfnresponse.send(event, context, cfnresponse.SUCCESS, response_data)
except Exception as e:
print(e)
cfnresponse.send(event, context, cfnresponse.FAILED, response_data)
Environment:
Variables:
NLB_LOADBALANCER_NAME: !Ref NLBLoadBalancerName
FunctionName: !Sub "${Prefix}-function"
Handler: !Ref Handler
Runtime: !Ref Runtime
Role: !GetAtt FunctionRole.Arn
Code language: YAML (yaml)
There are no special items in the configuration of the function itself.
One point to mention is the environment variable.
The NLB name is passed to the function as an environment variable.
Refer to the value of event[‘RequestType’] to implement the processing according to the stack operation.
This time, when this value is “Create” or “Update,” that is, when the stack is created or updated, the processing is executed.
Execute the describe_network_interfaces method to obtain information about the network interfaces that exist on the AWS account.
Use the Filters option to extract those that are attached to the NLB among all interfaces.
The condition to filter is the NLB name in the description.
Place the wildcard “*” before or after to search for ambiguity.
Using list comprehensions, retrieve the private addresses assigned to the NLBs from the extracted network interface information.
There are as many private addresses as there are subnets with which the NLB is associated.
Since the two public addresses in this case are associated with the NLB, two addresses can be obtained.
When we use cfnresponse.send to return a function execution completion message to the CloudFormation stack, we also pass these two addresses as a list type.
The two addresses can then be accessed from the CloudFormation template.
IAM Role
Resources:
FunctionRole:
Type: AWS::IAM::Role
Properties:
AssumeRolePolicyDocument:
Version: 2012-10-17
Statement:
- Effect: Allow
Action: sts:AssumeRole
Principal:
Service:
- lambda.amazonaws.com
ManagedPolicyArns:
- arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole
Policies:
- PolicyName: !Sub "${Prefix}-DescribeNetworkInterfaces"
PolicyDocument:
Version: 2012-10-17
Statement:
- Effect: Allow
Action:
- ec2:DescribeNetworkInterfaces
Resource: "*"
Code language: YAML (yaml)
As mentioned earlier, the Lambda function will access NLB information, so allow the “ec2:DescribeNetworkInterfaces” action.
Outputs section
Outputs:
NLBPrivateAddress1:
Value: !Select [0, !GetAtt CustomResource.PrivateAddresses]
NLBPrivateAddress2:
Value: !Select [1, !GetAtt CustomResource.PrivateAddresses]
Code language: YAML (yaml)
This section describes how to receive the private addresses of the NLB.
These addresses are listed in the Outputs section because they need to be referenced from an external template, but note the notation.
This is due to the fact that the values returned from this CloudFormation custom resource are of array type.
Only numbers or strings can be defined in the Outputs section.
Therefore, use the built-in function Fn::Select to define the outputs by retrieving the values from the array one by one.
Security Group
Resources:
InstanceSecurityGroup:
Type: AWS::EC2::SecurityGroup
Properties:
GroupName: !Sub "${Prefix}-InstanceSecurityGroup"
GroupDescription: Allow HTTP from NLB.
VpcId: !Ref VPC
SecurityGroupIngress:
- IpProtocol: tcp
FromPort: !Ref HTTPPort
ToPort: !Ref HTTPPort
CidrIp: !Sub "${NLBPrivateAddress1}/32"
- IpProtocol: tcp
FromPort: !Ref HTTPPort
ToPort: !Ref HTTPPort
CidrIp: !Sub "${NLBPrivateAddress2}/32"
Code language: YAML (yaml)
Define rules for security groups.
Specify NLB as the source at the host level for the two rules, using NLB’s private address.
Architecting
Use CloudFormation to build this environment and verify actual behavior.
Create CloudFormation stacks and check resources in stacks
Create CloudFormation stacks.
For information on how to create stacks and check each stack, please refer to the following page
After checking the resources in each stack, information on the main resources created this time is as follows
- NLB: fa-093-NLB
- DNS name of NLB: fa-093-NLB-e247ddf9edcb5d7a.elb.ap-northeast-1.amazonaws.com
- NLB target group: fa-093-NLBTargetGroup
- Lambda function: fa-093-function
- EC2 instance security group: sg-04b7daf27ae507cc1
Confirm the created resource from the AWS Management Console.
Confirm the NLB.
You can confirm the DNS name, etc. of the NLB.
Confirm the target group.
Looking at the Target tab, you can see that two EC2 instances have been registered.
The Attribute tab shows that the client IP preservation has been disabled.
Check the ENI associated with the NLB to see the private address used by the NLB.
As shown above, the private addresses used in this case were 10.0.1.166 and 10.0.2.152.
Check the execution log of the Lambda function, which is a CloudFormation custom resource.
You can see that the NLB private addresses were successfully obtained.
You can see that these values are also set in the Outputs of the stack for Lambda.
Check the security group of the EC2 instance.
If you look at the Source column, you will see that the NLB private address is specified.
This means that the rules for the security group were created using the values obtained from the CloudFormation custom resource.
Check Action
Now that we are ready, we can access the NLB.
We are now able to access the EC2 instance in the NLB target group.
From the above, we can see that the security group rules are working properly.
Summary
We have seen how to use a CloudFormation custom resource to obtain the private address of the NLB and designate it as the source of the security group.