Telnetでネットワーク機器を操作するときのBCIP - Ruby編

f:id:yuyarin:20161224171645p:plain:w480

こんにちは、ゆやりんです。今年のNetOpsCoding Advent Calendarもいよいよ最終日になりました。たくさんの役に立つ&頭のおかしい投稿ありがとうございます。お仕事が忙しかったので最終日を選びました。オオトリにふさわしい記事はなんだろうかと考えていましたが、やっぱりTelnetについて書こうと思います!!

はじめに

ネットワーク機器を操作する手段としてNETCONFやREST APIなどが登場する中、ネットワークの要件や制約からそういった機能を使うことができず、未だにTelnetSSHでコンフィグを行わないといけない場合ということがあります。え?どの機器かって?昨日のtaijijijiくんの記事を読んでみて下さい。で、そういった状況でどのようにこのTelnetを利用したクライアントを実装するとよいのか、私のRubyでの実装の最善策(Best Current Implementational Practices)を紹介したいと思います。

大変申し訳無いことにコードを全部公開できないのですが、なるべくエッセンシャルな部分は共有できればと思います。いずれプロジェクトの全貌も公開できればなぁあと思います。

アーキテクチャ

Telnetクライアント周りの全体像はこんな感じなります。

f:id:yuyarin:20161224174349p:plain:w480

Telnetクライアントの実装としては王道のNet::Telnetを使って、プロキシとしてrancidのxloginを使うのがベストでした。

それをラップする機器ごとのTelnetクライアントを作り、コマンドを投げたら必要なレスポンスだけが返ってくるようにします。

その機能を使って、各機器のCLI相当の機能を提供するのが「機器固有レイヤーのClient」です。例えばインターフェイスにデスクリプションを設定するなどの具体的なメソッドを提供います。

その上位アプリケーションとして「ビジネスロジックレイヤーのClient」が存在します。これら2つの違いは、同じインターフェイスにデスクリプションを設定するというタスクについて、機器固有レイヤーのクライアントがルータのインターフェイス名を取るのに対し、ビジネスロジックレイヤーのClientはお客様の回線IDなどを引数に取ることです。ビジネスロジックレイヤーのClientでは与えられたIDから機器固有のIDに変換する役割を持ちます。また、複数の機器がある時に共通のインターフェイスを提供することでさらに上位のアプリケーションに対して抽象化された機能を提供します。例えばLAGを作るという機能は、CiscoではPort-Channelを作ることですし、Juniperではaeインターフェイスを作ることですし、Brocade(Ironware)ではLAGオブジェクトを作ることで、それぞれ手順が異なります。これらの差分を吸収します。

ここで作られたクライアントはビジネスロジックレイヤーのワークフローの中から呼び出されます。

Net::Telnet + xlogin

チームメイトの goto_infamouse 君が検証をしてくれているので、あわせてそちらの記事もお読み下さい。

Net::Telnet + xlogin の特徴

Telnet経由で機器を操作する方法として他にpty + IO#expectや、ruby_expectなども検討しましたが、実装と比較を行った結果Net::Telnet + xloginが一番扱いやすいと判断しました。

rancidに付属するxloginはexpectのスクリプト群で、Cisco IOS用のcloginやFoundary用のfloginなど、数十種類の機器に対するログインスクリプトが用意されています。一からexpectのスクリプトを書くよりは、これらを利用したほうが効率が良いです。

使っている機器はFoundaryやCiscoそのものではないので、スクリプトはfloginやcloginを改造してhogeloginを機器ごとに作って別リポジトリで管理しています。

xloginの良いところはコードにパスワードを書く必要が無いところです。実行者の ~/.cloginrc に書く必要があるので、パスワードを誤ってGitリポジトリに突っ込んでしまうみたいなミスも起きません。

Net::Telnet + xlogin の異常系の処理

商用ネットワーク環境で動かすシステムを作るときに一番重要になるのが異常系の処理です。想定される色々なエラーに対してNet::Telnetの動作を検証しました。たとえば、xloginスクリプトが無い、パーミッションが違う、異常終了する、ホスト名が引けない、機器へのIP接続性がない、ACLで接続拒否される、パスワードが間違っている、などなど、色々なパターンです。だいたいは Errno::EPIPENet::ReadTimeout で処理できますが、以下の3パターンは、正しく処理しないとルータのvtyを専有し続けるという点で、気をつけなければならないエラーでした。

間違ったenableパスワード

.cloginrc に記載されているパスワードが間違っていた場合、xloginのタイムアウト秒後に Net::ReadTimeout の例外が発生しますが、正しく処理しないとxloginの親プロセスがinitの子プロセスに移って、ルータのvtyを専有し続けます。 Net::ReadTimeout を補足した時は即座に xlogin のプロセスを kill する必要があります。

ネットワークの切断でtelnetセッションが切れる

途中でネットワーク機器への接続が切れる場合です。さきほどのケースと同様に Net::ReadTimeout が発生しますが、 xlogin を kill してもネットワーク接続性が無いため、ルータの vty は専有されたままになり、ルータ側の telnet-timeout を待つことになります。接続性が切れてもすぐに回復するような場合や、ACLやHWが原因でそのセッションだけ停止される場合に問題になります。今のところ対処方法はありません。

Matchで指定したプロンプトが来なかった

Net::Telnetではプロンプトのマッチングを行うことができます。期待したプロンプトが来なかった場合に「間違ったenableパスワード」の場合と同様の動作が発生します。

A::Telnet

A::Telnet の役割

Net::Telnetのラッパーを各機器ごとに作っています。ルータAのライブラリをA::Telnetとしましょう。このラッパーの主な目的は「コマンドを投入して、表示結果を得る」という機能を上位アプリケーションに提供することです。更に挙げれば次のような機能を提供しています

  • Net::Telnetの例外を適切に処理する
  • ログイン関連の機器固有のエラーを処理する
  • コマンドに対する出力だけを返す
  • コマンド実行後のプロンプトの判定を可能にする
  • 機器の操作ログの保存

A::Telnet のコマンド処理

例えば show clock というコマンドを実行すると時刻が表示されて、次のプロンプトが出る表示が出るような場合、Net::Telnetでcmdメソッドを実行すると次のような値が帰ってきます。

puts telnet.cmd('show clock')
=> "show clock\n22:02:20.947 JST Sat Dec 24 2016\n\e]1hostname\ahostname"

入力したコマンドそのものがエコーされ、実際に欲しい表示が出力され、その次のプロンプト(しかもターミナルのタイトルを変更するエスケープシーケンスが入っていることがある)が含まれる3行の文字列が返ってきます。これらを処理して本来欲しいコマンドの出力結果だけを上位アプリケーションに返します。

この時に次のプロンプトを得ることができるので、保存しておくとプロンプトの判定に利用することができます。Telnet経由の操作では正しい階層に入ったかどうかプロンプトで確認する必要があります。例えば global レベルと interface レベルの両方で実行可能なコマンドがあったときに、interfaceレベルでコマンドを実行しようとしたのに interface GigabitEthernet 1/1 が失敗して global レベルで投入してしまった、というようなことが起こりうるからです(そういうCLI設計もやめてほしいですが)。

あるネットワークに対する実装は次のようになっています。

    def execute_cmd(opt)
      result = @connection.cmd(opt) # @connectionはNet::Telnetのインスタンスです
      response = result.split($/)
      # 先頭行は入力したコマンドなので除外
      command = response.shift
      # 最終行はコマンド入力後のプロンプトなので結果から除外
      next_prompt = response.pop
      _log(response.join($/))
      @current_prompt = next_prompt
      return response.join($/)
    end

A::Telnet の接続開始部分

