Transit Gateway + VPN の冗長化を検討してみる

こんにちは、米須です。
前回に引き続き Transit Gateway ネタです。

検討していた構成

元々、左下図のように2台のルータを使ってサイト間 VPN を冗長化していました。今回、冗長化構成という点はそのままで、Transit Gateway + VPN に置き換える右下図の構成を検討することになりました。なお、検討にあたっては、「VPN はスタティックルーティングで設定する」という条件も設定されていました。

スタティックルーティングで冗長化してみる

事前に似たような構成について書かれたサイトを探してみたのですが、BGP(Border Gateway Protocol) を利用したダイナミックルーティングの記事がほとんどでスタティックルーティングの記事は見つからなかったので、実際に試してみました。

前回の記事(VGW を利用したサイト間 VPN を Transit Gateway に切り替えてみた)を参考に1つ目の VPN を作成します。この時点では下図の構成となります。

Transit Gateway

Transit Gateway のルートテーブル(TGW_RTB_VPC)を作成し、VPC 側のアタッチメントを紐づけています。AWS → オンプレ向けのルートなので、CIDR に 192.168.0.0/24 を指定し1つ目の VPN のアタッチメントを紐づけています。同様にもう一つルートテーブル(TGW_RTB_VPN)を作成し、VPN 側のアタッチメントを紐づけた後、オンプレ → AWS 向けのルートを設定します。ここまでは問題なく設定できると思います。

次に2つ目の VPN を作成していきます。
2つ目の VPN 用アタッチメントを作成し、先程のルートテーブル(TGW_RTB_VPN)へ関連付けます。その後、ルートテーブル(TGW_RTB_VPC)のルートに2つ目の VPN 向けの宛先を追加しようとすると「静的ルートの作成中にエラーが発生しました」というエラーが表示されてしまいました。

静的ルート作成

どうやら「単一のアタッチメントへのプレフィックスの静的ルートの数」のクォータに引っかかっているようです。

[AWS ドキュメント]
Transit Gateway のクォータ
https://docs.aws.amazon.com/ja_jp/vpc/latest/tgw/transit-gateway-quotas.html

CIDR が異なれば2つの VPN アタッチメントをルートに登録できるのですが、冗長化が目的だったので異なる CIDR で登録するのはちょっと違うなと思い、AWS サポートに相談しました。冗長化するための解決策は下記2つということだったので、まずは ① の方法を検証してみました。
  ① 自分で障害検知の仕組みを構築し、ReplaceTransitGatewayRoute APIを使って CIDR 宛の VPN アタッチメントを切り替える
  ② スタティックルーティングを諦めて、ダイナミックルーティングにする

障害時に CIDR 宛の VPN アタッチメントを切り替える

VPN アタッチメントを切り替える仕組み

障害検知の仕組みは CloudWatch アラームを使用し、VPN の TunnelState < 1 になったら Lambda を起動します。Lambda では障害前(左下図)から障害後(右下図)に切り替えるために、下記の2つの処理を行うことにしました。
  a) ReplaceTransitGatewayRoute API により、ルートテーブル(TGW_RTB_VPC)のルートを1つ目の VPN アタッチメントから2つ目の VPN アタッチメントへ変更
  b) disassociate_transit_gateway_route_table API と associate_transit_gateway_route_table APIにより、ルートテーブル(TGW_RTB_VPN)から1つ目の VPN アタッチメントの関連付けを削除し、2つ目の VPN アタッチメントの関連付けを追加

Lambda を作ってみる

実際のプログラムは下記のとおりです。

import json
import boto3

VPN_CIDR    = '192.168.0.0/24'
TGW_RTB_VPC = 'tgw-rtb-06xxxxxxxxxxxxxxx'
TGW_RTB_VPN = 'tgw-rtb-0axxxxxxxxxxxxxxx'
TGW_VPN1    = 'tgw-attach-06xxxxxxxxxxxxxxx'
TGW_VPN2    = 'tgw-attach-08xxxxxxxxxxxxxxx'

def lambda_handler(event, context):

    print("---VPN tunnel down!---")
    
    client = boto3.client('ec2')

# << change route table >>
    query = client.replace_transit_gateway_route(
        DestinationCidrBlock=VPN_CIDR,
        TransitGatewayRouteTableId=TGW_RTB_VPC,
        TransitGatewayAttachmentId=TGW_VPN2
    )

# << disassociate route table >>
    query = client.disassociate_transit_gateway_route_table(
        TransitGatewayRouteTableId=TGW_RTB_VPN,
        TransitGatewayAttachmentId=TGW_VPN1
    )

# << associate route table >>
    query = client.associate_transit_gateway_route_table(
        TransitGatewayRouteTableId=TGW_RTB_VPN,
        TransitGatewayAttachmentId=TGW_VPN2
    )

検証結果は下記の通りとなります。
Lambda 実行前は下記画像の通りとなっており、ルートには VPN 1 が指定され、関連付けも VPN1 が紐づいています。

アラームをトリガーに Lambda が実行されるとルートは VPN2 に変更され、関連付けも VPN1 の削除と VPN2 の追加が行われています。

CloudWatch アラームの設定については記載を省きますが、このような手順によりスタティックルートでも冗長化構成ができそうです。

さいごに

ここまで検証しましたが、CloudWatch アラームがアラーム状態を検知するまでに1分ほどかかるため、もう少し短い時間で切り替えたいということになり、最終的にはダイナミックルーティングで構築することになりました(^^;