2
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Transit Gateway用サブネットを"CIDRの末尾"に配置したい問題 — 既存ツールで解決できず自作した

2
Posted at

はじめに

VPCのサブネットを設計する上で、どのようにCIDRを計算していますか? Web上のネットワーク計算ツールで1つずつ計算でしょうか。

もしくは、AWS Subnet CalculatorAWSドキュメント>Amazon EKS>Amazon VPC CNIに紹介があるエクセルのサブネット計算ツールでしょうか?

これらのツールでも便利なのですが、Transit Gateway用のサブネットをVPCに割り当てたCIDRの最後にしたい場合に不便さがありました。
というのも、ある組織ではAWS Direct ConnectとTransit Gatewayによってオンプレミスとの接続を構築しており、新規AWSアカウント発行時には、VPCとTransit Gateway用のサブネットが作成済みで払い出される運用でした。
しかもこのTransit Gateway用のサブネットは、VPC CIDRの最後から払い出す仕様となっていました。

業務アプリケーション用のサブネットを計算する上で、Transit Gatewaty用のサブネットがCIDRの末尾で計算できるツールが必要でした。そのため、ツールを自作することにしました。

作成したツールは、GitHub Pagesで公開しています。
ソースコードは、こちらです。

screenshot
screenshot-result

機能

このツールでは以下のことが実施できます。

  • VPC CIDR 可変入力 — 任意のCIDRブロックを入力可能(例:10.0.0.0/22
  • マルチティア設計 — ティアの追加・削除・サブネットマスク指定が自由に行える
  • ドラッグ&ドロップで並び替え — ティアの順序をドラッグ操作で自由に入れ替え可能
  • 2AZ対応ap-northeast-1a / ap-northeast-1c へ自動で振り分けて割り当て
  • TGW末尾割り当て — ティア名に「TGW」を含む場合、VPCアドレス空間の末尾から逆算して割り当て(ON/OFF切り替え可能)
  • 空きアドレス空間の検出 — アライメントギャップ・前詰めとTGWの間・VPC末尾の余剰など、すべての空き範囲を検出・表示
  • AWSの予約アドレス考慮 — サブネットごとに5アドレスを控除した有効ホスト数を表示
  • 日英言語切り替え — ボタンでUI表示言語を切り替え可能。初回表示時はブラウザの言語設定を自動判定
  • ダークモード対応 — システムのカラースキームに自動追従

CIDR計算方法

このツールの計算処理は、サーバーもDBも使わず、ブラウザ内のJavaScriptによるビット演算だけで完結しています。処理の流れは大きく4ステップです。

1. CIDR文字列を32bit整数に変換する

まず 10.0.0.0/22 のような入力文字列を、ビット演算できるように32bit整数(ネットワークアドレスと総アドレス数)へ変換します。

function parseVPC(cidr) {
  var m = cidr.trim().match(/^(\d+\.\d+\.\d+\.\d+)\/(\d+)$/);
  if (!m) return null;
  var parts = m[1].split('.').map(Number);
  if (parts.some(function(p) { return p < 0 || p > 255; })) return null;
  var prefix = parseInt(m[2]);
  if (prefix < 8 || prefix > 28) return null;
  var base = ((parts[0]<<24)|(parts[1]<<16)|(parts[2]<<8)|parts[3]) >>> 0;
  var total = Math.pow(2, 32 - prefix);
  var network = (base & (~((1 << (32 - prefix)) - 1) >>> 0)) >>> 0;
  return { base: network, prefix: prefix, total: total, end: network + total - 1 };
}

ポイントは次の2つです。

  • parts[0]<<24 | parts[1]<<16 | parts[2]<<8 | parts[3] で4つのオクテットを1つの32bit整数に合成する
  • >>> 0 をつけて符号なし整数として扱う(JavaScriptのビット演算は32bit符号付きのため、これをつけないと第1オクテットが128以上のときに負の数になってしまう)

さらに ~((1 << (32 - prefix)) - 1) でサブネットマスクを作り base & mask を取ることで、たとえ 10.0.0.5/22 のようにホスト部が0でない値を入力しても、自動的に 10.0.0.0/22 のネットワークアドレスへ丸め込んでいます。

2. 通常ティアの前詰め割り当て(アライメント境界への切り上げ)

Private-AppPublicのような通常ティアは、VPCの先頭からAZごとに順番に詰めて割り当てます。ここでのポイントは「アライメント」です。たとえば /26 のサブネットは64刻みの境界(x.x.x.0, x.x.x.64, x.x.x.128 ...)にしか配置できません。

var fwdCursor = vpc.base;
for (var ni = 0; ni < normalTiers.length; ni++) {
  var ntier = normalTiers[ni];
  var sz = Math.pow(2, 32 - ntier.mask);
  for (var az = 0; az < 2; az++) {
    var aligned = Math.ceil(fwdCursor / sz) * sz;
    var end = aligned + sz - 1;
    if (end > vpc.end) { overflow = true; break; }
    allocRows.push({ kind:'subnet', tier: ntier.name, az: AZS[az], azIdx: az,
      cidr: ipToStr(aligned) + '/' + ntier.mask, first: ipToStr(aligned), last: ipToStr(end),
      total: sz, usable: sz-5, tail: false, startNum: aligned, endNum: end });
    fwdCursor = end + 1;
  }
  if (overflow) break;
}

Math.ceil(fwdCursor / sz) * sz が要です。現在のカーソル位置を「サブネットサイズの倍数」に切り上げることで、前のティアの終端に隙間なく詰めても、必ずCIDRとして有効な境界にスナップされます。VPCの範囲を超えたら overflow フラグを立てて計算を打ち切ります。

3. TGWティアの末尾割り当て(アライメント境界への切り下げ)

ティア名に「TGW」を含む場合は、VPCの末尾から逆順に割り当てます。これがこのツールを自作するきっかけになった機能です。

function floorAligned(addr, size) { return Math.floor(addr / size) * size; }

var bwdCursor = vpc.end;
var tgwBuilt = [];
var tgwRev = tgwTiers.slice().reverse();
for (var gi = 0; gi < tgwRev.length; gi++) {
  var gtier = tgwRev[gi];
  var gsz = Math.pow(2, 32 - gtier.mask);
  for (var gaz = 1; gaz >= 0; gaz--) {
    var galigned = floorAligned(bwdCursor - gsz + 1, gsz);
    var gend = galigned + gsz - 1;
    if (galigned < vpc.base) { overflow = true; break; }
    tgwBuilt.unshift({ kind:'subnet', tier: gtier.name, az: AZS[gaz], azIdx: gaz,
      cidr: ipToStr(galigned) + '/' + gtier.mask, first: ipToStr(galigned), last: ipToStr(gend),
      total: gsz, usable: gsz-5, tail: true, startNum: galigned, endNum: gend });
    bwdCursor = galigned - 1;
  }
  if (overflow) break;
}

考え方は前詰めの逆です。bwdCursor - gsz + 1 でサブネット先頭の候補位置を求めてから、サイズの倍数に切り下げfloorAligned)ることで、末尾側から見て有効な境界にスナップします。TGWティアの配列を reverse() してから処理し、結果は unshift で先頭に積んでいくため、画面上はティアの入力順を保ったまま末尾から埋まっていくように見えます。

