Skip to content

Active/Standby Centralized Inspection Using VPC Route Server and Transit Gateway

16 minute read
Content level: Advanced
0

This article demonstrates how to build a centralized multi-VPC inspection architecture using AWS Transit Gateway and Amazon VPC Route Server, where BGP and AS-path prepending enable automatic active/standby failover of inspection appliances for both east-west and north-south traffic — without static routes or manual intervention.

Overview

In the blog post Dynamic routing using Amazon VPC Route Server, we explored how Amazon VPC Route Server enables dynamic BGP-based routing within a VPC. That post covered two scenarios: floating IP failover for application high availability (Scenario 1) and VPC ingress traffic inspection through a firewall with IGW route table integration (Scenario 2).

In this article, we extend those patterns to a centralized multi-VPC inspection architecture using AWS Transit Gateway. This is a common enterprise pattern where multiple spoke VPCs route traffic through a shared inspection VPC for security enforcement. By combining Transit Gateway with VPC Route Server, inspection appliances can dynamically attract traffic via BGP — enabling automatic active/standby failover without static routes or manual intervention.

This pattern supports both east-west traffic (spoke-to-spoke communication routed through inspection) and north-south traffic (spoke-to-external resources routed through inspection), making it suitable for centralized security enforcement across your AWS network.

In this walk-through, we deploy:

  • Two spoke VPCs that send all traffic through a Transit Gateway to a dedicated inspection VPC
  • Two inspection instances (one per AZ) running GoBGP, peered with a VPC Route Server
  • Active/standby routing via AS-path prepending — AZ1 is preferred, AZ2 is standby
  • Automatic failover when the active instance's BGP session drops, with automatic failback on recovery

When to use this pattern

As noted in the original blog post, AWS recommends Gateway Load Balancer (GWLB) as the first choice for high availability and redundancy with inspection appliances. However, there are scenarios where GWLB may not be suitable:

  • Your appliance does not support GWLB. GWLB requires appliances to support the GENEVE protocol for traffic encapsulation.
  • You need active/standby rather than active/active. GWLB distributes traffic across a target group (active/active). If your requirement is to perform stateful inspection with a single active instance and a warm standby, GWLB's load-balancing model doesn't fit.
  • You need fine-grained routing control. BGP gives you control over path selection using standard attributes (AS-path, MED), which may be required for specific compliance or operational requirements.

If any of these apply, the VPC Route Server approach described here provides an alternative path to dynamic, automated failover for centralized inspection.

Architecture

The following diagram shows the centralized inspection architecture. Traffic from spoke VPCs traverses the Transit Gateway and enters the inspection VPC, where the VPC Route Server dynamically steers it to the active inspection instance.

Traffic flows

This architecture supports two traffic patterns through the same inspection path:

East-west (spoke-to-spoke): When an instance in Spoke1 communicates with an instance in Spoke2, traffic follows this path:

Spoke1 instance → TGW (spokes RT: 0/0 → inspection attachment)
  → Inspection VPC TGW subnet → Route Server selects AZ1 (shorter AS-path)
  → AZ1 inspects and forwards → TGW (inspection RT: propagated spoke routes)
  → Spoke2 instance

North-south (spoke-to-internet): When a spoke instance sends traffic to the internet, the same inspection path is used. The inspection VPC has an Internet Gateway and public subnets, so after inspection, traffic can be forwarded to the internet:

Spoke instance → TGW (spokes RT: 0/0 → inspection attachment)
  → Inspection VPC TGW subnet → Route Server selects AZ1
  → AZ1 inspects and forwards → Internet Gateway → Internet

In both cases, the Transit Gateway's spokes route table sends all traffic (0.0.0.0/0) to the inspection VPC attachment. Inside the inspection VPC, the route server propagates the preferred route into the TGW subnet route table, directing traffic to the active inspection instance's ENI.

How Active/Standby Works

Both inspection instances advertise 10.0.0.0/8 (optionally can advertise 0.0.0.0/0 to cater for internet traffic) to the route server via BGP. The difference is in the AS-path:

InstanceAS-PathEffect
AZ1 (active)65550Shorter path — preferred by route server
AZ2 (standby)65550 65550Prepended path — used only when AZ1 is unavailable

The route server propagates the preferred route (AZ1) into the VPC route table. If AZ1's BGP session drops, the route server automatically updates the route table to point to AZ2.

Prerequisites

  • An AWS account with permissions to create VPCs, EC2 instances, Transit Gateways, and VPC Route Servers
  • AWS CLI configured, or access to the AWS Management Console
  • Familiarity with BGP concepts (ASN, AS-path, peering)
  • VPC Route Server available in your target region