Net::Telnetでxloginを使った時のやっかいな部分は、デバッグが非常にやりづらいということです。何が原因でどこで死んでいるのかぱっと見分からないからです。さらにプロセスが残りる続けたりルータのvtyを食ったままになっていたりと大変でした。接続を開始するメソッドはこんな感じです。Proxy オプションに xlogin を IO.popen で作ったオブジェクトを渡してあげることで、ログイン処理をrancidに丸投げすることができます。プロセス終了時や異常終了時に xlogin を kill することに備えて、pidを保存しておきます。

    def open_connection
      proxy_command = "#{@xlogin} #{@hostname}"
      prompt = /^telnet@#{@hostname}(>|.*#)/
      prompt_enable = /^telnet@#{@hostname}#/
      timeout = @timeout
      wait_time = 0

      begin
        @proxy = IO.popen(proxy_command, 'r+')
        @proxy_pid = @proxy.pid
        _log("proxy opened #{proxy_command} with pid #{@proxy_pid}")
        @connection = Net::Telnet.new({
          'Prompt'     => prompt,
          'Timeout'    => timeout,
          'Waittime'   => wait_time,
          'Binmode'    => false,
          'Telnetmode' => false,
          'Proxy'      => @proxy,
        })
        _log("created Net::Telnet instance")

        r = execute_waitfor(prompt_enable)
        # 1. proxyコマンドが異常な挙動を起こしている場合
        unless r
          error_message = "unexpected behavior of proxy command"
          _log(error_message)
          raise Errno::EPIPE, error_message
        end
        # 1. Error: Couldn't login
        #   a. ホスト名が解決できない場合(Name or service not known)
        #   b. ネットワーク接続性はあるがホストに接続できない場合(No route to host)
        # 2. Error: Connection Refused
        #   a. telnetのACLでdenyされている(Connection refused)
        # 3. Error: Connection closed
        #   a. ログインパスワードが間違っている
        if r =~ /Error:/
          raise A::LoginError, r
        end
        # 2回waitforするとうまくいくというおまじない
        r = execute_waitfor(prompt_enable)

        _log("login completed")
        _log(@current_prompt)

        cmd("terminal length 0")

      # Errno::ENOENT
      #   proxyコマンドが存在しない場合
      rescue Errno::ENOENT => e
        raise e
      # Errno::EACCES
      #   proxyコマンドの実行権限がない場合
      rescue Errno::EACCES => e
        raise e
      # Errno::EPIPE
      #   proxyコマンドとのパイプが確立できない場合
      #   接続エラーなのでコネクションを閉じる必要が無い
      rescue Errno::EPIPE => e
        raise e
      # Net::ReadTimeout
      #   enableパスワードの不一致
      #   ネットワーク接続性の喪失
      # X::LoginError
      #   17行上で投げてる
      #
      # 接続した状態でのexpectのエラーなのでコネクションを閉じる
      rescue => e
        close_connection
        raise e
      end
   end

このメソッドはpriavteで、コンストラクタから呼ばれます。

「2回waitforするとうまくいくというおまじない」という部分に苦労を感じ取って頂けるとうれしいです。。。

A::Telnet のコマンド実行部分

publicメソッドとして外部に出しているのがこのcmdメソッドです。コマンドを受け取って、その表示結果だけを返す機能を提供しています。当初はここでプロンプト判定も行っていましたが、current_promptメソッドを提供して、判定は上位アプリケーションに任せる設計に変更しました。

    def cmd(command)
      result = ''
      begin
        @current_command = command
        _log("#{@current_prompt}#{command}")
        result = execute_cmd(command)
      rescue Errno::EPIPE => e
        raise e
      rescue => e
        close_connection
        raise e
      end

      return result
    end

A::Telnet の接続終了部分

接続終了時には SIGHUP で xlogin のプロセスを殺します。先にexitなどのコマンドでtelnetセッションから抜けている場合には xlogin のプロセスが終了しているためはkillを行おうとしても IOError が発生します。この例外はここで無視します。

    def close_connection
      return unless @connection
      _log("closing connection...")
      begin
        unless @proxy_pid==0
          _log("sending SIGHUP to proxy with pid #{@proxy_pid}...")
          # telnetなのでSIGHUPで適切に接続を終了してくれることを期待する
          Process.kill(:HUP, @proxy_pid)
        end
      # Errno::ESRCH
      #   プロセスが存在しない場合
      rescue Errno::ESRCH => e
        _log("process with pid #{@proxy_pid} not found")
        # NOP
      end
      begin
        @connection.close
      # IOError
      #   @connectionがcloseされている場合
      #   @proxyをkillしているので必ず発生するはず
      rescue IOError => e
        # NOP
      end
      _log("connection closed")
      @connection = nil
      @proxy_pid = 0
    end

いまや安全にこのクライアントを使って機器操作できているのはチームメイトの goto_infamouse 君がNet::Telnetの動作検証をしっかりやってくれたおかげです。

機器固有レイヤの A::Client

機器固有レイヤの A::Client の役割

このA::Clientクラスは機器固有の処理をする部分で一番実装が膨らむ箇所です。具体的にはCLIコマンドに相当するメソッドを提供して、その結果を構造化されたデータとして返します。

例えば Cisco IOSshow interface summary コマンドを例にすると、次のような利用を想定しています。

c = CiscoIOS::Client.new(hostname, options)
c.show_interface_summary
=> [{
  'Interface' => 'FastEthernet0',
  'Up' => true,
  'IHQ' => 0,
  ...
},{
  'Interface' => 'FastEthernet1',
  'Up' => false,
  'IHQ' => 0,
  ...
},
...
]

また、各種コマンドを実行したときのエラーハンドルやプロンプトのチェックもこのクラスで実施します。

機器固有レイヤの A::Client の構成

こんな感じの仕組みになっています。

module A
  class Client

    include ::A::CliParser

    def initialize(hostname, options = {})
      @hostname = hostname
      @telnet = start_client(hostname, options)
    end

    def close
      stop_client
    end

    private

    def start_client(hostname, options)
      A::Telnet.new(hostname, options)
    end

    def stop_client
      @telnet.close
    end

    def exec_cmd(command)
      @telnet.cmd(command)
    end

    public

    def show_interface(interface_names)
      interface_names = validate_interface_names(interface_names)
      res = exec_cmd("show interface #{interface_names}")
      parse_show_interface(res)
    end
  end

  module CliParser
    def parse_show_interface(res)
      # CLIの結果をパースして構造化して返す
    end
  end
end

CLIのパーサはテストをしやすくするためにモジュールとして切り出してあります。start_clientやexec_cmdなどもメソッドとして切り出しているのは、テストのときにモックのメソッドに差し替えられるようにするためです。

機器固有レイヤの A::Client のCLIパーサのテスト

このクラスでは愚直にCLIコマンドに相当するメソッドとそのパーサを実装するだけなので、それほど悩む点はありませんが、ひとつあるとするならばCLIパーサのテストです。

CLIの表示結果から、得られるHashオブジェクトが一意に定まるはずなので、テストケースとして入力をテキストで、出力をYAMLで用意しておきます。

# spec/fixtures/ios/15.0/show_interface_summary/mixed-interfaces.txt

 *: interface is up
 IHQ: pkts in input hold queue     IQD: pkts dropped from input queue
 OHQ: pkts in output hold queue    OQD: pkts dropped from output queue
 RXBS: rx rate (bits/sec)          RXPS: rx rate (pkts/sec)
 TXBS: tx rate (bits/sec)          TXPS: tx rate (pkts/sec)
 TRTL: throttle count

  Interface                   IHQ       IQD       OHQ       OQD      RXBS      RXPS      TXBS      TXPS      TRTL
-----------------------------------------------------------------------------------------------------------------
* FastEthernet0                 0         0         0       130    194000        50    195000        52         0
  FastEthernet1                 0         0         0         0         0         0         0         0         0
# spec/fixtures/ios/15.0/show_interface_summary/mixed-interfaces.yml
- Interface: "FastEthernet0"
  Up: true
  IHQ: 0
  ...
- Interface: "FastEthernet1"
  Up: false
  IHQ: 0
  ...

テストはRSpecを使って次のように書いています。

# spec/ios/cli_parser_spec.rb 
def path_of_example(version, command, case_name)
  command = command.tr(' ', '_')
  "spec/fixtures/ios/#{version}/#{command}/#{case_name}"
end

def load_input(version, command, case_name)
  File.read("#{path_of_example(version, command, case_name)}.txt")
end

def load_expected_output(version, command, case_name)
  YAML.load_file("#{path_of_example(version, command, case_name)}.yml")
end

def get_method_name(command)
  command = command.tr(' ', '_').tr('-', '_')
  "parse_#{command}"
end

def exec_parse(parser, version, command, case_name)
  input = load_input(version, command, case_name)
  method_name = get_method_name(command)
  parser.method(method_name).call(input)
end

RSpec.shared_examples "interface mixed-interfaces" do
  it "parses normal output" do
    case_name = 'mixed-interfaces'
    expected = load_expected_output(version, command, case_name)
    expect(exec_parse(parser, version, command, case_name)).to eq(expected)
  end
end

RSpec.shared_examples "interface error-invalid-slot-number" do
  it "raises error for invalid slot number" do
    case_number = 'error-invalid-slot-number'
    expect{ exec_parse(parser, version, command, case_number) }.to raise_error(A::InvalidInputError, 'Invalid input -> 100/1')
  end
end


RSpec.describe A::CliParser do

  let(:test_class) { Struct.new(:parser) { include A::CliParser } }
  let(:parser) { test_class.new }

  context "with IOS Version 12.0" do
    let(:version) { '12.0' }

    describe "parse show interface summary" do
      let(:command) { 'show interface summary' }
      include_examples "interface mixed-interfaces"
      include_examples "interface error-invalid-slot-number"
      ...
    end
    describe "parse show ip ospf" do
    ...
  end

  context "with IOS Version 15.0" do
    let(:version) { '15.0' }
    ...
  end

  ...
end

工夫したポイントとしては、バージョンが複数あり、コマンドが複数あり、ケースが複数あるので、それぞれに識別子をつけてそれを指定すればテストができるようにしたところです。ここでいうケースは「インターフェイス名に無効な値を指定した場合」とか「10Gの正しく設定されたインターフェイスの場合」とかです。

