CFNカスタムリソースを使用してNLBのプライベートアドレスを取得し、セキュリティグループの送信元に指定する

CFNカスタムリソースを使用して、NLBのプライベートアドレスを取得して、セキュリティグループの送信元に指定する

CloudFormationカスタムリソースを使用してNLBのプライベートアドレスを取得し、セキュリティグループの送信元に指定する

ALBとは異なり、NLBにはセキュリティグループをアタッチすることはできません。

Network Load Balancer には関連付けられたセキュリティグループがありません。

ターゲットグループへのターゲットの登録

この仕様から、NLBに関連づけるEC2インスタンス用のセキュリティグループは、IPアドレスベースのルールを定義する必要があります。

今回はCloudFormationカスタムリソースを使用して、NLBのプライベートアドレスを取得し、セキュリティグループの送信元に指定することを目標とします。

構築する環境

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

NLBを作成します。
ターゲットグループにプライベートサブネット内のAuto Scalingグループを指定します。

ターゲットグループは2つのAZにまたがるように配置します。
グループにAuto Scalingグループを関連付け、内部に最新のAmazon Linux 2ベースのEC2インスタンスを作成します。
EC2インスタンスはApacheをインストールし、Webサーバとして動作させます。

Apacheをインストールするために、S3バケット上に構築されたAmazon Linux 2用のyumリポジトリにアクセスします。
このリポジトリにアクセスするために、S3用VPCエンドポイントを作成します。

Lambda関数を定義し、この関数をCloudFormationカスタムリソースとして設定します。
この関数の働きは、NLBに関連づけられたプライベートアドレスを取得することです。
関数のランタイムはPython3.8とします。

CloudFormationテンプレートファイル

上記の構成をCloudFormationで構築します。
以下のURLにCloudFormationテンプレートを配置しています。

https://github.com/awstut-an-r/awstut-fa/tree/main/093

テンプレートファイルのポイント解説

本ページはCloudFormationカスタムリソースを使用して、NLBのプライベートアドレスを取得し、セキュリティグループの送信元に指定する方法を中心に取り上げます。

CloudFormationカスタムリソースの基本的な事項については、以下のページをご確認ください。

https://awstut.com/2022/05/04/introduction-to-cloudformation-custom-resources

プライベートサブネット内のリソースをELB(ALB)にアタッチする方法については、以下のページをご確認ください。

https://awstut.com/2021/11/28/attach-private-ec2-to-elb

プライベートサブネット内のAmazon Linuxインスタンスでyumを実行する方法については、以下のページをご確認ください。

https://awstut.com/2021/12/01/yum-in-private-subnet

スケーリングポリシーなしのAuto Scalingについては、以下のページをご確認ください。

https://awstut.com/2022/10/08/introduction-to-ec2-auto-scaling-no-scaling-policy

NLBのクライアントIPの保存を無効化する

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)

ポイントはNLBターゲットグループの属性です。
今回はクライアントIPアドレスの保持を無効化します。

Network Load Balancer は、リクエストをバックエンドターゲットにルーティングするときに、クライアントのソース IP アドレスを保持できます。クライアント IP の保存を無効にした場合、Network Load Balancer のプライベート IP アドレスは、すべての受信トラフィックのクライアント IP になります。

クライアント IP の保存

本設定が有効化された状態ですと、クライアントが使用しているパブリックアドレスが送信元になるため、セキュリティグループの送信元として指定することができません。
これはクライアントの送信元アドレスが不特定多数であるためです。
ですから本設定を無効化し、NLBのプライベートアドレスを送信元とします。

CloudFormationカスタムリソースでNLBのプライベートアドレスを取得する

カスタムリソース

Resources:
  CustomResource:
    Type: Custom::CustomResource
    Properties:
      ServiceToken: !GetAtt Function.Arn
Code language: YAML (yaml)

ServiceTokenプロパティにバックエンドで動作するリソースのARNを設定します。
今回は後述のLambda関数が動作しますので、この関数のARNを設定します。

Lambda関数

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)

関数そのものの設定に特別な項目はありません。
1つ挙げるとすると、環境変数がポイントです。
環境変数としてNLB名を関数に渡します。

event[‘RequestType’]の値を参照して、スタック操作に応じた処理を実装します。
今回はこの値が「Create」または「Update」の時、つまりスタック作成時・更新時に処理を実行します。

