AWS ルーティングテーブルの書き換えによるAZまたぎでのフェイルオーバ


オンプレミスとawsをダイレクトコネクト接続するにあたり、ルータのインスタンスを作成しました。awsではVRRPなどを使って冗長化することができませんが、lambdaを使ってawsのルーティングテーブルを書き換えることで、似たようなことが実現できます。

この内容は、以下のブログを参考にしています。
Making Application Failover Seamless by Failing Over Your Private Virtual IP Across Availability Zones

以下のように、AZを超えた切り替えを目指します。


cloudwatchが検知する、インスタンスの"stopping"イベントをトリガーにして切り替えることにしました。cloudwatchの設定で、ステータスチェック(ping)に失敗すると、インスタンスを再起動するようにしてあります。


lambdaは以下のように、cloudwatchをトリガにして起動する関数を設定します。

RouterFailover_Fn

import json, boto3
from boto3.session import Session

def get_instance(response, instance_id):
  for reservation in response["Reservations"]:
    for instance in reservation["Instances"]:
      if instance["InstanceId"] == instance_id:
        return instance

def get_iface(instance):
  if instance.get("NetworkInterfaces", False):
      return instance["NetworkInterfaces"][0]


def lambda_handler(event, context):
    host1eni = 'eni-XXXXXXXXXXXXXX'   #ルータ1のeniをセット
    host2eni = 'eni-YYYYYYYYYYYYYY'   #ルータ2のeniをセット
    vpcid = 'vpc-qqqqqqqqq' #vpcをセット
    region = 'ap-northeast-1' #リージョンをセット
    print(event)
    my_instance_id = event['detail']['instance-id']
    session = boto3.session.Session()
    ec2client = session.client('ec2', region_name = region)
    response = ec2client.describe_instances()
    instance = get_instance(response, my_instance_id)
    iface = get_iface(instance)
    print(iface['NetworkInterfaceId'])
    ifid = iface['NetworkInterfaceId']
    if ifid == host1eni:
        altifid = host2eni
    elif ifid == host2eni:
        altifid = host1eni
    else:
        return
    route_table = ec2client.describe_route_tables(Filters=[{'Name': 'vpc-id','Values': [vpcid,]},])['RouteTables']
    for iter in route_table: 
        for each_route in  iter['Routes']: 
            try:
                if each_route['NetworkInterfaceId'] == ifid:
                    try:
                        print (iter['RouteTableId'] + " Route Deleting started for "+str(each_route['DestinationCidrBlock'])+" with eniid "+ifid )
                        ec2client.delete_route(DestinationCidrBlock=each_route['DestinationCidrBlock'],RouteTableId=iter['RouteTableId'])
                        print (iter['RouteTableId'] + " Route Deleted for "+str(each_route['DestinationCidrBlock'])+" with eniid "+ifid )
                    except Exception as e:
                        print (e)
                    try:
                        print (iter['RouteTableId'] + " Route creating started for "+str(each_route['DestinationCidrBlock'])+" with eniid "+altifid)
                        ec2client.create_route(DestinationCidrBlock=each_route['DestinationCidrBlock'],NetworkInterfaceId=altifid,RouteTableId=iter['RouteTableId'])
                        print (iter['RouteTableId'] + " Route created for "+str(each_route['DestinationCidrBlock'])+" with eniid "+altifid)
                    except Exception as e:
                        print (e)
            except:
                continue

ルータ1のインスタンスを停止させると、対象のeniが書き換わります。
書き換わる前

書き換わった後

最後に

awsのロードバランサはIPレベルでの切り替えができないので、ルータを切り替えたいような
場合に便利なソリューションです。
参考元のブログのように、cloudwatchの検知対象を広げれば、プロセスやサービスレベルのダウンなどのイベントでの切り替えが可能になります。
高度なHAクラスタの仕組みを導入しなくとも、少ないコードだけで実現できるのもリーズナブルで良いのではないでしょうか。