🛡️

CloudFormationでNetwork Firewall経由の通信を同一AZ内に閉じる方法

に公開

概要

CloudFormation(以下、CFn)でNetwork Firewall( AWS::NetworkFirewall::Firewall )を作成後に Fn::GetAttEndpointIds を取得できます。
しかし、 AWS::NetworkFirewall::FirewallFn::GetAttEndpointIds は順序が保証されず、単純なリスト参照ではクロスAZトラフィックが発生する可能性があります。
AWS::NetworkFirewall::Firewall Return values

EndpointIds
The unique IDs of the firewall endpoints for all of the subnets that you attached to the firewall. The subnets are not listed in any particular order. For example: ["us-west-2c:vpce-111122223333", "us-west-2a:vpce-987654321098", "us-west-2b:vpce-012345678901"].

クロスAZトラフィックを防ぐためにCFnのカスタムリソースや組み込み関数を使っていましたが、それぞれ以下の課題がありました。

方式 課題
カスタムリソース方式 呼び出すLambdaの作成・テスト・運用コストの増加
組み込み関数方式 CFnの可読性低下

このような状況の中、 AWS::NetworkFirewall::VpcEndpointAssociation リソースが2025/8/4に公開されました。
更新履歴での説明を引用します。

Document history for the AWS CloudFormation Template Reference

Use the AWS::NetworkFirewall::VpcEndpointAssociation resource to specify a VPC endpoint association to expand the protections of a firewall endpoint. You can define VPC endpoint associations only in the Availability Zones that already have a subnet mapping defined in the Firewall resource.

この新しいリソースを使うことで、CFnで通信経路を明示してクロスAZトラフィックを防止できないか確認しました。

簡単な結論

  • VpcEndpointAssociation を使うことでカスタムリソースや複雑な組み込み関数なしで通信をAZ内に閉じさせることが可能
  • 元々作られるVPCエンドポイントに加えて追加のVPCエンドポイントが作られるので、Network Firewall Endpointの料金が嵩む

確認内容

確認に使用したCFnテンプレート

CFnテンプレート
---
AWSTemplateFormatVersion: '2010-09-09'
Description: Sample AWS Network Firewall deployment with supporting VPC resources.

Parameters:
  VpcCidr:
    Type: String
    Default: 10.0.0.0/16
    Description: CIDR block for the inspection VPC.
  FirewallName:
    Type: String
    Default: sample-fw
    Description: Name to assign to the Network Firewall resources.