describe_network_interfacesメソッドを実行し、AWSアカウント上に存在するネットワークインターフェース情報を取得します。
Filtersオプションを使用して、全インターフェースの中から、NLBにアタッチしているものを抽出します。
フィルタする条件は、説明文に含まれるNLB名です。
前後にワイルドカードである「*」を配置して、あいまい検索します。

リスト内包表記を使って、抽出したネットワークインターフェースの情報の中から、NLBに割り当てられたプライベートアドレスを取得します。
プライベートアドレスはNLBが関連づけられているサブネットの分だけあります。
今回の2つのパブリックアドレスがNLBに関連づけられていますので、2アドレスが取得できます。

cfnresponse.sendを使って、関数の実行完了メッセージをCloudFormationスタックに返す際に、この2アドレスもリスト型で渡します。
これでCloudFormationテンプレートから、この2アドレスにアクセスすることができるようになります。

IAMロール

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)

先述の通り、Lambda関数でNLBの情報にアクセスしますので、「ec2:DescribeNetworkInterfaces」アクションを許可します。

Outputsセクション

Outputs:
  NLBPrivateAddress1:
    Value: !Select [0, !GetAtt CustomResource.PrivateAddresses]

  NLBPrivateAddress2:
    Value: !Select [1, !GetAtt CustomResource.PrivateAddresses]
Code language: YAML (yaml)

NLBのプライベートアドレスを受け取る方法についてです。
これらのアドレスは、外部のテンプレートから参照する必要があるため、Outputsセクションに記載しますが、この記法に注意してください。
それは今回のCloudFormationカスタムリソースから返される値が配列型であるということに起因します。
Outputsセクションに定義できる値は数値か文字列のみです。
ですから組み込み関数Fn::Selectを使用し、配列から値を1つずつ取り出して出力を定義します。

セキュリティグループ

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)

セキュリティグループのルールを定義します。
NLBのプライベートアドレスを使用して、2つのルールに対して、ホストレベルでNLBを送信元として指定します。

環境構築

CloudFormationを使用して、本環境を構築し、実際の挙動を確認します。

CloudFormationスタックを作成し、スタック内のリソースを確認する

CloudFormationスタックを作成します。
スタックの作成および各スタックの確認方法については、以下のページをご確認ください。

https://awstut.com/2021/12/02/cloudformation-nested-stacks

各スタックのリソースを確認した結果、今回作成された主要リソースの情報は以下の通りです。

  • NLB:fa-093-NLB
  • NLBのDNS名:fa-093-NLB-e247ddf9edcb5d7a.elb.ap-northeast-1.amazonaws.com
  • NLBのターゲットグループ:fa-093-NLBTargetGroup
  • Lambda関数:fa-093-function
  • EC2インスタンスのセキュリティグループ:sg-04b7daf27ae507cc1

作成されたリソースをAWS Management Consoleから確認します。
NLBを確認します。

Detail of NLB 1.

NLBのDNS名等が確認できます。

ターゲットグループを確認します。

Detail of NLB 2.

ターゲットタブを見ると、2つのEC2インスタンスが登録されていることがわかります。

Detail of NLB 3.

属性タブを見ると、クライアントIPアドレスの保存が無効になっていることがわかります。

NLBに関連づいているENIを確認すると、NLBが使用しているプライベートアドレスがわかります。

Detail of NLB 4.
Detail of NLB 5.

上記の通り、今回使用しているプライベートアドレスは「10.0.1.166」「10.0.2.152」でした。

CloudFormationカスタムリソースであるLambda関数の実行ログを確認します。

Detail of CloudFormation Custom Resource 1.

正常にNLBのプライベートアドレスが取得できていることがわかります。

Detail of CloudFormation Custom Resource 2.

これらの値がLambda用スタックのOutputsにも設定されていることがわかります。

EC2インスタンスのセキュリティグループを確認します。

Detail of EC2 Security Group 1.

送信元の列を見ると、NLBのプライベートアドレスが指定されていることがわかります。
つまりCloudFormationカスタムリソースで取得した値を用いて、セキュリティグループのルールが作成されたということです。

動作確認

準備が整いましたので、NLBにアクセスします。

Detail of NLB 6.
Detail of NLB 7.

NLBターゲットグループ内のEC2インスタンスにアクセスすることができました。
以上のことから、セキュリティグループのルールが、正常に動作しているということがわかります。

まとめ

CloudFormationカスタムリソースを使用して、NLBのプライベートアドレスを取得し、セキュリティグループの送信元に指定する方法を確認しました。