Walkthrough

The following steps deploy the complete architecture using a CloudFormation template, then verify BGP peering, route propagation, end-to-end connectivity, and failover behavior. We use GoBGP as a lightweight BGP speaker on the inspection instances. In production, you would replace GoBGP with your preferred network virtual appliance (for example, a firewall vendor appliance that supports BGP).

Step 1: Deploy the CloudFormation Stack

Save the template below and deploy it:

AWSTemplateFormatVersion: '2010-09-09'
Description: Centralized Inspection Architecture with Transit Gateway and VPC Route Server using GoBGP

Parameters:
  LatestAmiId:
    Type: AWS::SSM::Parameter::Value<AWS::EC2::Image::Id>
    Default: /aws/service/ami-amazon-linux-latest/al2023-ami-kernel-default-x86_64

Resources:
  # ==========================================
  # Transit Gateway
  # ==========================================
  TransitGateway:
    Type: AWS::EC2::TransitGateway
    Properties:
      Description: Transit Gateway for centralized inspection
      DefaultRouteTableAssociation: disable
      DefaultRouteTablePropagation: disable
      Tags:
        - Key: Name
          Value: TGW-Route-server

  TGWRouteTableSpokes:
    Type: AWS::EC2::TransitGatewayRouteTable
    Properties:
      TransitGatewayId: !Ref TransitGateway
      Tags:
        - Key: Name
          Value: spokes

  TGWRouteTableInspection:
    Type: AWS::EC2::TransitGatewayRouteTable
    Properties:
      TransitGatewayId: !Ref TransitGateway
      Tags:
        - Key: Name
          Value: inspection

  # Default route in spokes route table to inspection attachment
  SpokesDefaultRoute:
    Type: AWS::EC2::TransitGatewayRoute
    DependsOn: InspectionTGWAttachment
    Properties:
      TransitGatewayRouteTableId: !Ref TGWRouteTableSpokes
      DestinationCidrBlock: 0.0.0.0/0
      TransitGatewayAttachmentId: !Ref InspectionTGWAttachment

  # ==========================================
  # Internet Gateway
  # ==========================================
  InternetGateway:
    Type: AWS::EC2::InternetGateway
    Properties:
      Tags:
        - Key: Name
          Value: inspection-igw

  # ==========================================
  # Spoke1 VPC
  # ==========================================
  Spoke1VPC:
    Type: AWS::EC2::VPC
    Properties:
      CidrBlock: 10.45.0.0/16
      EnableDnsSupport: true
      EnableDnsHostnames: true
      Tags:
        - Key: Name
          Value: spoke1

  InstanceSubnetSpoke1:
    Type: AWS::EC2::Subnet
    Properties:
      VpcId: !Ref Spoke1VPC
      CidrBlock: 10.45.1.0/24
      AvailabilityZone: !Select [0, !GetAZs '']
      Tags:
        - Key: Name
          Value: instance-subnet-spoke1

  TgwSubnetSpoke1:
    Type: AWS::EC2::Subnet
    Properties:
      VpcId: !Ref Spoke1VPC
      CidrBlock: 10.45.2.0/24
      AvailabilityZone: !Select [0, !GetAZs '']
      Tags:
        - Key: Name
          Value: tgw-subnet-spoke1

  Spoke1RouteTable:
    Type: AWS::EC2::RouteTable
    Properties:
      VpcId: !Ref Spoke1VPC
      Tags:
        - Key: Name
          Value: instance-route-table-spoke1

  Spoke1RouteTableAssociation:
    Type: AWS::EC2::SubnetRouteTableAssociation
    Properties:
      SubnetId: !Ref InstanceSubnetSpoke1
      RouteTableId: !Ref Spoke1RouteTable

  Spoke1DefaultRoute:
    Type: AWS::EC2::Route
    DependsOn: Spoke1TGWAttachment
    Properties:
      RouteTableId: !Ref Spoke1RouteTable
      DestinationCidrBlock: 0.0.0.0/0
      TransitGatewayId: !Ref TransitGateway

  Spoke1TGWAttachment:
    Type: AWS::EC2::TransitGatewayAttachment
    DependsOn: TransitGateway
    Properties:
      TransitGatewayId: !Ref TransitGateway
      VpcId: !Ref Spoke1VPC
      SubnetIds:
        - !Ref TgwSubnetSpoke1
      Tags:
        - Key: Name
          Value: spoke1-attachment

  Spoke1TGWAssociation:
    Type: AWS::EC2::TransitGatewayRouteTableAssociation
    Properties:
      TransitGatewayAttachmentId: !Ref Spoke1TGWAttachment
      TransitGatewayRouteTableId: !Ref TGWRouteTableSpokes

  Spoke1TGWPropagation:
    Type: AWS::EC2::TransitGatewayRouteTablePropagation
    Properties:
      TransitGatewayAttachmentId: !Ref Spoke1TGWAttachment
      TransitGatewayRouteTableId: !Ref TGWRouteTableInspection

  Spoke1EICEndpoint:
    Type: AWS::EC2::InstanceConnectEndpoint
    Properties:
      SubnetId: !Ref TgwSubnetSpoke1
      Tags:
        - Key: Name
          Value: eic-endpoint-spoke1

  Spoke1SecurityGroup:
    Type: AWS::EC2::SecurityGroup
    Properties:
      GroupDescription: Security group for spoke1 instance
      VpcId: !Ref Spoke1VPC
      SecurityGroupIngress:
        - IpProtocol: -1
          CidrIp: 10.0.0.0/8
      SecurityGroupEgress:
        - IpProtocol: -1
          CidrIp: 0.0.0.0/0
      Tags:
        - Key: Name
          Value: spoke1-sg

  # ==========================================
  # Spoke2 VPC
  # ==========================================
  Spoke2VPC:
    Type: AWS::EC2::VPC
    Properties:
      CidrBlock: 10.46.0.0/16
      EnableDnsSupport: true
      EnableDnsHostnames: true
      Tags:
        - Key: Name
          Value: spoke2

  InstanceSubnetSpoke2:
    Type: AWS::EC2::Subnet
    Properties:
      VpcId: !Ref Spoke2VPC
      CidrBlock: 10.46.1.0/24
      AvailabilityZone: !Select [0, !GetAZs '']
      Tags:
        - Key: Name
          Value: instance-subnet-spoke2

  TgwSubnetSpoke2:
    Type: AWS::EC2::Subnet
    Properties:
      VpcId: !Ref Spoke2VPC
      CidrBlock: 10.46.2.0/24
      AvailabilityZone: !Select [0, !GetAZs '']
      Tags:
        - Key: Name
          Value: tgw-subnet-spoke2

  Spoke2RouteTable:
    Type: AWS::EC2::RouteTable
    Properties:
      VpcId: !Ref Spoke2VPC
      Tags:
        - Key: Name
          Value: instance-route-table-spoke2

  Spoke2RouteTableAssociation:
    Type: AWS::EC2::SubnetRouteTableAssociation
    Properties:
      SubnetId: !Ref InstanceSubnetSpoke2
      RouteTableId: !Ref Spoke2RouteTable

  Spoke2DefaultRoute:
    Type: AWS::EC2::Route
    DependsOn: Spoke2TGWAttachment
    Properties:
      RouteTableId: !Ref Spoke2RouteTable
      DestinationCidrBlock: 0.0.0.0/0
      TransitGatewayId: !Ref TransitGateway

  Spoke2TGWAttachment:
    Type: AWS::EC2::TransitGatewayAttachment
    DependsOn: TransitGateway
    Properties:
      TransitGatewayId: !Ref TransitGateway
      VpcId: !Ref Spoke2VPC
      SubnetIds:
        - !Ref TgwSubnetSpoke2
      Tags:
        - Key: Name
          Value: spoke2-attachment

  Spoke2TGWAssociation:
    Type: AWS::EC2::TransitGatewayRouteTableAssociation
    Properties:
      TransitGatewayAttachmentId: !Ref Spoke2TGWAttachment
      TransitGatewayRouteTableId: !Ref TGWRouteTableSpokes

  Spoke2TGWPropagation:
    Type: AWS::EC2::TransitGatewayRouteTablePropagation
    Properties:
      TransitGatewayAttachmentId: !Ref Spoke2TGWAttachment
      TransitGatewayRouteTableId: !Ref TGWRouteTableInspection

  Spoke2EICEndpoint:
    Type: AWS::EC2::InstanceConnectEndpoint
    Properties:
      SubnetId: !Ref TgwSubnetSpoke2
      Tags:
        - Key: Name
          Value: eic-endpoint-spoke2

  Spoke2SecurityGroup:
    Type: AWS::EC2::SecurityGroup
    Properties:
      GroupDescription: Security group for spoke2 instance
      VpcId: !Ref Spoke2VPC
      SecurityGroupIngress:
        - IpProtocol: -1
          CidrIp: 10.0.0.0/8
      SecurityGroupEgress:
        - IpProtocol: -1
          CidrIp: 0.0.0.0/0
      Tags:
        - Key: Name
          Value: spoke2-sg

  # ==========================================
  # Inspection VPC
  # ==========================================
  InspectionVPC:
    Type: AWS::EC2::VPC
    Properties:
      CidrBlock: 10.47.0.0/16
      EnableDnsSupport: true
      EnableDnsHostnames: true
      Tags:
        - Key: Name
          Value: inspection-VPC

  IGWAttachment:
    Type: AWS::EC2::VPCGatewayAttachment
    Properties:
      VpcId: !Ref InspectionVPC
      InternetGatewayId: !Ref InternetGateway

  # Private subnets for TGW attachment
  TgwSubnetInspectionAZ1:
    Type: AWS::EC2::Subnet
    Properties:
      VpcId: !Ref InspectionVPC
      CidrBlock: 10.47.1.0/24
      AvailabilityZone: !Select [0, !GetAZs '']
      Tags:
        - Key: Name
          Value: tgw-subnet-inspection-az1

  TgwSubnetInspectionAZ2:
    Type: AWS::EC2::Subnet
    Properties:
      VpcId: !Ref InspectionVPC
      CidrBlock: 10.47.2.0/24
      AvailabilityZone: !Select [1, !GetAZs '']
      Tags:
        - Key: Name
          Value: tgw-subnet-inspection-az2

  # Public subnets
  PublicSubnetInspectionAZ1:
    Type: AWS::EC2::Subnet
    Properties:
      VpcId: !Ref InspectionVPC
      CidrBlock: 10.47.3.0/24
      AvailabilityZone: !Select [0, !GetAZs '']
      MapPublicIpOnLaunch: true
      Tags:
        - Key: Name
          Value: public-subnet-inspection-az1

  PublicSubnetInspectionAZ2:
    Type: AWS::EC2::Subnet
    Properties:
      VpcId: !Ref InspectionVPC
      CidrBlock: 10.47.4.0/24
      AvailabilityZone: !Select [1, !GetAZs '']
      MapPublicIpOnLaunch: true
      Tags:
        - Key: Name
          Value: public-subnet-inspection-az2

  # TGW Subnet Route Table
  InspectionTGWSubnetRouteTable:
    Type: AWS::EC2::RouteTable
    Properties:
      VpcId: !Ref InspectionVPC
      Tags:
        - Key: Name
          Value: inspection-tgw-subnet-route-table

  InspectionTGWSubnetAZ1Association:
    Type: AWS::EC2::SubnetRouteTableAssociation
    Properties:
      SubnetId: !Ref TgwSubnetInspectionAZ1
      RouteTableId: !Ref InspectionTGWSubnetRouteTable

  InspectionTGWSubnetAZ2Association:
    Type: AWS::EC2::SubnetRouteTableAssociation
    Properties:
      SubnetId: !Ref TgwSubnetInspectionAZ2
      RouteTableId: !Ref InspectionTGWSubnetRouteTable

  # Public Subnet Route Table
  InspectionPublicRouteTable:
    Type: AWS::EC2::RouteTable
    Properties:
      VpcId: !Ref InspectionVPC
      Tags:
        - Key: Name
          Value: public-route-table-inspection

  PublicSubnetAZ1Association:
    Type: AWS::EC2::SubnetRouteTableAssociation
    Properties:
      SubnetId: !Ref PublicSubnetInspectionAZ1
      RouteTableId: !Ref InspectionPublicRouteTable

  PublicSubnetAZ2Association:
    Type: AWS::EC2::SubnetRouteTableAssociation
    Properties:
      SubnetId: !Ref PublicSubnetInspectionAZ2
      RouteTableId: !Ref InspectionPublicRouteTable

  PublicDefaultRoute:
    Type: AWS::EC2::Route
    DependsOn: IGWAttachment
    Properties:
      RouteTableId: !Ref InspectionPublicRouteTable
      DestinationCidrBlock: 0.0.0.0/0
      GatewayId: !Ref InternetGateway

  PublicRFC1918Route:
    Type: AWS::EC2::Route
    DependsOn: InspectionTGWAttachment
    Properties:
      RouteTableId: !Ref InspectionPublicRouteTable
      DestinationCidrBlock: 10.0.0.0/8
      TransitGatewayId: !Ref TransitGateway

  # Inspection TGW Attachment
  InspectionTGWAttachment:
    Type: AWS::EC2::TransitGatewayAttachment
    DependsOn: TransitGateway
    Properties:
      TransitGatewayId: !Ref TransitGateway
      VpcId: !Ref InspectionVPC
      SubnetIds:
        - !Ref TgwSubnetInspectionAZ1
        - !Ref TgwSubnetInspectionAZ2
      Tags:
        - Key: Name
          Value: inspection-attachment

  InspectionTGWAssociation:
    Type: AWS::EC2::TransitGatewayRouteTableAssociation
    Properties:
      TransitGatewayAttachmentId: !Ref InspectionTGWAttachment
      TransitGatewayRouteTableId: !Ref TGWRouteTableInspection

  # Inspection EC2 Instance Connect Endpoint
  InspectionEICEndpoint:
    Type: AWS::EC2::InstanceConnectEndpoint
    Properties:
      SubnetId: !Ref TgwSubnetInspectionAZ1
      Tags:
        - Key: Name
          Value: eic-endpoint-inspection

  # ==========================================
  # Security Group for Inspection Instances
  # ==========================================
  InspectionSecurityGroup:
    Type: AWS::EC2::SecurityGroup
    Properties:
      GroupDescription: Security group for inspection instances
      VpcId: !Ref InspectionVPC
      SecurityGroupIngress:
        - IpProtocol: -1
          CidrIp: 10.0.0.0/8
        - IpProtocol: -1
          CidrIp: 172.16.0.0/12
        - IpProtocol: -1
          CidrIp: 192.168.0.0/16
      SecurityGroupEgress:
        - IpProtocol: -1
          CidrIp: 0.0.0.0/0
      Tags:
        - Key: Name
          Value: inspection-sg

  # ==========================================
  # EC2 Instances in Inspection VPC
  # ==========================================
  InspectionInstanceAZ1:
    Type: AWS::EC2::Instance
    Properties:
      ImageId: !Ref LatestAmiId
      InstanceType: t3.micro
      SubnetId: !Ref PublicSubnetInspectionAZ1
      SecurityGroupIds:
        - !Ref InspectionSecurityGroup
      SourceDestCheck: false
      Tags:
        - Key: Name
          Value: inspection-instance-az1
      UserData:
        Fn::Base64:
          Fn::Sub:
            - |       
              #!/bin/bash
              set -e

              endpointIP=${endpointIP}

              # Enable IP forwarding
              echo "net.ipv4.ip_forward = 1" | sudo tee -a /etc/sysctl.conf
              sysctl -p

              # Install GoBGP
              cd /tmp
              wget https://github.com/osrg/gobgp/releases/download/v3.20.0/gobgp_3.20.0_linux_amd64.tar.gz
              tar -xzf gobgp_3.20.0_linux_amd64.tar.gz
              mv gobgp gobgpd /usr/local/bin/
              chmod +x /usr/local/bin/gobgp*

              # Create config directory
              mkdir -p /etc/gobgp

              # Get instance IP
              INSTANCE_IP=$(hostname -I | awk '{print $1}')

              # Create GoBGP configuration
              cat > /etc/gobgp/gobgpd.conf << EOF
              [global.config]
                as = 65550
                router-id = "$INSTANCE_IP"

              [[neighbors]]
                [neighbors.config]
                  neighbor-address = "$endpointIP"
                  peer-as = 65500
                [[neighbors.afi-safis]]
                  [neighbors.afi-safis.config]
                    afi-safi-name = "ipv4-unicast"
              EOF

              # Create route advertisement script
              cat > /usr/local/bin/gobgp-advertise.sh << 'EOF'
              #!/bin/bash
              sleep 15
              /usr/local/bin/gobgp global rib add 10.0.0.0/8
              EOF
              chmod +x /usr/local/bin/gobgp-advertise.sh

              # Create systemd service for GoBGP
              cat > /etc/systemd/system/gobgpd.service << 'EOF'
              [Unit]
              Description=GoBGP Daemon
              After=network.target

              [Service]
              Type=simple
              ExecStart=/usr/local/bin/gobgpd -f /etc/gobgp/gobgpd.conf
              Restart=always
              RestartSec=10

              [Install]
              WantedBy=multi-user.target
              EOF

              # Create systemd service for route advertisement
              cat > /etc/systemd/system/gobgp-advertise.service << 'EOF'
              [Unit]
              Description=GoBGP Route Advertisement
              After=gobgpd.service
              Requires=gobgpd.service

              [Service]
              Type=oneshot
              ExecStart=/usr/local/bin/gobgp-advertise.sh
              RemainAfterExit=yes

              [Install]
              WantedBy=multi-user.target
              EOF

              # Start services
              systemctl daemon-reload
              systemctl enable gobgpd
              systemctl enable gobgp-advertise
              systemctl start gobgpd
              systemctl start gobgp-advertise
            - endpointIP: !GetAtt RouteServerEndpointAZ1.EniAddress

  InspectionInstanceAZ2:
    Type: AWS::EC2::Instance
    Properties:
      ImageId: !Ref LatestAmiId
      InstanceType: t3.micro
      SubnetId: !Ref PublicSubnetInspectionAZ2
      SecurityGroupIds:
        - !Ref InspectionSecurityGroup
      SourceDestCheck: false
      Tags:
        - Key: Name
          Value: inspection-instance-az2
      UserData:
        Fn::Base64:
          Fn::Sub:
            - |       
              #!/bin/bash
              set -e

              endpointIP=${endpointIP}

              # Enable IP forwarding
              echo "net.ipv4.ip_forward = 1" | sudo tee -a /etc/sysctl.conf
              sysctl -p

              # Install GoBGP
              cd /tmp
              wget https://github.com/osrg/gobgp/releases/download/v3.20.0/gobgp_3.20.0_linux_amd64.tar.gz
              tar -xzf gobgp_3.20.0_linux_amd64.tar.gz
              mv gobgp gobgpd /usr/local/bin/
              chmod +x /usr/local/bin/gobgp*

              # Create config directory
              mkdir -p /etc/gobgp

              # Get instance IP
              INSTANCE_IP=$(hostname -I | awk '{print $1}')

              # Create GoBGP configuration
              cat > /etc/gobgp/gobgpd.conf << EOF
              [global.config]
                as = 65550
                router-id = "$INSTANCE_IP"

              [[neighbors]]
                [neighbors.config]
                  neighbor-address = "$endpointIP"
                  peer-as = 65500
                [[neighbors.afi-safis]]
                  [neighbors.afi-safis.config]
                    afi-safi-name = "ipv4-unicast"
              EOF

              # Create route advertisement script with AS-path prepending
              cat > /usr/local/bin/gobgp-advertise.sh << 'EOF'
              #!/bin/bash
              sleep 15
              /usr/local/bin/gobgp global rib add 10.0.0.0/8 aspath "65550 65550" -a ipv4
              EOF
              chmod +x /usr/local/bin/gobgp-advertise.sh

              # Create systemd service for GoBGP
              cat > /etc/systemd/system/gobgpd.service << 'EOF'
              [Unit]
              Description=GoBGP Daemon
              After=network.target

              [Service]
              Type=simple
              ExecStart=/usr/local/bin/gobgpd -f /etc/gobgp/gobgpd.conf
              Restart=always
              RestartSec=10

              [Install]
              WantedBy=multi-user.target
              EOF

              # Create systemd service for route advertisement
              cat > /etc/systemd/system/gobgp-advertise.service << 'EOF'
              [Unit]
              Description=GoBGP Route Advertisement
              After=gobgpd.service
              Requires=gobgpd.service

              [Service]
              Type=oneshot
              ExecStart=/usr/local/bin/gobgp-advertise.sh
              RemainAfterExit=yes

              [Install]
              WantedBy=multi-user.target
              EOF

              # Start services
              systemctl daemon-reload
              systemctl enable gobgpd
              systemctl enable gobgp-advertise
              systemctl start gobgpd
              systemctl start gobgp-advertise
            - endpointIP: !GetAtt RouteServerEndpointAZ2.EniAddress

 # ==========================================
  # EC2 Instance in Spoke1 VPC
  # ==========================================
  Spoke1Instance:
    Type: AWS::EC2::Instance
    Properties:
      ImageId: !Ref LatestAmiId
      InstanceType: t3.micro
      SubnetId: !Ref InstanceSubnetSpoke1
      SecurityGroupIds:
        - !Ref Spoke1SecurityGroup
      Tags:
        - Key: Name
          Value: spoke1-instance

 # ==========================================
  # EC2 Instance in Spoke2 VPC
  # ==========================================
  Spoke2Instance:
    Type: AWS::EC2::Instance
    Properties:
      ImageId: !Ref LatestAmiId
      InstanceType: t3.micro
      SubnetId: !Ref InstanceSubnetSpoke2
      SecurityGroupIds:
        - !Ref Spoke2SecurityGroup
      Tags:
        - Key: Name
          Value: spoke2-instance

  # ==========================================
  # VPC Route Server
  # ==========================================
  RouteServer:
    Type: AWS::EC2::RouteServer
    Properties:
      AmazonSideAsn: 65500
      Tags:
        - Key: Name
          Value: inspection-route-server

  RouteServerAssociation:
    Type: AWS::EC2::RouteServerAssociation
    Properties:
      RouteServerId: !Ref RouteServer
      VpcId: !Ref InspectionVPC

  RouteServerEndpointAZ1:
    Type: AWS::EC2::RouteServerEndpoint
    DependsOn: RouteServerAssociation
    Properties:
      RouteServerId: !Ref RouteServer
      SubnetId: !Ref PublicSubnetInspectionAZ1
      Tags:
        - Key: Name
          Value: rs-endpoint-az1

  RouteServerEndpointAZ2:
    Type: AWS::EC2::RouteServerEndpoint
    DependsOn: RouteServerAssociation
    Properties:
      RouteServerId: !Ref RouteServer
      SubnetId: !Ref PublicSubnetInspectionAZ2
      Tags:
        - Key: Name
          Value: rs-endpoint-az2

  RouteServerPeer1:
    Type: AWS::EC2::RouteServerPeer
    DependsOn:
      - RouteServerEndpointAZ1
      - RouteServerEndpointAZ2
      - InspectionInstanceAZ1
    Properties:
      RouteServerEndpointId: !Ref RouteServerEndpointAZ1
      PeerAddress: !GetAtt InspectionInstanceAZ1.PrivateIp
      BgpOptions:
        PeerAsn: 65550
      Tags:
        - Key: Name
          Value: rs-peer-az1

  RouteServerPeer2:
    Type: AWS::EC2::RouteServerPeer
    DependsOn:
      - RouteServerEndpointAZ1
      - RouteServerEndpointAZ2
      - InspectionInstanceAZ2
    Properties:
      RouteServerEndpointId: !Ref RouteServerEndpointAZ2
      PeerAddress: !GetAtt InspectionInstanceAZ2.PrivateIp
      BgpOptions:
        PeerAsn: 65550
      Tags:
        - Key: Name
          Value: rs-peer-az2

  RouteServerPropagation:
    Type: AWS::EC2::RouteServerPropagation
    DependsOn: RouteServerAssociation
    Properties:
      RouteServerId: !Ref RouteServer
      RouteTableId: !Ref InspectionTGWSubnetRouteTable

