CloudFormationカスタムリソースを使用してNLBのプライベートアドレスを取得し、セキュリティグループの送信元に指定する
ALBとは異なり、NLBにはセキュリティグループをアタッチすることはできません。
Network Load Balancer には関連付けられたセキュリティグループがありません。
ターゲットグループへのターゲットの登録
この仕様から、NLBに関連づけるEC2インスタンス用のセキュリティグループは、IPアドレスベースのルールを定義する必要があります。
今回はCloudFormationカスタムリソースを使用して、NLBのプライベートアドレスを取得し、セキュリティグループの送信元に指定することを目標とします。
構築する環境
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カスタムリソースの基本的な事項については、以下のページをご確認ください。
プライベートサブネット内のリソースをELB(ALB)にアタッチする方法については、以下のページをご確認ください。
プライベートサブネット内のAmazon Linuxインスタンスでyumを実行する方法については、以下のページをご確認ください。
スケーリングポリシーなしのAuto Scalingについては、以下のページをご確認ください。
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スタックを作成します。
スタックの作成および各スタックの確認方法については、以下のページをご確認ください。
各スタックのリソースを確認した結果、今回作成された主要リソースの情報は以下の通りです。
- 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を確認します。
NLBのDNS名等が確認できます。
ターゲットグループを確認します。
ターゲットタブを見ると、2つのEC2インスタンスが登録されていることがわかります。
属性タブを見ると、クライアントIPアドレスの保存が無効になっていることがわかります。
NLBに関連づいているENIを確認すると、NLBが使用しているプライベートアドレスがわかります。
上記の通り、今回使用しているプライベートアドレスは「10.0.1.166」「10.0.2.152」でした。
CloudFormationカスタムリソースであるLambda関数の実行ログを確認します。
正常にNLBのプライベートアドレスが取得できていることがわかります。
これらの値がLambda用スタックのOutputsにも設定されていることがわかります。
EC2インスタンスのセキュリティグループを確認します。
送信元の列を見ると、NLBのプライベートアドレスが指定されていることがわかります。
つまりCloudFormationカスタムリソースで取得した値を用いて、セキュリティグループのルールが作成されたということです。
動作確認
準備が整いましたので、NLBにアクセスします。
NLBターゲットグループ内のEC2インスタンスにアクセスすることができました。
以上のことから、セキュリティグループのルールが、正常に動作しているということがわかります。
まとめ
CloudFormationカスタムリソースを使用して、NLBのプライベートアドレスを取得し、セキュリティグループの送信元に指定する方法を確認しました。