まず、CLIParserをモジュールとして分離しているので、let(:test_class) { Struct.new(:parser) { include A::CliParser } }let(:parser) { test_class.new } でパーサの機能だけを持った試験用のクラスを作成します。

contextlet(:version) { '12.0' } を使ってOSのバージョンを指定し、describelet(:command) { 'show interface summary' } で対象のコマンドを指定します。

ケースは RSpec.shared_examples を使ってバージョンやコマンドを通して共通化しておきます。これを include_examples case_name で呼び出します。

バージョン、コマンド、ケース名の3つがわかると、入力ファイルのパス、出力ファイルのパス、CLIパーサのメソッド名が自動的にわかるように命名規則を定めています。

shared_example の中では、入力のテキストを読み込み、パーサに食わせて、出力のYAMLと比較する、ということをやっています。

このようにテストを書いておけば、少なくとも既知のコマンド出力に対しての動作を保証できるのでとても安心です。

ビジネスロジックレイヤのClient

ビジネスロジックレイヤでは次のことを行うつもりです。というのもまだしっかりと実装ができていなくて、この設計でいいのかわからないからです。

たとえばVLANを変更するといった処理をするときに、機器Aではインターフェイスをshutdownする必要があるが、機器Bでは必要ない、といった場合に、これらのワークフローの違いをどう扱えばいいのかという課題に直面します。上位のワークフローで扱うのか、このクラスでワークフローを隠蔽したメソッドを提供するのか、難しいです。単にやるだけならこのレイヤーで吸収してしまえばいいのですが、例えば「Web画面にshutdownされることの警告を出したい」となってしまうと困ります(実際にはshutdownされないのに警告を出して「shutdownされる可能性があります」みたいに言う手もありますが)。

このあたりに関しては試行錯誤している最中なので、もう少し知見が溜まってきたら記事を書いてみたいですし、もしも経験のある方がいらっしゃれば記事を書いてほしいです。

さいごに

今年のクリスマスイブはTelnetの記事を書いて過ごしました。

Merry Christmas and Happy NetOpsCoding!!

保育園を増やすか移民を受け入れるかどっちがいい?

