Transit Gatewayでインスペクション用VPCに通信を集約する場合はTransit Gatewayのアプライアンスモードを有効にしよう
Transit GatewayのVPC間でAZを跨いだ通信は注意しよう
こんにちは、のんピ(@non____97)です。
皆さんはTransit Gatewayを完全に理解していますか? 私は以下のTweetの通り、完全に理解しています。
無事、案件どーん
Transit Gateway完全に理解した(n回目)
— のんピ / non-97 (@non____97) August 19, 2021
Transit Gatewayを完全に理解するにあたって、個人的に考える一番の落とし穴は、VPC間でAZを跨いだ通信です。
Transit GatewayのBlack Beltでも紹介されている通り、「VPC間の通信は同一AZのENI(Transit Gateway attachment)を経由して通信が行われる」と紹介されています。
下の画像では、行きの通信と、戻りの通信のAZが異なっていることが図示されています。
抜粋: [AWS Black Belt Online Seminar] AWS Transit Gateway
このように、行きの通信と、戻りの通信のAZが異なっていると困る場面は、間にさらに別のVPCを経由させている場合です。
こちらのAWS公式ドキュメントで紹介している通り、以下の構成図では、後述するアプライアンスモードを有効にしていない場合、VPC AのEC2インスタンスから、VPC BのEC2インスタンスに通信ができません。
抜粋: 例: 共有サービス VPC のアプライアンス - ステートフルアプライアンスおよびアプライアンスモード
通信できない理由を把握するために、図中の破線を辿る形で、「VPC AのEC2インスタンスから、VPC BのEC2インスタンスへの通信フロー」を以下に整理します。
- VPC AのEC2インスタンスから、Availability Zone 1のENI(※ 図には記載されていない)及び、Transit Gateway、VPC CのAvailability Zone 1にあるENIを経由して、Availability Zone 1のApplianceに通信が到達する
- Availability Zone 1のApplianceは通信内容が問題なければ、本来の送信先であるVPC BのEC2インスタンス宛にリクエストを投げる。
- Availability Zone 1のApplianceの通信は、VPC CのAvailability Zone 1にあるENI及び、Transit Gateway、VPC BのAvailability Zone 1のENI(※ 図には記載されていない)を経由して、VPC BのEC2インスタンスに到達する。
- VPC BのEC2インスタンスはVPC AのEC2インスタンスにレスポンスを送信する。
- VPC BのEC2インスタンスから、Availability Zone 2のENI(※ 図には記載されていない)及び、Transit Gateway、VPC CのAvailability Zone 2にあるENIを経由して、Availability Zone 2のApplianceに通信が到達する
- Availability Zone 2のApplianceは、VPC AのEC2インスタンスからのリクエストを受け取っていないため、この通信がVPC BのEC2インスタンスからのレスポンスとして認識できず、ドロップする。
このような場合に活躍するのが、Transit Gatewayのアプライアンスモードです。
Transit Gatewayのアプライアンスモードとは
Transit Gatewayのアプライアンスモードは一言で言うと、「ルーティングが対照的になるようにする機能」です。
AWS公式ドキュメントでは、Transit Gatewayのアプライアンスモードは以下のように説明されています。
アプライアンスモードが有効な場合、トランジットゲートウェイは、フローハッシュアルゴリズムを使用して、アプライアンス VPC 内の 1 つのネットワークインターフェイスを選択し、フローの有効期間中トラフィックを送信します。トランジットゲートウェイは、リターントラフィックに同じネットワークインターフェイスを使用します。これにより、双方向トラフィックは対称的にルーティングされます。つまり、フローの有効期間中、VPC アタッチメント内の同じアベイラビリティーゾーンを経由してルーティングされます。アーキテクチャ内に複数のトランジットゲートウェイがある場合、各トランジットゲートウェイは独自のセッションアフィニティを維持し、各トランジットゲートウェイは異なるネットワークインターフェイスを選択できます。
VPC アタッチメントが複数のアベイラビリティーゾーンにまたがっており、ステートフルな検査のために送信元ホストと送信先ホスト間のトラフィックを同じアプライアンスを介してルーティングする必要がある場合は、アプライアンスが配置されている VPC アタッチメントのアプライアンスモードサポートを有効にします。
詳細については、AWSブログの一元化された検査アーキテクチャを参照してください。
アプライアンスモードを有効にすることで、以下のように、通信先が異なるAZであっても、戻りの通信は行きの通信と同じ経路を辿るようになります。 抜粋: 例: 共有サービス VPC のアプライアンス - Overview
検証の構成
今回の検証の構成は以下の通りです。
2つのAZを使って、以下4つのVPCを作成し、Transit Gatewayに接続しています。
- Spoke VPC A: 通信元のEC2インスタンスがいるVPC
- Spoke VPC B: 通信先のEC2インスタンスがいるVPC
- Inspection VPC: Network FirewallがいるVPC
- Egress VPC: インターネットに出ていくためのVPC
検証では、以下2つの通信パターンを試してみます。
- Spoke VPC AのEC2インスタンスAから、Spoke VPC BのEC2インスタンスAへの通信
-
Spoke VPC AのEC2インスタンスAから、Spoke VPC BのEC2インスタンスBへの通信
やってみた
各種リソースのデプロイ
私は大量のVPCを真心を込めて構築するタイプではないので、AWS CDKで各種リソースをデプロイします。
ただし、Transit Gatewayのアプライアンスモードの有効化は、AWS CDK及び、CloudFormationでサポートされていなかったため、デプロイ後にAWS CLIを使って有効化します。
なお、AWS CDKのコードはかなり長くなってしまったので、以下に折りたたみます。
AWS CDK関連の情報
> tree 金 8/20 13:27:14 2021 . ├── .gitignore ├── .npmignore ├── .vscode │ └── settings.json ├── README.md ├── bin │ └── tgw-appliance-mode-network-firewall.ts ├── cdk.json ├── jest.config.js ├── lib │ └── tgw-appliance-mode-network-firewall-stack.ts ├── package-lock.json ├── package.json ├── test │ └── tgw-appliance-mode-network-firewall.test.ts └── tsconfig.json
import * as cdk from "@aws-cdk/core"; import * as ec2 from "@aws-cdk/aws-ec2"; import * as logs from "@aws-cdk/aws-logs"; import * as iam from "@aws-cdk/aws-iam"; import * as networkfirewall from "@aws-cdk/aws-networkfirewall"; export class TgwApplianceModeNetworkFirewallStack extends cdk.Stack { constructor(scope: cdk.Construct, id: string, props?: cdk.StackProps) { super(scope, id, props); // Get the string after the stack name in the stack id to append to the end of the Log Group name to make it unique. const stackId = new cdk.ScopedAws(this).stackId; const stackIdAfterStackName = cdk.Fn.select(2, cdk.Fn.split("/", stackId)); // Create CloudWatch Logs for VPC Flow Logs const flowLogsLogGroup = new logs.LogGroup(this, "FlowLogsLogGroup", { logGroupName: `/aws/vendedlogs/vpcFlowLogs-${stackIdAfterStackName}`, retention: logs.RetentionDays.ONE_WEEK, }); // Create CloudWatch Logs for Network Firewall Flow Logs const networkFirewallFlowLogsLogGroup = new logs.LogGroup( this, "NetworkFirewallFlowLogsLogGroup", { logGroupName: `/aws/vendedlogs/networkFirewallFlowLogs-${stackIdAfterStackName}`, retention: logs.RetentionDays.ONE_WEEK, } ); // Create CloudWatch Logs for Network Firewall Alert Logs const networkFirewallAlertLogsLogGroup = new logs.LogGroup( this, "NetworkFirewallAlertLogsLogGroup", { logGroupName: `/aws/vendedlogs/networkFirewallAlertLogs-${stackIdAfterStackName}`, retention: logs.RetentionDays.ONE_WEEK, } ); // Create VPC Flow Logs IAM role const flowLogsIamrole = new iam.Role(this, "FlowLogsIamrole", { assumedBy: new iam.ServicePrincipal("vpc-flow-logs.amazonaws.com"), }); // Create SSM IAM role const ssmIamRole = new iam.Role(this, "SsmIamRole", { assumedBy: new iam.ServicePrincipal("ec2.amazonaws.com"), managedPolicies: [ iam.ManagedPolicy.fromAwsManagedPolicyName( "AmazonSSMManagedInstanceCore" ), ], }); // Create VPC Flow Logs IAM Policy const flowLogsIamPolicy = new iam.Policy(this, "FlowLogsIamPolicy", { statements: [ new iam.PolicyStatement({ effect: iam.Effect.ALLOW, actions: ["iam:PassRole"], resources: [flowLogsIamrole.roleArn], }), new iam.PolicyStatement({ effect: iam.Effect.ALLOW, actions: [ "logs:CreateLogStream", "logs:PutLogEvents", "logs:DescribeLogStreams", ], resources: [flowLogsLogGroup.logGroupArn], }), ], }); // Atach VPC Flow Logs IAM Policy flowLogsIamrole.attachInlinePolicy(flowLogsIamPolicy); // Create a VPC for inspection const inspectionVpc = new ec2.Vpc(this, "InspectionVpc", { cidr: "10.0.0.0/24", enableDnsHostnames: true, enableDnsSupport: true, maxAzs: 2, subnetConfiguration: [ { name: "TgwAttachment", subnetType: ec2.SubnetType.ISOLATED, cidrMask: 28, }, { name: "Firewall", subnetType: ec2.SubnetType.ISOLATED, cidrMask: 28, }, ], }); // Setting VPC Flow Logs for InspectionVpc new ec2.CfnFlowLog(this, "FlowLogToLogsForInspectionVpc", { resourceId: inspectionVpc.vpcId, resourceType: "VPC", trafficType: "ALL", deliverLogsPermissionArn: flowLogsIamrole.roleArn, logDestination: flowLogsLogGroup.logGroupArn, logDestinationType: "cloud-watch-logs", logFormat: "${version} ${account-id} ${interface-id} ${srcaddr} ${dstaddr} ${srcport} ${dstport} ${protocol} ${packets} ${bytes} ${start} ${end} ${action} ${log-status} ${vpc-id} ${subnet-id} ${instance-id} ${tcp-flags} ${type} ${pkt-srcaddr} ${pkt-dstaddr} ${region} ${az-id} ${sublocation-type} ${sublocation-id} ${pkt-src-aws-service} ${pkt-dst-aws-service} ${flow-direction} ${traffic-path}", maxAggregationInterval: 60, }); // Create a VPC for Egress const egressVpc = new ec2.Vpc(this, "EgressVpc", { cidr: "10.0.1.0/24", enableDnsHostnames: true, enableDnsSupport: true, maxAzs: 2, natGateways: 2, subnetConfiguration: [ { name: "TgwAttachment", subnetType: ec2.SubnetType.PRIVATE, cidrMask: 28, }, { name: "Public", subnetType: ec2.SubnetType.PUBLIC, cidrMask: 28, }, ], }); // Setting VPC Flow Logs for EgressVpc new ec2.CfnFlowLog(this, "FlowLogToLogsForEgressVpc", { resourceId: egressVpc.vpcId, resourceType: "VPC", trafficType: "ALL", deliverLogsPermissionArn: flowLogsIamrole.roleArn, logDestination: flowLogsLogGroup.logGroupArn, logDestinationType: "cloud-watch-logs", logFormat: "${version} ${account-id} ${interface-id} ${srcaddr} ${dstaddr} ${srcport} ${dstport} ${protocol} ${packets} ${bytes} ${start} ${end} ${action} ${log-status} ${vpc-id} ${subnet-id} ${instance-id} ${tcp-flags} ${type} ${pkt-srcaddr} ${pkt-dstaddr} ${region} ${az-id} ${sublocation-type} ${sublocation-id} ${pkt-src-aws-service} ${pkt-dst-aws-service} ${flow-direction} ${traffic-path}", maxAggregationInterval: 60, }); // Create a VPC for Spoke A const spokeVpcA = new ec2.Vpc(this, "SpokeVpcA", { cidr: "10.0.2.0/24", enableDnsHostnames: true, enableDnsSupport: true, maxAzs: 2, subnetConfiguration: [ { name: "TgwAttachment", subnetType: ec2.SubnetType.ISOLATED, cidrMask: 28, }, { name: "Workload", subnetType: ec2.SubnetType.ISOLATED, cidrMask: 28, }, ], }); // Setting VPC Flow Logs for SpokeVpcA new ec2.CfnFlowLog(this, "FlowLogToLogsForSpokeVpcA", { resourceId: spokeVpcA.vpcId, resourceType: "VPC", trafficType: "ALL", deliverLogsPermissionArn: flowLogsIamrole.roleArn, logDestination: flowLogsLogGroup.logGroupArn, logDestinationType: "cloud-watch-logs", logFormat: "${version} ${account-id} ${interface-id} ${srcaddr} ${dstaddr} ${srcport} ${dstport} ${protocol} ${packets} ${bytes} ${start} ${end} ${action} ${log-status} ${vpc-id} ${subnet-id} ${instance-id} ${tcp-flags} ${type} ${pkt-srcaddr} ${pkt-dstaddr} ${region} ${az-id} ${sublocation-type} ${sublocation-id} ${pkt-src-aws-service} ${pkt-dst-aws-service} ${flow-direction} ${traffic-path}", maxAggregationInterval: 60, }); // Create a VPC for Spoke B const spokeVpcB = new ec2.Vpc(this, "SpokeVpcB", { cidr: "10.0.3.0/24", enableDnsHostnames: true, enableDnsSupport: true, maxAzs: 2, subnetConfiguration: [ { name: "TgwAttachment", subnetType: ec2.SubnetType.ISOLATED, cidrMask: 28, }, { name: "Workload", subnetType: ec2.SubnetType.ISOLATED, cidrMask: 28, }, ], }); // Setting VPC Flow Logs for SpokeVpcB new ec2.CfnFlowLog(this, "FlowLogToLogsForSpokeVpcB", { resourceId: spokeVpcB.vpcId, resourceType: "VPC", trafficType: "ALL", deliverLogsPermissionArn: flowLogsIamrole.roleArn, logDestination: flowLogsLogGroup.logGroupArn, logDestinationType: "cloud-watch-logs", logFormat: "${version} ${account-id} ${interface-id} ${srcaddr} ${dstaddr} ${srcport} ${dstport} ${protocol} ${packets} ${bytes} ${start} ${end} ${action} ${log-status} ${vpc-id} ${subnet-id} ${instance-id} ${tcp-flags} ${type} ${pkt-srcaddr} ${pkt-dstaddr} ${region} ${az-id} ${sublocation-type} ${sublocation-id} ${pkt-src-aws-service} ${pkt-dst-aws-service} ${flow-direction} ${traffic-path}", maxAggregationInterval: 60, }); // Create Transit Gateway const tgw = new ec2.CfnTransitGateway(this, "Tgw", { defaultRouteTableAssociation: "disable", defaultRouteTablePropagation: "disable", }); // Create Transit Gateway attachment for InspectionVpc const tgwAttachmentForInspectionVpc = new ec2.CfnTransitGatewayAttachment( this, "TgwAttachmentForInspectionVpc", { subnetIds: inspectionVpc.selectSubnets({ subnetGroupName: "TgwAttachment", }).subnetIds, transitGatewayId: tgw.attrId, vpcId: inspectionVpc.vpcId, tags: [ { key: "Name", value: "TgwAttachmentForInspectionVpc", }, ], } ); // Create Transit Gateway attachment for EgressVpc const tgwAttachmentForEgressVpc = new ec2.CfnTransitGatewayAttachment( this, "TgwAttachmentForEgressVpc", { subnetIds: egressVpc.selectSubnets({ subnetGroupName: "TgwAttachment", }).subnetIds, transitGatewayId: tgw.attrId, vpcId: egressVpc.vpcId, tags: [ { key: "Name", value: "TgwAttachmentForEgressVpc", }, ], } ); // Create Transit Gateway attachment for SpokeVpcA const tgwAttachmentForSpokeVpcA = new ec2.CfnTransitGatewayAttachment( this, "TgwAttachmentForSpokeVpcA", { subnetIds: spokeVpcA.selectSubnets({ subnetGroupName: "TgwAttachment", }).subnetIds, transitGatewayId: tgw.attrId, vpcId: spokeVpcA.vpcId, tags: [ { key: "Name", value: "TgwAttachmentForSpokeVpcA", }, ], } ); // Create Transit Gateway attachment for SpokeVpcB const tgwAttachmentForSpokeVpcB = new ec2.CfnTransitGatewayAttachment( this, "TgwAttachmentForSpokeVpcB", { subnetIds: spokeVpcB.selectSubnets({ subnetGroupName: "TgwAttachment", }).subnetIds, transitGatewayId: tgw.attrId, vpcId: spokeVpcB.vpcId, tags: [ { key: "Name", value: "TgwAttachmentForSpokeVpcB", }, ], } ); // Create Transit Gateway route table for InspectionVpc const tgwRouteTableForInspectionVpc = new ec2.CfnTransitGatewayRouteTable( this, "TgwRouteTableForInspectionVpc", { transitGatewayId: tgw.attrId, tags: [ { key: "Name", value: "TgwRouteTableForInspectionVpc", }, ], } ); // Associate Transit Gateway attachment for InspectionVpc new ec2.CfnTransitGatewayRouteTableAssociation( this, "TgwRouteTableAssociationForInspectionVpc", { transitGatewayAttachmentId: tgwAttachmentForInspectionVpc.ref, transitGatewayRouteTableId: tgwRouteTableForInspectionVpc.ref, } ); // Add Route InspectionVpc To EgressVpc new ec2.CfnTransitGatewayRoute(this, "TgwRouteInspectionVpcToEgressVpc", { transitGatewayAttachmentId: tgwAttachmentForEgressVpc.ref, transitGatewayRouteTableId: tgwRouteTableForInspectionVpc.ref, destinationCidrBlock: "0.0.0.0/0", }); // Add Route InspectionVpc To SpokeVpcA new ec2.CfnTransitGatewayRoute(this, "TgwRouteInspectionVpcToSpokeVpcA", { transitGatewayAttachmentId: tgwAttachmentForSpokeVpcA.ref, transitGatewayRouteTableId: tgwRouteTableForInspectionVpc.ref, destinationCidrBlock: spokeVpcA.vpcCidrBlock, }); // Add Route InspectionVpc To SpokeVpcB new ec2.CfnTransitGatewayRoute(this, "TgwRouteInspectionVpcToSpokeVpcB", { transitGatewayAttachmentId: tgwAttachmentForSpokeVpcB.ref, transitGatewayRouteTableId: tgwRouteTableForInspectionVpc.ref, destinationCidrBlock: spokeVpcB.vpcCidrBlock, }); // Create Transit Gateway route table for EgressVpc const tgwRouteTableForEgressVpc = new ec2.CfnTransitGatewayRouteTable( this, "TgwRouteTableForEgressVpc", { transitGatewayId: tgw.attrId, tags: [ { key: "Name", value: "TgwRouteTableForEgressVpc", }, ], } ); // Associate Transit Gateway attachment for EgressVpc new ec2.CfnTransitGatewayRouteTableAssociation( this, "TgwRouteAssociationTableForEgressVpc", { transitGatewayAttachmentId: tgwAttachmentForEgressVpc.ref, transitGatewayRouteTableId: tgwRouteTableForEgressVpc.ref, } ); // Add Route EgressVpc To InspectionVpc new ec2.CfnTransitGatewayRoute(this, "TgwRouteEgressVpcToInspectionVpc", { transitGatewayAttachmentId: tgwAttachmentForInspectionVpc.ref, transitGatewayRouteTableId: tgwRouteTableForEgressVpc.ref, destinationCidrBlock: "0.0.0.0/0", }); // Create Transit Gateway route table for SpokeVpcA const tgwRouteTableForSpokeVpcA = new ec2.CfnTransitGatewayRouteTable( this, "TgwRouteTableForSpokeVpcA", { transitGatewayId: tgw.attrId, tags: [ { key: "Name", value: "TgwRouteTableForSpokeVpcA", }, ], } ); // Associate Transit Gateway attachment for SpokeVpcA new ec2.CfnTransitGatewayRouteTableAssociation( this, "TgwRouteAssociationTableForSpokeVpcA", { transitGatewayAttachmentId: tgwAttachmentForSpokeVpcA.ref, transitGatewayRouteTableId: tgwRouteTableForSpokeVpcA.ref, } ); // Add Route SpokeVpcA To InspectionVpc new ec2.CfnTransitGatewayRoute(this, "TgwRouteSpokeVpcAToInspectionVpc", { transitGatewayAttachmentId: tgwAttachmentForInspectionVpc.ref, transitGatewayRouteTableId: tgwRouteTableForSpokeVpcA.ref, destinationCidrBlock: "0.0.0.0/0", }); // Create Transit Gateway route table for SpokeVpcB const tgwRouteTableForSpokeVpcB = new ec2.CfnTransitGatewayRouteTable( this, "TgwRouteTableForSpokeVpcB", { transitGatewayId: tgw.attrId, tags: [ { key: "Name", value: "TgwRouteTableForSpokeVpcB", }, ], } ); // Associate Transit Gateway attachment for SpokeVpcB new ec2.CfnTransitGatewayRouteTableAssociation( this, "TgwRouteAssociationTableForSpokeVpcB", { transitGatewayAttachmentId: tgwAttachmentForSpokeVpcB.ref, transitGatewayRouteTableId: tgwRouteTableForSpokeVpcB.ref, } ); // Add Route SpokeVpcB To InspectionVpc new ec2.CfnTransitGatewayRoute(this, "TgwRouteSpokeVpcBToInspectionVpc", { transitGatewayAttachmentId: tgwAttachmentForInspectionVpc.ref, transitGatewayRouteTableId: tgwRouteTableForSpokeVpcB.ref, destinationCidrBlock: "0.0.0.0/0", }); // Create Network Firewall rule group const icmpAlertStatefulRuleGroup = new networkfirewall.CfnRuleGroup( this, "IcmpAlertStatefulRuleGroup", { capacity: 100, ruleGroupName: "icmp-alert", type: "STATEFUL", ruleGroup: { rulesSource: { statefulRules: [ { action: "ALERT", header: { destination: "10.0.0.0/16", destinationPort: "ANY", direction: "ANY", protocol: "ICMP", source: "10.0.0.0/16", sourcePort: "ANY", }, ruleOptions: [ { keyword: `msg:"icmp alert"`, }, { keyword: "sid:1000001", }, { keyword: "rev:1", }, ], }, ], }, }, } ); // Create Network Firewall policy const networkfirewallPolicy = new networkfirewall.CfnFirewallPolicy( this, "NetworkFirewallPolicy", { firewallPolicyName: "InspectionPolicy", firewallPolicy: { statelessDefaultActions: ["aws:forward_to_sfe"], statelessFragmentDefaultActions: ["aws:forward_to_sfe"], statefulRuleGroupReferences: [ { resourceArn: icmpAlertStatefulRuleGroup.attrRuleGroupArn, }, ], }, } ); // Create Network Firewall const networkFirewall = new networkfirewall.CfnFirewall( this, "NetworkFirewall", { firewallName: "NetworkFirewall", firewallPolicyArn: networkfirewallPolicy.attrFirewallPolicyArn, vpcId: inspectionVpc.vpcId, subnetMappings: (() => { const firewallSubnetIds: { subnetId: string }[] = new Array(); inspectionVpc .selectSubnets({ subnetGroupName: "Firewall" }) .subnets.forEach((subnet) => { firewallSubnetIds.push({ subnetId: subnet.subnetId }); }); return firewallSubnetIds; })(), } ); // // Setting Network Firewall logs new networkfirewall.CfnLoggingConfiguration( this, "NetworkFirewallFlowLogsToLogs", { firewallArn: networkFirewall.ref, loggingConfiguration: { logDestinationConfigs: [ { logDestination: { logGroup: networkFirewallFlowLogsLogGroup.logGroupName, }, logDestinationType: "CloudWatchLogs", logType: "FLOW", }, { logDestination: { logGroup: networkFirewallAlertLogsLogGroup.logGroupName, }, logDestinationType: "CloudWatchLogs", logType: "ALERT", }, ], }, } ); // Add Route InspectionVpc to Transit Gateway inspectionVpc .selectSubnets({ subnetGroupName: "Firewall" }) .subnets.map((subnet, index) => { new ec2.CfnRoute(this, `InspectionVpcRouteToTgwt-${index}`, { routeTableId: subnet.routeTable.routeTableId, destinationCidrBlock: "0.0.0.0/0", transitGatewayId: tgw.ref, }).addDependsOn(tgwAttachmentForInspectionVpc); }); // Add Route EgressVpc to Transit Gateway egressVpc .selectSubnets({ subnetGroupName: "Public" }) .subnets.map((subnet, index) => { new ec2.CfnRoute(this, `EgressVpcRouteToTgw-${index}`, { routeTableId: subnet.routeTable.routeTableId, destinationCidrBlock: "10.0.0.0/16", transitGatewayId: tgw.ref, }).addDependsOn(tgwAttachmentForEgressVpc); }); // Add Route SpokeVpcA to Transit Gateway spokeVpcA .selectSubnets({ subnetGroupName: "Workload" }) .subnets.map((subnet, index) => { new ec2.CfnRoute(this, `SpokeVpcARouteToTgw-${index}`, { routeTableId: subnet.routeTable.routeTableId, destinationCidrBlock: "0.0.0.0/0", transitGatewayId: tgw.ref, }).addDependsOn(tgwAttachmentForSpokeVpcA); }); // Add Route SpokeVpcB to Transit Gateway spokeVpcB .selectSubnets({ subnetGroupName: "Workload" }) .subnets.map((subnet, index) => { new ec2.CfnRoute(this, `SpokeVpcBRouteToTgw-${index}`, { routeTableId: subnet.routeTable.routeTableId, destinationCidrBlock: "0.0.0.0/0", transitGatewayId: tgw.ref, }).addDependsOn(tgwAttachmentForSpokeVpcB); }); // Add Route InspectionVpc to Network Firewall inspectionVpc .selectSubnets({ subnetGroupName: "TgwAttachment" }) .subnets.map((subnet, index) => { new ec2.CfnRoute(this, `InspectionVpcRouteToNetworkFirewall-${index}`, { routeTableId: subnet.routeTable.routeTableId, destinationCidrBlock: "0.0.0.0/0", vpcEndpointId: cdk.Fn.select( 1, cdk.Fn.split( ":", cdk.Fn.select(index, networkFirewall.attrEndpointIds) ) ), }).addDependsOn(networkFirewall); }); // Create Security Group const spokeVpcASg = new ec2.SecurityGroup(this, "SpokeVpcASg", { allowAllOutbound: true, vpc: spokeVpcA, }); spokeVpcASg.addIngressRule( ec2.Peer.ipv4("10.0.0.0/16"), ec2.Port.allTraffic() ); const spokeVpcBSg = new ec2.SecurityGroup(this, "SpokeVpcBSg", { allowAllOutbound: true, vpc: spokeVpcB, }); spokeVpcBSg.addIngressRule( ec2.Peer.ipv4("10.0.0.0/16"), ec2.Port.allTraffic() ); // Create EC2 instance spokeVpcA .selectSubnets({ subnetGroupName: "Workload" }) .subnets.map((subnet, index) => { new ec2.Instance(this, `SpokeVpcAEc2Instance-${index}`, { machineImage: ec2.MachineImage.latestAmazonLinux({ generation: ec2.AmazonLinuxGeneration.AMAZON_LINUX_2, }), instanceType: new ec2.InstanceType("t3.micro"), vpc: spokeVpcA, vpcSubnets: spokeVpcA.selectSubnets({ subnetGroupName: "Workload", availabilityZones: [spokeVpcA.availabilityZones[index]], }), securityGroup: spokeVpcASg, role: ssmIamRole, }); }); spokeVpcB .selectSubnets({ subnetGroupName: "Workload" }) .subnets.map((subnet, index) => { new ec2.Instance(this, `SpokeVpcBEc2Instance-${index}`, { machineImage: ec2.MachineImage.latestAmazonLinux({ generation: ec2.AmazonLinuxGeneration.AMAZON_LINUX_2, }), instanceType: new ec2.InstanceType("t3.micro"), vpc: spokeVpcB, vpcSubnets: spokeVpcB.selectSubnets({ subnetGroupName: "Workload", availabilityZones: [spokeVpcB.availabilityZones[index]], }), securityGroup: spokeVpcBSg, role: ssmIamRole, }); }); } }
{ "name": "tgw-appliance-mode-network-firewall", "version": "0.1.0", "bin": { "tgw-appliance-mode-network-firewall": "bin/tgw-appliance-mode-network-firewall.js" }, "scripts": { "build": "tsc", "watch": "tsc -w", "test": "jest", "cdk": "cdk" }, "devDependencies": { "@aws-cdk/assert": "1.118.0", "@types/jest": "^26.0.10", "@types/node": "10.17.27", "aws-cdk": "^1.118.0", "jest": "^26.4.2", "ts-jest": "^26.2.0", "ts-node": "^9.0.0", "typescript": "~3.9.7" }, "dependencies": { "@aws-cdk/aws-ec2": "^1.118.0", "@aws-cdk/aws-iam": "^1.118.0", "@aws-cdk/aws-logs": "^1.118.0", "@aws-cdk/aws-networkfirewall": "^1.118.0", "@aws-cdk/core": "1.118.0", "source-map-support": "^0.5.16" } }
npx cdk deploy
で、AWS CDKでリソースをデプロイすると、以下のようなログが出力されます。
> npx cdk deploy MFA token for arn:aws:iam::<AWSアカウントID>:mfa/<IAMユーザー名> 644834 This deployment will make potentially sensitive changes according to your current security approval level (--require-approval broadening). Please confirm you intend to make the following modifications: IAM Statement Changes ┌───┬─────────────────────────────────────┬────────┬─────────────────────────────────────┬───────────────────────────────────────┬───────────┐ │ │ Resource │ Effect │ Action │ Principal │ Condition │ ├───┼─────────────────────────────────────┼────────┼─────────────────────────────────────┼───────────────────────────────────────┼───────────┤ │ + │ ${FlowLogsIamrole.Arn} │ Allow │ sts:AssumeRole │ Service:vpc-flow-logs.amazonaws.com │ │ │ + │ ${FlowLogsIamrole.Arn} │ Allow │ iam:PassRole │ AWS:${FlowLogsIamrole} │ │ ├───┼─────────────────────────────────────┼────────┼─────────────────────────────────────┼───────────────────────────────────────┼───────────┤ │ + │ ${FlowLogsLogGroup.Arn} │ Allow │ logs:CreateLogStream │ AWS:${FlowLogsIamrole} │ │ │ │ │ │ logs:DescribeLogStreams │ │ │ │ │ │ │ logs:PutLogEvents │ │ │ ├───┼─────────────────────────────────────┼────────┼─────────────────────────────────────┼───────────────────────────────────────┼───────────┤ │ + │ ${SsmIamRole.Arn} │ Allow │ sts:AssumeRole │ Service:ec2.${AWS::URLSuffix} │ │ └───┴─────────────────────────────────────┴────────┴─────────────────────────────────────┴───────────────────────────────────────┴───────────┘ IAM Policy Changes ┌───┬───────────────┬────────────────────────────────────────────────────────────────────┐ │ │ Resource │ Managed Policy ARN │ ├───┼───────────────┼────────────────────────────────────────────────────────────────────┤ │ + │ ${SsmIamRole} │ arn:${AWS::Partition}:iam::aws:policy/AmazonSSMManagedInstanceCore │ └───┴───────────────┴────────────────────────────────────────────────────────────────────┘ Security Group Changes ┌───┬────────────────────────┬─────┬────────────┬─────────────────┐ │ │ Group │ Dir │ Protocol │ Peer │ ├───┼────────────────────────┼─────┼────────────┼─────────────────┤ │ + │ ${SpokeVpcASg.GroupId} │ In │ Everything │ 10.0.0.0/16 │ │ + │ ${SpokeVpcASg.GroupId} │ Out │ Everything │ Everyone (IPv4) │ ├───┼────────────────────────┼─────┼────────────┼─────────────────┤ │ + │ ${SpokeVpcBSg.GroupId} │ In │ Everything │ 10.0.0.0/16 │ │ + │ ${SpokeVpcBSg.GroupId} │ Out │ Everything │ Everyone (IPv4) │ └───┴────────────────────────┴─────┴────────────┴─────────────────┘ (NOTE: There may be security-related changes not in this list. See https://github.com/aws/aws-cdk/issues/1299) Do you wish to deploy these changes (y/n)? y TgwApplianceModeNetworkFirewallStack: deploying... [0%] start: Publishing a95c578082cad9562243b7caf4d4c1cc289f0fb43ebb4239be5a6a7a0fa383ea:current [100%] success: Published a95c578082cad9562243b7caf4d4c1cc289f0fb43ebb4239be5a6a7a0fa383ea:current TgwApplianceModeNetworkFirewallStack: creating CloudFormation changeset... ✅ TgwApplianceModeNetworkFirewallStack Stack ARN: arn:aws:cloudformation:us-east-1: <AWSアカウントID>:stack/TgwApplianceModeNetworkFirewallStack/b9c5cbd0-0172-11ec-b571-0a1540e225b5
npx cdk deploy
実行完了後にマネージメントコンソールを確認すると、以下の通り、VPCや、Transit Gateway、Network Firewallが作成されていることが確認できます。
アプライアンスモードが無効な状態での疎通確認
検証で作成した、Spoke VPC AのEC2インスタンスAにはSSMセッションマネージャーでログインできます。
SSMセッションマネージャーを使うためには、SSMのエンドポイントに対して、tcp/443で通信できる必要があります。Spoke VPC Aには、SSM用のVPCエンドポイントも、Internet Gatewayも存在していませんが、Egress VPCのNAT Gatewayを経由してSSMのエンドポイントと通信ができるため、SSMセッションマネージャーが使用できます。
Spoke VPC AのEC2インスタンスAからインターネットに出ていく通信の経路は以下の通りです。
それでは、「Spoke VPC AのEC2インスタンスAから、Spoke VPC BのEC2インスタンスAへの通信」をしてみます。
Spoke VPC AのEC2インスタンスAにSSMセッションマネージャーでログインして、Spoke VPC BのEC2インスタンスA(10.0.3.46
)に対してping
を打つと、パケットがロスなく全て到達できていることが分かります。
sh-4.2$ ping 10.0.3.46 PING 10.0.3.46 (10.0.3.46) 56(84) bytes of data. 64 bytes from 10.0.3.46: icmp_seq=1 ttl=251 time=3.90 ms 64 bytes from 10.0.3.46: icmp_seq=2 ttl=251 time=1.80 ms 64 bytes from 10.0.3.46: icmp_seq=3 ttl=251 time=1.77 ms 64 bytes from 10.0.3.46: icmp_seq=4 ttl=251 time=1.75 ms 64 bytes from 10.0.3.46: icmp_seq=5 ttl=251 time=1.91 ms 64 bytes from 10.0.3.46: icmp_seq=6 ttl=251 time=1.78 ms ^C --- 10.0.3.46 ping statistics --- 6 packets transmitted, 6 received, 0% packet loss, time 5008ms rtt min/avg/max/mdev = 1.750/2.155/3.901/0.783 ms sh-4.2$
続いて、「Spoke VPC AのEC2インスタンスAから、Spoke VPC BのEC2インスタンスBへの通信」をしてみます。
Spoke VPC AのEC2インスタンスAにSSMセッションマネージャーでログインして、Spoke VPC BのEC2インスタンスB(10.0.3.58
)に対してping
を打つと、全てのパケットがロスしていることが分かります。
sh-4.2$ ping 10.0.3.58 PING 10.0.3.58 (10.0.3.58) 56(84) bytes of data. ^C --- 10.0.3.58 ping statistics --- 35 packets transmitted, 0 received, 100% packet loss, time 34793ms sh-4.2$
アプライアンスモードの有効化
それでは、Transit Gatewayのアプライアンスモードを有効化します。
「Transit Gatewayのアプライアンスモードを有効化」と表現していますが、アプライアンスモードの設定は、Transit Gateway attachmentに対して行います。今回は、Inspection VPCのTransit Gateway attachmentに対して、アプライアンスモードを有効化します。
まず、Inspection VPCのTransit Gateway attachmentのIDをマネージメントコンソールから確認します。
そして、CloudShellを起動して、以下コマンドでTransit Gatewayのアプライアンスモードを有効化します。
[cloudshell-user@ip-10-1-32-124 ~]$ aws ec2 modify-transit-gateway-vpc-attachment \ > --transit-gateway-attachment-id tgw-attach-0166964d4a20f8c28 \ > --options ApplianceModeSupport=enable { "TransitGatewayVpcAttachment": { "TransitGatewayAttachmentId": "tgw-attach-0166964d4a20f8c28", "TransitGatewayId": "tgw-078e5acef1eef851f", "VpcId": "vpc-0d94bfedea274dbf3", "VpcOwnerId": "984900217833", "State": "modifying", "SubnetIds": [ "subnet-071ffea9d3764240d", "subnet-0cb97b2ab114dae87" ], "CreationTime": "2021-08-20T04:56:28+00:00", "Options": { "DnsSupport": "enable", "Ipv6Support": "disable", "ApplianceModeSupport": "enable" } } }
"ApplianceModeSupport": "enable"
となっている通り、アプライアンスモードを有効化されました。また、"State": "modifying",
となっている通り、反映には5分程度時間がかかります。
アプライアンスモードが有効な状態での疎通確認
アプライアンスモードを有効化したので、改めて「Spoke VPC AのEC2インスタンスAから、Spoke VPC BのEC2インスタンスAへの通信」から検証してみます。
Spoke VPC AのEC2インスタンスAにSSMセッションマネージャーでログインして、Spoke VPC BのEC2インスタンスA(10.0.3.46
)に対してping
を打つと、アプライアンスモードの有効化前と変わらず、パケットがロスなく全て到達できていることが分かります。
sh-4.2$ ping 10.0.3.46 PING 10.0.3.46 (10.0.3.46) 56(84) bytes of data. 64 bytes from 10.0.3.46: icmp_seq=1 ttl=251 time=3.80 ms 64 bytes from 10.0.3.46: icmp_seq=2 ttl=251 time=1.81 ms 64 bytes from 10.0.3.46: icmp_seq=3 ttl=251 time=1.73 ms 64 bytes from 10.0.3.46: icmp_seq=4 ttl=251 time=1.69 ms 64 bytes from 10.0.3.46: icmp_seq=5 ttl=251 time=1.65 ms 64 bytes from 10.0.3.46: icmp_seq=6 ttl=251 time=1.74 ms ^C --- 10.0.3.46 ping statistics --- 6 packets transmitted, 6 received, 0% packet loss, time 5009ms rtt min/avg/max/mdev = 1.652/2.072/3.801/0.774 ms sh-4.2$
続いて、「Spoke VPC AのEC2インスタンスAから、Spoke VPC BのEC2インスタンスBへの通信」をしてみます。
Spoke VPC AのEC2インスタンスAにSSMセッションマネージャーでログインして、Spoke VPC BのEC2インスタンスB(10.0.3.58
)に対してping
を打つと、アプライアンスモードが有効になったことで、パケットがロスなく全て到達できていることが分かります。
sh-4.2$ ping 10.0.3.58 PING 10.0.3.58 (10.0.3.58) 56(84) bytes of data. 64 bytes from 10.0.3.58: icmp_seq=1 ttl=251 time=3.72 ms 64 bytes from 10.0.3.58: icmp_seq=2 ttl=251 time=2.83 ms 64 bytes from 10.0.3.58: icmp_seq=3 ttl=251 time=2.37 ms 64 bytes from 10.0.3.58: icmp_seq=4 ttl=251 time=2.33 ms 64 bytes from 10.0.3.58: icmp_seq=5 ttl=251 time=2.50 ms 64 bytes from 10.0.3.58: icmp_seq=6 ttl=251 time=2.28 ms ^C --- 10.0.3.58 ping statistics --- 6 packets transmitted, 6 received, 0% packet loss, time 5008ms rtt min/avg/max/mdev = 2.289/2.677/3.727/0.506 ms sh-4.2$
Network Firewallのエンドポイントを一つ削除した状態での疎通確認
最後に番外編ということで、Network Firewallのエンドポイントを一つ削除した状態での疎通確認をしてみたいと思います。
以下の図のように、Availability Zone AにあったNetwork Firewall Endpoint A
を削除した状態で正しく通信ができるかを検証します。
まず、Inspection VPCのTransit Gateway subnet A用のRoute Tableの0.0.0.0/0
のルートの宛先を、Network Firewall Endpoint A
からNetwork Firewall Endpoint B
に変更します。
- 変更前
-
変更後
続いて、Network Firewallのエンドポイントを削除します。まず、対象のNetwork Firewallを選択して、関連付けられたポリシーと VPC
の編集
をクリックします。
そして、削除対象のエンドポイントがあるus-east-1a
の削除
をクリックして、保存
します。
保存後、エンドポイントの削除処理が始まり、10分程度で以下のようにエンドポイントの削除が完了します。
この状態で、「Spoke VPC AのEC2インスタンスAから、Spoke VPC BのEC2インスタンスAへの通信」から検証してみます。
Spoke VPC AのEC2インスタンスAにSSMセッションマネージャーでログインして、Spoke VPC BのEC2インスタンスA(10.0.3.46
)に対してping
を打つと、パケットがロスなく全て到達できていることが分かります。
sh-4.2$ ping 10.0.3.46 PING 10.0.3.46 (10.0.3.46) 56(84) bytes of data. 64 bytes from 10.0.3.46: icmp_seq=1 ttl=251 time=3.50 ms 64 bytes from 10.0.3.46: icmp_seq=2 ttl=251 time=2.36 ms 64 bytes from 10.0.3.46: icmp_seq=3 ttl=251 time=2.33 ms 64 bytes from 10.0.3.46: icmp_seq=4 ttl=251 time=2.42 ms 64 bytes from 10.0.3.46: icmp_seq=5 ttl=251 time=2.33 ms 64 bytes from 10.0.3.46: icmp_seq=6 ttl=251 time=2.42 ms ^C --- 10.0.3.46 ping statistics --- 6 packets transmitted, 6 received, 0% packet loss, time 5004ms rtt min/avg/max/mdev = 2.330/2.563/3.502/0.425 ms sh-4.2$
続いて、「Spoke VPC AのEC2インスタンスAから、Spoke VPC BのEC2インスタンスBへの通信」をしてみます。
Spoke VPC AのEC2インスタンスAにSSMセッションマネージャーでログインして、Spoke VPC BのEC2インスタンスB(10.0.3.58
)に対してping
を打つと、パケットがロスなく全て到達できていることが分かります。
sh-4.2$ ping 10.0.3.58 PING 10.0.3.58 (10.0.3.58) 56(84) bytes of data. 64 bytes from 10.0.3.58: icmp_seq=1 ttl=251 time=4.54 ms 64 bytes from 10.0.3.58: icmp_seq=2 ttl=251 time=2.78 ms 64 bytes from 10.0.3.58: icmp_seq=3 ttl=251 time=2.71 ms 64 bytes from 10.0.3.58: icmp_seq=4 ttl=251 time=2.65 ms 64 bytes from 10.0.3.58: icmp_seq=5 ttl=251 time=2.69 ms 64 bytes from 10.0.3.58: icmp_seq=6 ttl=251 time=2.70 ms ^C --- 10.0.3.58 ping statistics --- 6 packets transmitted, 6 received, 0% packet loss, time 5006ms rtt min/avg/max/mdev = 2.651/3.015/4.547/0.687 ms sh-4.2$
Network Firewallのエンドポイントが一つ削除されていても、残っているNetwork FirewallのエンドポイントをVPCのルートテーブルでルーティングすれば、通信できることが分かりました。
Transit Gatewayのアプライアンスモード完全に理解した
あまり使う場面は少ないかも知れないですが、Transit Gatewayのアプライアンスモードを試してみました。
今回は、Network Firewallを間に挟みましたが、他にもGateway Load Balancerと仮想アプライアンス製品を組み合わせたパターンでも対応可能です。
この記事が誰かの助けになれば幸いです。
以上、AWS事業本部 コンサルティング部の のんピ(@non____97)でした!