Transit Gateway の新機能、ポリシーベースルーティング(PBR)を試してみた

Transit Gateway の新機能、ポリシーベースルーティング(PBR)を試してみた

2026年7月30日、Transit Gateway のポリシーベースルーティング(PBR)が GA になりました。送信元・宛先・プロトコル・ポートの条件に応じて、パケットが参照するルートテーブルを切り替えられる機能です。ルートテーブル関連付けとの排他関係、マッチ条件の挙動、CloudFormation の対応範囲を確認しました。
2026.07.31

はじめに

2026年7月30日、AWS Transit Gateway のポリシーベースルーティング(PBR)が GA になりました。

https://aws.amazon.com/about-aws/whats-new/2026/07/aws-transit-gateway-policy-based-routing/

PBR はパケットの次ホップを直接指定する機能ではありません。送信元・宛先・プロトコル・ポートの条件に応じて、そのパケットがどのルートテーブルを参照するかを選ぶ機能です。仕様はポリシーテーブルの概念、エントリの作り方はポリシーテーブルエントリの作成にまとまっています。

本記事では、既存のルートテーブル関連付けとの関係、マッチ条件の挙動と既定値、ルールの評価順、マッチしなかったパケットの扱い、CloudFormation の対応範囲を確認しました。

検証内容

検証環境

構成図

検証は us-east-1 で実施し、ポリシーテーブルは送信元 VPC-A(10.0.0.0/16)のアタッチメントに関連付けています。条件に応じて、normal と inspect のどちらかのルートテーブルが選ばれます。inspect ルートテーブルには VPC-B 宛(10.1.0.0/16)の blackhole 静的ルートがあるため、inspect が選ばれた通信は TGW で落ちます。

以降の疎通試験では、Ncat: TIMEOUT. は未到達を示します。Connection refused とペイロード(LAB-OK-80)の受信は、宛先の VPC へ到達したこと、つまり normal が選ばれたことを意味します。

ポリシーテーブルはルートテーブルと排他になる

すでにルートテーブルを関連付けているアタッチメントに、ポリシーテーブルを関連付けてみました。

aws ec2 associate-transit-gateway-policy-table \
  --transit-gateway-policy-table-id tgw-ptb-xxxxxxxxxxxxxxxxx \
  --transit-gateway-attachment-id tgw-attach-xxxxxxxxxxxxxxxxx

次のエラーになりました。

An error occurred (IncorrectState) when calling the AssociateTransitGatewayPolicyTable operation:
Cannot have both PolicyTableAssociation and RouteTableAssociation on the same TransitGateway Attachment:
tgw-attach-xxxxxxxxxxxxxxxxx

同一アタッチメントには、ポリシーテーブルとルートテーブルを同時に関連付けられません。ポリシーテーブルのドキュメントにも An attachment can be associated with either a policy table or a route table, but not both. と明記されています。

マッチ条件と既定値

マッチ条件は送信元 CIDR・送信元ポート・宛先 CIDR・宛先ポート・プロトコルの5項目で、すべて任意です。このうち宛先ポート・送信元 CIDR・送信元ポート・プロトコルの4項目については、後述の疎通試験でマッチが効きました(宛先 CIDR 単独の切り分けは実測していません)。

エントリの作成には create-transit-gateway-policy-table-entry を使います。

aws ec2 create-transit-gateway-policy-table-entry \
  --transit-gateway-policy-table-id tgw-ptb-xxxxxxxxxxxxxxxxx \
  --policy-rule-number 100 \
  --policy-rule '{"DestinationCidrBlock":"10.1.0.0/16","Protocol":"6","DestinationPortRange":"80"}' \
  --target-route-table-id tgw-rtb-xxxxxxxxxxxxxxxxx1

--policy-rule '{"Protocol":"1"}' のように一部だけを指定した場合、未指定のフィールドはレスポンスで * に補完されます。

フィールド レスポンスの値
SourceCidrBlock *
SourcePortRange *
DestinationCidrBlock *
DestinationPortRange *
Protocol 1

全項目を省略した --policy-rule '{}' も受け付けられ、全フィールドが * の状態で State: active になりました。

ポート指定にはプロトコルの制約があります。非 TCP・UDP に実ポート値を指定すると、API がバリデーションエラーを返しました。

InvalidParameterValue: SourcePortRange and DestinationPortRange can only be
specified when Protocol is TCP (6) or UDP (17)

非 TCP・UDP でも、{"Protocol":"1","DestinationPortRange":"*"} のように * を明示指定した場合は成功し、State: active になります。指定できるプロトコルは、対応外の値を渡したときのエラーメッセージから確認できます。

Protocol must be one of ICMP (1), TCP (6), UDP (17), GRE (47), or '*'