Outputs:
  TransitGatewayId:
    Value: !Ref TransitGateway

  Spoke1VPCId:
    Value: !Ref Spoke1VPC

  Spoke2VPCId:
    Value: !Ref Spoke2VPC

  InspectionVPCId:
    Value: !Ref InspectionVPC

  RouteServerId:
    Value: !Ref RouteServer

  InspectionInstance1PrivateIP:
    Value: !GetAtt InspectionInstanceAZ1.PrivateIp

  InspectionInstance2PrivateIP:
    Value: !GetAtt InspectionInstanceAZ2.PrivateIp

  1. Navigate to the AWS CloudFormation console in your desired region
  2. Create a new stack by uploading the template and providing a stack name

The template deploys:

  • Transit Gateway with two route tables: spokes (default route to inspection VPC) and inspection (propagated spoke routes)
  • Spoke1 VPC (10.45.0.0/16) and Spoke2 VPC (10.46.0.0/16) — each with an EC2 instance and TGW attachment
  • Inspection VPC (10.47.0.0/16) with:
    • Two inspection EC2 instances running GoBGP (one per AZ)
    • A VPC Route Server (ASN 65500) with endpoints in each AZ
    • BGP peering between each inspection instance and its local route server endpoint
    • Route server propagation enabled on the TGW subnet route table
  • EC2 Instance Connect Endpoints in each Spoke VPC for private SSH access