あんまりプライベートなこととか政治的な意見は公に書きたくないんだけど、 id:gomi-box から待機児童ゼロチャレンジバトン(#taikijidou0challenge)なるものが回ってきたので書きます。

gomi-box.hatenablog.com

時間が無い人のためのまとめ

タイトルはちょっと飛躍してますが本質は結局はそういうことなんじゃないのかなーと思う。

最初にまとめておくとこの記事にはこういう話が書いてある:

  • 待機児童を減らすことはあくまで手段で、目的は日本の人口を増やすことだよ
  • 日本の人口を増やそうと思うと3人は産まないとだめだよ
  • 働きたいから保育園に預けるってのはワガママだよ
  • でも今の世論や政策のようにワガママを聞いてもらえない状況だと、みんな子供は多くても1人で産むの止めちゃうよ
  • 日本政府がやるべきことは国民個人がワガママ=自己利益の最大化に走ろうとした時に日本の人口を増やせるような方向へ誘導する政策を実行することだよ
  • それができないと年金は破綻するし、経済を維持するために移民を受け入れないといけなくなるよ
  • ちなみに移民も子供を産んで保育園に預けようとするから、日本人は更に保育園に預けられなくなって子供を産む数が減るよ
  • みんなそれでも平気?

そもそも日本の人口を増やさなくていいとか移民大歓迎って人にとっては、この記事は価値がありませんので、ここで読むのを止めていただくことを推奨する。

ここから国に対する要求として次の3点を主張する。

  • 子供を3人産める環境を作れ
  • 育児と仕事の選択をさせるな、両立させろ
  • 保育を親ではなく国の義務・責任にしろ

ここでは「保育園」を0歳から小学校入学までの保育施設をひっくるめた意味で使いたいので幼稚園の延長保育や認定こども園なども含めたいと思う。

最低3人産まないと日本の人口は増えない

女性1人当たり2.08人産まないと日本の人口は維持できないらしい。結婚しない女性もいるだとうし、子供を産まない夫婦もいるので、産む気のある夫婦では3人は少なくとも産まないと日本の人口を維持できないし増やすこともできない思う。なので3人産む前提で話を進めたいと思う。

子供が1人である程度活動できるようになるのを小学校1年生だとすると、1人産んだ時につきっきりで子供の面倒を見ないといけなくなるのは0歳から6歳頃までの6年間になる。3人産むとなると最低でも8年はかかる。法的な育児休業は1歳6ヶ月までだし企業規定の育児休業も3歳までのところが多く、年子で3人とか無理なので実際は間隔を空けて10年近くかかることになる。

また共働きしないと家計が成り立たない世帯は保育園に預けざるを得ないので、彼らの話はしない。共働きしなくても家計は成り立つけど働きたいから預けたいという世帯を対象にする。

それから、3歳までは親が育てたほうがいい等の育児論や教育論もここでは扱わない。

男女平等と自己実現を尊重する価値観

昔は男は外で稼ぎ女は家の面倒を見ろという価値観だったかもしれないけど、現代は違う。男女平等が叫ばれ女性の社会進出が叫ばれ、それが実現できてきているように思う。

男女関係なく個人の自己実現を尊重する風潮が少なくとも若い世代ではあるんじゃないかなと思う。いわゆるやりたいことをやってなりたい自分になるってやつだ。

僕も妻には働いて欲しいというか、自分のやりたいことをやってほしいと思う。だから育児の負担は半分ずつにしたいと思っている。

なのでここでは男が女がどっちがという話はしない。

女性の権利等の話はちょうど今日素晴らしい記事ホッテントリ入りしていましたので、そちらを読むことをお勧めします。

働き盛りに仕事を長期間休まないといけないのは辛い

3人の子供を育てるとなると、2人で負担を分担しても5年、片方だけが負担するなら10年を乳幼児の育児に専念することになる。

5年とか10年の休業というのは20歳で就職して60歳で定年したときに就労できる40年の間のそれぞれ12%、25%に相当する。

しかもただの5年、10年ではない。大半の人は20代後半から30代の一番仕事がノってきて頑張らないといけない期間に休業することになる。復帰する頃には30代後半や40代に突入している可能性が高い。

その期間働けないということはこの年功序列の日本社会においては残念ながら出世に大きく影響がある。出世を望まなくとも10年のハンデは非常に大きい。

転職をしようと思ってもただでさえ雇用の流動性の低い日本でブランクを抱えて40歳前後で転職というのもなかなか能力と経験がずば抜けていないと難しい話だ。

だから活躍したいのに保育園に預けられなくて日本死ねって言いたくなる気持ちは痛いほどよく分かる。

働きたいから子供を保育園にあずけるのは確かにワガママです

そうした自己実現のために子供を保育園に預けたいという願望は確かにワガママだと言える。それは間違いない。

ただ、そういうワガママな人たちに国のせいにすんなとかワガママだとか自己責任だとか努力が足りないとか地方に引っ越せとか言っても何の意味もない。むしろ逆効果だ。子供を産んだのに日本の社会はなんて酷い仕打ちをするんだと思ってしまう。

そもそも共働きしないと行きていけない人たちしか保育園に預けてはいけないなんていう決まりはない。会見で化粧している余裕があるとかブランド物の抱っこ紐だとかいう批判は筋違いも甚だしいので取り合う必要はないだろう。

保育園問題に対する批判は出産を萎縮する

子供を産んでも自己責任といわれ何かを要求するとワガママだと叩かれる風潮が出来上がってしまうと、子供を産みたいと思う人は減ってしまう。

今の御時世結婚は損だから独身でいようとか子供を産んだら自分たちの時間や使えるお金が少なくなるから産まないでおこうみたいな主張が少なくなく聞かれる状況なのだ。

その上更に、子供を産んだうえで自分の生きたいように生きようとすると社会から叩かれるような空気を膨らませるとますます少子化が加速する。

レベル0の勇者をパーティーに加えて稼いだゴールドを勇者の装備につぎ込みスライム狩りを始めるのだ。2人で海外旅行に行くのもおしゃれなレストランにいくのも無理どころか外食すら厳しい。親達の自己実現など程遠くなってしまう。

個人の最適解は子供を産まないことになる

そうなってくると彼らの個人としての最適解としては子供を産まないということになる。

これは結婚しない・子供を産まないのがお得という風潮にも既に現れている。

子供のために家賃の安い郊外に引っ越して長時間かけて通勤し、趣味も我慢して節制に励む。そんなことするぐらいだったら子供なんてイラネって考える人達は今後ますます増えてくるんじゃないかなと思う。

この発想は特に自己実現欲の強うそうな高所得者層やインテリ層に多いんじゃないかなと思う。そうだとすると後は言わずもがな。

こんな風にもし個人が自己実現のために局所最適化を行うこと自体が嫌なのであれば「1億総活躍」みたいなことは言わないでおいて欲しい。

産んだとしても1人で終わり

もう一度言うが、こういう人たちに「ワガママ言うな、死ね」といっても何の意味もないのだ。「じゃあもう子供産むのやめよー」ってなるだけなのだ。

1人産んで保育園に入れられず、それに文句を言ったら袋叩きに遭う、こんな仕打ちなんだったら2人目を産みたいと思えるか、というより1人目の育児で精一杯で現実的に産めなくなるよね。そんな苦渋を舐めた人たちが2人目を産もうと思わないよね。

それに1人目を仮に保育園に入れられたとしても2人目も保育園に入れられる保証はないのだから(優先度は上がる)、保育園に1人目を預けられたとしても不安は拭えない。

自分たちの遺伝子を残したいという目的であれば子供は1人で十分なのだ。

国がやるべきことは「囚人のジレンマ」の解消

個々が自分の利益を最大化しようとした結果、系全体にとって望ましくない結果になることをゲーム理論の用語で「囚人のジレンマ」と言う。

今のままでは個人が自己実現のために人生を最適化すると子供を産まない選択肢をとるようになり少子化が進行する。これは国家にとって望ましい結果ではないと思う。

そうならないように、1億総活躍の自己実現を求めて国民が個人の人生の最適化に走る時に、国家として利益を最大化できるような仕組みを作ることが政治家の仕事であると思う。保育園を作らない理由をたらたら述べるだけの人はいらない。

もし出生率が増えることが国家として望ましい結果なのであれば、子供を産めば産むほどお得になる制度を整備しないといけない。例えば環境問題のために「エコカー減税」でやったように。出生で言うなら政策ではないが一企業であるソフトバンクがやっているように。

僕は保育園問題の専門家ではないので、保育士の給料がどうとか騒音問題がどうとか独身税の是非とか不妊の人への配慮といった個々の戦術について提案・議論するだけの知識を持ち合わせていないので、そのへんの議論は専門家にお任せするが、日本国家の戦略としてこの方向性は間違っていないと思う。

保育園が増やせないなら移民を受け入れることになる

3人産まないといけないのに産む気のある人が1人で産むのを止めてしまう社会になると当然人口は減少の一途をたどる。そうすると経済を支える生産年齢層がむちゃくちゃ減る

そうすると年金システムが破綻するのは容易に想像がつくだろう。

そうならないよう生産年齢層を増やすためには外国から人を招かざるを得ない。つまり移民の受け入れだ。

日本の風土や文化を鑑みると、移民受け入れは合わないと思う。受け入れたとしても慣習や文化の違いから様々な場所で軋轢を生み、日本人のストレスはマッハだろう。すでにそのようなことが起きていることは度々マスメディアで取り上げられていると思う。今後それが加速すると、今は関係ないと思っている人も巻き込まれることになるだろう。

保育はもはや福祉ではない

生存のためには必須ではないが最低限の幸福の実現のために行われるのが福祉であるならば、もはや保育は福祉だと思ってはいけない。シラク三原則のブコメでも指摘があるように国家の義務として保育を扱い、家族構成や両親の生き方や経済状況に関係なく産まれてくる全ての子供を育て上げる仕組みを作れないのであればこの国に未来はない。

保育園問題は20年先の種まきだ

当たり前の話だが生まれたての赤ん坊が成人するまで20年かかる。

つまりこの保育園問題は20年先への投資であることを理解して欲しい。

逆に言うならば、問題が生じてから対応しようとしても効果が現れるまでに20年かかるということだ。問題に気づいた時には既に手遅れになっている可能性が高い。

そうならないためにも今このタイミングで何かしらの対策は講じなければならない。

もしかしたらもう遅いのかもしれないけどね。

誰が声を上げるのか?

で、この問題に誰が声を上げるのだろうか?

保育園に受かった親たちは通知が届くまでは皆同じ気持で元ネタの記事を読んでいたに違いないと思う。1つ歯車が狂っていたら自分も日本死ねという側になっていたのだ。

でも保育園に受かったら受かったで今度はそっちに集中しなきゃいけなくなる。彼らとしては政治活動をする余裕があるなら子供にリソースをつぎ込みたいのだ。送り迎えがあるから早く起きて家をでなければならない。デモする余裕があるなら少しでも睡眠時間に充てて体力の回復に努めたいというのが本音だろう。

そうなると声を上げるのは落ちてしまった親たちになる。彼らも3歳児クラスで保育園に預けられるようになるかもしれないし、小学校に入るようになったら過去の話になってしまう(今度は学童問題が待っているんだけどね)。

すでに指摘があるように継続してこの保育園問題を政治に訴える人はいないのだ。

保育園問題は国家の生存戦略の問題です

保育園に預けられる預けられないとかで困ってる人が声を上げるべき問題では無いんです。

国家として次の世代をどう繋いでいくかという問題だと思うんです。

家族構成や両親の生き方や経済状況に関係なく、産まれてくる全ての子供を育て上げられる仕組みを国家として作り上げて、全ての夫婦が安心して子供を産めるようにして下さい。

政治家の皆様、そこんとこよろしく。

FAQ

Q. 家から働けばよくね?

A. 子供の面倒見ながら仕事とか無理

Q. 保育園じゃなくて親に預ければよくね?

A. 自己実現のために親の自己実現を犠牲にするとか鬼畜。子育て終わったと思ったら孫育てとか地獄っしょ。

あとは思いついたら足していく

コンテンツプロバイダこそがIPv6に対応しておいた方がいい1つの理由

要約:NATによるIPv4アドレス枯渇対策には限界があり、Webサービスのパフォーマンスの低下を引き起こす可能性があるので、自衛のためにもIPv6に対応しておいたほうが良い

コンテンツプロバイダからするとIPv6に対応してもコストがかかるだけで売上は上がらず、対応する意味のないもので、私もそのように感じていたのですが、色々なカンファレンスネットワークでCGNやIPv4-IPv6移行・共存技術の検証を行っていた結果、ISP側でできる努力の限界が見えてきて、コンテンツプロバイダがさっさとIPv6に対応したほうがインターネット的にもコンテンツプロバイダ自身も幸せになるんじゃないかなと、いちネットワークエンジニア&ソフトウェアエンジニアとして感じたのでこの記事を書きました。

IPv4アドレスの枯渇

IPv4アドレスは枯渇しました。2015年12月時点で各RIR(地域インターネットレジストリ)の/8のアドレス保有数は次のとおりです(IPv4枯渇時計より)。

地域 RIR 残り/8数
アフリカ AfriNIC 2.14
アジア・パシフィック APNIC 0.64
北米・中米 ARIN 0
南米 LACNIC 0.4
欧州・中東 RIPE NCC 1.01

日本を管轄しているAPNICでも既に最後の/8に手を付けており、IPv4アドレスの通常割り振りは終了しています。現在では最大でも/22(1024アドレス)の割り振りしか行われません。

NATによるIPv4アドレス枯渇対策の仕組み

おもに一般家庭向けISPの視点で話を進めます。

NAT(正確にはNAPTですがこの記事ではNATとします)を行うことでIPv4アドレスを節約することができます。通常のISPのユーザだと宅内ではプライベートIPv4アドレスを利用しており、ルータで割り当てられた1つのグローバルIPv4アドレスにNATすることでインターネットへアクセスすることができます。

通常は通信ホストの識別を32-bitのIPv4アドレスだけで行うのですが、NATではこれに加えて16-bitのTCP/UDPのポート番号を使用することで、同じIPv4アドレスを持っていても通信ホストの識別を行うことを可能にしています。これにより通信の識別に使えるbit数はAddress 32bit + Port 16bit の 48bit になります。この考え方はA+Pと呼ばれます。

f:id:yuyarin:20151211002201p:plain:w480

しかしながらアドレス枯渇により各家庭にそれぞれグローバルIPv4アドレスを割り当てることも難しくなってきました。そこで登場するのがCGN(NAT444), DS-Lite, MAP-EなどのIPv4枯渇対策技術です。

これらの技術の詳細は私の過去の講演資料をご覧頂くとして、共通する基本的なコンセプトは 複数のご家庭をまとめて1つのIPv4アドレスにNATする」 ということです。つまり同じグローバルIPv4アドレスを複数の家庭で共有することになります(ここでは「家庭」を加入者=subscriberの意味で使用しています)。特にCGN(Career Grade NAT)では各家庭のルータでNATした後に、ISPでNATをする多段NATになります。

f:id:yuyarin:20151211002204p:plain:w480

これらの技術は全てNATを利用しています。つまり 各家庭の識別子にポート番号を利用します。この時ISPは各家庭に何ポート割り当てるかを設計する必要があります。仮に256ポートとしましょう。すると1つのグローバルIPv4アドレスに対して65536/256 = 256の家庭を収容することができます。このようにすることでISPはアドレスの利用効率を256倍にすることができます。

これ以降の文脈ではNATは主にISPによって提供され複数の家庭を1つのIPv4グローバルアドレスに収容するNAT(狭義のCGN, DS-Lite, MAP-E, NAT64等の技術のNAT機能)を指すことにします。

NATの2つのパラーメータにまつわる問題

ポート割当数

さて、ここで問題になるのはISPは1家庭あたり何ポートを割り当てるべきかということです。総務省のレポートVol.1, Vol.2によると端末当たり平均50ポート、最大500ポートぐらいのポート消費があるため、設計値としては1000ポートを推奨と書かれています。この推奨値に従って1家庭あたり1024ポートを割り当てた場合、アドレスの利用効率は65536/1024 = 64倍になります。この値は大きいでしょうか?小さいでしょうか?

また、家庭という単位も問題になります。一人暮らしの家庭とビッグダディみたいな方の家庭では同じ1回線でも接続する端末の数が異なります。端末が多いご家庭では1024ポートの割当でも足りないかもしれません。

ポートが足りなくなるとどのようになるかというと、少し古い資料ですがNTT Comの宮川さんの発表資料Google Mapsが穴あきになってしまう例が示されています。

ポート割当数を多くすれば通信不能になるセッションを減らすことはできますし、ISPとしても通信の劣化に繋がるようなことはしたくありませんから、なるべくポート数を多く割り当てたいです。しかし、もともとアドレスが足りなくてこのようなことをやっているので、あまり大きな値を設定することはできませんし、アドレスが足りなくなってくるとやむを得ずポート割当数を減らす事になるかもしれません。そうなった時には 空いているポートがなくて通信ができないセッションが発生するかもしれません。

まとめると、 ポート割当数を多くすればアドレスの利用効率が落ちてアドレス枯渇が早くなり、少なくすれば通信できないセッションの発生確率が上がるというトレードオフが存在します。

タイムアウト

Skypeや対戦ゲームなどのP2P通信を可能にするためには通信が終わった後も一定時間NATのセッションテーブルを持ち続けなければなりません。これをどれぐらいの時間保持するのかがタイムアウト値です。

タイムアウト値を大きくすればポートの利用効率が悪くなり、早くポート割当数の上限に達してしまい、通信できないセッションが発生する可能性が高くなります。短くすればポートを効率よく使うことができますが、P2P通信に失敗する可能性がありますし、その他にも次のような弊害があります。

Keepaliveをしているアプリケーションが利用できなくなる:制御用のセッションを張りっぱなしにして一定の間隔でKeepaliveを送るようなアプリがあります。例えばGoogle Hangoutは15分間隔でKeepaliveを送ります。このため、TCPタイムアウトが15分以下になっているとこのアプリケーションは利用できないということになります。これを回避するためには十分長いタイムアウト値を設定する必要があります。他にもAndroidiPhoneなどモバイル端末のKeepaliveもあります。一部のNAT機器にはポート番号ごとにタイムアウト値を設定できるものもありますが、全てのISPが世の中に存在する全てのアプリケーションに対してカスタマイズされた十分なタイムアウト値を設定することは難しいでしょう。

NAT機器でのポートの再利用周期とサーバ側のタイムアウト値の関係:NAT機器で空いているポートをどのように使用していくかは実装依存です。TCPのFINが来ていないポートの再利用禁止期間はRFC6888で2分間と決められていますが、FINが来て一度開放されたポートがすぐに再利用されてしまって、しかも同じ宛先サーバの同じポートに通信しようとした場合、 サーバ側のソケットがまだ閉じていなくて通信できない ということが起きる可能性があります。ISP側とサーバ側とのタイムアウト設定値の相性問題ですので、これを回避するためには世の中の全てのサーバに対して十分だと思われる時間ポートの再利用を行わないようにする必要があります。しかしあるISPが全てのコンテンツのサーバに対して問題が起きない十分なタイムアウト値を設定することは不可能ですし、またコンテンツプロバイダもNATを使用する全てのISPに対して正常に動作するサービスを提供することも難しいでしょう。

NATの限界

全てのアプリケーションを快適に動作させるにはNATのタイムアウト値を長く設定する必要があります。しかしそうするとポートの消費効率が悪くなるため、1家庭当たりたくさんのポート数を割り当てる必要が出てきます。たくさんのポート数を割り当てると1グローバルIPv4アドレスに収容できる家庭の数が減ってきます。

逆に言うと収容する端末が増えてきたにもかかわらず、IPv4アドレスがこれ以上入手不可能になった場合には、ISPは1家庭あたりのポート割当数を減らしたりタイムアウト値を短くしたりする必要に迫られるわけです。その分サービス品質は低下しますが、IPv4を生かすことに迫られている間は他に選択肢はありません。

この他にも、NATを通した時に特定のWebサイトに対してうまく通信ができずパフォーマンスに問題が出るような事例が見つかっていますが、未だ原因を解明できていません。特に1画面に多量に画像などを表示するサイトでは問題が出る可能性が高いです。

このような状況が発生したに我々が提供するWebサービスは快適に利用できるでしょうか?

もしそのようなISPのユーザに対しても 自分のWebサービスだけに対しては高いパフォーマンスでアクセスできるようにしたいのであれば、IPv4でアクセスさせることを避けるべきです。

IPv6に対応する

ここまで読んでいただいた皆様には なぜGoogleFacebookApple、それにCDN事業者がIPv6対応に本気を出しているのかがお分かりいただけるかと思います。

我々が提供しているWebサービスで画面の読み込みが遅くなったりできなくなったりして、ユーザから遅くて使えないサービスだと思われるのが問題になったり、そのパフォーマンス低下が損益に直結したりするような場合であれば、 ISPのNATに頼るのではなくコンテンツプロバイダ側で根本的な対策が必要になるでしょう。その 自衛方法の1つがIPv6対応になるでしょう。

特に中国、インド、東南アジアなどの地域でサービス提供を目指すコンテンツプロバイダにはIPv4の枯渇は死活問題になります。JANOG35で発表された人口あたりのIPv4アドレス数を考えると日本が一人あたり1.5アドレス以上あるのに対して、中国では0.24、インドでは0.02です。こういった国々に対してサービスを提供したい場合にはこのIPv4アドレス枯渇問題に対して真剣に取り組む必要があります。これらの国々ではIPv6のインターネットが主流になる可能性が十分にあります。

AppleはNAT64環境下で動作しないアプリはAppStoreで承認しないという方針を発表しました。これ自体はAppleがMVNOをIPv6+NAT64で提供しようと考えているからだと推測されますが、これに対応するだけでは不十分です。なぜならNAT64もIPv4へのNATを行う技術ですので、これまで述べてきた問題が同様に発生します。今年10月に総務省から発表された携帯キャリア3社の「モバイルネットワークのIPv6推進に向けた取り組み」も、おそらくこのIPv4 NATの限界問題を認識してのことではないかと思います。

IPv6に一気に対応するコストを払うのはコンテンツプロバイダにとって多大な負担になるので、今のうちから徐々にIPv6に対応することをオススメいたします。単純にフロントエンドのサーバにIPv6の到達性を持たせればいいわけではないですし、自前でネットワークを持っている規模のコンテンツプロバイダであればネットワーク機器もIPv6に対応したものにする必要があり、その検証から導入に時間もコストもかかります。もしあなたが今からサービスを立ち上げるのであればIPv6に対応したクラウドで、IPv4に依存しないアプリケーション設計を行うことをオススメいたします。

別の方向のアプローチとしてHTTP2に対応するという方法もあります。HTTP2では1つのTCPセッションの上で複数のHTTPセッションを通信させる機能が提供されています。Google, FacebookはSPDY/HTTP2に対応しています。HTTP2では1サイトに対して基本的には1ポートしか使わないのでポート数を節約することができ、NATの負荷を軽減することができます。ただし他のWebサービスが多数のポートを使った結果ポート枯渇が発生して、自分のWebサービスHTTP2のセッション自体が張れないという可能性は残ります。

まとめ

しばらくの間はNATによりIPv4インターネットは生き続けますが、NATにも限界があります。その限界に達した時に我々のWebサービスのパフォーマンスが低下しているように見えるユーザが現れ、サービスが重いと思われるリスクがあります。これに対してIPv6に対応することは効果的な対策になるでしょう。問題が起きる前に早いうちからIPv6に対応するためのコストを積み立てておいたほうが良いででしょう。

以上がコンテンツプロバイダこそがIPv6に対応しておいたほうがいい理由です。

運用自動化のためにネットワーク機器に求めるもの

f:id:yuyarin:20151130235731p:plain

12月1日のNetOpsCoding Advent Calender 2015の記事です。

NetOpsCodingは、ネットワークインフラにおける運用を自動化した事例や システム開発の知見、有用なツールなどを共有する勉強会です。2015年10月30日に第一回目の勉強会「NetOpsCoding #1」を開催しました。

運用自動化のためにネットワーク機器に求めるもの

ネットワーク運用を自動化しなければならないということは、もはや言うまでもないことだと思います。自分たちのネットワークサービスをソフトウェア制御で提供しようと思った場合に大きな壁になるのが、ネットワーク機器を外部のソフトウェアから制御する方法です。

ネットワーク機器の制御には大きく分けて次のような項目があるかと思います。

  • 情報取得 (show系コマンド)
  • 設定投入 (conf t)
  • アラート (syslog, SNMP trap)

これらの制御を外部のソフトウェアから行おうと思った場合に、どういう機能がネットワーク機器にあれば嬉しいのかを、ネットワーク運用者かつ自動化ソフトウェア開発者の立場から書いてみたいと思います。

運用自動化を目指すネットワーク運用者の目的は綺麗なコードを書くことではありません。一番大事なものはお客さまや自分たちのコンテンツのトラフィックです。そのため自動化にあたっては多少コードが汚くなったとしても 安全なソフトウェアを書くことが第一 です。

ちなみにここで言うネットワーク機器というのはCisco, Juniper, Brocade, Huawei, Alcatelなどのキャリア・DC向けのルータやスイッチを中心に考えていますが、あまり標準化が進んでいないPXCやWDMなどの機器も念頭に置いています。

全機能にアクセスできる統一的手段

APIとかNETCONFやSNMPとか対応していても、サービスに必要な全ての機能にアクセスできなければ意味がありません。 結局はtelnet/sshしてCLIから操作する羽目になりNet::Telnet最強説の暗黒面に足を踏み出すことになります。

例えば、インターフェイスの設定はNETCONFでできるけど、リンクアグリゲーションの設定はNETCONFだとできない。インターフェイスのUp/DownはSNMPで取得できるけど、LACP BlockedのステートはSNMPで取得できない。こういった状況にある場合は結局のところtelnet/sshしなくてはいけなくなります。

1つの種類の機器に対して複数の手段での操作を行う必要があると、それだけ検証の工数が倍増していくだけでなく、バグを踏むリスクが増えるので非常に辛いです。例えばSNMPでこのMIBを叩くとrebootするとかいうバグを見たことがある方は多いかと思います。逆に言えばメーカー間や機種間で共通したプロトコルやアクセス手段は必要ありません。実装が違うためどのみち検証工数はかかりますし、バグはそれぞれ存在するからです。

独自APIでも何でも構いません、その機器が提供している全ての機能に同じ方法でアクセスできる手段を提供してください。

さもなければ我々はtelnet/sshの闇から抜け出すことはできないでしょう。

コマンドの実行結果をJSON形式で必ず返す

残念ながらtelnetCLIからコマンドを実行する必要が出てきた場合に、次に困ることが実行結果の解釈です。ただしこの項目に関してはAPIも含めて全ての制御方式に当てはまることです。

show系コマンド

show系のコマンドを打った時にその結果をパースして必要な箇所を抜き出す必要があります。そしてこの表示フォーマットは人間には読みやすいですが、機械処理しようとすると非常に厄介なフォーマットになっている場合がほとんどです。また、バージョンによって表示フォーマットが変わっている場合、バージョンごとに応じたパーサを用意する必要があります。さらに、特殊な条件で表示結果が変わる場合、例えば「この機能を有効にしていたら表示にこの行が追加される」みたいなケースで、知らずに機能を有効化してしまってパーサが死ぬみたいなことが起きると悲惨です。

人間用の表示フォーマットと機械処理用のJSONフォーマットの2つの表示フォーマットに対応してください。

JUNOSでは show ... | display xmlXML形式で出力できるため非常にありがたいです。

config系コマンド

conf tしてからコンフィグ系のコマンドを実行した時、コマンドが成功た場合は結果は表示されず、コマンドが失敗した場合は何か文章が表示されることが多いかと思います。表示があったら失敗、なかったら成功、と判定しようとした時に厄介なのがwarningです。たとえば「rebootが必要です」みたいな警告が表示されるコマンドがあります。この場合設定の投入は成功していますが、表示はあります。こうしたものをいちいち場合分けして判断して対応する労力は大変です。

成功・失敗などのコマンド実行結果や、付随するwarningなどが構造化されたデータとして必ず返ってくるようにしてください。

階層移動系コマンド

Cisco系のCLIで設定する場合はinterface ...など階層を移動するようなコマンドを打つ必要があります。自動設定の時に怖いのが、例えば「誤ったインターフェイスに入ってshutdownしてしまう」といった階層移動ミスです。

GigabitEthernet 1/10 に入るつもりが、コマンドを流しこんだ際に最後の 0 がバッファ溢れで落ちて奇跡的に GigabitEthernet 1/1 に入ってしまって、そこでshutdownを打ってしまうと大惨事です。

1行程度のコマンドであればこのようなことは起きないとは思いますが、大量のコマンドを一度に流し込んだ場合にバッファが溢れる機器は実際にあるため、実装する側としては流し込まんだコマンドでバッファ溢れが起きていないか確認する必要があります。

階層移動のコマンドを打った場合も必ずその結果に移動先の階層の情報を含めて返して、対象の階層に確実に移動できたかどうかを確認できるようにしてください。

本当はJUNOSのset文のように階層はあるけれども階層に入らずに設定が行えるような仕組みになっていると嬉しいです。ターミナルのプロンプトに入ったインターフェイス名が表示される機器だとまだマシです。

トランザクション

先ほどの話は設定の際も安全性のために1つ1つコマンドを入れて結果を確認するという話でした。でもそれをやると時間がかかってしまうので、できれば設定をひとかたまりで投入できると嬉しいです。

このように複数の設定を同時に設定しようとした場合に怖いのが、途中のコマンドで失敗する場合です。失敗したコマンドの前に投入したコマンドが残っていても大丈夫な場合もありますが、できれば消しておきたいです。

ひとかたまりの設定を投入した時に途中で失敗したら、そもそも設定を投入する前の状態にロールバックできるような、トランザクションの機能を実装してください。

JUNOSやIOS-XRのcommitの機能がこれにあたります。IOSIOSライクな機器ではできません。

おかしなコンフィグを通さずにエラーで返す

なるべくそのようなことは起きないように設計・実装するのですが、例えば「存在しないaccess-listを設定する」といったことが起きてしまった時に大惨事にならないようになっていると嬉しいです。この例の場合、機器の挙動は「全てpermit」「全て暗黙のdeny」などメーカによって異なりますが、自動設定の観点からはエラーを返すという挙動になっていると嬉しいです。

グラマーが正しくてもセマンティクスがおかしい設定が投入された時にはエラーを返すようにしてください。

コンフィグをテキストで渡してload

ネットワーク運用自動化の初期の段階として、データベースとテンプレートを用いてコンフィグを全文生成するという段階があります。このコンフィグ全文から1つ前の全文との差分を作成し、適切に機器に投入する機能を実装することは非常に難しいです。このような場合にコンフィグ全文を送って今の状態との差分をネットワーク機器側で適切に抽出して適用してくれると嬉しいです。

コンフィグ全文をファイルで送ってreloadすることで差分を適切に適用する機能を実装してください。

JUNOSではload overrideでできます。

またコンフィグの投入方式として、コマンドをscpなどでファイル転送して、ファイルから読み込みできるようにすると、telnetでのバッファ溢れなどの悲劇が防げるので嬉しいです。

投入するコマンドをファイルで送ってloadすることで設定投入できる機能を実装してください。

JUNOSではload setでできます。

デスクリプションの変更が他の設定に影響を与えないようにする

自動化を可能にするためにdescriptionの命名規則を制定して、descriptionを書き直す作業が発生することがあります。たとえばLAGのインターフェイス名にdescriptionが含まれているような機器の場合、descriptionを変更するためには一度LAGを解体する必要がある、つまりトラフィック影響があるわけです。本来description変更はトラフィック影響が無い作業であるべきですので、そのようになっていると嬉しいです。もしくはそのようになっていたとしても、名前部分のreplaceが効いたり、atomicにLAGの解体・構築ができてトラフィックに影響がないような仕組みになっていれば十分です。

description変更がトラフィックに影響を与えないようにコンフィグを設計してください。

すべてのものに一意の型番をつける

モジュール型のネットワーク機器のラインカードモジュールやファブリックモジュール、ひいてはファンモジュールや電源モジュールなど、オブジェクトとして存在するものに対しては一意の型番を付与してください。メーカ側でそれらが決まっている場合、非常に楽になります。また、それらの型番と同じ文字列がshow系のコマンドで表示されるようにしてください。ただ、型番に括弧()を入れたりするのはやめてください…

ネットワーク機器を構成するすべてのものに一意の型番を付けてください。

ある機器のあるモジュールではシャーシサイズによって別のモジュールになっているにもかかわらず、機器の内部では同じ型番で扱われていたりします。こうなるとデータベース上でのモジュールの識別子を、メーカが決めた型番や機器内部で扱われている文字列とは別のものにする必要が発生します。こういったモジュールが1つ存在するだけで、全てのモジュールに対して「識別用の型番」と「設定用の型番」の2つの型番を管理する必要が生じます。これは避けたいです。

違うものに同じ型番を絶対につけないでください。

機械処理可能で十分な情報が含まれたプッシュ型通知

Link Down/UpやBGP Up/Downなどのイベントに自動で対応したり、集計・蓄積しようとしようと思うと、プッシュ型通知の機能が必要になります。この機能はsyslogやSNMP Trapとして存在します。

ネットワーク運用自動化の際にこれらのプッシュ型通知機能に求められるのは次の要件です。

  • データが構造化されていること
  • 対象を一意に特定するための情報が含まれていること

データが構造化された通知

syslogは人間に読みやすい文字列です。フォーマットがメーカでバラバラですし、同じ機器でもバージョンやログの種別によってフォーマットがバラバラになっています。syslogから情報を得るためには正規表現でマッチングすることが多いかと思います。この時に正規表現で上手くマッチングできないようなログがあると非常に厄介になります。JSONなどのデータ構造で送ることができれば機械処理は非常にやりやすくなります。

特定の宛先にはJSONフォーマットで送ることができるような機能があると嬉しいです。

Dec 01 00:00:00 INFO router01 GigabitEthernet0/1 Link Down (remote fault)

といったsyslogは人間には見やすいかもしれませんが、機械処理するのは面倒です。次のようなJSONフォーマットで出力できると非常に楽です。

{"hostname":"router01","hostaddr":"10.20.30.40","datetime":"2015-12-01 00:00:00 +0900","priorty":"info","event":"Link Down","detail":{"ifIndex":"1","ifDescr":"GigabitEthernet0/1","description":"AS65500","reason":"Remote Fault"}}

これはあくまでも例で、共通部分に関しては色々なメーカの機器でスキーマを揃えたほうが良いので、標準化してみたいと思います。

通知をJSONフォーマットで送ることができるような機能を実装してください。そして全てのアラートのスキーマを公開してください

SNMP Trapは構造化された通知です。MIBによりスキーマが定義されています。しかしバイナリフォーマットでありスキーマの解釈にはMIBファイルの読み込みが必要なので、スクリプトから扱うには敷居が高く、結局snmptrapdを使ってsyslogに変換して扱う状況が見られます。私はSNMP Trapをまだ上手く扱えていないので、SNMP Trapの可能性に言及することはできませんが、上手く使えば良い通知になるのではないでしょうか。

WDMなどの製品ではsyslogには対応しておらず、SNMP Trapのみに対応しているという場合が多いです。

対象を一意に特定するための情報が含まれた通知

アラートを使って何かを行いたいと思うと、その対象を一意に特定する情報が必要です。この時に単にID的な要素を含むよりかは、ユーザが決めることができるdescriptionなどの要素が含まれていると非常に運用が楽になります。

さきほどのsyslogの例だと

Dec 01 00:00:00 INFO router01 GigabitEthernet0/1 (AS65500) Link Down (remote fault)

このようにdescriptionが入っていると、desciptionを使った分類や、イベントのトリガーが可能になります。そうでなければインターフェイス名やifIndex値からdescriptionなどを逆引きする仕組みを実装する必要に迫られます。例えばインターフェイスが変わったとしても、AS65500のためにアラートを蓄積するシステムの構築が容易になります。この例だとGigabitEthernet0/1で対象を一意に特定できますが、ifIndexのほうが一意性は高い場合もあります。また、コンフィグを行う際に使用するインターフェイス名と、ifDescrの内容が違う場合もあります。この場合syslogに記載されている内容が役に立たない場合もあります。SNMP TrapでifIndexしか含まれていないものがあり、それを人間が識別可能な文字列に直すために16進数演算を含む複雑な処理をようするようなものもあります。

通知には対象が容易に特定可能になる情報を含めてください。通知には1.一意のID、2.人間にとって可読性の高い識別子、3.ユーザ定義のdescriptionを含めさせてください

ちなみにUDPだとパケット長問題やパケロス問題が出てくるので、TCPにせざるを得ないかなぁと感じています。そういう点からも TCPでsyslogを送ることができる という機能もあると嬉しいです。できないネットワーク機器はそれなりに多いです。

API Firstな設計

なんだかんだいって、REST APIが欲しいです。でも人間のオペレーションにはネットワーク機器のCLIは最高です。Linuxサーバで?を押してがっかりしたことが何度合ったか。WDM機器に多く見られるNMSもネイティブGUIやWebGUIで良く出来ているものがあります。しかしそれらのCLIGUIインターフェイスは全てAPIのラッパーになっていて、そのAPIはユーザに対しても開かれている方が嬉しいです。そうすることで運用者が独自に運用に必要なシステムを作ることが可能になります。

全ての機能をAPIとして実装して、そのラッパーとしてCLIGUIを提供してください。そしてそのAPIは運用者が利用できるようにしてください

まとめ

かなり長くなってしまいましたが、ネットワーク運用者視点で自動化のためにどのような機能があったら嬉しいかをまとめてみました。途中特定のメーカの賛美に思われる部分もありましたが、先進的な機能を実装しているので、やはり紹介しないわけにはいきません。

この記事が、少しでもネットワーク機器を開発されるメーカの皆様の助けになり、自動化しやすいネットワーク機器がたくさん世に送られることを願います。

CEDEC-Net 2015 で IPv6 の会場ネットワークを提供してきました

 

ゲーム開発者の祭典CEDEC

f:id:yuyarin:20150828141303j:plain

毎年8月末から9月頭頃に開催されるゲーム開発者のためのカンファレンスです。今年もパシフィコ横浜で3日間に渡って開催されました。昨年実績で1日あたり約5000名が来場します。

来場者のための無線ネットワーク「CEDEC-Net」

このCEDECではまともなインターネット環境が提供されていませんでした。5000名が来場するイベントに対して会場デフォルトのネットワークでは、無線アクセスポイントのスペックやDHCPのリース数、NAPTのセッション数などリソースが足りず、使えないネットワークになっていました。

これに危機感を覚えたコナミデジタルエンタテインメントの佐藤さんが日本ネットワークオペレーターズグループ(JANOG)に相談を持ちかけ、発足したのがCEDEC-Netです。JANOGでは年2回開催される数百人規模のカンファレンスで無線ネットワークを提供しています。蓄積されたノウハウもありますし、何より本職の人たちです。CEDEC-NetはJANOGからのメンバーに加え、ゲーム業界からネットワークに興味のあるエンジニアが集まって、CEDECでの安定かつ挑戦的な会場無線ネットワークの構築を目的として結成されました。

IPv6がテーマのCEDEC-Net 2015

f:id:yuyarin:20150830231859p:plain

昨年のCEDEC-Netのテーマは「CGN」でした。IPv4枯渇に立ち向かう技術の中で特にCGN(Carrier Grade NAT)に注目し、IPv4インターネットの変化を来場者にアピールしました。

今年は6月末に行った発足ミーティングで今年のCEDEC-Netのテーマを「IPv6」に決めました。6月のWWDC2015でApple社がiOSアプリの承認条件にIPv6対応を加えたからです。CEDEC-NetでAppleの推奨するIPv6オンリーかつNAT64+DNS64の環境を提供することで、ゲーム開発者にIPv6対応の必要性を訴えたいと考えました。一方で来場者に使えるネットワークを提供する必要があるので、IPv4のあるネットワークも提供することにしました。対外接続にはIPv6 IPoEの回線を利用し、その上でIPv4IPv6移行共存技術を利用することも決めました。

CEDEC-Netは安定したネットワークを提供することだけを目的とするのではなく、ゲーム業界へのメッセージ性やNOCメンバーの技術向上を目的としています。そのために我々が決断したことはIPv6オンリーネットワークをデフォルトのネットワークとして、そして、IPv6オンリーであることをアピールせずに提供するということです。

私は過去のカンファレンス経験でIPv6オンリーネットワークの構築や利用を経験していますが、あくまでそのようにアナウンスした上で、利用する人もネットワーク業界のエンジニアでした。業界の人でない参加者に、何も説明せずにIPv6オンリーネットワークを使わせるという試みは初めてでした。つながらないという報告や相談が来ることは目に見えているのですが、それをアピールチャンスだと捉えました。

 

CEDEC-Net 2015からのメッセージ

SSID: cedec-netとして提供したIPv6オンリーネットワークは「この無線ネットワークで動作しないアプリはAppleの審査に通らないかもしれない」というインパクトでゲーム業界に伝わりました。

 

 

 裏で色々聞いた話では大手ゲーム会社でもcedec-netを利用して自社アプリの動作確認を行う指令や翌週すぐにNAT64+DNS64の環境を構築するような指令が出たとか出なかったとか。

NOCチームで今回のネットワークについて講演する時間を頂きましたが、100人程度の部屋が満席になりました。この発表はなんとGAME Watchさんにも記事にしていただきましたGAME Watchというゲーム専門媒体にIPv6やNAT64などのネットワークの話が掲載されるというのも想像だにしなかった事態でした。

「CEDEC-Net 2015」がiOSアプリ開発者に警鐘を鳴らす! - GAME Watch

これまで色々なネットワーク業界でIPv6に対する検証や啓蒙活動が行われてきましたが、これほど他の業界にダイレクトに伝わった瞬間を目撃するのは初めてです。我々がいきなり何か達成したわけではなく、今までのいろいろな人の地道な努力や積み重ねた知見があって今回我々がこのようなネットワークを提供できたので、ネットワーク業界の努力が実って繋がった思うととても感慨深いです。

IPv6 IPoEとDS-Liteの対外接続

f:id:yuyarin:20150830233214p:plain

対外接続の選定

対外接続にはIPv6 IPoEのサービスである、インターネットマルチフィード社のtransixサービスを利用しました。自社サービスなので無償提供がしやすかったというのがありますが、技術的にもテーマ的にも適しているということで選定しています。

IPv6 IPoEでは本来閉域網であったフレッツ回線に割り当てられるIPv6アドレスでそのままインターネットに抜けられることが特徴です。MTU1500でトンネルオーバーヘッドもNATもないので、ゲーム通信には最適な回線だと言えます。

ちなみにtransixですが、IIJmioのFiberAccess/NFというサービスを申し込むと利用できます。僕の自宅もこれです。Google(AS15169)までIPv6で4msと快適です(ステマ)。

IIJmio:FiberAccess/NF概要

IPv4の接続性には同社で提供しているDS-Liteを利用しています。DS-Lite(Dual-Stack Lite)は家庭側機器(CPE)であるB4とISP側機器であるAFTRの間で、IPv4 over IPv6トンネルを張ってIPv4パケットを運び、AFTRでNAPTを行い、複数の家庭でグローバルIPv4アドレスを共有する技術です。CPEでNAPTを行うMAP-Eなどに比べると、ISP側機器の負荷が高くスケールしにくいのですが、IPv4アドレスを柔軟に使えたり、CPEの実装負荷が軽いという利点があります。あと名前が某社の携帯端末みたいなのでゲーム業界のネットワークに使うのにピッタリです。

B4にはYAMAHAの新製品であるRTX1210とCiscoのASR1001を日を分けて使いました。RTX1210は配下のYAMAHAのスイッチも一括管理できるルータです。

DS-Liteの設計

DS-Liteは1IPアドレス=65536ポートを複数の家庭で共有する技術のため、本来こういった数百数千のクライアントを収容するネットワークに用いる技術ではありませんが、同社に特別な回線を用意してもらい65536ポートをフルで使えるようにして頂きました。1端末あたり64ポートを見込んでいたので1024端末まではこれでサーポートできます。昨年は1200端末が同時接続していたので、この設計では足りません。しかしポートが枯渇して通信できなくなる事自体がIPv4をこのまま使い続けるとまずいというアピールになるのではと考えました。とはいえ読みが外れて本当に使えないレベルでポート足りなくなると問題なので、もう1回線用意しておいて、緊急時はトラフィックを分散できるように設計してありました。

f:id:yuyarin:20150831000712p:plain

2日目の午後4時頃に1400端末が接続しているタイミングで、予定通りNAPTのプール数が枯渇しました。上のグラフが16時過ぎに65kあたりに達しているのが分かるかと思います。プールの枯渇なのでプール内でのポートの使い回しがありますが、一部の通信がポート枯渇で通信できていなかったものと思われます。17時頃に別回線にトラフィックを分散した後は70k近くまでポートを使っているのがわかります。1端末あたり約50ポート程度を見積もっておくと良いという知見が得られました。

NAT64+DNS64の構築

今回NAT64に用いたのはA10 NetworksのvThunderという仮想アプライアンスです。CEDEC-Netには無償でお貸し出しいただきました。コンフィグ例はWebの記事に動作含めてまとまっていますのでこの通りにやれば動くものができます。

NAT64でIPv6端末をIPv4サーバにつなげよう(1/2) - @IT

DNS64は同じvThunderでもできるのですが、今回はBINDを使いました。Cisco ASR1001でもNAT64をやってみようという計画があったのですが、ASRではDNS64ができないので、DNS64は切り離しておこうという計画によるものです。

NAT64についてはググればLinuxBSDの実装やコンフィグ例が出てくると思います。

CEDECで使ったコンフィグについてはいずれ機会と需要があればどこかにまとめてみたいと思います。

IPv6オンリーネットワークへの接続

f:id:yuyarin:20150831004630p:plain

今年の同時最大接続数は1695でした。この結果は5分間隔のポーリングですが、リアルタイムの無線管理のツールの結果では1713端末の同時接続が確認されたようです。

 昨年の1200端末からすると1.5倍の端末に接続していただきました。今年はA0版のポスターまで作って頂くほどの広報の努力もあり、そしてCEDECWiFiは使えるという認識が徐々に参加者の間に広まってきたのだと思います。

 同時接続数より重要なことがこのグラフから分かります。それは実に60%の端末がIPv6オンリーネットワークであるSSID:cedec-netに接続していたということです。

f:id:yuyarin:20150831005116p:plain

ここで無線ネットワークの端末のベンダーコードの統計を見ていただきたいのですが、65%をAppleの端末が占めています。我々の検証の結果、MaciPhoneも、Apple端末自身はIPv6オンリーネットワーク上でも完璧に動作することがわかっています。残念ながらSSIDごとのベンダーコードを記録していなかったので確証はないのですが、この60%がIPv6オンリーネットワークを使い続けたという結果は、Appleの端末によるものではないかと推測されます。iOSの審査要件にIPv6対応を入れるだけのことはあります。

IPv6トラフィックの割合

NAT64の外側で取得したフローにもとづくデータです。Download, UploadともにIPv6トラフィック(bps)の割合が25%程度になっています。

f:id:yuyarin:20151112093826p:plain

無線ネットワークの運用

アクセスポイントの配置設計

CEDECの無線ネットワーク設計で特徴的なのがアクセスポイントの配置計画です。通常のIT系カンファレンスであれば来場者がPCを開くホール内や部屋内にアクセスポイントを集中的に配置し、廊下はコネクションが切れない程度の密度で配置します。CEDECでは真逆の傾向にあります。

f:id:yuyarin:20150831001912p:plain

これは2日目のトラフィックの様子ですが、このピークが立っている時間帯というのは、各セッションの開始直前の時間帯に合致します。

CEDECでは講演開始前に講演部屋の前に入場のための行列を形成します。この入場待ちの時間に廊下のアクセスポイントを通して多くのトラフィックが流れます。みなさん待ち時間は暇なようです。一方でみなさんまじめに講演を聞いているからなのか、講演時間中はあまりトラフィックは流れません。そのため部屋の中のアクセスポイントの負荷はそれほど高くありません。たまに小部屋に超人気セッションが押し込まれることがありますが、その時は非常にヤバイです。特に今年は竹迫さんのSECCONセッションがヤバかったです。

 

f:id:yuyarin:20150831002236p:plain

 

この独特の傾向からCEDEC-Netでは通常のカンファレンスとは異なり廊下やロビーのアクセスポイントを多めに配置しています。特に1階のホール前については通常であれば2、3個のアクセスポイントで十分ですが、非常に長い待ち行列が形成されるため、6個のアクセスポイントを配置してアクセスポイントあたりの接続端末数を抑えています。これは昨年の経験を活かした設計です。

無線APへの給電にはPoEスイッチを使いました。壁から出ているLANジャックはパシフィコ横浜の普通のスイッチと接続されているため、部屋の中でPoEスイッチを噛ませる必要がありました。そのため結構な数のスイッチが必要でしたがYAMAHA様にSWX2200-8PoEというゲートウェイルータのRTX1210から一括管理できる8ポートのPoEスイッチをお貸し出しいただきました。

無線ネットワークの監視

f:id:yuyarin:20150831010225p:plain

昨年のCEDECcisco-wlc-monitorというCLIツールを作成したのですが、今年はNOCに展示ブースに何かを出したい、しかもFirefoxのタブ切り替えで見せるものを切り替えていく、という話になったので、Webブラウザから監視できるツールを作りました。10秒ごとにアクセスポイントの状態やトラフィックなどの統計情報が更新されていきます。Rubyで書いたsinatraAPIと、websocketのデーモンがバックエンドで動いています。

動かすことを目標に前日に1晩で作ったものなのでコードが非常に汚いのですが、時間があれば整理して他のカンファレンスネットワークの皆様にも使っていただける形で公表したいと思います。

総評

書きたいことはやまほどあるのですが、そろそろまとめます。今年のCEDEC-Netは非常にチャレンジングかつインパクトのある取り組みができたと思います。昨年から引き続きNOCをやってくれたメンバーや、今年から新しくNOCに入ってくれたメンバー、それぞれの活躍おかげです。ありがとうございました。

これからもネットワーク業界とゲーム業界の実践交流の場として盛り上がっていくと思いますので、NOCチームに興味のある方は遠慮なく連絡ください。

関連資料

私がNOCチームの講演で使用したスライドはこちらです。

www.slideshare.net

CEDEC-Netに対するみなさんの反応はこちらにまとまっているのでぜひご覧ください。

https://twitter.com/cedecnet/favorites

CEDEC公式サイトに掲載されたネットワークの案内にもまとまった情報があります。

cedec.cesa.or.jp

iOS9で必要な対応はこちらの記事を参照ください。

qiita.com