TECH PLAY

R

イベント

マガジン

技術ブログ

Containerlab と Juniper の無償仮想イメージ vJunos を使用し、データセンターネットワーク(NW)の定番アーキテクチャ「Leaf-Spine 構成」をゼロから構築するハンズオン記事です。eBGP による Underlay 構成を皮切りに、EVPN/VXLAN によるテナント L2 拡張、ESI-LAG を用いた冗長化、VRF Route Leaking によるインターネット接続ゲートウェイ模擬まで、クラウド NW の中核技術をひと通り体験できます。ハードウェア不要・無償イメージのみで動く手順と全設定例を掲載しています。 はじめに 前提知識 全体像 構築するトポロジ ポイント 進め方 前提とする環境 vJunos イメージの準備(vrnetlab でコンテナ化) 1. 環境構築 — Containerlab で機器を起動 1.1 トポロジ定義(clos-network.clab.yml) 1.2 デプロイ ✅ 完成状態チェックリスト 2. Underlay の構築 — eBGP で Loopback 同士を疎通させる 2.1 IP アドレス設計 2.2 各ノードの IP / Loopback 設定 2.3 eBGP の設定 ポイント spine1 leaf1-1 2.4 動作確認 ✅ 完成状態チェックリスト 3. Overlay の構築 — EVPN/VXLAN 3.1 アーキテクチャの整理 3.2 C-Plane: iBGP EVPN ピア leaf2-1 rr1 hv1(FRR) 確認 3.3 D-Plane: VXLAN セグメントを作る hv1:Linux Kernel + FRR leaf2-1 bm1 動作確認 3.4 EVPN Multihoming(ESI-LAG)で BM の接続を冗長化 方針 leaf2-1(既存設定の置き換え) leaf2-2(新規) bm1(Linux Bond) 切断試験 ✅ 完成状態チェックリスト(Overlay全体) 4. Border Leaf と Internet Gateway 4.1 leaf3-1(Border Leaf として VTEP 化) 4.2 inet-gw1: 共通の下準備(I/F + ASN) 4.3 inet-gw1: VRF と Route Leaking 4.4 inet-gw1: 外部 ISP との eBGP 4.5 isp1 (外部 ISP 模擬) 4.6 bm1 (Global IP 付与とデフォルトゲートウェイ) 4.7 動作確認 ✅ 完成状態チェックリスト(Border GW) 全体の完成確認 おまけ: ハマったら見るところ(Troubleshooting) Junos 側で何が起きているか覗くコマンド FRR 側 まとめ はじめに こんにちは。NTTドコモビジネスのクラウド、 SDPFクラウド/サーバー (以降、SDPF クラウド)の内製開発に従事している堀岡勇杜と申します。 私が主に担当しているのは、ESI(Elastic Service Infrastructure)という名称の、クラウドのNWオーケストレータの開発です。ESI チームの業務内容については、 こちらの記事 で詳しく紹介しています。 ※ 本記事で出てくるEVPN Multihoming の関連用語であるESI(Ethernet Segment Identifier)とは全くの別物です。 突然ですが、データセンター NW の定番アーキテクチャである Leaf-Spine(CLOS) と、その上で動く EVPN/VXLAN は、クラウド NW の中核技術です。しかし実機やシミュレータがなければ、その挙動を肌で理解するのは大変難しいです。私自身も入社当時(2025年4月)はクラウド NW については完全な初学者だったのですが、この 1 年間で勉強させていただき、その全貌の一端が理解できるようになってきました。本記事を通じて、私が得た知見を共有できましたら幸いです。さまざまな用語が出てきますが、それらを全て解説するのは不可能であるため、気になる点は適宜調べながら進めていただければと思います。 用語解説 Leaf-Spine … サーバーを収容する「Leaf(葉)」スイッチと、Leaf 同士をつなぐ「Spine(背骨)」スイッチの 2 階層でネットワークを組む CLOS の定番設計。どのサーバー間も同じホップ数で通信でき、帯域を横に足しやすいのが特長です。SDPFクラウドでは、Spineのさらに上のSuper Spineが存在しており、3 階層の CLOS になっています。 EVPN/VXLAN … 物理的に離れたサーバー同士を「同じ LAN にいるかのように」つなぐ仮想ネットワーク技術です。EVPN が「誰がどこにいるか」の情報交換(制御プレーン)、VXLAN が実データのトンネル転送(データプレーン)を担います。 本記事では、OSS である Containerlab と Juniper Networks 社の vJunos (無償の仮想ルータ/スイッチイメージ)を使用し、典型的な Leaf-Spine 構成をゼロから組み立てます。最終的には以下を達成します。 eBGP による Underlay の経路交換 iBGP + EVPN Type-2/3 による MAC/IP 学習(C-Plane = 制御プレーン) VXLAN によるテナント(利用者)トラフィックのカプセル化(D-Plane = データプレーン) ESI-LAG による EVPN Multihoming(サーバーの複数スイッチへの冗長接続) Border Leaf + VRF Route Leaking (仮想ルーティングテーブル間の経路共有)によるインターネット接続ゲートウェイ模擬 「SDPFクラウドの SDN コントローラや NW オーケストレータが裏で何をしているのか」をハンズオンで体験し、クラウド NW の裏側を覗いていただける構成になっています。 ⚠️ 本記事は学習用ラボ構成を前提としています 動作させることを優先しているため、本番設計とは異なる選択をしている箇所が多々あります(例: MTU デフォルトのまま、BGP 認証なし、など) 前提知識 本記事の主な対象読者は、 BGP の基本概念(AS・ピアリング・経路広告)を理解している、データセンター NW の Overlay 技術を手を動かして学びたいインフラエンジニアや学生 です。以下の知識があるとスムーズに進められます。 分野 期待するレベルの目安 Linux 操作 コマンドライン操作( ip , ping , tcpdump など)、Network Namespace の概念 TCP/IP 基礎 IP アドレス・サブネットマスク・ルーティングの仕組み、L2(Ethernet)と L3(IP)の違い BGP 基礎 AS(Autonomous System)・ASN・eBGP / iBGP の区別、経路広告・ピアリングの概念 Docker 基礎 docker ps / docker pull などの基本操作、コンテナとイメージの違い 各セクションに設けている「用語解説」や「Q&A」は補足情報です。すでにご存知の方は読み飛ばしてください。 全体像 構築するトポロジ 最終的なゴールは、 物理的に異なるラックに収容された HV1(ハイパーバイザー上の VM)と BM1(ベアメタルサーバー)が同一の L2 ネットワーク上で通信でき、さらにそこからインターネットへ抜けられる 状態を作ることです。これを実現するために、Underlay(物理 IP 転送の土台)→ Overlay(EVPN/VXLAN による L2 延伸)→ Internet GW(VRF Route Leaking による外部接続)の順に積み上げていきます。 主な登場人物は以下の通りです。 ノード 役割 ASN Loopback spine1 Spine #1 64512 10.255.255.1 spine2 Spine #2 64513 10.255.255.2 leaf1-1 Underlay Leaf(HV=ハイパーバイザー収容) 65000 10.255.255.11 leaf2-1 / leaf2-2 Overlay Leaf(BM=ベアメタルサーバー収容、ESI-LAG ペア検証) 65000 10.255.255.21 / .22 leaf3-1 Border Leaf(Internet GW 収容) 65000 10.255.255.31 rr1 Route Reflector(経路情報を集めて他に配る中継役) 65000 10.255.255.101 hv1 ハイパーバイザー模擬(Linux + FRR、VTEP=VXLANトンネルの端点 を持つ) 65000 10.255.255.201 bm1 ベアメタルサーバー模擬(Linux) — — inet-gw1 Internet Gateway 模擬(vJunos-router) 65002 198.51.100.1 isp1 外部 ISP 模擬 65001 192.0.2.1 用語解説 Underlay(アンダーレイ) … 物理スイッチ・ルータが実際に IP パケットを転送する「土台」のネットワーク層です。本記事では Leaf-Spine 間の P2P リンクと eBGP がこれにあたります。 Overlay(オーバーレイ) … Underlay の上に「論理的に重ねた」仮想ネットワーク層です。本記事では VXLAN がデータのカプセル化(D-Plane)、EVPN が経路情報の交換(C-Plane)を担い、テナントの L2 セグメントを物理的に離れたノード間へ延伸します。Underlay が「道路」なら Overlay は「宅配便」のようなイメージです。 ポイント Spine ごとに ASN を分ける (64512 / 64513)。Leaf 側は multipath multiple-as で Spine 2 台への ECMP(Equal-Cost Multi-Path)を有効にしています。この設定により、障害発生時はベストパス再選定なしで残存パスに切り替わります。 Leaf は全て同じ ASN(65000) にして増設コストを下げています。詳細は以下を参照。 RR は Underlay にも参加 しますが、直接データを転送せず Overlay の経路反射(他のノードの経路情報をまとめて再配布する役割)を担当します。 HV 側は FRR で EVPN ピアリングを代用 し、RR と EVPN ピアを張ります(実際のクラウドでは SDN コントローラが HV 上の経路を収集し EVPN に変換します)。 Q. なぜ Leaf を全て同じ ASN にするのか? RFC 7938 は「Leaf ごとに別 ASN」を推奨していますが、本構成では共通 ASN(65000)を採用しました。トレードオフは次の通り。 共通 ASN(本記事) 別 ASN(RFC 7938 推奨) 増設運用 ✅ Leaf テンプレ流用、Spine 側も peer-as 1 つ ❌ Leaf ごとに ASN 採番、Spine 側も増やすたび neighbor 別設定 AS-PATH ループ防止 ❌ as-override で無効化される ✅ そのまま効く トラブルシュート ❌ AS-PATH に Leaf 識別子が出ない ✅ どの Leaf 経由か AS-PATH で分かる 進め方 Containerlab で機器を起動 Underlay(IP / eBGP)構築 Overlay(iBGP EVPN / VXLAN / ESI-LAG)構築 Border Leaf + Internet GW 構築 各セクションの末尾には「動作確認」と「完成状態チェックリスト」を載せています。 前提とする環境 以下を準備してください。 項目 内容 マシンの推奨スペック メモリ 32GB 以上(vJunos は 1 ノードあたり 4GB ほど使います)、ディスク空き容量 20GB 以上 (vJunos の qcow2 は 1 イメージ約 5GB、Docker ビルド後は合計 10GB 超になります) ホスト OS Linux(Ubuntu 24.04 で検証)。Docker が動く環境ならOK Docker v29.2.0 で検証 Containerlab v0.75.0 で検証。 インストール手順 vJunos イメージ vJunos-switch-25.4R1.12 , vJunos-router-25.4R1.12 (手順は以下) ゲスト OS 軽量 Linux nicolaka/netshoot (HV / BM 用)。コンテナ内の FRR は v10.6.1 で検証 エディタ VS Code + Containerlab 拡張があると便利 vJunos イメージの準備(vrnetlab でコンテナ化) vJunos は Juniper が 無償で公開している仮想ルータ/スイッチイメージ です。ただし配布形式は qcow2(KVM 用)なので、Containerlab で使うには vrnetlab で Docker イメージに変換する必要があります。詳細は こちら を参照。 # 1. vrnetlab をクローン git clone https://github.com/srl-labs/vrnetlab && cd vrnetlab/juniper # 2. Juniper 公式サイトからダウンロードした qcow2 を vrnetlab 下に配置 # https://support.juniper.net/support/downloads で vjunos-switch (または vjunos-router) で検索 cp ~/Downloads/vJunos-router-25.4R1.12.qcow2 vjunosrouter/ cp ~/Downloads/vJunos-switch-25.4R1.12.qcow2 vjunosswitch/ # 3. Docker イメージをビルド(それぞれ数分かかる) cd vjunos-switch && make && cd .. cd vjunos-router && make && cd .. # 4. ビルド結果を確認 docker images | grep vjunos # vrnetlab/juniper_vjunos-switch 25.4R1.12 ... # vrnetlab/juniper_vjunos-router 25.4R1.12 ... # HV / BM 用イメージ docker pull nicolaka/netshoot:v0.15 ⚠️ vJunos のライセンスについて vJunos は 評価・検証・学習用途で無償利用可能 です(商用サポートなし)。以下の点に注意してください。 機能制限はほぼありませんが、 commit 時に warning: requires 'bgp' license 等の警告が出ます。 これはライセンスキーを投入していないだけで、BGP 自体は問題なく動作します (本記事の全手順はライセンスなしで完走できます) 本番環境や商用目的での利用には正規ライセンスが必要です。詳しくは Juniper vJunos ページ を参照してください 1. 環境構築 — Containerlab で機器を起動 この章では、下図のように Spine / Leaf / RR / サーバー模擬など全 11 ノードを Containerlab で一括起動し、SSH でログインできる状態を目指します。 1.1 トポロジ定義(clos-network.clab.yml) name : clos-network topology : nodes : # --- SPINE --- spine1 : kind : juniper_vjunosswitch image : vrnetlab/juniper_vjunos-switch:25.4R1.12 spine2 : kind : juniper_vjunosswitch image : vrnetlab/juniper_vjunos-switch:25.4R1.12 # --- LEAF --- leaf1-1 : kind : juniper_vjunosswitch image : vrnetlab/juniper_vjunos-switch:25.4R1.12 leaf2-1 : kind : juniper_vjunosswitch image : vrnetlab/juniper_vjunos-switch:25.4R1.12 leaf2-2 : kind : juniper_vjunosswitch image : vrnetlab/juniper_vjunos-switch:25.4R1.12 leaf3-1 : kind : juniper_vjunosswitch image : vrnetlab/juniper_vjunos-switch:25.4R1.12 # --- Route Reflector --- rr1 : kind : juniper_vjunosrouter image : vrnetlab/juniper_vjunos-router:25.4R1.12 # --- HV / BM 模擬 --- hv1 : kind : linux image : nicolaka/netshoot:v0.15 bm1 : kind : linux image : nicolaka/netshoot:v0.15 # --- Internet Gateway / 外部 ISP 模擬 --- inet-gw1 : kind : juniper_vjunosrouter image : vrnetlab/juniper_vjunos-router:25.4R1.12 isp1 : kind : juniper_vjunosrouter image : vrnetlab/juniper_vjunos-router:25.4R1.12 links : # Spine <-> Leaf - endpoints : [ "spine1:eth1" , "leaf1-1:eth1" ] - endpoints : [ "spine1:eth2" , "leaf2-1:eth1" ] - endpoints : [ "spine1:eth3" , "leaf2-2:eth1" ] - endpoints : [ "spine1:eth4" , "leaf3-1:eth1" ] - endpoints : [ "spine2:eth1" , "leaf1-1:eth2" ] - endpoints : [ "spine2:eth2" , "leaf2-1:eth2" ] - endpoints : [ "spine2:eth3" , "leaf2-2:eth2" ] - endpoints : [ "spine2:eth4" , "leaf3-1:eth2" ] # Spine <-> RR - endpoints : [ "spine1:eth10" , "rr1:eth1" ] - endpoints : [ "spine2:eth10" , "rr1:eth2" ] # Server / GW - endpoints : [ "leaf1-1:eth3" , "hv1:eth1" ] - endpoints : [ "leaf2-1:eth3" , "bm1:eth1" ] - endpoints : [ "leaf2-2:eth3" , "bm1:eth2" ] - endpoints : [ "leaf3-1:eth3" , "inet-gw1:eth1" ] - endpoints : [ "inet-gw1:eth2" , "isp1:eth1" ] 1.2 デプロイ clab deploy -t clos-network.clab.yml # トポロジを Web UI で確認(リモートサーバーの場合はポートフォワード) clab graph -t clos-network.clab.yml # 全部壊してやり直したくなった時は clab destroy -t clos-network.clab.yml ノードへの SSH は ssh admin@clab-clos-network-spine1 (初期パスワードは admin@123 )。なお、Linux コンテナ(hv1 / bm1)は docker exec -it clab-clos-network-hv1 bash のように直接シェルに入る方が確実です。 ContainerlabのVS Code拡張を入れておくと、コンテナをまとめて管理できて便利です。 Spine・Leaf・RR・Inet-GW・ISPにおいて、あらかじめ以下の手順で root パスワードを設定して commit しておきます。これは以降の設定をコミットする上で必須の操作です。admin ユーザーのパスワード変更ではないことに留意。 configure set system root-authentication plain-text-password commit ✅ 完成状態チェックリスト clab deploy がエラーなく完了する docker ps で全 11 ノードが Up になっている ssh admin@clab-clos-network-spine1 でログインできる clab graph の Web UI でトポロジが想定通り表示される ここまでで、冒頭の図で示した全ノードが起動し、各機器へログインできる状態になりました。まだ NW 設定は入っていないので、次のセクションで Underlay から構築していきます。 2. Underlay の構築 — eBGP で Loopback 同士を疎通させる この章では、下図のように各ノードに IP アドレスを振り、Spine-Leaf 間で eBGP を張ることで、全ノードの Loopback 同士が疎通できる状態を目指します。この「下地」が後の Overlay(EVPN/VXLAN)の土台になります。 2.1 IP アドレス設計 P2P リンク(機器同士を 1対1 でつなぐリンク)は /30 (IP 2 個分)、Loopback は /32 (IP 1 個)。Spine 側を .1 、Leaf 側を .2 の規則で設計しています。 リンク Spine 側 Leaf 側 spine1 ↔ leaf1-1 10.1.11.1/30 10.1.11.2/30 spine1 ↔ leaf2-1 10.1.21.1/30 10.1.21.2/30 spine1 ↔ leaf2-2 10.1.22.1/30 10.1.22.2/30 spine1 ↔ leaf3-1 10.1.31.1/30 10.1.31.2/30 spine1 ↔ rr1 10.1.101.1/30 10.1.101.2/30 spine2 ↔ * 10.2.x.1/30 10.2.x.2/30(同様) leaf1-1 ↔ hv1 10.11.201.1/30 10.11.201.2/30 2.2 各ノードの IP / Loopback 設定 代表として spine1, hv1 を掲載します。他のノードも同じパターン(IP のみ差し替え)。 spine1 configure set routing-options router-id 10.255.255.1 set interfaces lo0 unit 0 family inet address 10.255.255.1/32 set interfaces ge-0/0/0 unit 0 family inet address 10.1.11.1/30 set interfaces ge-0/0/1 unit 0 family inet address 10.1.21.1/30 set interfaces ge-0/0/2 unit 0 family inet address 10.1.22.1/30 set interfaces ge-0/0/3 unit 0 family inet address 10.1.31.1/30 set interfaces ge-0/0/9 unit 0 family inet address 10.1.101.1/30 commit spine2 / leaf1-1 / leaf2-1 / leaf2-2 / leaf3-1 / rr1 のコマンドを開く spine2 configure set routing-options router-id 10.255.255.2 set interfaces lo0 unit 0 family inet address 10.255.255.2/32 set interfaces ge-0/0/0 unit 0 family inet address 10.2.11.1/30 set interfaces ge-0/0/1 unit 0 family inet address 10.2.21.1/30 set interfaces ge-0/0/2 unit 0 family inet address 10.2.22.1/30 set interfaces ge-0/0/3 unit 0 family inet address 10.2.31.1/30 set interfaces ge-0/0/9 unit 0 family inet address 10.2.101.1/30 commit leaf1-1 configure set routing-options router-id 10.255.255.11 set interfaces lo0 unit 0 family inet address 10.255.255.11/32 set interfaces ge-0/0/0 unit 0 family inet address 10.1.11.2/30 set interfaces ge-0/0/1 unit 0 family inet address 10.2.11.2/30 set interfaces ge-0/0/2 unit 0 family inet address 10.11.201.1/30 commit leaf2-1 configure set routing-options router-id 10.255.255.21 set interfaces lo0 unit 0 family inet address 10.255.255.21/32 set interfaces ge-0/0/0 unit 0 family inet address 10.1.21.2/30 set interfaces ge-0/0/1 unit 0 family inet address 10.2.21.2/30 commit leaf2-2 configure set routing-options router-id 10.255.255.22 set interfaces lo0 unit 0 family inet address 10.255.255.22/32 set interfaces ge-0/0/0 unit 0 family inet address 10.1.22.2/30 set interfaces ge-0/0/1 unit 0 family inet address 10.2.22.2/30 commit leaf3-1 configure set routing-options router-id 10.255.255.31 set interfaces lo0 unit 0 family inet address 10.255.255.31/32 set interfaces ge-0/0/0 unit 0 family inet address 10.1.31.2/30 set interfaces ge-0/0/1 unit 0 family inet address 10.2.31.2/30 commit rr1 configure set routing-options router-id 10.255.255.101 set interfaces lo0 unit 0 family inet address 10.255.255.101/32 set interfaces ge-0/0/0 unit 0 family inet address 10.1.101.2/30 set interfaces ge-0/0/1 unit 0 family inet address 10.2.101.2/30 commit hv1 (Linux なので別物) # FRR のインストール apk add frr sed -i 's/bgpd=no/bgpd=yes/' /etc/frr/daemons /usr/lib/frr/frrinit.sh start # IP 設定 ip addr add 10.255.255.201/32 dev lo ip addr add 10.11.201.2/30 dev eth1 ip link set eth1 up ip route del default ip route add default via 10.11.201.1 ⚠️ ip route del default を実行すると management NW 経由の SSH が切れます。以降 hv1 への操作は docker exec -it clab-clos-network-hv1 bash で入ってください。 直結リンクで ping が通れば OK です。 2.3 eBGP の設定 Underlay の目標は「全ノードの Loopback 同士を疎通させること」。これができれば、あとで Overlay (EVPN/VXLAN)がその「下地」の上に乗れます。Spine と Leaf で eBGP(異なる ASN 同士の BGP)を張り、Loopback 経路を広告します。 ポイント Spine ごとに ASN を分ける : spine1=64512 , spine2=64513 Spine 側に as-override :Leaf 共通 ASN 方式の代償として、BGP の「AS-PATH(経路が通ってきた ASN の履歴)に同じ ASN があるとループとみなして破棄する」ルールを回避するため、Spine 側で AS-PATH 上の Leaf ASN を Spine 自身の ASN に書き換えます Leaf 側に multipath multiple-as :spine1/spine2 の ASN が異なるため、両方を ECMP として使うために必須 ECMP 有効化 : load-balance per-packet という名前ですがこれは Junos のレガシーな仕様で、実態は 5-tuple(送信元/宛先 IP・ポート・プロトコル)のハッシュによるフロー単位の負荷分散 です 広告経路は Loopback のみ : from interface lo0.0 で Loopback に絞ることで、リンク IP が広告されず経路がスッキリします commit 時に以下の警告が出ますが、無視して OK です(詳しくは「前提とする環境」のライセンス注記を参照)。 warning: requires 'bgp' license commit complete spine1 configure # ECMP 有効化 set policy-options policy-statement PFE-LB term 1 then load-balance per-packet set routing-options forwarding-table export PFE-LB # 経路広告ポリシー(Loopback + BGP 経路) set policy-options policy-statement EXPORT-UNDERLAY term ALLOW-BGP from protocol bgp set policy-options policy-statement EXPORT-UNDERLAY term ALLOW-BGP then accept set policy-options policy-statement EXPORT-UNDERLAY term ALLOW-DIRECT from interface lo0.0 set policy-options policy-statement EXPORT-UNDERLAY term ALLOW-DIRECT then accept # BGP(ASN 64512) set routing-options autonomous-system 64512 set protocols bgp group UNDERLAY type external set protocols bgp group UNDERLAY multipath set protocols bgp group UNDERLAY export EXPORT-UNDERLAY set protocols bgp group UNDERLAY peer-as 65000 set protocols bgp group UNDERLAY as-override # 各 Leaf / RR への neighbor set protocols bgp group UNDERLAY neighbor 10.1.11.2 set protocols bgp group UNDERLAY neighbor 10.1.21.2 set protocols bgp group UNDERLAY neighbor 10.1.22.2 set protocols bgp group UNDERLAY neighbor 10.1.31.2 set protocols bgp group UNDERLAY neighbor 10.1.101.2 commit spine2 のコマンドを開く configure set policy-options policy-statement PFE-LB term 1 then load-balance per-packet set routing-options forwarding-table export PFE-LB set policy-options policy-statement EXPORT-UNDERLAY term ALLOW-BGP from protocol bgp set policy-options policy-statement EXPORT-UNDERLAY term ALLOW-BGP then accept set policy-options policy-statement EXPORT-UNDERLAY term ALLOW-DIRECT from interface lo0.0 set policy-options policy-statement EXPORT-UNDERLAY term ALLOW-DIRECT then accept # BGP(ASN 64513) set routing-options autonomous-system 64513 set protocols bgp group UNDERLAY type external set protocols bgp group UNDERLAY multipath set protocols bgp group UNDERLAY export EXPORT-UNDERLAY set protocols bgp group UNDERLAY peer-as 65000 set protocols bgp group UNDERLAY as-override set protocols bgp group UNDERLAY neighbor 10.2.11.2 set protocols bgp group UNDERLAY neighbor 10.2.21.2 set protocols bgp group UNDERLAY neighbor 10.2.22.2 set protocols bgp group UNDERLAY neighbor 10.2.31.2 set protocols bgp group UNDERLAY neighbor 10.2.101.2 commit leaf1-1 leaf1-1 は HV1(FRR)が直接 BGP を喋らないため、HV1 Loopback 宛の static route を広告に含めます。 configure # ECMP set policy-options policy-statement PFE-LB term 1 then load-balance per-packet set routing-options forwarding-table export PFE-LB # Loopback + static を広告 set policy-options policy-statement EXPORT-UNDERLAY term ALL-DIRECT from interface lo0.0 set policy-options policy-statement EXPORT-UNDERLAY term ALL-DIRECT then accept set policy-options policy-statement EXPORT-UNDERLAY term ALL-STATIC from protocol static set policy-options policy-statement EXPORT-UNDERLAY term ALL-STATIC then accept # BGP(multiple-as は必須) set routing-options autonomous-system 65000 set protocols bgp group UNDERLAY type external set protocols bgp group UNDERLAY multipath multiple-as set protocols bgp group UNDERLAY export EXPORT-UNDERLAY set protocols bgp group UNDERLAY neighbor 10.1.11.1 peer-as 64512 set protocols bgp group UNDERLAY neighbor 10.2.11.1 peer-as 64513 # HV1 Loopback への static route set routing-options static route 10.255.255.201/32 next-hop 10.11.201.2 commit leaf2-1 / leaf2-2 / leaf3-1 / rr1 のコマンドを開く leaf2-1 configure set policy-options policy-statement PFE-LB term 1 then load-balance per-packet set routing-options forwarding-table export PFE-LB set policy-options policy-statement EXPORT-UNDERLAY term ALL-DIRECT from interface lo0.0 set policy-options policy-statement EXPORT-UNDERLAY term ALL-DIRECT then accept set routing-options autonomous-system 65000 set protocols bgp group UNDERLAY type external set protocols bgp group UNDERLAY multipath multiple-as set protocols bgp group UNDERLAY export EXPORT-UNDERLAY set protocols bgp group UNDERLAY neighbor 10.1.21.1 peer-as 64512 set protocols bgp group UNDERLAY neighbor 10.2.21.1 peer-as 64513 commit leaf2-2 configure set policy-options policy-statement PFE-LB term 1 then load-balance per-packet set routing-options forwarding-table export PFE-LB set policy-options policy-statement EXPORT-UNDERLAY term ALL-DIRECT from interface lo0.0 set policy-options policy-statement EXPORT-UNDERLAY term ALL-DIRECT then accept set routing-options autonomous-system 65000 set protocols bgp group UNDERLAY type external set protocols bgp group UNDERLAY multipath multiple-as set protocols bgp group UNDERLAY export EXPORT-UNDERLAY set protocols bgp group UNDERLAY neighbor 10.1.22.1 peer-as 64512 set protocols bgp group UNDERLAY neighbor 10.2.22.1 peer-as 64513 commit leaf3-1 configure set policy-options policy-statement PFE-LB term 1 then load-balance per-packet set routing-options forwarding-table export PFE-LB set policy-options policy-statement EXPORT-UNDERLAY term ALL-DIRECT from interface lo0.0 set policy-options policy-statement EXPORT-UNDERLAY term ALL-DIRECT then accept set routing-options autonomous-system 65000 set protocols bgp group UNDERLAY type external set protocols bgp group UNDERLAY multipath multiple-as set protocols bgp group UNDERLAY export EXPORT-UNDERLAY set protocols bgp group UNDERLAY neighbor 10.1.31.1 peer-as 64512 set protocols bgp group UNDERLAY neighbor 10.2.31.1 peer-as 64513 commit rr1 configure set policy-options policy-statement PFE-LB term 1 then load-balance per-packet set routing-options forwarding-table export PFE-LB set policy-options policy-statement EXPORT-UNDERLAY term ALL-DIRECT from interface lo0.0 set policy-options policy-statement EXPORT-UNDERLAY term ALL-DIRECT then accept set routing-options autonomous-system 65000 set protocols bgp group UNDERLAY type external set protocols bgp group UNDERLAY multipath multiple-as set protocols bgp group UNDERLAY export EXPORT-UNDERLAY set protocols bgp group UNDERLAY neighbor 10.1.101.1 peer-as 64512 set protocols bgp group UNDERLAY neighbor 10.2.101.1 peer-as 64513 commit 2.4 動作確認 admin@spine1# run show bgp summary ... Peer AS InPkt OutPkt ... State|#Active/Received/Accepted/Damped... 10.1.11.2 65000 6 8 ... 58 Establ 10.1.21.2 65000 5 7 ... 33 Establ 10.1.22.2 65000 4 7 ... 22 Establ 10.1.31.2 65000 4 7 ... 13 Establ 10.1.101.2 65000 4 7 ... 6 Establ Spine から各 Leaf / RR とのピアが Establ (Established) になっていれば OK です。最終的に Border Leaf (leaf3-1) から HV1 の Loopback まで疎通できます。 admin@leaf3-1# run ping 10.255.255.201 source 10.255.255.31 count 3 64 bytes from 10.255.255.201: icmp_seq=0 ttl=62 time=3.254 ms ... 3 packets transmitted, 3 packets received, 0% packet loss ✅ 完成状態チェックリスト Spine から見て、全 Leaf / RR の eBGP セッションが Establ show route 10.255.255.0/24 で全ノードの Loopback が学習されている 任意の Leaf から任意の Leaf の Loopback へ ping が通る HV1 の Loopback ( 10.255.255.201/32 ) が leaf3-1 から見える ここまでで Underlay が完成し、全ノードの Loopback 同士が IP で疎通できる「下地」が整いました。次のセクションで、この上に EVPN/VXLAN の Overlay を被せます。 3. Overlay の構築 — EVPN/VXLAN ここからが核心です。Underlay の上に EVPN(C-Plane) + VXLAN(D-Plane) を被せ、テナントの L2 セグメントを複数 Leaf 間で拡張します。 Q. なぜ VLAN ではダメなのか? データセンターでは複数のテナント(利用者)が同じ物理スイッチを共有します。テナント同士のトラフィックを分離する最も基本的な手段が VLAN (Virtual LAN)です。しかし VLAN には以下の限界があります。 課題 VLAN の限界 EVPN/VXLAN での解決 ID 数 最大 4,094 個 → 大規模クラウドでは枯渇する VNI は最大約 1,600 万個(VNIについては後述) L2 の拡張 VLAN をラック間に伸ばすにはループ対策(STP)が必要になり、構成が複雑化する VXLAN で L3(IP Underlay)上にトンネルを張るため、STP 不要で任意のラック間へ L2 を延伸可能 MAC 学習のスケーラビリティ データプレーンのフラッディングで MAC を学習するため、規模拡大に伴い帯域を圧迫する EVPN が制御プレーンで MAC/IP を配布するため、不要なフラッディングを抑制できる つまり VLAN は「1 台のスイッチ内 or 隣接スイッチ間」の L2 分離 、 VXLAN は「DC 全体規模」の L2 分離 と役割が違います。本記事で構築するのは後者です。 テナントごとに L3(ルーティング)も分離 したい場合は VRF (Virtual Routing and Forwarding)を使います。VRF はスイッチ / ルータの中に「テナント専用の経路表」を作る技術で、テナント A とテナント B が同じ 192.168.1.0/24 を使っていてもルーティングが混ざりません。本記事のセクション 4 で VRF を使った外部接続を構築します。 3.1 アーキテクチャの整理 各ノードが Overlay において担う役割を整理します。 役割 C-Plane D-Plane Spine (単なる中継) (単なる IP 転送) RR (rr1) iBGP RR、EVPN 経路反射 — Underlay Leaf (leaf1-1) 参加しない 参加しない Overlay Leaf (leaf2-1/2-2/3-1) iBGP/EVPN ピア VTEP(VXLAN トンネルの出入口) HV1 (FRR) iBGP/EVPN ピア VTEP Q. なぜ leaf1-1 は EVPN に参加しないのか? HV1 が VTEP を持つので、leaf1-1 は単なる IP ルータとして VXLAN パケットを Underlay 越しに転送するだけで済みます。実環境でも同様で、HV 上のソフトウェア VTEP(Contrail vRouter 等)が VTEP の役割を担います。本記事ではそれを FRR で代用しています。 3.2 C-Plane: iBGP EVPN ピア leaf2-1 configure # RR との iBGP(Overlay) set protocols bgp group OVERLAY type internal set protocols bgp group OVERLAY local-address 10.255.255.21 set protocols bgp group OVERLAY neighbor 10.255.255.101 set protocols bgp group OVERLAY family evpn signaling # EVPN/VXLAN 基本設定 set switch-options vtep-source-interface lo0.0 set switch-options route-distinguisher 10.255.255.21:1 set protocols evpn encapsulation vxlan # IRB(L3 ゲートウェイ)を使わない宣言。Type-2 ルートに default-gateway コミュニティを付けない set protocols evpn default-gateway no-gateway-community set protocols evpn extended-vni-list all # RT を明示指定。FRR の advertise-all-vni は AS:VNI 形式(65000:10010)で RT を生成するため、 # Junos 側も同じ値を使う(Junos の vrf-target auto は AS:(VNI+0x10000000) と計算式が異なり互換性がない) set switch-options vrf-target target:65000:10010 commit leaf2-2 / leaf3-1 のコマンドを開く leaf2-2 configure set protocols bgp group OVERLAY type internal set protocols bgp group OVERLAY local-address 10.255.255.22 set protocols bgp group OVERLAY neighbor 10.255.255.101 set protocols bgp group OVERLAY family evpn signaling set switch-options vtep-source-interface lo0.0 set switch-options route-distinguisher 10.255.255.22:1 set protocols evpn encapsulation vxlan set protocols evpn default-gateway no-gateway-community set protocols evpn extended-vni-list all set switch-options vrf-target target:65000:10010 commit leaf3-1 configure set protocols bgp group OVERLAY type internal set protocols bgp group OVERLAY local-address 10.255.255.31 set protocols bgp group OVERLAY neighbor 10.255.255.101 set protocols bgp group OVERLAY family evpn signaling set switch-options vtep-source-interface lo0.0 set switch-options route-distinguisher 10.255.255.31:1 set protocols evpn encapsulation vxlan set protocols evpn default-gateway no-gateway-community set protocols evpn extended-vni-list all set switch-options vrf-target target:65000:10010 commit rr1 configure set protocols bgp group OVERLAY type internal set protocols bgp group OVERLAY cluster 10.255.255.101 set protocols bgp group OVERLAY local-address 10.255.255.101 set protocols bgp group OVERLAY neighbor 10.255.255.21 set protocols bgp group OVERLAY neighbor 10.255.255.22 set protocols bgp group OVERLAY neighbor 10.255.255.31 set protocols bgp group OVERLAY neighbor 10.255.255.201 set protocols bgp group OVERLAY family evpn signaling commit hv1(FRR) vtysh configure terminal router bgp 65000 bgp router-id 10.255.255.201 no bgp ebgp-requires-policy neighbor 10.255.255.101 remote-as 65000 neighbor 10.255.255.101 update-source 10.255.255.201 address-family l2vpn evpn neighbor 10.255.255.101 activate exit-address-family exit end write exit 確認 admin@rr1# run show bgp summary group OVERLAY ... 10.255.255.21 65000 ... Establ bgp.evpn.0: 0/0/0/0 10.255.255.22 65000 ... Establ bgp.evpn.0: 0/0/0/0 10.255.255.31 65000 ... Establ bgp.evpn.0: 0/0/0/0 10.255.255.201 65000 ... Establ bgp.evpn.0: 0/0/0/0 全ピアが Establ (Established) になれば完成です。まだ VNI を作っていないので EVPN 経路は 0 件です。 3.3 D-Plane: VXLAN セグメントを作る VNI 10010 (VLAN 10)を用意し、 VM1(HV1 上、192.168.10.10) と BM1(leaf2-1 配下、192.168.10.101) が同じ L2(同一サブネット、つまり「同じ LAN」)で通信できるようにします。 Q. VNI とは? VXLAN Network Identifier の略。VLAN ID の「拡張版」のようなもので、最大約 1,600 万のセグメントを作れます(VLAN は 4,094 まで)。 ⚠️ 本番では MTU 設計が必須 VXLAN は外側に IP(20) + UDP(8) + VXLAN(8) + Inner Ethernet(14) = 50 バイトのオーバーヘッド が乗ります。テナントが MTU 1500 で通信したいなら、Underlay の物理 MTU を 9000(Jumbo Frame) にするのが定番です。 hv1:Linux Kernel + FRR # VM 模擬の Network Namespace 作成 ip netns add vm1 ip link add veth-host type veth peer name veth-vm ip link set veth-vm netns vm1 ip netns exec vm1 ip link set dev veth-vm address 02:00:00:00:01:10 ip netns exec vm1 ip addr add 192.168.10.10/24 dev veth-vm ip netns exec vm1 ip link set veth-vm up ip netns exec vm1 ip link set lo up # VTEP(VXLAN I/F)作成 ip link add vxlan10 type vxlan id 10010 local 10.255.255.201 dstport 4789 nolearning # Bridge 経由で VM 側と VXLAN を接続 ip link add br10 type bridge ip link set veth-host master br10 ip link set vxlan10 master br10 ip link set vxlan10 up ip link set veth-host up ip link set br10 up vtysh configure terminal router bgp 65000 address-family l2vpn evpn advertise-all-vni exit-address-family end write exit leaf2-1 configure set vlans v10 vlan-id 10 set vlans v10 vxlan vni 10010 set vlans v10 vxlan ingress-node-replication # BM1 接続ポート set interfaces ge-0/0/2 unit 0 family ethernet-switching interface-mode trunk set interfaces ge-0/0/2 unit 0 family ethernet-switching vlan members v10 commit Q. ingress-node-replication とは? SDPF クラウドでは、お客さまに自由な L2 ネットワークを構成していただけるよう、Overlay ネットワークになるべく制限を与えない形でサービスを提供します。そのため、BUM(Broadcast / Unknown unicast / Multicast)トラフィックも通す必要があります。 ingress-node-replication は、この BUM パケットをフラッドする際の方式を指定する設定です。これを有効にすると、Type-3(IM)ルートで学習した各リモート VTEP に対して ユニキャストで複製送信 します。 bm1 ip link set eth1 up ip link add link eth1 name eth1.10 type vlan id 10 ip link set eth1.10 up ip addr add 192.168.10.101/24 dev eth1.10 動作確認 VM1 ↔ BM1 で ping が通り、HV1 で tcpdump を取ると UDP/4789 の VXLAN にカプセル化 されているのが分かります。 # BM1で実行 ~ # ping 192.168.10.10 64 bytes from 192.168.10.10: icmp_seq=1 ttl=64 time=3.35 ms ... # HV1で実行 ~ # tcpdump -i eth1 -n -vv udp port 4789 tcpdump: listening on eth1, link-type EN10MB (Ethernet), snapshot length 262144 bytes ... IP (tos 0x0, ttl 253, id 21304, offset 0, flags [none], proto UDP (17), length 134) 10.255.255.21.55534 > 10.255.255.201.4789: [no cksum] VXLAN, flags [I] (0x08), vni 10010 IP (tos 0x0, ttl 64, id 44875, offset 0, flags [DF], proto ICMP (1), length 84) 192.168.10.101 > 192.168.10.10: ICMP echo request, id 102, seq 8, length 64 ... IP (tos 0x0, ttl 64, id 21000, offset 0, flags [none], proto UDP (17), length 134) 10.255.255.201.49139 > 10.255.255.21.4789: [udp sum ok] VXLAN, flags [I] (0x08), vni 10010 IP (tos 0x0, ttl 64, id 6907, offset 0, flags [none], proto ICMP (1), length 84) 192.168.10.10 > 192.168.10.101: ICMP echo reply, id 102, seq 8, length 64 leaf2-1 側でも EVPN Type-2(MAC/IP)と Type-3(IM)の経路がリモート VTEP 宛に学習されています。 admin@leaf2-1# run show route table bgp.evpn.0 bgp.evpn.0: 5 destinations, 5 routes (5 active, 0 holddown, 0 hidden) + = Active Route, - = Last Active, * = Both 2:10.255.255.21:1::10010::aa:c1:ab:0a:18:4a/304 MAC/IP *[EVPN/170] 00:01:12 Indirect 2:10.255.255.201:2::0::02:00:00:00:01:10/304 MAC/IP *[BGP/170] 00:03:21, localpref 100, from 10.255.255.101 AS path: I, validation-state: unverified to 10.1.21.1 via ge-0/0/0.0 > to 10.2.21.1 via ge-0/0/1.0 2:10.255.255.21:1::10010::aa:c1:ab:0a:18:4a::192.168.10.101/304 MAC/IP *[EVPN/170] 00:01:05 Indirect 3:10.255.255.21:1::10010::10.255.255.21/248 IM *[EVPN/170] 00:03:09 Indirect 3:10.255.255.201:2::0::10.255.255.201/248 IM *[BGP/170] 00:03:10, localpref 100, from 10.255.255.101 AS path: I, validation-state: unverified > to 10.1.21.1 via ge-0/0/0.0 to 10.2.21.1 via ge-0/0/1.0 確認のポイントを整理します。 [EVPN/170] Indirect のエントリ(RD 10.255.255.21:1 )は leaf2-1 自身がローカルで生成した EVPN 経路(自分のポートに接続された BM1 の MAC/IP、および自分が属する VNI の IM ルート)。 [BGP/170] from 10.255.255.101 のエントリ(RD 10.255.255.201:2 )は RR(rr1)経由で受け取ったリモート VTEP(HV1)からの経路。 to 10.1.21.1 / to 10.2.21.1 の 2 経路が並んでいるのは、spine1・spine2 の両方を経由した ECMP が有効になっているためです。 EVPN の経路情報は BGP によって制御プレーン上でやり取りされ、実際のパケット転送は VXLAN(UDP/4789)がデータプレーンとして担います。tcpdump で確認した通り、 vni 10010 のカプセル化で転送されていることが確認できます。 3.4 EVPN Multihoming(ESI-LAG)で BM の接続を冗長化 セクション 3.3 で VXLAN の疎通は確認できましたが、このままでは BM1 が leaf2-1 の 1 本だけでぶら下がっている状態です。クラウドサービスでは物理故障の単一障害点を排除することが基本要件であり、リンクやスイッチの障害でサーバーが孤立しないよう、複数 Leaf へ冗長接続します。 ここからは BM1 を leaf2-1 / leaf2-2 の両方に接続して冗長化します。BM1 のネットワーク設定をいったん削除して bond に組み直すため、一時的に BM1 ↔ VM1 間の通信が途絶えます。 EVPN Type-1 / Type-4 を活かす ESI-LAG(All-Active) で構成します。 EVPN Multihoming の商用導入事例として、SDPF クラウド開発メンバーが JANOG48 で発表した EVPN Anycast Gateway を商用導入した話 も大変参考になります。 Q. ESI-LAG とは? 通常の LAG(Link Aggregation)は 1 台のスイッチへの束ねですが、ESI-LAG は 複数台のスイッチをまたいで LAG を組める技術です。サーバーから見ると 1 台のスイッチにつないでいるように見えます。 方針 2 台の Leaf で 同じ ESI と 同じ LACP System ID を設定 → BM1 から見ると単一の LAG 相手に見える BM1 側は Linux bond(mode=802.3ad) leaf2-1(既存設定の置き換え) configure delete interfaces ge-0/0/2 unit 0 set chassis aggregated-devices ethernet device-count 1 set interfaces ge-0/0/2 ether-options 802.3ad ae0 set interfaces ae0 unit 0 family ethernet-switching interface-mode trunk set interfaces ae0 unit 0 family ethernet-switching vlan members v10 # ESI(leaf2-1/2-2 で完全一致させる) set interfaces ae0 esi 00:00:00:00:00:00:00:00:00:01 set interfaces ae0 esi all-active set interfaces ae0 aggregated-ether-options lacp active set interfaces ae0 aggregated-ether-options lacp periodic fast set interfaces ae0 aggregated-ether-options lacp system-id 00:00:00:00:00:01 commit leaf2-2(新規) leaf2-1 と完全に同じ ESI / LACP System ID を投入します。VLAN/VNI/RT もここで作成。 configure set vlans v10 vlan-id 10 set vlans v10 vxlan vni 10010 set vlans v10 vxlan ingress-node-replication set chassis aggregated-devices ethernet device-count 1 set interfaces ge-0/0/2 ether-options 802.3ad ae0 set interfaces ae0 unit 0 family ethernet-switching interface-mode trunk set interfaces ae0 unit 0 family ethernet-switching vlan members v10 set interfaces ae0 esi 00:00:00:00:00:00:00:00:00:01 set interfaces ae0 esi all-active set interfaces ae0 aggregated-ether-options lacp active set interfaces ae0 aggregated-ether-options lacp periodic fast set interfaces ae0 aggregated-ether-options lacp system-id 00:00:00:00:00:01 commit bm1(Linux Bond) ip addr flush dev eth1.10 ip link del eth1.10 ip link add bond0 type bond mode 802.3ad miimon 100 lacp_rate 1 xmit_hash_policy layer3+4 ip link set eth1 down ip link set eth2 down ip link set eth1 master bond0 ip link set eth2 master bond0 ip link set bond0 up ip link set eth1 up ip link set eth2 up ip link add link bond0 name bond0.10 type vlan id 10 ip link set bond0.10 up ip addr add 192.168.10.101/24 dev bond0.10 切断試験 BM1 から VM1 に ping を流したまま、leaf2-1 → leaf2-2 の順にリンクを落とすと、片方が生きている間は ping が継続することを確認できます。 # leaf2-1 で接続断 → ping 継続 configure set interfaces ge-0/0/2 disable commit # leaf2-2 でも接続断 → ping 停止 configure set interfaces ge-0/0/2 disable commit # leaf2-1 を復活 → ping 復活 configure delete interfaces ge-0/0/2 disable commit # leaf2-2 で実行 configure delete interfaces ge-0/0/2 disable commit HV1 から見ると、BM1 の bond0 MAC が leaf2-1 / leaf2-2 の両方から同じ ESI で広告 されています。 ~ # vtysh -c "show bgp l2vpn evpn" Route Distinguisher: 10.255.255.21:1 *>i [2]:[10010]:[48]:[xx:xx:xx:xx:xx:xx] ← bond0 の MAC 10.255.255.21 ESI:00:00:00:00:00:00:00:00:00:01 RT:65000:10010 Route Distinguisher: 10.255.255.22:1 *>i [2]:[10010]:[48]:[xx:xx:xx:xx:xx:xx] 10.255.255.22 ESI:00:00:00:00:00:00:00:00:00:01 RT:65000:10010 💡 bond0 の MAC はカーネルが自動付与します(通常 eth1 の MAC を継承)。実際の値は ip link show bond0 で確認してください。 ESI が両ノードで一致しているため、HV1 はこの 2 経路を Aliasing (ECMP 的にロードバランス)して扱います。つまり、「leaf2-1 および leaf2-2 のいずれを経由しても、BM1 に届く」状態が完成しています。これが ESI-LAG の本質です。 ✅ 完成状態チェックリスト(Overlay全体) RR の OVERLAY グループで全 Leaf / HV1 が Establ HV1 で show evpn vni に VNI 10010 が表示される BM1 → VM1(VXLAN 経由)の ping が通る HV1 の tcpdump -i eth1 udp port 4789 で VXLAN ヘッダ(vni 10010)が見える leaf2-1/2-2 のどちらかのリンクを落としても BM1 → VM1 の通信が継続する show bgp l2vpn evpn で BM1 の MAC が leaf2-1/2-2 の両方から同じ ESI で広告されている ここまでで、EVPN/VXLAN によるテナント L2 拡張と ESI-LAG による冗長接続が完成しました。VM1(HV1 上)と BM1 が物理的に離れていても同一 L2 で通信でき、かつ片方のリンクが落ちても通信が継続する状態です。 4. Border Leaf と Internet Gateway 最後に、テナント網(VRF = 仮想ルーティングテーブル、「テナント専用の経路表」と考えてOK)と外部網(Global = インターネット側)を接続します。 leaf3-1 を VTEP 化 して Overlay の VLAN10 を Internet GW まで延伸 inet-gw1 で VRF-User と Global を Route Leaking inet-gw1 ↔ isp1 で eBGP 、isp1 から default route を受け取る 以下の内容を構成します。 4.1 leaf3-1(Border Leaf として VTEP 化) configure set vlans v10 vlan-id 10 set vlans v10 vxlan vni 10010 set vlans v10 vxlan ingress-node-replication # inet-gw1 接続ポート set interfaces ge-0/0/2 unit 0 family ethernet-switching interface-mode trunk set interfaces ge-0/0/2 unit 0 family ethernet-switching vlan members v10 commit 4.2 inet-gw1: 共通の下準備(I/F + ASN) configure set routing-options router-id 198.51.100.1 set routing-options autonomous-system 65002 # I/F set interfaces ge-0/0/0 vlan-tagging set interfaces ge-0/0/0 unit 10 vlan-id 10 set interfaces ge-0/0/0 unit 10 family inet address 192.168.10.1/24 set interfaces ge-0/0/1 unit 0 family inet address 203.0.113.2/30 set interfaces lo0 unit 0 family inet address 198.51.100.1/32 commit ge-0/0/0.10 がテナント側(leaf3-1 経由で BM1 と同セグ)、 ge-0/0/1.0 が インターネット側(外部)です。 4.3 inet-gw1: VRF と Route Leaking ユーザーの VRF( VRF-User )と Global テーブル( inet.0 = Junos の通常の経路表)を分離しつつ、必要な経路だけ相互にリーク(漏らす)します。これが「Route Leaking」で、「テナント内部の経路をインターネット側に教える(またはその逆)」ことで、テナント内のサーバーがインターネットと通信できるようになります。 configure # Route Leaking ポリシー(双方向) # Global -> VRF: default route のみ VRF に流す set policy-options policy-statement LEAK-GLOBAL-TO-VRF term ALLOW-DEFAULT from instance master set policy-options policy-statement LEAK-GLOBAL-TO-VRF term ALLOW-DEFAULT from route-filter 0.0.0.0/0 exact set policy-options policy-statement LEAK-GLOBAL-TO-VRF term ALLOW-DEFAULT then accept set policy-options policy-statement LEAK-GLOBAL-TO-VRF term DENY-REST then reject # VRF -> Global: BM1 の Global IP(/32)のみ Global へ流す set policy-options policy-statement LEAK-VRF-TO-GLOBAL term ALLOW-BM1 from instance VRF-User set policy-options policy-statement LEAK-VRF-TO-GLOBAL term ALLOW-BM1 from route-filter 198.51.100.41/32 exact set policy-options policy-statement LEAK-VRF-TO-GLOBAL term ALLOW-BM1 then accept set policy-options policy-statement LEAK-VRF-TO-GLOBAL term DENY-REST then reject # VRF 本体 set routing-instances VRF-User instance-type virtual-router set routing-instances VRF-User interface ge-0/0/0.10 set routing-instances VRF-User routing-options instance-import LEAK-GLOBAL-TO-VRF # BM1 の Global IP への static set routing-instances VRF-User routing-options static route 198.51.100.41/32 next-hop 192.168.10.101 # Global 側にも VRF からの経路を取り込む set routing-options instance-import LEAK-VRF-TO-GLOBAL commit 4.4 inet-gw1: 外部 ISP との eBGP 最後に、上で Global にリークした経路を eBGP で 外部 ISP へ広告します。 configure # 外部 ISP へ広告するポリシー(static のみ → 198.51.100.41/32 が乗る) set policy-options policy-statement ADVERTISE-TO-ISP term 1 from protocol static set policy-options policy-statement ADVERTISE-TO-ISP term 1 then accept set protocols bgp group TO-ISP type external set protocols bgp group TO-ISP peer-as 65001 set protocols bgp group TO-ISP neighbor 203.0.113.1 set protocols bgp group TO-ISP export ADVERTISE-TO-ISP commit 4.5 isp1 (外部 ISP 模擬) configure set routing-options router-id 192.0.2.1 set routing-options autonomous-system 65001 set interfaces ge-0/0/0 unit 0 family inet address 203.0.113.1/30 set interfaces lo0 unit 0 family inet address 192.0.2.1/32 set protocols bgp group TO-GW type external set protocols bgp group TO-GW peer-as 65002 set protocols bgp group TO-GW neighbor 203.0.113.2 set policy-options policy-statement SEND-DEFAULT term 1 from protocol static set policy-options policy-statement SEND-DEFAULT term 1 from route-filter 0.0.0.0/0 exact set policy-options policy-statement SEND-DEFAULT term 1 then accept set protocols bgp group TO-GW export SEND-DEFAULT set routing-options static route 0.0.0.0/0 discard commit 4.6 bm1 (Global IP 付与とデフォルトゲートウェイ) ip addr add 198.51.100.41/32 dev bond0.10 ip route replace default via 192.168.10.1 Q. なぜ /32 で付与するのか BM1 が「自分は 198.51.100.41 である」とさえ名乗れれば十分です。戻りパケットのルーティングは inet-gw1 の VRF 内に 198.51.100.41/32 next-hop 192.168.10.101 の static route があるため、ISP → inet-gw1 → (VRF) → leaf3-1 → VXLAN → leaf2-1/2-2 → BM1 と正しく転送されます。 4.7 動作確認 inet-gw1 で Global の inet.0 にも 198.51.100.41/32 が現れます (= VRF からのリーク成功)。 admin@inet-gw1# run show route 198.51.100.41 inet.0: 198.51.100.41/32 *[Static/5] > to 192.168.10.101 via ge-0/0/0.10 VRF-User.inet.0: 198.51.100.41/32 *[Static/5] > to 192.168.10.101 via ge-0/0/0.10 そして BM1 → 外部 ISP(192.0.2.1)への ping 、および逆方向の 外部 ISP → BM1(198.51.100.41)への ping がともに通れば、End-to-End の経路が完成です。 ~ # ping -c 3 -I 198.51.100.41 192.0.2.1 64 bytes from 192.0.2.1: icmp_seq=1 ttl=63 time=5.87 ms ... 3 packets transmitted, 3 received, 0% packet loss admin@isp1# run ping 198.51.100.41 count 3 64 bytes from 198.51.100.41: icmp_seq=0 ttl=63 time=6.070 ms ... 3 packets transmitted, 3 packets received, 0% packet loss ✅ 完成状態チェックリスト(Border GW) inet-gw1 の inet.0 と VRF-User.inet.0 の両方に 198.51.100.41/32 が存在 inet-gw1 ↔ isp1 の eBGP が Establ 、isp1 から default route を受信 BM1 → 192.0.2.1 (外部 ISP) の ping が通る( -I 198.51.100.41 で source 指定) 外部 ISP → 198.51.100.41 (BM1) の ping が通る ここまでで、テナント内の BM1 が VRF Route Leaking を経由して外部 ISP と双方向に通信できる End-to-End の経路が完成しました。本記事で目標としていた全構成の構築が完了です。 全体の完成確認 全セクションを通して構築した結果、冒頭で掲げたゴール — 物理的に離れた HV1 上の VM と BM1 が同一 L2 で通信でき、さらにインターネットへ抜けられる — が達成できています。完成した NW で実現できていることを整理します。 通信パス 経由するレイヤ VM1(HV1)↔ BM1 HV1 (VTEP) → VXLAN → Underlay (Spine経由) → leaf2-1/2-2 (VTEP) → BM1 BM1 → インターネット BM1 → leaf2-1/2-2 → VXLAN → leaf3-1 → inet-gw1 ( VRF Route Leaking ) → isp1 インターネット → BM1 isp1 → inet-gw1 (Global→VRF) → leaf3-1 → VXLAN → leaf2-1/2-2 → BM1 Underlay :eBGP + ECMP により、どちらの Spine を経由しても全ノードの Loopback が到達可能 Overlay :EVPN/VXLAN により、異なるラックの VM1 と BM1 が同一 L2(VNI 10010)で通信 冗長化 :ESI-LAG により、leaf2-1 または leaf2-2 のどちらか一方が故障しても BM1 の通信が継続 外部接続 :VRF Route Leaking により、テナント内部の経路とインターネット側の経路を相互にリークし、BM1 が外部と双方向に通信可能 これらが組み合わさることで、クラウド NW の基本構成 — マルチテナント対応の L2 延伸・物理冗長・外部接続 — が 1 つのラボ上で再現できています。 おまけ: ハマったら見るところ(Troubleshooting) 手順通りやってもうまくいかないとき、上から順に切り分けると早いです。 症状 確認ポイント BGP が Active から進まない 直結 ping → Loopback 間 ping → peer-as の値 → ポリシーで自分の経路を絞ってないか Underlay は OK だが Overlay iBGP が Active Loopback 間の ping、 local-address が自分の Loopback になっているか EVPN ピアは張れたが経路が来ない 両端の RT が一致しているか、 extended-vni-list の VNI 範囲 ESI-LAG で片側だけしか流れない 2 台の esi と lacp system-id が 完全に 一致しているか Junos 側で何が起きているか覗くコマンド run show bgp summary run show route table bgp.evpn.0 run show route advertising-protocol bgp <neighbor> run show route receive-protocol bgp <neighbor> run show ethernet-switching vxlan-tunnel-end-point remote run show evpn database FRR 側 vtysh -c "show bgp l2vpn evpn summary" vtysh -c "show evpn vni" vtysh -c "show evpn mac vni all" まとめ Containerlab + vJunos でクラウド NW の王道構成を一通り体験しました。しかし、本記事はあくまで学習用であり、実際の運用では次のような事項も検討が必要です。 IRB / Anycast Gateway (各 Leaf に同じ GW IP を持たせる分散 L3 ルーティング) MTU 設計 (Underlay 9000 / テナント 1500) BGP 認証 (MD5) マルチテナント運用 (VNI 数千規模の管理) セキュリティポリシー (マイクロセグメンテーション、ACL) 監視連携 本記事を通じてクラウド NW を少しでも知っていただき、「面白い!」と思っていただけましたら幸いです。 クラウド NW 開発に興味を持っていただけた学生の方は、ぜひインターンのポスト「 【B19】エンタープライズ向け大規模クラウド/ネットワークサービスを支えるコントローラ開発 」や「 【B25】エンタープライズ向け大規模クラウドサービスを支える仮想ネットワークソフトウェア開発 」にご応募ください。
私は 4 月 27 日週、英国のヨークで休暇を過ごしました。ヨークは、国内で最も幽霊に取りつかれた街として知られています。千年近くにわたってそこに立ち続けてきた修道院跡を散策し、中世の城壁沿いを散策し、夜はゴーストツアーに参加して、何世紀にもわたって語り継がれてきた物語に耳を傾けました。これほど多くの歴史を見守ってきた場所に立つと、不思議と心が落ち着きます。今はデスクに戻ってきましたが、その対比は否応なく感じられます。修道院の石は千年もの間、ほとんど変わることなくそこに立っているのに、たった 1 週間の休暇の間に、技術革新のスピードはまた一段進展していました。 ノースヨークシャーにあるウィットビー修道院跡。千年の歴史を見守ってきた石がある一方で、この 1 週間だけで新たな変化の波が押し寄せました。 それでは、2026 年 5 月 4 日週の AWS ニュースを見ていきましょう。 主なトピック 4 月 28 日、AWS の CEO であるマット ガーマン、SVP Amazon Applied AI Solutions である Colleen Aubrey 氏、AWS の CMO である Julia White、そして OpenAI のリーダーたちが登壇し、お客様がエージェントを利用してビジネスのあり方をどのように変えているのかについて語りました。このイベントでは、Amazon Quick、Amazon Connect、OpenAI とのパートナーシップの強化に関する数々の発表が行われました。このイベントでの主な発表のまとめをご紹介します。 Amazon Quick が、デスクトップアプリケーション、新しい料金プラン、ビジュアルアセット生成で拡張 – Amazon Quick は、お客様のアプリケーションと連携し、お客様にとって重要なことを学習して、お客様のためにアクションを実行する、業務用 AI アシスタントです。今週、Quick は新しいデスクトップアプリケーション (プレビュー) を発表しました。このアプリケーションを利用することで、ブラウザを開かなくても、ローカルファイル、カレンダー、コミュニケーションに接続できます。個人のメールアドレス、または既存の Google、Apple、Github、Amazon 認証情報を使用して数分でサインアップできます。AWS アカウントは不要です。Quick は、チャットインターフェイスから直接、洗練されたドキュメント、プレゼンテーション、インフォグラフィック、画像を生成できるようになりました。また、ネイティブ統合機能も拡張され、Google Workspace、Zoom、Airtable、Dropbox、Microsoft Teams が含まれるようになりました。新しい「Quick でカスタムアプリケーションを構築」機能 (プレビュー) では、自然言語を使用してビジネス全体と連携するインテリジェントなアプリケーション、ダッシュボード、ウェブページを作成できます。 Amazon Connect が 4 つのエージェンティック AI ソリューションに拡張 – Amazon Connect は、単一の製品から、お客様既存のワークフロー内で動作するように設計された、一連の 4 つのエージェンティック AI ソリューションに拡張されます。Amazon Connect Decisions は、サプライチェーンプランニングおよびインテリジェンスソリューションであり、30 年にわたる Amazon の運用科学と 25 を超える専門的なサプライチェーンツールを組み合わせることで、チームの業務を危機管理からプロアクティブな計画に移行させます。Amazon Connect Talent (プレビュー) は、エージェンティック AI 採用ソリューションであり、大規模な採用を管理する人材獲得リーダー向けに、AI 主導の面接、科学的根拠に基づいた評価、一貫性のある評価を提供します。Amazon Connect Customer (旧称: Amazon Connect) は、音声、チャット、デジタルチャネル全体でパーソナライズされたカスタマーエクスペリエンスを提供します。新しい設定機能により、組織は、対話型 AI を数か月間ではなく数週間でセットアップできます。Amazon Connect Health は、エージェントによる患者確認、予約管理、患者インサイト、アンビエントドキュメンテーション、医療コーディングを提供することで、患者がより迅速に医療ケアを利用し、臨床医がその提供により多くの時間を割けるようになります。 AWS と OpenAI が Amazon Bedrock におけるパートナーシップを拡大 – AWS と OpenAI は、最新の OpenAI モデルを Amazon Bedrock に導入し、Codex on Amazon Bedrock をリリースするほか、OpenAI を利用する Amazon Bedrock Managed Agents を提供します。すべて限定プレビューです。Amazon Bedrock 上の OpenAI モデル (限定プレビュー) は、GPT-5.5 や GPT-5.4 を含む最新の OpenAI モデルを、お客様が既にご利用の Bedrock API に統合し、統合されたセキュリティ、ガバナンス、コストコントロールを提供します。追加のインフラストラクチャを設定する必要も、新しいセキュリティモデルを学ぶ必要もありません。Codex on Amazon Bedrock (限定プレビュー) を使用すると、既存の AWS 環境内で OpenAI コーディングエージェントにアクセスして、AWS 認証情報を使用して認証し、Bedrock を通じて推論処理を実行するほか、Codex の使用量を AWS クラウドのコミットメントに算入できます。Codex on Bedrock は Bedrock API を通じて利用でき、まずは Codex CLI、Codex デスクトップアプリケーション、Visual Studio Code 拡張機能で利用可能です。OpenAI を利用する Amazon Bedrock Managed Agents (限定プレビュー) は、OpenAI のフロンティアモデルと AWS インフラストラクチャを組み合わせて、OpenAI を利用する本番対応のエージェントをクラウド内に構築します。OpenAI ハーネスを利用して構築することで、より迅速な実行、より鋭い推論、長時間実行タスクの確実な制御を実現します。 詳細については、「 Top announcements of the What’s Next with AWS, 2026 」にアクセスしてください。 2026 年 4 月 27 日週のリリース 4 月 27 日週のリリースのうち、私が注目したリリースをいくつかご紹介します: Amazon EC2 M8in および M8ib インスタンスの一般提供を開始 – カスタムの第 6 世代インテル Xeon Scalable プロセッサと第 6 世代 AWS Nitro Card を搭載したこれらのインスタンスは、M6in および M6ib と比較して、最大 43% 高いパフォーマンスを提供します。M8in は 600 Gbps のネットワーク帯域幅を提供し、M8ib は最大 300 Gbps の EBS 帯域幅を提供します。米国東部 (バージニア北部)、米国西部 (オレゴン)、アジアパシフィック (東京)、欧州 (スペイン) で利用可能です。 Amazon EC2 R8in および R8ib インスタンスの一般提供を開始 – 同じ第 6 世代インテル Xeon Scalable プロセッサと Nitro Card をベースに構築されたメモリ最適化インスタンス。600 Gbps のネットワークと 300 Gbps の EBS 帯域幅プロファイルを備えています。大規模な商用データベース、データレイク、SAP HANA などのインメモリデータベースに適しています。米国東部 (バージニア北部、オハイオ)、米国西部 (オレゴン)、欧州 (スペイン) で利用可能です。 Amazon EC2 C8ine および M8ine インスタンスの一般提供を開始 – ネットワーク最適化インスタンス。C6in および M6in と比較して、最大 2.5 倍の vCPU あたりのパケットパフォーマンスと、最大 2 倍のインターネットゲートウェイ経由のトラフィックのネットワークスループットを提供します。仮想ファイアウォール、ロードバランサー、5G UPF ワークロードなどのセキュリティおよびネットワーク仮想アプライアンス向けに設計されています。C8ine は米国東部 (バージニア北部)、米国西部 (オレゴン)、アジアパシフィック (東京) で、M8ine は米国東部 (バージニア北部) および米国西部 (オレゴン) で利用可能です。 Amazon Bedrock AgentCore が最適化機能を追加 (プレビュー) – AgentCore は、本番でのエージェントの「観察、評価、改善」のサイクルを完了するために、レコメンデーション、バッチ評価、A/B テストを提供するようになりました。レコメンデーションは、本番のトレースと評価の出力を分析し、最適化されたシステムプロンプトとツールの説明を提案します。これらは、事前定義済みのテストケースに対するバッチ評価、または実際のトラフィックに対する A/B テストで検証できます。すべてのレコメンデーションは、リリース前に承認が必要です。 AWS Lambda が Ruby 4.0 のサポートを追加 – 最新の LTS リリースである Ruby 4.0 が、Lambda マネージドランタイムおよびコンテナベースイメージとして利用可能になりました。JSON 構造化ログ、設定可能なログ記録レベル、ターゲット CloudWatch ロググループ設定など、Lambda の高度なログ記録コントロールのサポートを含んでいます。中国リージョンおよび AWS GovCloud (米国) を含む、すべての AWS リージョンで利用可能です。 AWS のお知らせに関する詳しいリストについては、「 AWS の最新情報 」ページをご覧ください。 その他の AWS ニュース 興味深いと思われる追加の記事やリソースをいくつかご紹介します: Amazon Q Developer サポート終了のお知らせ – Amazon Q Developer IDE プラグインと有料サブスクリプションのサポートは、2027 年 4 月 30 日に終了します。お客様は Kiro に移行する期間として 12 か月の猶予があります。新規サインアップは 2026 年 5 月 15 日からブロックされますが、既存のサブスクリプションでは引き続きユーザーを追加できます。2026 年 5 月 29 日より、Q Developer Pro では Opus 4.6 が利用できなくなります。Opus 4.5 および他の既存モデルは引き続き利用可能ですが、Opus 4.7 を含む最新のコーディングモデルは Kiro でのみ利用可能です。AWS マネジメントコンソールの Amazon Q Developer および AWS 公式エクスペリエンス (ドキュメント、モバイルアプリ、Slack、Microsoft Teams) に影響はありません。 AWS 10,000 AIdeas Competition: 受賞者の発表 – AWS は、ビルダーが Kiro と AWS 無料利用枠のみを使用して構築された AI アプリケーションを応募するグローバルコンテストである 10,000 AIdeas Competition の受賞者 20 名を発表しました。115 か国から応募があり、4 回の審査と 2 回のコミュニティ投票を経て選考されました。受賞者は、Global Champions、Regional Champions、Innovation Awards、Creative Track の各カテゴリに分かれており、各カテゴリで賞金と AWS クレジットが授与されます。 AWS Student Builder Groups  – AWS Cloud Clubs は AWS Student Builder Groups に進化しました。コミュニティは現在、63 か国、600 以上の大学やカレッジに広がっています。既存の Cloud Club メンバーシップ、バッジ、進捗状況は引き継がれ、Cloud Club Captain は Group Leader になります。メンバーシップは 18 歳以上の学習者であれば誰でも参加できます。AWS Builder Center で最寄りのグループを探すか、またはキャンパスで新しいグループを立ち上げるための申請をすることができます。 近日開催予定の AWS イベント カレンダーを確認して、近日開催予定の AWS イベントにサインアップしましょう: AWS Summits – AWS Summits は、クラウドと AI をカバーする無料の実地イベントです。5 月の開催予定: シンガポール (5 月 6 日)、 テルアビブ (5 月 6 日)、 ワルシャワ (5 月 6 日)、 ストックホルム (5 月 7 日)、 シドニー (5 月 13 日~14 日)、 ハンブルク (5 月 20 日)、 ソウル (5 月 20 日)、 アムステルダム (5 月 27 日)、 ミラノ (5 月 28 日)、 ムンバイ (5 月 28 日)。 AWS Community Days – コミュニティリーダーが企画および提供するコミュニティ主導のカンファレンス。今後のイベントには、 イスタンブール (トルコ) (5 月 9 日) と パナマシティ (パナマ) (5 月 23 日) が含まれています。 AWS Builder Center にアクセスして、他のビルダーと交流したり、ソリューションを提供したり、構築を継続するのに役立つリソースを見つけたりしましょう。また、今後開催される AWS 主導の実地およびオンラインイベント や、 デベロッパー向けセッション もご覧いただけます。 – Esra この記事は、Weekly Roundup シリーズの一部です。AWS からの興味深いニュースや発表を簡単にまとめて毎週ご紹介します! 原文は こちら です。
Claude Code を快適に使うための macOS デスクトップ通知セットアップ 背景 なぜ alerter を採用したのか 1. alerter のインストール 2. 通知スクリプトの作成 2-1. notify_alerter.sh(Stop / Notification hook 用) 2-2. notify_pretool.sh(PreToolUse hook 用) 3. Claude Code の hooks 設定 各 Hook の役割 4. VSCode 拡張での Notification hook の扱い 5. macOS のセキュリティ許可 6. 動作確認 通知テスト 確認項目 デバッグログ 7. alerter のプロセス管理で学んだこと 問題: プロセスのゾンビ化 対策1: --group(プロセス蓄積の防止) 対策2: --timeout(最終的なプロセス回収) 溜まったプロセスの手動クリーンアップ 8. なぜ nohup + disown が必要だったか 9. 通知のカスタマイズ 特定ツールの通知をスキップする サウンド --sender(通知アイコン) まとめ 最後に  こんにちは、開発本部 開発2部 RetailHUB NetSuperグループに所属するホーク🦅アイ👁️です。 背景  弊社ではClaude を非エンジニアも含めた全社に展開しており、業務のあらゆる場面で生成AI の活用を推進しています。  そんな中、我々のチーム内でも今年3月から本格的にCursor から移行してClaude Code (VSCode 拡張機能)を日常的に使うようになってから、両者の明らかな違いを実感することになりました。  それは、Cursor が標準搭載しているmacOS デスクトップ通知機能でした。Claude Code にはその機能がないためAgent にプロンプトを投げた後、私自身が他の作業を並行しているとClaude Code 側が permission_prompt のWait でタスクが一向に完了できない状態やタスク完了状態に気付くのが随分遅れてしまうということがしばしばありました(業務効率化のためのAgent ツールなのに、、)。  Claude Code には Hooks という仕組みが用意されています。これは Stop(応答終了)や Notification(許可待ち等)、PreToolUse(ツール実行直前)といったライフサイクルイベントに対して任意のシェルコマンドを実行できる公式機能で、JSON がイベント情報として標準入力から渡ってきます。  本記事ではこの Hooks と alerter というコマンドラインツールを組み合わせて、 タスク完了・許可待ち・入力待ちの デスクトップ通知を出す 通知を クリックすると、対象プロジェクトの VSCode ウィンドウが自動でアクティブになる (全画面の別アプリ上からでも切り替わる) VSCode 拡張版 でも許可待ち通知を取りこぼさない という環境を構築した内容をまとめます。macOS 26 系(Tahoe)環境で動作確認しています。 なぜ alerter を採用したのか  macOS から通知を出すだけなら選択肢は複数あります。今回の要件「通知をクリックしたら VSCode がアクティブになる」を満たせるものを比較した結果を表にまとめます。 ツール 通知表示 クリックイベントの取得 備考 terminal-notifier 環境依存 可能(旧来の定番) 公式リポジトリ の最新リリースは 2017 年 11 月(v2.0.0)で、近年の macOS での動作不具合 Issue( #307 、 #312 、 #319 ほか)が未解決のままです。私の環境(macOS 26 系)では通知が出ませんでした。 osascript ( display notification ) 動作する 不可 AppleScript 公式ドキュメント( Standard Additions: display notification )に「戻り値なし」と明記されており、クリック結果を取得する手段がありません。 alerter 動作する 可能 公式リポジトリ によれば、 terminal-notifier を Swift で書き直した後継で、macOS 13.0 以降対応。クリック時に @CONTENTCLICKED / @ACTIONCLICKED を stdout に出力するため、外部プロセスでの後処理が可能です。   alerter がクリック結果を stdout に返してくれるおかげで、「クリック → open -a "Visual Studio Code" で対象プロジェクトを開く」という連携を、標準ツールの組み合わせだけで実現できました。 1. alerter のインストール  Homebrew で導入します( 公式の導入手順 に準拠)。 brew install vjeantet/tap/alerter  インストール確認: which alerter # /opt/homebrew/bin/alerter alerter --version 2. 通知スクリプトの作成  2 つのスクリプトを ~/.claude/ に配置し、実行権限を付与します。前者は Stop / Notification hook 用、後者は VSCode 拡張向けの PreToolUse hook 用です。 chmod +x ~/.claude/notify_alerter.sh chmod +x ~/.claude/notify_pretool.sh 2-1. notify_alerter.sh (Stop / Notification hook 用)  タスク完了通知および、CLI 版 Claude Code での許可待ち通知を処理します。Hook に渡ってくる JSON の仕様は 公式リファレンスの Stop / Notification セクション に従っています。 notification_type として permission_prompt / idle_prompt が返ってくるため、これで分岐しています。 #!/bin/bash input = $( cat ) echo " $( date ' +%H:%M:%S ' ) $input " >> /tmp/claude_notify_debug.log cwd = $( echo " $input " | jq -r ' .cwd ' ) project = $( basename " $cwd " ) notification_type = $( echo " $input " | jq -r ' .notification_type ' ) # ターミナルアプリの Bundle ID を自動検出 get_terminal_bundle_id() { if [[ -n " ${__CFBundleIdentifier} " ]] ; then echo " ${__CFBundleIdentifier} " return fi case " ${TERM_PROGRAM} " in " Apple_Terminal ") echo " com.apple.Terminal " ;; " iTerm.app ") echo " com.googlecode.iterm2 " ;; " ghostty ") echo " com.mitchellh.ghostty " ;; " WarpTerminal ") echo " dev.warp.Warp-Stable " ;; * ) local pid parent comm pid = $$ while [[ " ${pid} " -ne 1 ]] 2 >/dev/null; do parent = $( ps -p " ${pid} " -o ppid = 2 > /dev/null | tr -d ' ' ) || break [[ -z " ${parent} " ]] && break comm = $( ps -p " ${parent} " -o comm = 2 > /dev/null ) case " ${comm} " in *Terminal* ) echo " com.apple.Terminal "; return ;; *iTerm* ) echo " com.googlecode.iterm2 "; return ;; *Cursor* ) echo " com.todesktop.230313mzl4w4u92 "; return ;; *Code* ) echo " com.microsoft.VSCode "; return ;; *ghostty* ) echo " com.mitchellh.ghostty "; return ;; *warp* ) echo " dev.warp.Warp-Stable "; return ;; * ) ;; esac pid = " ${parent} " done echo "" ;; esac } BUNDLE_ID = $( get_terminal_bundle_id ) send_notification() { local message =" $1 " local sound =" $2 " local group =" $3 " local args = (--title " Claude Code " --subtitle " ${project} " --message " ${message} " ) if [[ -n " ${sound} " ]] ; then args += ( --sound " ${sound} " ) fi args += ( --sender " com.microsoft.VSCode " ) # --group: 同じグループの通知は前のプロセスを自動終了して置き換える args += ( --group " ${group :- claude-default } " ) # --timeout: プロセスのゾンビ化防止(秒)。通知自体は macOS 通知センターに残る local timeout = 86400 local timeout_file =" $HOME /.claude/notify_timeout.conf " if [[ -f " ${timeout_file} " ]] ; then timeout = $( cat " ${timeout_file} " | tr -d ' [:space:] ' ) fi args += ( --timeout " ${timeout} " ) # alerter はクリック待ちでブロックするため、nohup + disown で完全にデタッチ nohup bash -c " result= \$ (alerter $( printf ' %q ' " ${args[ @ ]} " ) 2>/dev/null) if [[ \"\$ {result} \" == \" @CONTENTCLICKED \" || \"\$ {result} \" == \" @ACTIONCLICKED \" ]] && [[ -n \" ${cwd} \" ]]; then open -a \" Visual Studio Code \" \" ${cwd} \" fi " &> /dev/null & disown } case " ${notification_type} " in " permission_prompt ") send_notification " 許可待ち " " Ping " " claude-permission " ;; " idle_prompt ") send_notification " 入力待ち " " Purr " " claude-idle " ;; " stop ") send_notification " タスク完了 " " Glass " " claude-stop " ;; * ) send_notification " 通知 " " default " " claude-other " ;; esac 2-2. notify_pretool.sh (PreToolUse hook 用)  こちらは VSCode 拡張環境向けの「許可待ち通知」の代替実装です。詳細は「4. VSCode 拡張での Notification hook の扱い」で後述します。  ざっくり説明すると、次の 4 つの設定ファイルの permissions.allow リストと照合し、 自動許可されないツールの実行前にのみ 通知を送るというロジックです。 ~/.claude/settings.json (グローバル) ~/.claude/settings.local.json (グローバルローカル) $cwd/.claude/settings.json (プロジェクト) $cwd/.claude/settings.local.json (プロジェクトローカル) #!/bin/bash # PreToolUse hook: 許可が必要なツール実行前に通知を送る # settings.json の allow リストにマッチするツールはスキップする input = $( cat ) tool_name = $( echo " $input " | jq -r ' .tool_name ' ) cwd = $( echo " $input " | jq -r ' .cwd ' ) project = $( basename " $cwd " ) # 常に自動許可されるツール(通知不要) case " ${tool_name} " in Glob|Grep|TodoWrite|Agent|Skill|ToolSearch|SendMessage ) exit 0 ;; esac # ユーザー個別のスキップリスト(~/.claude/notify_skip_tools.txt) SKIP_FILE = " $HOME /.claude/notify_skip_tools.txt " if [[ -f " ${SKIP_FILE} " ]] ; then while IFS = read -r skip_tool; do [[ -z " ${skip_tool} " || " ${skip_tool} " == \# * ]] && continue if [[ " ${tool_name} " == " ${skip_tool} " ]] ; then exit 0 fi done < " ${SKIP_FILE} " fi # allow リストと照合する関数 check_allow_list() { local settings_file =" $1 " [[ -f " ${settings_file} " ]] || return # Bash ツール: コマンドプレフィックスで照合 if [[ " ${tool_name} " == " Bash " ]] ; then local command command= $( echo " $input " | jq -r ' .tool_input.command ' ) while IFS = read -r pattern; do if [[ " ${pattern} " =~ ^Bash\((.+)(:\*|\*)?\)$ ]] ; then local prefix =" ${BASH_REMATCH[ 1 ]} " prefix = " ${prefix % :* } " if [[ " ${command} " == " ${prefix} " * ]] ; then exit 0 fi fi done < < ( jq -r ' .permissions.allow[] ' " ${settings_file} " 2 > /dev/null ) fi # Read ツール: パスパターンで照合 if [[ " ${tool_name} " == " Read " ]] ; then local file_path file_path = $( echo " $input " | jq -r ' .tool_input.file_path ' ) while IFS = read -r pattern; do if [[ " ${pattern} " =~ ^Read\(//(.+)\)$ ]] ; then local path_pattern =" ${BASH_REMATCH[ 1 ]} " local path_prefix =" ${path_pattern %% /** } " if [[ " ${file_path} " == " ${path_prefix} " * ]] ; then exit 0 fi fi done < < ( jq -r ' .permissions.allow[] ' " ${settings_file} " 2 > /dev/null ) fi # MCP ツール・WebSearch 等: 完全一致で照合 while IFS = read -r pattern; do if [[ " ${pattern} " == " ${tool_name} " ]] ; then exit 0 fi done < < ( jq -r ' .permissions.allow[] ' " ${settings_file} " 2 > /dev/null ) } # グローバル設定 check_allow_list " $HOME /.claude/settings.json " check_allow_list " $HOME /.claude/settings.local.json " # プロジェクト設定 check_allow_list " $cwd /.claude/settings.json " check_allow_list " $cwd /.claude/settings.local.json " # 許可リストにマッチしない → 通知を送る echo " $( date ' +%H:%M:%S ' ) PRETOOL_NOTIFY: ${tool_name} " >> /tmp/claude_notify_debug.log nohup bash -c " timeout=86400 timeout_file= \"\$ HOME/.claude/notify_timeout.conf \" if [[ -f \"\$ {timeout_file} \" ]]; then timeout= \$ (cat \"\$ {timeout_file} \" | tr -d '[:space:]') fi result= \$ (alerter --title 'Claude Code' --subtitle ' ${project} ' --message '許可待ち: ${tool_name} ' --sound Ping --sender com.microsoft.VSCode --group claude-pretool --timeout \"\$ {timeout} \" 2>/dev/null) if [[ \"\$ {result} \" == '@CONTENTCLICKED' || \"\$ {result} \" == '@ACTIONCLICKED' ]] && [[ -n ' ${cwd} ' ]]; then open -a 'Visual Studio Code' ' ${cwd} ' fi " & > /dev/null & disown exit 0 3. Claude Code の hooks 設定   ~/.claude/settings.json の hooks セクションに以下を追加します( 公式リファレンス の書式に準拠)。 { " hooks ": { " Stop ": [ { " matcher ": "", " hooks ": [ { " type ": " command ", " command ": " echo '{ \" cwd \" : \" ' \" $(pwd) \" ' \" , \" notification_type \" : \" stop \" }' | ~/.claude/notify_alerter.sh " } ] } ] , " Notification ": [ { " matcher ": "", " hooks ": [ { " type ": " command ", " command ": " ~/.claude/notify_alerter.sh " } ] } ] , " PreToolUse ": [ { " matcher ": "", " hooks ": [ { " type ": " command ", " command ": " ~/.claude/notify_pretool.sh " } ] } ] } } 各 Hook の役割 Hook 発火タイミング 用途 VSCode 拡張 CLI Stop Claude が応答を終えて停止したタイミング 「タスク完了」通知 動作する 動作する Notification 許可待ち・入力待ちなどの通知イベント 「許可待ち」「入力待ち」通知 permission_prompt が発火しないケースあり 動作する PreToolUse ツール実行の直前 VSCode での「許可待ち」通知の代替 動作する 動作する 4. VSCode 拡張での Notification hook の扱い   公式リファレンス では、 Notification hook の notification_type として permission_prompt / idle_prompt / auth_success / elicitation_dialog の 4 種が定義されています。しかし、私の環境で動作確認したところ、 VSCode 拡張版では許可ダイアログが出ても Notification hook( permission_prompt )が発火しないケース があり、「許可待ちなのに通知が来ない」という状態になっていました。CLI 版では同じ設定で期待どおり発火しています。  そのため、VSCode 拡張で使う場合は PreToolUse hook(必ず発火する)でツール実行直前に自前で判定する という回避策を取っています。流れは以下です。 PreToolUse hook がツール実行直前に発火する notify_pretool.sh がツール名(と Bash の場合はコマンド、Read の場合はファイルパス)を受け取り、4 つの設定ファイルの permissions.allow と照合する allow リストに マッチしなかったとき だけ通知を送る(=「このあと許可ダイアログが出るはず」というタイミング)  この方式であれば、 Notification hook の発火有無にかかわらず、VSCode でも CLI でも漏れなく許可待ち通知を届けられます。CLI 版では Notification hook が正常動作するため、重複しないよう --group を claude-permission と claude-pretool で分けています(後述)。 5. macOS のセキュリティ許可   alerter + open -a の組み合わせは、macOS のアクセシビリティ・オートメーション等の追加許可なしで動作しました。初回のみ通知センター側で通知の表示許可を求められる程度で、特別な設定は不要です。 6. 動作確認 通知テスト # タスク完了通知 echo ' {"cwd": " ' $( pwd ) ' ", "notification_type": "stop"} ' | ~/.claude/notify_alerter.sh # 許可待ち通知(CLI の Notification hook 用) echo ' {"cwd": " ' $( pwd ) ' ", "notification_type": "permission_prompt"} ' | ~/.claude/notify_alerter.sh 確認項目 タスク完了通知がデスクトップに表示される 許可待ち通知が表示される(VSCode: PreToolUse / CLI: Notification) VSCode アイコンが通知に表示される( --sender com.microsoft.VSCode ) 通知をクリックすると対象プロジェクトの VSCode ウィンドウがアクティブになる 全画面の別アプリ(Chrome 等)から通知をクリックしても正しいウィンドウに切り替わる 通知後に Claude が WAIT 状態にならず即座に続行する デバッグログ  通知が来ないときはデバッグログを確認します: tail -f /tmp/claude_notify_debug.log 7. alerter のプロセス管理で学んだこと  運用してみて一番ハマったのがプロセス管理です。 問題: プロセスのゾンビ化   alerter は クリックされるまで stdout をブロックし続ける 仕様です( 公式リポジトリ の README にある @CONTENTCLICKED / @ACTIONCLICKED / @TIMEOUT / @CLOSED のいずれかが出力されるまでプロセスが生きる)。通知バッジを macOS 通知センターから消去しても alerter プロセスは終了しません。放置すると各プロセスがメモリを消費し、長時間の利用で数 GB に達するケースがありました。 対策1: --group (プロセス蓄積の防止)  同じ --group の通知が新たに発行されると、前のプロセスが自動で kill されます。グループは用途別に分けており、同時に存在するプロセスは最大 4 つになる設計です: グループ 用途 claude-stop タスク完了 claude-permission 許可待ち(CLI Notification hook) claude-pretool 許可待ち(VSCode PreToolUse hook) claude-idle 入力待ち 対策2: --timeout (最終的なプロセス回収)   --group だけでは最後の 4 プロセスが残り続けるため、 --timeout でプロセスの最大生存時間を設定して確実に回収します。 デフォルト: 86400 秒(1 日) カスタム: ~/.claude/notify_timeout.conf に秒数を書く # 例: 2 時間に変更 echo 7200 > ~/.claude/notify_timeout.conf  なお、timeout が切れてもプロセスが終了するだけで、macOS 通知センターの通知バッジは残ります。 溜まったプロセスの手動クリーンアップ # alerter プロセス数を確認 ps aux | grep alerter | grep -v grep | wc -l # 全 alerter プロセスを終了 pkill -f alerter 8. なぜ nohup + disown が必要だったか  前述のとおり alerter はクリック待ちでブロックします。単純に (...) & でバックグラウンド実行しても、 Claude Code の hook ランナーが子プロセスの終了を待ってしまい、Claude 本体が WAIT 状態のまま止まる (トークンも消費し続けてしまう)という問題がありました。   nohup ... & で SIGHUP を無視させ、さらに disown でジョブテーブルから外すことで、hook プロセスから完全に切り離せます。これにより、通知の表示・クリック待ちとは独立して Claude が動作を継続できるようになりました。 9. 通知のカスタマイズ 特定ツールの通知をスキップする  VSCode の「Edit Automatically」などセッションレベルで自動許可しているツールは settings.json に記録されないため、 ~/.claude/notify_skip_tools.txt に 1 行 1 ツール名で記載する仕組みを入れてあります: # セッションレベルで自動許可しているツール名を 1 行 1 つで記載 Edit  もしくは notify_pretool.sh の先頭付近にあるスキップリスト( Glob|Grep|TodoWrite|... )に追記する方法でも同等です。 サウンド  macOS 標準のサウンド名を指定できます: Ping , Purr , Glass , default , Basso , Blow , Bottle , Frog , Funk , Hero , Morse , Pop , Sosumi , Submarine , Tink 。 --sender (通知アイコン)   --sender に Bundle ID を指定すると通知アイコンが変わります。現在は com.microsoft.VSCode を指定して VSCode アイコンを表示しています。 アプリ Bundle ID VSCode com.microsoft.VSCode Cursor com.todesktop.230313mzl4w4u92 Terminal com.apple.Terminal iTerm2 com.googlecode.iterm2 Ghostty com.mitchellh.ghostty  ただし --sender を指定すると、そのアプリの macOS 通知設定に依存することになります。対象アプリの通知を OFF にしていると通知が表示されなくなるため注意が必要です。 まとめ  本記事では、Claude Code の Hooks 機能と alerter を組み合わせて、 タスク完了・許可待ち・入力待ちのデスクトップ通知を出す 通知クリックでプロジェクトの VSCode ウィンドウを自動でアクティブにする VSCode 拡張でも PreToolUse hook で許可待ち通知を取りこぼさない というセットアップ方法と、その過程で踏んだプロセス管理の落とし穴(ゾンビ化 → --group / --timeout / nohup + disown での回収)をご紹介しました。  Claude Code をバックグラウンドで走らせつつ他の作業を並行して進めるスタイルにおいては、「気づかずに長時間止まっていた」という時間を減らすだけで、体感の生産性が目に見えて向上します。CLI と VSCode 拡張で挙動が異なる部分は PreToolUse hook で吸収できるので、Hooks の仕様を把握したうえで自分の開発スタイルに合わせてカスタマイズしてみてください。 通知例 最後に エブリーでは、ともに働く仲間を募集しています。 テックブログを読んで少しでもエブリーに興味を持っていただけた方は、ぜひ一度カジュアル面談にお越しください! corp.every.tv

動画

該当するコンテンツが見つかりませんでした

書籍