Step 2: Verify BGP Peering

Connect to the AZ1 inspection instance using EC2 Instance Connect endpoint:

Check BGP neighbor status:

/usr/local/bin/gobgp neighbor

You should see the route server endpoint as an established BGP neighbor:

Peer             AS  Up/Down State       |#Received  Accepted
<rs-endpoint-ip> 65500 00:05:00 Establ      |        0         0

Check advertised routes:

/usr/local/bin/gobgp global rib

Expected output:

   Network              Next Hop             AS_PATH              Age
*> 10.0.0.0/8           <instance-ip>        65550                00:05:00

Repeat for the AZ2 instance — you should see the same route but with AS-path 65550 65550.

Step 3: Verify Route Propagation

Check the inspection VPC's TGW subnet route table. The route server should have propagated a route for 10.0.0.0/8 pointing to the AZ1 inspection instance's ENI (the active path):

Enter image description here

You should see the route pointing to the AZ1 instance's network interface.

Step 4: Test End-to-End Connectivity

East-west test (spoke-to-spoke): Connect to the spoke1 instance and ping the spoke2 instance:

[ec2-user@ip-10-45-1-205 ~]$ ping 10.46.1.48
PING 10.46.1.48 (10.46.1.48) 56(84) bytes of data.
64 bytes from 10.46.1.48: icmp_seq=1 ttl=124 time=2.09 ms
64 bytes from 10.46.1.48: icmp_seq=2 ttl=124 time=1.75 ms
64 bytes from 10.46.1.48: icmp_seq=3 ttl=124 time=1.03 ms
[ec2-user@ip-10-45-1-205 ~]$ traceroute 10.46.1.48
traceroute to 10.46.1.48 (10.46.1.48), 30 hops max, 60 byte packets
 1  * * *
 2  ip-10-47-3-42.us-west-2.compute.internal (10.47.3.42)  1.084 ms  1.077 ms  0.884 ms   < -------- Use traceroute to confirm traffic is routed through the appliance in AZ 1
 3  * * *
 4  ip-10-46-1-48.us-west-2.compute.internal (10.46.1.48)  1.939 ms  1.928 ms *