4. 空きアドレス空間の検出

前詰め・末尾詰めの結果を開始アドレス順にソートし、隣接するサブネットの間に隙間があれば「空き」として記録します。

var allSubnets = allocRows.concat(tgwBuilt).sort(function(a,b) { return a.startNum - b.startNum; });

var gaps = [];
var cursor = vpc.base;
for (var si = 0; si < allSubnets.length; si++) {
  var s = allSubnets[si];
  if (s.startNum > cursor) gaps.push({ kind:'free', startNum: cursor, endNum: s.startNum-1 });
  cursor = s.endNum + 1;
}
if (cursor <= vpc.end) gaps.push({ kind:'free', startNum: cursor, endNum: vpc.end });

やっていることは「区間の隙間検出」という定番のアルゴリズムです。VPCの先頭からカーソルを進めながら、各サブネットとカーソルの間に隙間があればそれを記録し、最後まで進んでもVPCの末尾に到達していなければ、余った範囲も「空き」として追加します。アライメントによって生じる半端なアドレス(前詰めの終端とTGWの間のスペースなど)も、この仕組みだけで漏れなく検出できます。

以上のように、「アライメント境界への切り上げ・切り下げ」と「区間の隙間検出」という2つのシンプルなアルゴリズムを組み合わせるだけで、複数ティア・複数AZ・末尾割り当てというやや複雑な要件を、サーバーレスかつ数十行のコードで実現しています。

2
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?