GRE も指定できる値として挙げられていますが、GRE のマッチ動作は今回検証していません。

以下の結果は、後述の「ルールは番号順に評価される」で追加するルール 50 が存在しない状態のものです。

宛先ポートは、宛先 10.1.0.0/16 / TCP のポート 80 を normal、443 を inspect に振り分けるルール 100 と 110 で試しました。全条件が * の catch-all はルール 200(normal)です。疎通試験は VPC-A の 10.0.1.100 から実行しました。表の VPC-C(10.2.0.0/16)は、inspect が選ばれても blackhole にならない対照の宛先です。

ncat -w 4 -v 10.1.1.100 80
試行 マッチするルール 選ばれる RT 結果
A → B:80(TCP) 100(dport 80) normal ペイロード受信(待ち受けあり)
A → B:443(TCP) 110(dport 443) inspect Ncat: TIMEOUT.
A → B:8080(TCP) 200(catch-all) normal Connection refused
A → C:80(TCP) 200(catch-all) normal ペイロード受信

同じ宛先・同じプロトコルでも、宛先ポート 80 は到達し 443 は落ちました。VPC-B のインスタンスは 80 と 443 の両方で待ち受けており、セキュリティグループもラボの CIDR からの全プロトコルを許可しています。443 だけが落ちるのは、ルール 110 で inspect が選ばれ、blackhole 静的ルートに当たるためです。

送信元 CIDR には、ターゲットをすべて inspect にした3ルールを使いました。いずれも宛先 10.1.0.0/16 / TCP で、宛先ポートは 9011(ルール 51)・9012(52)・9013(53)です。

試行 マッチするルール(SourceCidrBlock) 結果 判定
A → B:9011 51(10.0.1.100/32、送信元と完全一致) Ncat: TIMEOUT. inspect
A → B:9012 200(52 の 10.0.99.0/24 は送信元を含まない) Connection refused normal
A → B:9013 53(10.0.0.0/16、送信元を含む広い CIDR) Ncat: TIMEOUT. inspect
A → B:9014 200(該当ルールなし・対照) Connection refused normal

/32 の完全一致でも、送信元を含む /16 でもマッチしました。送信元を含まない CIDR では catch-all に落ちています。

送信元ポートには、単一値のルール 61(送信元ポート 44444、宛先ポート 9021)と範囲指定のルール 62(44000-45000、宛先ポート 9022)を用意しました。いずれも宛先 10.1.0.0/16 / TCP で、ターゲットは inspect です。送信元ポートは ncat-p で固定しました。

ncat -w 4 -v -p 44444 10.1.1.100 9021
試行 マッチするルール 結果 判定
ncat -p 44444 で B:9021 61(単一値に一致) Ncat: TIMEOUT. inspect
送信元ポート自動で B:9021 200(61 に不一致) Connection refused normal
ncat -p 44500 で B:9022 62(範囲内) Ncat: TIMEOUT. inspect
ncat -p 43000 で B:9022 200(62 の範囲外) Connection refused normal

送信元ポートでの振り分けは単一値と範囲指定のいずれでも効き、同じ宛先ポート 9021 でも送信元ポートが 44444 かどうかで選ばれるルートテーブルが変わりました。

プロトコルは UDP のルールを2つ作り、TCP と UDP で挙動を比べました。いずれも宛先 10.1.0.0/16 で、ルール 71 は UDP / 宛先ポート 9031 を inspect、ルール 72 は UDP / 宛先ポート 9032 を normal に振り分けます。UDP の到達結果は宛先側で確認しました。

試行 マッチするルール 結果 判定
A → B:9031(TCP) 200(UDP ルールは適用されない) Connection refused normal
A → B:9032(TCP) 200(UDP ルールは適用されない) Connection refused normal
A → B:9031(UDP、3発) 71 B に届いたパケット 0 inspect(blackhole)
A → B:9032(UDP、3発) 72 B に届いたパケット 3(応答あり) normal

同じ宛先ポートでも、TCP では UDP のルールにマッチせず、catch-all へ落ちました。UDP では 9031 が inspect、9032 が normal に分岐しました。

ルールは番号順に評価される

ルール 100・110・120 と catch-all の 200 がある状態に対して、宛先 10.1.0.0/16 / TCP で dport を * にした広いルール 50 を追加しました。測定時にポリシーテーブルへ存在したエントリは次のとおりです。

# 条件 ターゲット
50 dst 10.1.0.0/16 / TCP / dport * inspect
100 dst 10.1.0.0/16 / TCP / dport 80 normal
110 dst 10.1.0.0/16 / TCP / dport 443 inspect
120 ICMPv4 inspect
200 *(catch-all) normal
9200 dst 10.1.0.0/16 / TCP / dport 9200 normal