Step 5: Test Failover

Stop the AZ1 inspection instance to simulate a failure:

aws ec2 stop-instances --instance-ids <inspection-instance-az1-id>

The BGP session between AZ1 and the route server will drop. The route server detects this via BGP keepalive timeout and updates the VPC route table to point to the AZ2 instance (the standby path with the prepended AS-path). As discussed in the original blog post, enabling BFD can significantly reduce this detection time to sub-second convergence.

Verify the route table has updated:

Enter image description here

The route should now point to the AZ2 instance's network interface.

Test connectivity again from spoke1 — traffic should still flow, now via AZ2:

[ec2-user@ip-10-45-1-205 ~]$ ping 10.46.1.48
PING 10.46.1.48 (10.46.1.48) 56(84) bytes of data.
64 bytes from 10.46.1.48: icmp_seq=1 ttl=124 time=2.57 ms
64 bytes from 10.46.1.48: icmp_seq=2 ttl=124 time=1.84 ms
64 bytes from 10.46.1.48: icmp_seq=3 ttl=124 time=1.68 ms
[ec2-user@ip-10-45-1-205 ~]$ traceroute 10.46.1.48
traceroute to 10.46.1.48 (10.46.1.48), 30 hops max, 60 byte packets
 1  * * *
 2  ip-10-47-4-229.us-west-2.compute.internal (10.47.4.229)  1.523 ms  1.511 ms  1.603 ms   < -------- Use traceroute to confirm traffic is now routed through the appliance in AZ 2
 3  * * *
 4  ip-10-46-1-48.us-west-2.compute.internal (10.46.1.48)  2.418 ms  2.452 ms  2.594 ms

