Transit Gateway の新機能、ポリシーベースルーティング(PBR)を試してみた
はじめに
2026年7月30日、AWS Transit Gateway のポリシーベースルーティング(PBR)が GA になりました。
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::TransitGatewayPolicyTableAWS::EC2::TransitGatewayPolicyTableAssociationAWS::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 で絞った一部のホストだけ別経路にするといった振り分けが、ポリシーテーブルのルールで表現できます。
一方でポリシーテーブルはアタッチメント単位でルートテーブルと排他であるため、既存環境へ例外を足す形では導入できません。切り替えるとそのアタッチメントの全通信がルールの評価対象に入り、マッチしない通信は破棄されます。既存の通信要件を洗い出してポリシールールへ書き直し、切り替え後の疎通確認までを見込んだ移行作業になります。
参考リンク