ルール 9200 は、後述の CloudFormation の検証で作成したエントリです。番号が大きいため評価順には影響しません。

ルール 50 を追加する前、A → B:80 はルール 100 にマッチして normal が選ばれ、到達していました。ルール 50 を追加した後、安定状態での A → B:80 は Ncat: TIMEOUT. に変わりました。

より具体的なルール 100(dport 80)は評価されず、番号の小さい広いルール 50(dport *)が先に適用されています。冒頭のポリシーテーブルの概念にも Rules in a policy table are evaluated in ascending numeric order と明記されています。ロンゲストマッチではなく番号順の first-match であり、ルートテーブルの感覚で「具体的なルールが勝つ」と考えるのは誤りです。

ルールを変更した直後は新旧の挙動が混在するため、判定はいずれも収束後の安定状態で行いました。

マッチしないパケットは破棄される

ポリシーテーブルから catch-all のルール 200 を削除し、ルール 100・110・120 の3つだけがある状態で疎通を試しました。

試行 マッチするルール 結果
A → C:80(TCP) なし Ncat: TIMEOUT.
A → B:9003(TCP) なし Ncat: TIMEOUT.
A → B:80(TCP) 100 ペイロード受信

ルートテーブルで宛先が見つからない場合と同じく、マッチするエントリがないパケットは破棄されます(暗黙の deny)。

このドロップは CloudWatch の AWS/TransitGateway 名前空間から、5分間隔の Sum で観測できます。TGW ディメンションでの結果です。

期間 PacketDropCountNoPolicy PacketDropCountBlackhole
テスト前 0 0
テスト期間 8 3
テスト後 0 0

PacketDropCountNoPolicy の 8 は、マッチするルールがない2試行(C:80 と B:9003)で送信した SYN とその再送の合計に対応します。同じ期間に記録された PacketDropCountBlackhole の 3 は、並行して実行した A → B:443(inspect が選ばれる試行)で発生したものです。

アタッチメント単位でも確認したところ、8 件はすべてポリシーテーブルを関連付けた A のアタッチメントに計上され、B と C は 0 でした。

CloudFormation の対応範囲

CloudFormation では次の3リソース型が利用できます。

  • AWS::EC2::TransitGatewayPolicyTable
  • AWS::EC2::TransitGatewayPolicyTableAssociation
  • AWS::EC2::TransitGatewayPolicyTableEntry

describe-type でレジストリへの登録を確認しました。

aws cloudformation describe-type \
  --type RESOURCE \
  --type-name AWS::EC2::TransitGatewayPolicyTableEntry

結果は次のとおりです。

TypeName: AWS::EC2::TransitGatewayPolicyTableEntry
Type: RESOURCE
ProvisioningType: FULLY_MUTABLE
TimeCreated: 2026-05-05T22:06:08.525Z
handlers: create, read, update, delete, list
required: TransitGatewayPolicyTableId, PolicyRuleNumber, PolicyRule, TargetRouteTableId

エントリ型を含むスタックを作成したところ、ルール 9200 が State: active で CREATE_COMPLETE になりました。

cfn-lint は3リソース型を認識しません。1.51.2 と 1.53.0 のいずれでも、次のエラーになります(2026年7月31日時点)。

E3006 Resource type 'AWS::EC2::TransitGatewayPolicyTable' does not exist in 'us-east-1'
E3006 Resource type 'AWS::EC2::TransitGatewayPolicyTableAssociation' does not exist in 'us-east-1'
E3006 Resource type 'AWS::EC2::TransitGatewayPolicyTableEntry' does not exist in 'us-east-1'

cfn-lint が3リソース型を認識しない一方で、CloudFormation レジストリへの登録とスタック作成は成功しています。lint の「型が存在しない」という結果は、CloudFormation の対応状況を示すものではありません。

まとめ

宛先だけでは書き分けられなかった経路分岐が、Transit Gateway 単体で組めるようになりました。特定のポートやプロトコルの通信だけを別のルートテーブルへ寄せる、送信元 CIDR で絞った一部のホストだけ別経路にするといった振り分けが、ポリシーテーブルのルールで表現できます。

一方でポリシーテーブルはアタッチメント単位でルートテーブルと排他であるため、既存環境へ例外を足す形では導入できません。切り替えるとそのアタッチメントの全通信がルールの評価対象に入り、マッチしない通信は破棄されます。既存の通信要件を洗い出してポリシールールへ書き直し、切り替え後の疎通確認までを見込んだ移行作業になります。

参考リンク

https://docs.aws.amazon.com/vpc/latest/tgw/tgw-policy-tables-limitations.html

この記事をシェアする

AWSのお困り事はクラスメソッドへ

関連記事