Start the AZ1 instance again to restore the active path:

aws ec2 start-instances --instance-ids <inspection-instance-az1-id>

Once GoBGP re-establishes the BGP session, the route server will prefer AZ1 again (shorter AS-path) and update the route table automatically.

Cleanup

Delete the CloudFormation stack to remove all resources:

aws cloudformation delete-stack \
  --stack-name <Stack-Name> \
  --region us-east-1

Conclusion

In the original blog post, we demonstrated how Amazon VPC Route Server enables dynamic routing for floating IP failover and VPC ingress inspection. In this article, we extended those patterns to a centralized multi-VPC inspection architecture using Transit Gateway. The implementation of the VPC Router Server components remains the same if AWS CloudWAN is in use for either East-West and North-South connectivity.

By combining Transit Gateway with VPC Route Server and BGP AS-path prepending, traffic from multiple spoke VPCs is dynamically steered to the preferred inspection instance — covering both east-west (spoke-to-spoke) and north-south (spoke-to-internet) flows. When the active instance fails, the route server automatically shifts traffic to the standby, and shifts it back on recovery. No static routes or manual intervention required.

This pattern is particularly useful when Gateway Load Balancer is not an option — for example, when your appliance doesn't support GENEVE, or when you need active/standby rather than active/active failover. It can be extended to support additional spoke VPCs, more complex routing policies, or integration with commercial firewall vendors that support BGP.

For more details on VPC Route Server concepts and getting started, refer to the VPC Route Server documentation and the Getting Started tutorial.

AWS
EXPERT
published a month ago69 views
No comments

Relevant content