Use CFN custom resource to obtain NLB private address and set it as the source of the security group

TOC

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

Diagram of using CFN Custom Resource to obtain NLB private address and set as source of security group.

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

あわせて読みたい
Introduction to CloudFormation Custom Resources 【Configuration to check behavior of CloudFormation Custom resources】 One of the features of CloudFormation is custom resources. Custom resources enable you...

For information on how to attach a resource in a private subnet to an ELB (ALB), please refer to the following page

あわせて読みたい
Attaching instances in private subnet to ALB 【Configure instances in private subnets to be attached to ALB】 We will see how to attach an instance located in a private subnet to an ALB. The following m...

For information on how to run yum on Amazon Linux instances in a private subnet, please refer to the following page

あわせて読みたい
yum/dnf on private subnet instances 【Configuration for running yum/dnf on instance in private subnet】 We will check how to run yum/dnf on an instance in a private subnet. In this case, the fo...

For information on Auto Scaling without scaling policy, please refer to the following page

あわせて読みたい
Introduction to EC2 Auto Scaling – No Scaling Policy 【Introduction to EC2 Auto Scaling - No Scaling Policy】 EC2 Auto Scaling allows you to launch any number of EC2 instances to increase the availability of yo...

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

あわせて読みたい
CloudFormation’s nested stack 【How to build an environment with a nested CloudFormation stack】 Examine nested stacks in CloudFormation. CloudFormation allows you to nest stacks. Nested ...

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.

Detail of NLB 1.

You can confirm the DNS name, etc. of the NLB.

Confirm the target group.

Detail of NLB 2.

Looking at the Target tab, you can see that two EC2 instances have been registered.

Detail of NLB 3.

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.

Detail of NLB 4.
Detail of NLB 5.

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.

Detail of CloudFormation Custom Resource 1.

You can see that the NLB private addresses were successfully obtained.

Detail of CloudFormation Custom Resource 2.

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.

Detail of EC2 Security Group 1.

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.

Detail of NLB 6.
Detail of NLB 7.

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.

TOC