Resources:
  InspectionVpc:
    Type: AWS::EC2::VPC
    Properties:
      CidrBlock: !Ref VpcCidr
      EnableDnsHostnames: true
      EnableDnsSupport: true
      Tags:
        - Key: Name
          Value: !Sub "${AWS::StackName}-inspection-vpc"

  InternetGateway:
    Type: AWS::EC2::InternetGateway
    Properties:
      Tags:
        - Key: Name
          Value: !Sub "${AWS::StackName}-igw"

  VpcGatewayAttachment:
    Type: AWS::EC2::VPCGatewayAttachment
    Properties:
      InternetGatewayId: !Ref InternetGateway
      VpcId: !Ref InspectionVpc

  FirewallSubnetAz1:
    Type: AWS::EC2::Subnet
    Properties:
      VpcId: !Ref InspectionVpc
      CidrBlock: !Select [0, !Cidr [!Ref VpcCidr, 6, 8]]
      AvailabilityZone: !Select [0, !GetAZs '']
      MapPublicIpOnLaunch: false
      Tags:
        - Key: Name
          Value: !Sub "${AWS::StackName}-firewall-az1"

  FirewallSubnetAz2:
    Type: AWS::EC2::Subnet
    Properties:
      VpcId: !Ref InspectionVpc
      CidrBlock: !Select [1, !Cidr [!Ref VpcCidr, 6, 8]]
      AvailabilityZone: !Select [1, !GetAZs '']
      MapPublicIpOnLaunch: false
      Tags:
        - Key: Name
          Value: !Sub "${AWS::StackName}-firewall-az2"

  ProtectedSubnetAz1:
    Type: AWS::EC2::Subnet
    Properties:
      VpcId: !Ref InspectionVpc
      CidrBlock: !Select [2, !Cidr [!Ref VpcCidr, 6, 8]]
      AvailabilityZone: !Select [0, !GetAZs '']
      MapPublicIpOnLaunch: false
      Tags:
        - Key: Name
          Value: !Sub "${AWS::StackName}-protected-az1"

  ProtectedSubnetAz2:
    Type: AWS::EC2::Subnet
    Properties:
      VpcId: !Ref InspectionVpc
      CidrBlock: !Select [3, !Cidr [!Ref VpcCidr, 6, 8]]
      AvailabilityZone: !Select [1, !GetAZs '']
      MapPublicIpOnLaunch: false
      Tags:
        - Key: Name
          Value: !Sub "${AWS::StackName}-protected-az2"

  AdditionalSubnetAz1:
    Type: AWS::EC2::Subnet
    Properties:
      VpcId: !Ref InspectionVpc
      CidrBlock: !Select [4, !Cidr [!Ref VpcCidr, 6, 8]]
      AvailabilityZone: !Select [0, !GetAZs '']
      MapPublicIpOnLaunch: false
      Tags:
        - Key: Name
          Value: !Sub "${AWS::StackName}-additional-az1"

  AdditionalSubnetAz2:
    Type: AWS::EC2::Subnet
    Properties:
      VpcId: !Ref InspectionVpc
      CidrBlock: !Select [5, !Cidr [!Ref VpcCidr, 6, 8]]
      AvailabilityZone: !Select [1, !GetAZs '']
      MapPublicIpOnLaunch: false
      Tags:
        - Key: Name
          Value: !Sub "${AWS::StackName}-additional-az2"

  FirewallSubnetRouteTable:
    Type: AWS::EC2::RouteTable
    Properties:
      VpcId: !Ref InspectionVpc
      Tags:
        - Key: Name
          Value: !Sub "${AWS::StackName}-firewall-rt"

  FirewallSubnetRouteToInternet:
    Type: AWS::EC2::Route
    Properties:
      RouteTableId: !Ref FirewallSubnetRouteTable
      DestinationCidrBlock: 0.0.0.0/0
      GatewayId: !Ref InternetGateway
    DependsOn:
      - VpcGatewayAttachment

  FirewallSubnetAz1RouteTableAssociation:
    Type: AWS::EC2::SubnetRouteTableAssociation
    Properties:
      RouteTableId: !Ref FirewallSubnetRouteTable
      SubnetId: !Ref FirewallSubnetAz1

  FirewallSubnetAz2RouteTableAssociation:
    Type: AWS::EC2::SubnetRouteTableAssociation
    Properties:
      RouteTableId: !Ref FirewallSubnetRouteTable
      SubnetId: !Ref FirewallSubnetAz2

  AllowAllStatefulRuleGroup:
    Type: AWS::NetworkFirewall::RuleGroup
    Properties:
      Capacity: 100
      RuleGroupName: !Sub "${FirewallName}-stateful"
      Type: STATEFUL
      RuleGroup:
        RuleVariables: {}
        ReferenceSets: {}
        RulesSource:
          RulesString: |
            pass ip any any -> any any (sid:1; rev:1;)
        StatefulRuleOptions:
          RuleOrder: DEFAULT_ACTION_ORDER

  NetworkFirewallPolicy:
    Type: AWS::NetworkFirewall::FirewallPolicy
    Properties:
      FirewallPolicyName: !Sub "${FirewallName}-policy"
      FirewallPolicy:
        StatelessDefaultActions:
          - aws:forward_to_sfe
        StatelessFragmentDefaultActions:
          - aws:forward_to_sfe
        StatefulEngineOptions:
          RuleOrder: DEFAULT_ACTION_ORDER
        StatefulRuleGroupReferences:
          - ResourceArn: !Ref AllowAllStatefulRuleGroup

  NetworkFirewall:
    Type: AWS::NetworkFirewall::Firewall
    Properties:
      DeleteProtection: false
      FirewallName: !Ref FirewallName
      FirewallPolicyArn: !Ref NetworkFirewallPolicy
      SubnetChangeProtection: false
      VpcId: !Ref InspectionVpc
      SubnetMappings:
        - SubnetId: !Ref FirewallSubnetAz1
        - SubnetId: !Ref FirewallSubnetAz2

  RouteTableAz1:
    Type: AWS::EC2::RouteTable
    Properties:
      VpcId: !Ref InspectionVpc
      Tags:
        - Key: Name
          Value: !Sub "${AWS::StackName}-protected-rt-az1"

  RouteTableAz2:
    Type: AWS::EC2::RouteTable
    Properties:
      VpcId: !Ref InspectionVpc
      Tags:
        - Key: Name
          Value: !Sub "${AWS::StackName}-protected-rt-az2"

  AdditionalSubnetAz1Association:
    Type: AWS::EC2::SubnetRouteTableAssociation
    Properties:
      RouteTableId: !Ref FirewallSubnetRouteTable
      SubnetId: !Ref AdditionalSubnetAz1

  AdditionalSubnetAz2Association:
    Type: AWS::EC2::SubnetRouteTableAssociation
    Properties:
      RouteTableId: !Ref FirewallSubnetRouteTable
      SubnetId: !Ref AdditionalSubnetAz2

  ProtectedSubnetAz1Association:
    Type: AWS::EC2::SubnetRouteTableAssociation
    Properties:
      RouteTableId: !Ref RouteTableAz1
      SubnetId: !Ref ProtectedSubnetAz1

  ProtectedSubnetAz2Association:
    Type: AWS::EC2::SubnetRouteTableAssociation
    Properties:
      RouteTableId: !Ref RouteTableAz2
      SubnetId: !Ref ProtectedSubnetAz2

  RouteToFirewallAz1:
    Type: AWS::EC2::Route
    Properties:
      RouteTableId: !Ref RouteTableAz1
      DestinationCidrBlock: 0.0.0.0/0
      VpcEndpointId: !GetAtt FirewallEndpointAssociationAz1.EndpointId

  RouteToFirewallAz2:
    Type: AWS::EC2::Route
    Properties:
      RouteTableId: !Ref RouteTableAz2
      DestinationCidrBlock: 0.0.0.0/0
      VpcEndpointId: !GetAtt FirewallEndpointAssociationAz2.EndpointId

  FirewallEndpointAssociationAz1:
    Type: AWS::NetworkFirewall::VpcEndpointAssociation
    Properties:
      FirewallArn: !Ref NetworkFirewall
      VpcId: !Ref InspectionVpc
      SubnetMapping:
        SubnetId: !Ref AdditionalSubnetAz1

  FirewallEndpointAssociationAz2:
    Type: AWS::NetworkFirewall::VpcEndpointAssociation
    Properties:
      FirewallArn: !Ref NetworkFirewall
      VpcId: !Ref InspectionVpc
      SubnetMapping:
        SubnetId: !Ref AdditionalSubnetAz2

