PowerShell で AWS の RouteTable を変更する

クロスワープの大鷲です。

MODD では AWS の採用を積極的に進めています。
EC2 の仮想マシンから外部のパブリックなサービスを利用するために、インターネットにアクセスするには、NAT インスタンスを通すのが一般的でしょう。
Web サーバーは冗長化するのが常識ですが、NAT インスタンスも複数用意しておかないと、1つが落ちたらインターネットにアクセスできなくなってしまうのでは困ります。
そこで、NAT インスタンスも冗長化する、いわゆる High Availability NAT (HA-NAT) パターンというやつを構築する必要があります。

HA-NAT パターンを構成する方法も、簡単なものから複雑なものまで、いろいろあります。
NAT インスタンスのダウンを検出する方法、予備のインスタンスを立ち上げる方法、接続先を決める方法などに、それぞれバリエーションがあり、その組み合わせで多様に変化するからです。

今回はそのあたりの組み合わせの話は置いておいて、PowerShell で NAT の接続を切り替える方法について紹介したいと思います。
つまり、NAT インスタンスのダウンを検出した後の話です。

ネットワーク構成

まず、ネットワーク構成は、よくあるこんな感じです(アウトバウンド接続のみ記載しています)。
サブネットがルート テーブルを持っていて、そのルートテーブルに接続先の NAT インスタンスが登録されています。
NAT インスタンスは IGW(インターネット ゲートウェイ)を通してインターネットに接続されています。*1
NAT インスタンスについては、アクティブ/スタンバイではなく、常に両方動かしています。

f:id:cw_owashi:20150824184545p:plain

Amazon がこういう画像用のアイコン集を公開してくれているので、簡単に見栄えがする画像が作れていいですね。*2

PowerShell コマンド

ルート変更関連の PowerShell コマンドは以下の 3 つです。

それぞれどういうものかということを見る前に、上の図をもう一度見て頂きたいと思います。
あの図の中には、Amazon が公開しているアイコン集にない、手作りのアイコンが1つ含まれています。
白抜きの「Assoc」というやつがそれです。
f:id:cw_owashi:20150824190119p:plain
Association の略のつもりです。

Association

サブネットがどのルート テーブルを参照しているかという情報はどうすればわかるでしょうか。
Get-EC2Subnet というコマンドを叩いてみても、この戻り値は Subnet オブジェクトなのですが、このメンバーにルート テーブルを表すものはありません。
では逆に、ルート テーブルの側から辿れるでしょうか。
Get-EC2RouteTable コマンドの戻り値である RouteTable オブジェクトにも、やはり、サブネットのようなものは見当たりません。
しかし、Associations というメンバーがあります。RouteTableAssociation オブジェクトのリストで、こいつが、サブネットとルート テーブルを関連付けています。
1つのルートテーブルを、複数のサブネットと関連付けることができるために、このような設計になっているのだと思われます。

接続を切り替える

NAT インスタンスが片方死んでしまったので、もう一方に切り替えたいとします。
f:id:cw_owashi:20150824192140p:plain
Register-EC2RouteTable コマンドが、パラメーターに SubnetId と RouteTableId を取りますので、これで関連付けることができるように思えます。
しかし、実験してみると、Register-EC2RouteTable で関連付けを行うことができるのは、最初の1回だけです。

# $credential には認証情報が入っているとする

Register-EC2RouteTable -SubnetId 'subnet-id-1' -RouteTableId 'route-table-id-1' @credential
Register-EC2RouteTable -SubnetId 'subnet-id-2' -RouteTableId 'route-table-id-2' @credential

上記のような設定がされている状態で、NAT インスタンス 2 がダウンしたので、サブネット 2 をルートテーブル 1 に向けようとして、以下のようにしても、エラーになってしまいます。

Register-EC2RouteTable -SubnetId 'subnet-id-2' -RouteTableId 'route-table-id-1' @credential

