実践SDN:OpenFlow、OVS、Ryuで構築するプログラマブル・ネットワーク

Networking tutorial - IT technology blog
Networking tutorial - IT technology blog

従来のネットワークが限界を迎えている理由

ネットワーク管理はかつて、50台の異なるスイッチにSSHで接続し、50種類の独自CLIを操作して、VLANやACLの設定を手動で入力するという、退屈な作業の繰り返しでした。このハードウェア中心のアプローチは、現代のデータセンターにとって大きなボトルネックとなっています。新しいアプリケーションが特定のポリシーを必要とする際、手動の設定変更に3日間も待つことは、もはや選択肢になり得ません。

キャリアの初期、ある「スプリットブレイン」設定にまつわる悪夢を覚えています。クラスタ内の一つのスイッチがMTU 1500に設定され、他は9000になっていました。その一箇所の不一致を見つけ出すのに4時間のダウンタイムを要しました。ソフトウェア定義ネットワーク(SDN)は、「脳」(コントロールプレーン)を「筋肉」(データプレーン)から分離することで、この問題を解決します。

6ヶ月前にSDNベースのインフラに切り替えて以来、私のワークフローは手動のCLI入力からプログラマブルなロジックの記述へと移行しました。本番環境において、このセットアップは極めて安定しています。標準的なハードウェアでは不可能な、自動化されたテナント分離や動的なトラフィックエンジニアリングを実現しています。

スタック:OpenFlow、OVS、Ryu

機能的なSDN環境を構築するには、3つのコンポーネントを連携させる必要があります。コードを1行でも書く前に、それらの相互作用を理解することが不可欠です。

1. データプレーン:Open vSwitch (OVS)

Open vSwitchは、大規模な自動化のために設計されたマルチレイヤー仮想スイッチです。ProxmoxやKVMのような環境では、OVSはハイパーバイザー内に存在し、仮想マシン間のパケット移動という重労働を処理します。標準的なLinuxブリッジとは異なり、OVSはOpenFlowをサポートしているため、外部コントローラーから転送テーブルをリモート管理できます。

2. プロトコル:OpenFlow

OpenFlowは、コントローラーがスイッチと通信するための言語です。スイッチがMACアドレスに基づいて独立して判断を下す代わりに、コントローラーに「このようなヘッダーを持つパケットを受信しました。どうすればいいですか?」と問いかけます。するとコントローラーは「フローエントリ」をスイッチのメモリにプッシュします。これにより、スイッチは将来的に同様のパケットを転送、破棄、または修正するよう指示されます。

3. コントロールプレーン:Ryuコントローラー

RyuはPythonベース of SDNフレームワークです。私はONOSのような重量級の代替案よりも、軽量で開発者フレンドリーなRyuを好んで使用しています。Pythonの基礎知識があれば、ネットワークロジックを記述できます。クリーンなAPIを提供しており、カスタムファイアウォールルールやロードバランサーの作成が非常にスムーズです。

ハンズオン:実装手順

今回のセットアップでは、クリーンなUbuntu 22.04 LTS環境を使用します。ネットワークのシミュレーションにはMininetを使います。これは、物理スイッチのラックを用意することなくSDNトポロジのプロトタイプを作成するための業界標準ツールです。

ステップ1:インストール

まず、システムを更新し、コアとなるネットワークツールをインストールします。

sudo apt update
sudo apt install -y openvswitch-switch mininet python3-pip

次に、Ryuフレームワークをインストールします。通常は仮想環境(venv)が推奨されますが、専用のラボマシンであれば直接インストールしても問題ありません。

pip3 install ryu

ステップ2:学習スイッチの構築

OVSをインテリジェントな学習スイッチ(Learning Switch)として動作させるためのPythonスクリプトを書いてみましょう。このロジックにより、スイッチはどのMACアドレスがどのポートにあるかを記憶します。これを my_switch.py として保存してください。

from ryu.base import app_manager
from ryu.controller import ofp_event
from ryu.controller.handler import CONFIG_DISPATCHER, MAIN_DISPATCHER
from ryu.controller.handler import set_ev_cls
from ryu.ofproto import ofproto_v1_3
from ryu.lib.packet import packet
from ryu.lib.packet import ethernet