Outputs:
  VpcId:
    Description: VPC containing the Network Firewall deployment.
    Value: !Ref InspectionVpc
  FirewallArn:
    Description: ARN of the deployed Network Firewall.
    Value: !Ref NetworkFirewall
  FirewallEndpointIds:
    Description: Endpoint IDs generated for the firewall subnets.
    Value: !Join [",", !GetAtt NetworkFirewall.EndpointIds]
  AdditionalSubnetIds:
    Description: Subnets associated via VpcEndpointAssociation for workload routing.
    Value: !Join [",", [!Ref AdditionalSubnetAz1, !Ref AdditionalSubnetAz2]]

CFnテンプレートの説明

NetworkFirewallの追加のVPCエンドポイントを AdditionalSubnetAz1 に作成します。(以降ではAZ1を例示していますが、AZ2側も同様です。)

  FirewallEndpointAssociationAz1:
    Type: AWS::NetworkFirewall::VpcEndpointAssociation
    Properties:
      FirewallArn: !Ref NetworkFirewall
      VpcId: !Ref InspectionVpc
      SubnetMapping:
        SubnetId: !Ref AdditionalSubnetAz1

全ての宛先(0.0.0.0/0)への通信を上記の FirewallEndpointAssociationAz1 で作成したVPCエンドポイントに送るようにルートを設定します。
このVPCエンドポイントに送ることでNetwork Firewallで検査されます。

  RouteToFirewallAz1:
    Type: AWS::EC2::Route
    Properties:
      RouteTableId: !Ref RouteTableAz1
      DestinationCidrBlock: 0.0.0.0/0
      VpcEndpointId: !GetAtt FirewallEndpointAssociationAz1.EndpointId

EC2やECSなどが起動する ProtectedSubnetAz1 に上記の RouteTableAz1 を関連づけます。
これによりAZ1のEC2からの通信は、Network FirewallのAZ1に追加したサブネットに通信が送られ、AZ2側に通信しない設定が可能となります。

  ProtectedSubnetAz1Association:
    Type: AWS::EC2::SubnetRouteTableAssociation
    Properties:
      RouteTableId: !Ref RouteTableAz1
      SubnetId: !Ref ProtectedSubnetAz1

トラフィックフロー図

VpcEndpointAssociation を使用した場合の通信経路を図示します。

この構成により、AZ1のEC2インスタンスからの通信は以下の経路を辿り、AZ1内で完結します。

  1. ProtectedSubnetAz1 のEC2インスタンスから発信
  2. RouteTableAz1 のルートに従いVPCエンドポイント(AZ1)に転送
  3. Network Firewall(AZ1)で検査
  4. Internet Gatewayを経由してインターネットへ

各方式の比較

比較項目 追加エンドポイント方式 組み込み関数方式 カスタムリソース方式
同一AZ担保
コスト ×(追加のエンドポイントの費用がかかる)
実装・保守性 △(2AZならそこまで複雑にならないが、3AZになると複雑) ×(Lambdaの実装・テスト・保守が必要)
3AZ以上へのスケール ×(CFnの可読性低下)

所感

以下が許容できるなら、追加エンドポイント方式はCFnの可読性、Lambdaの保守が不要というメリットがあり、実装・保守性の観点では有力な選択肢です。

  • Network Firewall Endpointが増えることによる費用増
    • 参考価格:東京リージョンで1エンドポイントあたりUSD 0.395/時間
  • Network Firewallでデフォルト作成されるエンドポイントを未使用とする構成上の違和感

参考

https://docs.aws.amazon.com/ja_jp/AWSCloudFormation/latest/TemplateReference/aws-resource-networkfirewall-firewall.html#aws-resource-networkfirewall-firewall-return-values
https://docs.aws.amazon.com/ja_jp/AWSCloudFormation/latest/TemplateReference/aws-resource-networkfirewall-vpcendpointassociation.html
https://aws.amazon.com/jp/network-firewall/pricing/

Discussion