Register-EC2RouteTable : the specified association for route table route-table-id-1 conflicts with an existing association

Register-EC2RouteTable は、サブネットとルート テーブルの間に Association を作るコマンドです。
そして、既に Association があるルート テーブルに関しては、Register-EC2RouteTable コマンドは使えません。
サブネットが紐づく Association に対して、その向け先のルート テーブルを変える必要があります。
これをやるのが Set-EC2RouteTableAssociation コマンドです。

スクリプトの例

Set-EC2RouteTableAssociation コマンドを使うには、パラメーターに Association の ID を与える必要があります。
これは Register-EC2Route の戻り値として得られます。
が、サブネットやルート テーブルと違い、Association オブジェクトは AWS の管理画面から見ることができず、ID を調べることもできません。
また、初回と2回目以降で使うコマンドが違うというのも気持ちが悪いところです。
そのため、このあたりの処理はスクリプトの中に隠蔽してしまいましょう。

というわけで書いてみたのが、以下のスクリプトです。

function Set-RouteTable {

  param(
    $SubnetId,
    $RouteTableId)

  # この SubnetId が現在関連付けられている RouteTable を取得
  $filter = New-Object 'Amazon.EC2.Model.Filter' 'association.subnet-id', @($SubnetId)
  $rt = Get-EC2RouteTable -Filter $filter @credential

  # この Subnet と RouteTable の関連づけを取得
  $association = $rt.Associations | ? { $_.SubnetId -eq $SubnetId }

  if ($association) {
    # Association がある場合(Subnet が既に何らかの RouteTable に関連付けられている場合)
    $association = $association[0]
    if ($association.RouteTableId -ne $RouteTableId) {
      # それが目的の RouteTable でなければ、Association の RouteTable を目的のものに変更
      Set-EC2RouteTableAssociation -AssociationId $association.RouteTableAssociationId -RouteTableId $RouteTableId @credential
    }
  }
  else {
    # Association がない場合(Subnet がどの RouteTable にも関連付けられていない場合)
    # 新たに関連付けを作成
    Register-EC2RouteTable -RouteTableId $RouteTableId -SubnetId $SubnetId @credential
  }
}

まず、サブネット ID から、関連付けられているルート テーブルを取得します。検索ためのフィルターの組み立てが若干面倒です。
関連付けがなければ、Register-EC2RouteTable コマンドで初回の紐づけを行います。
既に何らかの関連付けがあり、それが今回切り替えたい先のルート テーブルでないのなら、Set-EC2RouteTableAssociation で向け先を変えるという処理をしています。

なお、Unregister-EC2RouteTable で一旦関連付けを切ってから Register-EC2RouteTable で再登録という方法でも、切り替えることはできます。
ただし、この場合、一時的に、サブネットとルート テーブルの関連付けが存在せず、ネットワーク接続ができない状態になってしまいます。
まぁ、もともと、NAT インスタンスが落ちた時の話なので、切り替えるまでは接続できないことに変わりはないのですが…。
Set-EC2RouteTableAssociation というコマンドが用意されているのは、関連付けを消さずに付け替える(設定の空白期間を作らない)ためなのかもしれません。

まとめ

元々、Register-EC2RouteTable だけでやろうとしていて、成功したりしなかったりするのは何故だろう? という調査から始まっています。
AWS に関することでは聖地と言っても過言ではない Developers.IO などでも、よく見れば、replace-route-table-association といったコマンドを使っていることに気づくのですが、シェル スクリプトは書いたことがないもので見落としてしまっていました。反省。

というわけでまとめると、

  • Register-EC2RouteTable で関連付けを設定できるのは、関連付けがまだ存在しない最初の1回のみ
  • 既に関連付けがある場合は、Set-EC2RouteTableAssociation で切り替える必要がある

ということになります。

*1:本当はこの間にもルートテーブルがいるのですが省略しています

*2:このアイコンを使うのが初めてなので、使い方を間違えていたら恥ずかしいですが…。