class MySwitch(app_manager.RyuApp):
    OFP_VERSIONS = [ofproto_v1_3.OFP_VERSION]

    def __init__(self, *args, **kwargs):
        super(MySwitch, self).__init__(*args, **kwargs)
        self.mac_to_port = {}

    @set_ev_cls(ofp_event.EventOFPSwitchFeatures, CONFIG_DISPATCHER)
    def switch_features_handler(self, ev):
        datapath = ev.msg.datapath
        ofproto = datapath.ofproto
        parser = datapath.ofproto_parser

        # テーブルミス・フローエントリをインストール
        match = parser.OFPMatch()
        actions = [parser.OFPActionOutput(ofproto.OFPP_CONTROLLER,
                                          ofproto.OFPCML_NO_BUFFER)]
        self.add_flow(datapath, 0, match, actions)

    def add_flow(self, datapath, priority, match, actions):
        ofproto = datapath.ofproto
        parser = datapath.ofproto_parser
        inst = [parser.OFPInstructionActions(ofproto.OFPIT_APPLY_ACTIONS, actions)]
        mod = parser.OFPFlowMod(datapath=datapath, priority=priority,
                                match=match, instructions=inst)
        datapath.send_msg(mod)

    @set_ev_cls(ofp_event.EventOFPPacketIn, MAIN_DISPATCHER)
    def _packet_in_handler(self, ev):
        msg = ev.msg
        datapath = msg.datapath
        ofproto = datapath.ofproto
        parser = datapath.ofproto_parser
        in_port = msg.match['in_port']

        pkt = packet.Packet(msg.data)
        eth = pkt.get_protocols(ethernet.ethernet)[0]
        dst = eth.dst
        src = eth.src

        dpid = datapath.id
        self.mac_to_port.setdefault(dpid, {})

        # 次回以降のフラッディングを避けるため、MACアドレスを学習する
        self.mac_to_port[dpid][src] = in_port

        if dst in self.mac_to_port[dpid]:
            out_port = self.mac_to_port[dpid][dst]
        else:
            out_port = ofproto.OFPP_FLOOD

        actions = [parser.OFPActionOutput(out_port)]

        if out_port != ofproto.OFPP_FLOOD:
            match = parser.OFPMatch(in_port=in_port, eth_dst=dst)
            self.add_flow(datapath, 1, match, actions)

        data = None
        if msg.buffer_id == ofproto.OFP_NO_BUFFER:
            data = msg.data

        out = parser.OFPPacketOut(datapath=datapath, buffer_id=msg.buffer_id,
                                  in_port=in_port, actions=actions, data=data)
        datapath.send_msg(out)

ステップ3:ネットワークのテスト

2つのターミナルウィンドウを開きます。1つ目で、作成したスクリプトを使用してRyuコントローラーを起動します。

ryu-manager my_switch.py

2つ目のターミナルで、Mininetを使用して1つのスイッチと3つのホストを持つトポロジを作成します。ローカルのRyuコントローラー(127.0.0.1:6633)を指定します。

sudo mn --topo single,3 --mac --controller remote,ip=127.0.0.1,port=6633 --switch ovsk,protocols=OpenFlow13

Mininet CLI(mininet>)の準備ができたら、接続テストを実行します。

mininet> pingall

最初のpingは、コントローラーが未知のMACアドレスを処理するため、通常は高いレイテンシ(約50〜100ms)を示します。その後のpingは1ミリ秒未満に低下します。これは、OVSがコントローラーを介さず、カーネル内でトラフィックを処理するようになるためです。

内部の仕組みを覗く

SDNの最大の利点は可視性です。スイッチがどのように判断を下しているかを正確に確認するには、3つ目のターミナルを開いて以下を実行してください。

sudo ovs-ofctl -O OpenFlow13 dump-flows s1

priority=1,in_port=1,eth_dst=00:00:00:00:00:02 actions=output:2 のようなルールが表示されます。これは、Ryuコントローラーがスイッチのハードウェアに送信した正確な命令です。従来の「ブラックボックス」なスイッチとは異なり、パケットの運命を決定するすべてのルールを確認できます。

本番環境への導入に向けた教訓

ラボから本番クラスタへの移行には、高Availability(HA)への注力が必要です。最初にステージング環境でOVSとRyuをデプロイした際、コントローラーが単一障害点(SPOF)であることに気づきました。コントローラーがクラッシュすると、スイッチは「ダム(無知)」な状態になり、トラフィックの転送を停止してしまいます。

これを解決するため、現在はロードバランサーの背後に3ノードのRyuクラスタを使用しています。また、OVSに複数のコントローラーIPを使用するように設定しています。これにより、ソフトウェアのアップデート中であってもデータプレーンは稼働し続けます。6ヶ月以上の運用の中で、SDNロジックが原因でネットワーク全体の停止が発生したことは一度もありません。

最後に

SDNを構築することで、複雑さは分厚いハードウェアマニュアルから、管理しやすいPythonコードへと移行します。この変化により、カスタムファイアウォールや自動化されたネットワークスライシングなどの機能を、数週間ではなく数時間でプロトタイプ化できるようになります。フローベースのロジックを学ぶ必要はありますが、その柔軟性は努力に見合う価値があります。インフラを自動化したいのであれば、これら3つのツールは最適な出発点となるでしょう。

Share: