OpenVNetを用いたDockerコンテナの仮想ネットワークオーバーレイ

  • 21
    いいね
  • 3
    コメント
この記事は最終更新日から1年以上が経過しています。

この記事は、Wakame-vdc / OpenVNet Advent Calendar 2014の23日目です。

Dockerを利用する際に困ることの一つとして、ネットワークの柔軟性に欠ける点が挙げられます。そこで今回は、OpenVNetとnetwork namespaceを活用し、ホストネットワーク上に任意のネットワークアドレスを持つ仮想ネットワークをオーバーレイしてDockerコンテナをオーバーレイネットワークに所属させてみようと思います。

検証した環境

ホスト

バージョン
OS CentOS Linux release 7.0.1406 (3.10.0-123.13.1.el7.x86_6)
VirtualBox 4.3.20r96996
vagrant 1.7.1

ゲスト

バージョン
OS CentOS Linux release 7.0.1406 (3.10.0-123.13.1.el7.x86_6)

OpenVNetインストール済みVMの起動

hostonlyな10.0.10.0/24のプライベートネットワークを追加し、プロミスキャスモードを「すべて許可」に変更します。

VM起動
[nmatsui@localhost openvnet]$ vi Vagrantfile
[nmatsui@localhost openvnet]$ vagrant up
Vagrantfile
# -*- mode: ruby -*-
# vi: set ft=ruby :

VAGRANTFILE_API_VERSION ||= "2"
Vagrant.configure(VAGRANTFILE_API_VERSION) do |config|
  config.vm.box = "nmatsui/openvnet-centos7"
  {"vnet-docker" => "10.0.10.11"}.each do |name, ipaddr|
    config.vm.define name do |vnet|
      vnet.vm.hostname = name
      vnet.vm.network :private_network,
                      ip: ipaddr,
                      netmask: "255.255.255.0"
      vnet.vm.provider :virtualbox do |vb|
        vb.name = name
        vb.customize ["modifyvm", :id, "--memory", "2048"]
        vb.customize ["modifyvm", :id, "--cpus", "2", "--ioapic", "on"]
        vb.customize ["modifyvm", :id, "--nicpromisc2", "allow-all"]
      end
      vnet.vbguest.auto_update = true
    end
  end
end

起動したVMに追加で割り当てられたNICのMACアドレスを調べておきます。

MACアドレス確認
[nmatsui@localhost openvnet]$ VBoxManage showvminfo vnet-docker | grep "NIC 2" | sed -e 's/,//g' | awk '{print $4}'
080027CDED36

ovsブリッジの作成

起動したVMでOpen vSwitchのブリッジを作成します。
この際、datapath-idには任意の16桁の16進数値を、hwaddrには上記で取得したMACアドレスを指定します(今回datapath-idは0000000000000001にしました)。

ovsブリッジ作成
[nmatsui@localhost openvnet]$ vagrant ssh
[vagrant@vnet-docker ~]$ sudo su -
[root@vnet-docker ~]# yum update -y
[root@vnet-docker ~]# vi /etc/sysconfig/network-scripts/ifcfg-enp0s8
[root@vnet-docker ~]# vi /etc/sysconfig/network-scripts/ifcfg-ovsbr0
[root@vnet-docker ~]# shutdown -r now
/etc/sysconfig/network-scripts/ifcfg-enp0s8
DEVICE=enp0s8
DEVICETYPE=ovs
TYPE=OVSPort
OVS_BRIDGE=ovsbr0
BOOTPROTO=none
ONBOOT=yes
HOTPLUG=no
/etc/sysconfig/network-scripts/ifcfg-ovsbr0
DEVICE=ovsbr0
DEVICETYPE=ovs
TYPE=OVSBridge
ONBOOT=yes
BOOTPROTO=static
IPADDR=10.0.10.11
NETMASK=255.255.255.0
HOTPLUG=no
OVS_EXTRA="
 set bridge     ${DEVICE} protocols=OpenFlow10,OpenFlow12,OpenFlow13 --
 set bridge     ${DEVICE} other_config:disable-in-band=true --
 set bridge     ${DEVICE} other-config:datapath-id=0000000000000001 --
 set bridge     ${DEVICE} other-config:hwaddr=08:00:27:CD:ED:36 --
 set-fail-mode  ${DEVICE} standalone --
 set-controller ${DEVICE} tcp:127.0.0.1:6633
"

再起動後、ip addr showによりovsブリッジが動作していることを確認してください。

ovsブリッジ動作確認
[root@vnet-docker ~]# ip addr show enp0s8
3: enp0s8: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc pfifo_fast master ovs-system state UP qlen 1000
    link/ether 08:00:27:cd:ed:36 brd ff:ff:ff:ff:ff:ff
[root@vnet-docker ~]# ip addr show ovsbr0
5: ovsbr0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UNKNOWN 
    link/ether 08:00:27:cd:ed:36 brd ff:ff:ff:ff:ff:ff
    inet 10.0.10.11/24 brd 10.0.10.255 scope global ovsbr0
       valid_lft forever preferred_lft forever
    inet6 fe80::a00:27ff:fecd:ed36/64 scope link 
       valid_lft forever preferred_lft forever

※注意

ovsブリッジを作成した以降は、vagrant haltやvagrant upでVMの停止/起動をしてはいけません(Vagrantがifcfg-enp0s8を上書きしてしまうため)。
VirtualBoxのGUIか、以下のコマンドでVMの停止/起動を行ってください。

  • VMの停止 :$ VBoxManage controlvm vnet-docker savestate
  • VMの起動 :$ VBoxManage startvm vnet-docker --type headless
  • vnet1へのSSH:$ vagrant ssh

dockerインストール

dockerをインストールし、SSH可能なコンテナを作成します。

dockerインストール
[root@vnet-docker ~]# yum install docker -y
[root@vnet-docker ~]# systemctl start docker
[root@vnet-docker ~]# mkdir vnet-docker
[root@vnet-docker ~]# cd vnet-docker/
[root@vnet-docker vnet-docker]# vi Dockerfile
[root@vnet-docker vnet-docker]# vi sshd.sh
[root@vnet-docker vnet-docker]# docker build -t centos_sshd .
Dockerfile
FROM centos:centos6
MAINTAINER nobuyuki.matsui <nobuyuki.matsui@gmail.com>
RUN yum -y update
RUN yum -y install openssh-server openssh-clients
RUN sed -ri 's/^#PermitEmptyPasswords no/PermitEmptyPasswords yes/' /etc/ssh/sshd_config
RUN sed -ri 's/^#PermitRootLogin yes/PermitRootLogin yes/' /etc/ssh/sshd_config
RUN sed -ri 's/^UsePAM yes/UsePAM no/' /etc/ssh/sshd_config
RUN passwd -d root
ADD sshd.sh /etc/profile.d/
sshd.sh
#!/bin/sh
service sshd start

OpenVNetの起動

systemctlを用いてOpenVNetを起動します。

OpenVNetの起動
[root@vnet-docker vnet-docker]# systemctl start vnet-vnmgr
[root@vnet-docker vnet-docker]# systemctl start vnet-webapi
[root@vnet-docker vnet-docker]# systemctl start vnet-vna

起動スクリプト

設定ファイルが複雑になったため、今回はbashは諦めてrubyで書きました。

各種スクリプトの作成
[root@vnet-docker vnet-docker]# mkdir lib
[root@vnet-docker vnet-docker]# vi config.yml
[root@vnet-docker vnet-docker]# vi startup
[root@vnet-docker vnet-docker]# vi lib/docker_starter.rb
[root@vnet-docker vnet-docker]# vi lib/vnet_starter.rb
[root@vnet-docker vnet-docker]# vi lib/route_setter.rb
[root@vnet-docker vnet-docker]# vi lib/nw_env.rb
[root@vnet-docker vnet-docker]# vi terminate
[root@vnet-docker vnet-docker]# vi lib/terminator.rb

設定ファイル

設定はかなり複雑ですが、Vagrantで起動したVMが所属するネットワークの情報と、Dockerコンテナが所属するオーバーレイネットワークの情報、起動するコンテナの情報などから構成されています。

注意点

  • MACアドレスは重複しないようにしてください
    • IEEEによれば、ベンダーコードが10:00:00のMACアドレスはPRIVATEだそうです
  • OpenVNetが、擬似的にDHCPとROUTERの役割を果たします。実際のVMに割り当てられていないIPアドレスを指定してください
  • スクリプトの都合上、ovsブリッジとDockerコンテナに与えるvethペアの名前(veth1bとveth1cとか)はアルファベットと数字だけにしてください
  • 起動するコンテナは複数記述することができますが、IPアドレスやMACアドレスだけでなく、vethペアの名前も重複しないように気をつけてください。
config.yml
host:
  broadcast:  '10:00:00:00:01:01'
  dhcp:
    ip_addr:    '10.0.10.254'
    mac_addr:   '10:00:00:00:01:02'
  router:
    ip_addr:    '10.0.10.1'
    mac_addr:   '10:00:00:00:01:03'
router:
  link:
    mac_addr:   '10:00:00:00:02:01'
  datapath:
    mac_addr:   '10:00:00:00:02:02'
virtual:
  nwaddr:     '192.168.99.0'
  mask:       '24'
  broadcast:  '10:00:00:00:03:01'
  dhcp:
    ip_addr:    '192.168.99.254'
    mac_addr:   '10:00:00:00:03:02'
  router:
    ip_addr:    '192.168.99.1'
    mac_addr:   '10:00:00:00:03:03'
  containers:
    - name:         'container1'
      image:        'centos_sshd'
      ip_addr:      '192.168.99.11'
      bridge_if:    'veth1b'
      container_if: 'veth1c'
      mac_addr:     '10:00:00:00:03:11'
    - name:         'container2'
      image:        'centos_sshd'
      ip_addr:      '192.168.99.12'
      bridge_if:    'veth2b'
      container_if: 'veth2c'
      mac_addr:     '10:00:00:00:03:12'

エントリスクリプト

以下の3つのクラスを呼び出し、Dockerコンテナの立ち上げからnetwork namespaceを持ちいたvethペアを設定、OpenVNetの設定、及びルーティングの設定を行わせます。

  • DockerStarter (Dockerコンテナの立ち上げとnetwork namespaceを持ちいたvethペアを設定)
  • VNetStarter (OpenVNetの設定)
  • RouteSetter (立ち上がったコンテナとホストに静的routeを追加)
startup
#!/usr/bin/env ruby
# -*- coding: utf-8 -*-

require_relative 'lib/docker_starter'
require_relative 'lib/vnet_starter'
require_relative 'lib/route_setter'

def puts_usage
  puts "usage:startup config.yml"
  exit 1
end

if __FILE__ == $0
  # 引数確認
  puts_usage if ARGV.empty?
  # 設定ファイル読み込み
  conf = YAML.load(ARGF.read)

  # Dockerコンテナの立ち上げとnetwork namespaceを持ちいたvethペアを設定
  pid_list = DockerStarter.new(conf).start_container
  # OpenVNetの設定
  VNetStarter.new(conf).clear_db.set_vnet.restart_vna
  # 立ち上がったコンテナとホストに静的routeを追加
  RouteSetter.new(conf).add_route(pid_list)
end

DockerStarter

設定ファイルのyamlの処理を行うためにrubyで書いていますが、コアな処理はコマンドを呼び出しているだけです。

以下のタスクを実行します。

  1. Dockerコンテナを起動
  2. network namespaceを用いてvethペアを作成
  3. vethペアの片方をovsブリッジに接続
  4. vethペアのもう一方にIPアドレスとMACアドレスを設定し、Dockerコンテナのプロセスに接続

静的routenの設定時に必要になりますので、各コンテナのプロセスIDとvethペア名などをハッシュに詰めてreturnします。

lib/docker_starter.rb
# -*- coding: utf-8 -*-

require 'yaml'
require_relative 'nw_env'

class DockerStarter
  def initialize(conf)
    @conf = conf
    `mkdir -p /var/run/netns/`
  end
  def start_container
    pid_list = {}
    mask = @conf["virtual"]["mask"]

    @conf["virtual"]["containers"].each do |c|
      image = c["image"]
      name  = c["name"]
      bif   = c["bridge_if"]
      cif   = c["container_if"]
      ip    = c["ip_addr"]
      mac   = c["mac_addr"] 

      # Dockerコンテナ起動
      id=`docker run --hostname=#{name} --net="none" -i -t -d #{image} /bin/bash`.chomp
      pid=`docker inspect --format {{.State.Pid}} #{id}`.chomp

      # 起動したDockerコンテナのprocをnetnsにリンク
      `ln -s /proc/#{pid}/ns/net /var/run/netns/#{pid}`

      # vethペアを作成
      `ip link add #{bif} type veth peer name #{cif}`

      # vethペアの一方をovsBridgenに設定し起動
      `ip link set #{bif} up`
      `ovs-vsctl add-port #{NWEnv.bridge} #{bif}`

      # vethペアの残りの一方をDockerコンテナにセットして起動
      `ip link set #{cif} netns #{pid}`
      `ip netns exec #{pid} ip link set dev #{cif} address #{mac}`
      `ip netns exec #{pid} ip addr add #{ip}/#{mask} dev #{cif}`
      `ip netns exec #{pid} ip link set #{cif} up`

      puts "start container NAME:#{name} IP:#{ip} MAC:#{mac}"
      pid_list[pid] = {:name=>name, :if=>cif}
    end
    return pid_list
  end
end

VNetStarter

素晴らしく長大なスクリプトですが、OpenVNetの設定をしています。

以下のタスクを実行します。(bundlerの都合上、Dir.chdirで一時的にカレントディレクトリを移してからコマンドを実行します。)

  1. OpenVNetのデータベースの初期化
  2. OpenVNetの設定
    1. datapathの設定
    2. ホストVMが所属するネットワークとオーバーレイする仮想ネットワークを設定
    3. ホストVMのインタフェースとDockerコンテナのインタフェースを登録
    4. Broadcast mac addressを設定?(実はいまいちこのコマンドの意味がわからない。。。)
    5. OpenVNetのDHCPサービスの設定
    6. OpenVNetのRouterサービスの設定
  3. VNAをリスタート

参考にしたのは、axshのgithub上で公開されているtestspec(dataset/base.ymldataset/router_p2v.yml)です。

lib/vnet_starter.rb
# -*- coding: utf-8 -*-

DEBUG=false

require 'yaml'
require_relative 'nw_env'

class VNetStarter
  BASE="/opt/axsh/openvnet"

  DP_NAME="node1"
  NW_PUB_NAME="public"
  NW_INT_NAME="internal"

  def initialize(conf)
    @conf = conf
  end

  # OpenVNetのデータベースの初期化
  def clear_db
    Dir.chdir("#{BASE}/vnet") do
      puts "OpenVNet database initialize"
      `bundle exec rake db:drop`
      `bundle exec rake db:create`
      `bundle exec rake db:init`
    end
    self
  end

  # OpenVNetの設定
  def set_vnet
    Dir.chdir("#{BASE}/vnctl") do
      set_datapaths
      set_networks
      set_interfaces
      set_broadcast
      set_dhcp
      set_router
    end
    self
  end

  # VNAをリスタート
  def restart_vna
    puts "restart vnet-vna"
    `systemctl restart vnet-vna`
  end

private
  # datapathの設定
  def set_datapaths
    puts "set datapath(#{DP_NAME})"
    ret = `bin/vnctl datapaths add \
      --uuid="dp-#{DP_NAME}" \
      --display-name="#{DP_NAME}" \
      --dpid="0x#{NWEnv.dpid}" \
      --node-id="#{DP_NAME}"`
    puts ret if DEBUG
  end

  # ネットワークの設定
  def set_networks
    # ホストVMが所属するネットワークの設定
    puts "set network(#{NW_PUB_NAME})"
    ret = `bin/vnctl networks add \
      --uuid="nw-#{NW_PUB_NAME}" \
      --display-name="#{NW_PUB_NAME}" \
      --ipv4-network="#{NWEnv.host_nw}" \
      --ipv4-prefix="#{NWEnv.host_mask}" \
      --network-mode="physical"`
    puts ret if DEBUG

    # オーバーレイする仮想ネットワークの設定
    puts "set network(#{NW_INT_NAME})"
    ret = `bin/vnctl networks add \
      --uuid="nw-#{NW_INT_NAME}" \
      --display-name="#{NW_INT_NAME}" \
      --ipv4-network="#{@conf["virtual"]["nwaddr"]}" \
      --ipv4-prefix="#{@conf["virtual"]["mask"]}" \
      --network-mode="virtual"`
    puts ret if DEBUG
  end

  # インタフェースの設定
  def set_interfaces
    # ホストVMのインタフェースを設定
    puts "set host interface(#{NWEnv.host_if})"
    ret = `bin/vnctl interfaces add \
      --uuid="if-#{NWEnv.host_if}" \
      --owner-datapath-uuid="dp-#{DP_NAME}" \
      --network-uuid="nw-#{NW_PUB_NAME}" \
      --mac-address="#{NWEnv.host_mac}" \
      --ipv4-address="#{NWEnv.host_ip}" \
      --port-name="#{NWEnv.host_if}" \
      --mode="host"`
    puts ret if DEBUG

    mask = @conf["virtual"]["mask"]
    @conf["virtual"]["containers"].each do |c|
      bif = c["bridge_if"]
      ip  = c["ip_addr"]
      mac = c["mac_addr"]

      # 設定ファイルで定義した各コンテナのインタフェースを設定
      puts "set virtual interface(#{bif})"
      ret = `bin/vnctl interfaces add \
        --uuid="if-#{bif}" \
        --owner-datapath-uuid="dp-#{DP_NAME}" \
        --network-uuid="nw-#{NW_INT_NAME}" \
        --mac-address="#{mac}" \
        --ipv4-address="#{ip}" \
        --port-name="#{bif}"`
      puts ret if DEBUG
    end
  end

  # Broadcast mac addressを設定?
  def set_broadcast
    host_bc = @conf["host"]["broadcast"]
    virt_bc = @conf["virtual"]["broadcast"]

    puts "set broadcast mac address(#{host_bc}) for netwark(#{NW_PUB_NAME}) and interface(#{NWEnv.host_if})"
    ret = `bin/vnctl datapaths network add dp-#{DP_NAME} nw-#{NW_PUB_NAME} \
      --interface_uuid="if-#{NWEnv.host_if}" \
      --broadcast-mac-address="#{host_bc}"`
    puts ret if DEBUG

    puts "set broadcast mac address(#{virt_bc}) for netwark(#{NW_INT_NAME}) and interface(#{NWEnv.host_if})"
    ret = `bin/vnctl datapaths network add dp-#{DP_NAME} nw-#{NW_INT_NAME} \
      --interface_uuid="if-#{NWEnv.host_if}" \
      --broadcast-mac-address="#{virt_bc}"`
    puts ret if DEBUG
  end

  # DHCPサービスの設定
  def set_dhcp
    # DHCPサービス用の疑似インタフェースをホストネットワーク上に作成
    puts "set simulated dhcp interface for #{NW_PUB_NAME}"
    ret = `bin/vnctl interfaces add \
      --uuid="if-dhcp#{NW_PUB_NAME}" \
      --owner-datapath-uuid="dp-#{DP_NAME}" \
      --network-uuid="nw-#{NW_PUB_NAME}" \
      --mac-address="#{@conf["host"]["dhcp"]["mac_addr"]}" \
      --ipv4-address="#{@conf["host"]["dhcp"]["ip_addr"]}" \
      --port-name="dhcp#{NW_PUB_NAME}" \
      --mode="simulated"`
    puts ret if DEBUG

    # ホストネットワークでDHCPサービス設定
    puts "set dhcp service on if-dhcp#{NW_PUB_NAME}"
    ret = `bin/vnctl network_services add \
      --display-name="ns-dhcp#{NW_PUB_NAME}" \
      --interface-uuid="if-dhcp#{NW_PUB_NAME}" \
      --type="dhcp"`
    puts ret if DEBUG

    # DHCPサービス用の疑似インタフェースをオーバーレイネットワーク上に作成
    puts "set simulated dhcp interface for #{NW_INT_NAME}"
    ret = `bin/vnctl interfaces add \
      --uuid="if-dhcp#{NW_INT_NAME}" \
      --owner-datapath-uuid="dp-#{DP_NAME}" \
      --network-uuid="nw-#{NW_INT_NAME}" \
      --mac-address="#{@conf["virtual"]["dhcp"]["mac_addr"]}" \
      --ipv4-address="#{@conf["virtual"]["dhcp"]["ip_addr"]}" \
      --port-name="dhcp#{NW_INT_NAME}" \
      --mode="simulated"`
    puts ret if DEBUG

    # オーバーレイネットワークでDHCPサービス設定
    puts "set dhcp service on if-dhcp#{NW_INT_NAME}"
    ret = `bin/vnctl network_services add \
      --display-name="ns-dhcp#{NW_INT_NAME}" \
      --interface-uuid="if-dhcp#{NW_INT_NAME}" \
      --type="dhcp"`
    puts ret if DEBUG
  end

  # Routerサービスの設定
  def set_router
    # Routerサービス用の疑似インタフェースをホストネットワーク上に作成
    puts "set simulated router interface for #{NW_PUB_NAME}"
    ret = `bin/vnctl interfaces add \
      --uuid="if-rt#{NW_PUB_NAME}" \
      --owner-datapath-uuid="dp-#{DP_NAME}" \
      --network-uuid="nw-#{NW_PUB_NAME}" \
      --mac-address="#{@conf["host"]["router"]["mac_addr"]}" \
      --ipv4-address="#{@conf["host"]["router"]["ip_addr"]}" \
      --mode="simulated" \
      --enable-routing="true"`
    puts ret if DEBUG

    # ホストネットワークでRouterサービス設定
    puts "set router service on if-rt#{NW_PUB_NAME}"
    ret = `bin/vnctl network_services add \
      --display-name="ns-rt#{NW_PUB_NAME}" \
      --interface-uuid="if-rt#{NW_PUB_NAME}" \
      --type="router"`
    puts ret if DEBUG

    # Routerサービス用の疑似インタフェースをオーバーレイネットワーク上に作成
    puts "set simulated router interface for #{NW_INT_NAME}"
    ret = `bin/vnctl interfaces add \
      --uuid="if-rt#{NW_INT_NAME}" \
      --owner-datapath-uuid="dp-#{DP_NAME}" \
      --network-uuid="nw-#{NW_INT_NAME}" \
      --mac-address="#{@conf["virtual"]["router"]["mac_addr"]}" \
      --ipv4-address="#{@conf["virtual"]["router"]["ip_addr"]}" \
      --mode="simulated" \
      --enable-routing="true"`
    puts ret if DEBUG

    # オーバーレイネットワークでRouterサービス設定
    puts "set router service on if-rt#{NW_INT_NAME}"
    ret = `bin/vnctl network_services add \
      --display-name="ns-rt#{NW_INT_NAME}" \
      --interface-uuid="if-rt#{NW_INT_NAME}" \
      --type="router"`
    puts ret if DEBUG

    # イマイチ役割がわからない。。。
    puts "set route link"
    ret = `bin/vnctl route_link add \
      --uuid="rl-pubint" \
      --mac-address="#{@conf["router"]["link"]["mac_addr"]}"`
    puts ret if DEBUG

    # イマイチ役割がわからない。。。
    ret = `bin/vnctl datapaths route_link add dp-#{DP_NAME} rl-pubint \
      --interface-uuid="if-#{NWEnv.host_if}" \
      --mac-address="#{@conf["router"]["datapath"]["mac_addr"]}"`
    puts ret if DEBUG

    # ルーティングルールの設定(今回は特段のルールは設定していない)
    puts "set router for #{NW_PUB_NAME}"
    ret = `bin/vnctl routes add \
      --interface-uuid="if-rt#{NW_PUB_NAME}" \
      --route-link-uuid="rl-pubint" \
      --network-uuid="nw-#{NW_PUB_NAME}" \
      --ipv4-network="#{NWEnv.host_nw}"`
    puts ret if DEBUG

    # ルーティングルールの設定(今回は特段のルールは設定していない)
    puts "set router for #{NW_INT_NAME}"
    ret = `bin/vnctl routes add \
      --interface-uuid="if-rt#{NW_INT_NAME}" \
      --route-link-uuid="rl-pubint" \
      --network-uuid="nw-#{NW_INT_NAME}" \
      --ipv4-network="#{@conf["virtual"]["nwaddr"]}"`
    puts ret if DEBUG
  end
end

RouteSetter

静的ルートを設定します。OpenVNetがgatewayとなる疑似インタフェースとRouterサービスを立ち上げた後でないとip route addできないので、最後に実施します。

以下のタスクを実行します。

  1. ホストOSにオーバーレイネットワークへ到達する静的ルートを設定
  2. 各Dockerコンテナにホストネットワークへ到達する静的ルートを設定
lib/route_setter.rb
# -*- coding: utf-8 -*-

require 'yaml'
require_relative 'nw_env'

class RouteSetter
  def initialize(conf)
    @conf = conf
  end
  def add_route(pid_list)
    add_host_route
    add_container_route(pid_list)
  end

private
  # ホストOSにオーバーレイネットワークへ到達する静的ルートを設定
  def add_host_route
    dest = "#{@conf["virtual"]["nwaddr"]}/#{@conf["virtual"]["mask"]}"
    gw   = @conf["host"]["router"]["ip_addr"]

    puts "static route (#{dest} via #{gw} dev #{NWEnv.bridge}) add to host"
    `ip route add #{dest} via #{gw} dev #{NWEnv.bridge}`
  end

  def add_container_route(pid_list)
    dest = "#{NWEnv.host_nw}/#{NWEnv.host_mask}"
    gw   = @conf["virtual"]["router"]["ip_addr"]
    pid_list.each do |pid, container|
      # Dockerコンテナにホストネットワークへ到達する静的ルートを設定
      puts "static route (#{dest} via #{gw} dev #{container[:if]}) add to container(#{container[:name]})"
      `ip netns exec #{pid} ip route add #{dest} via #{gw} dev #{container[:if]}`
    end
  end
end 

ホストのネットワーク情報などを取得するユーティリティ

ホストのネットワークアドレスやMACアドレスなどを調べるのは面倒なので、ovs-vsctlip addr shoなどから情報を取得するユーティリティも書きました。

lib/nw_env.rb
# -*- encoding: utf-8 -*-

class NWEnv
  @bridge  = `ovs-vsctl show`.match(/.*Bridge "(\w+)".*/)[1]
  @dpid    = `ovs-ofctl show #{@bridge}`.match(/.*dpid:(\w+).*/)[1]
  @addr    = `ip addr show #{@bridge}`.match(/.*link\/ether ([\w:]+) .*inet ([\d.]+)\/([\d]+).*/m)
  @host_nw = `ip route`.match(/^([\d.]+)\/\d.+ .* src #{@addr[2]}/)[1]
  @host_if = `ovs-vsctl show`.scan(/.*Port "([\w]+)".*/).flatten.find {|i| i.match(/^[enp|eth].*/)}

  class << self
    attr_reader :bridge, :dpid, :host_nw, :host_if
    def host_mac
      @addr[1]
    end
    def host_ip
      @addr[2]
    end
    def host_mask
      @addr[3]
    end
  end
end

停止スクリプト

今回はnetwork namespaceを用いてvethペアを作ったり、ホストOSにも静的routeを追加したりしているので、色々削除するスクリプトも書いてます。

以下のタスクを実行します。

  1. Dockerコンテナの削除
  2. vethペアの片割れをovsBridgeから削除
  3. ホストOSの静的routeの削除
terminate
#!/usr/bin/env ruby
# -*- coding: utf-8 -*-

require 'yaml'
require_relative 'lib/terminator'
require_relative 'lib/nw_env'

def puts_usage
  puts "usage:terminate config.yml"
  exit 1
end

if __FILE__ == $0
  puts_usage if ARGV.empty?
  conf = YAML.load(ARGF.read)

  Terminator.new(conf).terminate
end
lib/terminator.rb
# -*- coding: utf-8 -*-

require 'yaml'
require_relative 'nw_env'

class Terminator
  def initialize(conf)
    @conf = conf
  end
  def terminate
    delete_container
    delete_virtual_if
    delete_route
  end

private
  # Dockerコンテナの削除
  def delete_container
    pids = `docker ps -a -q`.chomp.split(/[\r|\n]/).each do |c|
      puts "delete container #{c}"
      `docker kill #{c}`
      `docker rm #{c}`
    end
  end

  # vethペアの片割れをovsBridgeから削除
  def delete_virtual_if
    @conf["virtual"]["containers"].each do |c|
      bif = c["bridge_if"]
      puts "delete interface #{bif} from #{NWEnv.bridge}"
      `ovs-vsctl del-port #{NWEnv.bridge} #{bif}`
    end
    `rm -rf /var/run/netns/*`
  end

  # ホストOSの静的routeの削除
  def delete_route
    dest = "#{@conf["virtual"]["nwaddr"]}/#{@conf["virtual"]["mask"]}"
    puts "delete route(#{dest}) from host"
    `ip route delete #{dest}`
  end
end

動作させてみよう

ということで、実際に動作させてみます。

設定ファイルが適切に書かれていれば、オーバーレイネットワーク上でDockerコンテナが立ち上がり接続可能になるまで自動で処理が進みます。

コンテナ起動
[root@vnet-docker vnet-docker]# chmod +x startup
[root@vnet-docker vnet-docker]# ./startup config.yml 
start container NAME:container1 IP:192.168.99.11 MAC:10:00:00:00:03:11
start container NAME:container2 IP:192.168.99.12 MAC:10:00:00:00:03:12
OpenVNet database initialize
set datapath(node1)
set network(public)
set network(internal)
set host interface(enp0s8)
set virtual interface(veth1b)
set virtual interface(veth2b)
set broadcast mac address(10:00:00:00:01:01) for netwark(public) and interface(enp0s8)
set broadcast mac address(10:00:00:00:03:01) for netwark(internal) and interface(enp0s8)
set simulated dhcp interface for public
set dhcp service on if-dhcppublic
set simulated dhcp interface for internal
set dhcp service on if-dhcpinternal
set simulated router interface for public
set router service on if-rtpublic
set simulated router interface for internal
set router service on if-rtinternal
set route link
set router for public
set router for internal
restart vnet-vna
static route (192.168.99.0/24 via 10.0.10.1 dev ovsbr0) add to host
static route (10.0.10.0/24 via 192.168.99.1 dev veth1c) add to container(container1)
static route (10.0.10.0/24 via 192.168.99.1 dev veth2c) add to container(container2)

もし上手く動作しない場合は、VNetStarterのDEBUGをtrueにして、OpenVNetのコマンドから返って来るエラーメッセージを確認してください。また/var/log/openvnet/のログを確認しても良いでしょう。
(我こそはという方は、ovs-ofctl dump-flows ovsbr0でOpenFlowのフローテーブルからデバッグしても良いでしょう。)

また停止する際は、terminate config.ymlです。

コンテナ停止
[root@vnet-docker vnet-docker]# chmod +x terminate
[root@vnet-docker vnet-docker]# ./terminate config.yml 
delete container cbc7d1915e90
delete container 4c862ebece37
delete interface veth1b from ovsbr0
delete interface veth2b from ovsbr0
delete route(192.168.99.0/24) from host

ネットワークの状態

ここまで進めると、ホストとコンテナのネットワークは以下のようになっています。

ホスト

ネットワークの状態
[root@vnet-docker vnet-docker]# ovs-vsctl show
857b9055-f4a7-4d5b-9966-53b9e3e10e63
    Bridge "ovsbr0"
        Controller "tcp:127.0.0.1:6633"
            is_connected: true
        fail_mode: standalone
        Port "veth1b"
            Interface "veth1b"
        Port "veth2b"
            Interface "veth2b"
        Port "ovsbr0"
            Interface "ovsbr0"
                type: internal
        Port "enp0s8"
            Interface "enp0s8"
    ovs_version: "2.1.3"
[root@vnet-docker vnet-docker]# ip route show
default via 10.0.2.2 dev enp0s3  proto static  metric 1024 
10.0.2.0/24 dev enp0s3  proto kernel  scope link  src 10.0.2.15 
10.0.10.0/24 dev ovsbr0  proto kernel  scope link  src 10.0.10.11 
169.254.0.0/16 dev ovsbr0  scope link  metric 1005 
172.17.0.0/16 dev docker0  proto kernel  scope link  src 172.17.42.1 
192.168.99.0/24 via 10.0.10.1 dev ovsbr0

コンテナ

container1
[root@container1 ~]# ip addr show
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN 
    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
    inet 127.0.0.1/8 scope host lo
       valid_lft forever preferred_lft forever
    inet6 ::1/128 scope host 
       valid_lft forever preferred_lft forever
209: veth1c: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc pfifo_fast state UP qlen 1000
    link/ether 10:00:00:00:03:11 brd ff:ff:ff:ff:ff:ff
    inet 192.168.99.11/24 scope global veth1c
       valid_lft forever preferred_lft forever
    inet6 fe80::1200:ff:fe00:311/64 scope link 
       valid_lft forever preferred_lft forever
[root@container1 ~]# ip route show
10.0.10.0/24 via 192.168.99.1 dev veth1c 
192.168.99.0/24 dev veth1c  proto kernel  scope link  src 192.168.99.11 
container2
[root@container2 ~]# ip addr show
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN 
    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
    inet 127.0.0.1/8 scope host lo
       valid_lft forever preferred_lft forever
    inet6 ::1/128 scope host 
       valid_lft forever preferred_lft forever
211: veth2c: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc pfifo_fast state UP qlen 1000
    link/ether 10:00:00:00:03:12 brd ff:ff:ff:ff:ff:ff
    inet 192.168.99.12/24 scope global veth2c
       valid_lft forever preferred_lft forever
    inet6 fe80::1200:ff:fe00:312/64 scope link 
       valid_lft forever preferred_lft forever
[root@container2 ~]# ip route show
10.0.10.0/24 via 192.168.99.1 dev veth2c 
192.168.99.0/24 dev veth2c  proto kernel  scope link  src 192.168.99.12 

接続テスト

ホストネットワークとは異なるアドレスを持つオーバーレイネットワーク上のコンテナと、無事に通信できました。

ホスト → 各コンテナ

ホスト→container1
[root@vnet-docker vnet-docker]# ping 192.168.99.11
PING 192.168.99.11 (192.168.99.11) 56(84) bytes of data.
64 bytes from 192.168.99.11: icmp_seq=1 ttl=64 time=17.1 ms
64 bytes from 192.168.99.11: icmp_seq=2 ttl=64 time=0.233 ms
ホスト→container2
[root@vnet-docker vnet-docker]# ping 192.168.99.12
PING 192.168.99.12 (192.168.99.12) 56(84) bytes of data.
64 bytes from 192.168.99.12: icmp_seq=1 ttl=64 time=13.8 ms
64 bytes from 192.168.99.12: icmp_seq=2 ttl=64 time=0.132 ms

コンテナ → ホスト

container1→ホスト
[root@vnet-docker vnet-docker]# ssh 192.168.99.11
[root@container1 ~]# ping 10.0.10.11
PING 10.0.10.11 (10.0.10.11) 56(84) bytes of data.
64 bytes from 10.0.10.11: icmp_seq=1 ttl=64 time=1.80 ms
64 bytes from 10.0.10.11: icmp_seq=2 ttl=64 time=0.077 ms
container2→ホスト
[root@vnet-docker vnet-docker]# ssh 192.168.99.12
[root@container2 ~]# ping 10.0.10.11
PING 10.0.10.11 (10.0.10.11) 56(84) bytes of data.
64 bytes from 10.0.10.11: icmp_seq=1 ttl=64 time=2.46 ms
64 bytes from 10.0.10.11: icmp_seq=2 ttl=64 time=0.085 ms

コンテナ → コンテナ

container1→container2
[root@container1 ~]# ping 192.168.99.12
PING 192.168.99.12 (192.168.99.12) 56(84) bytes of data.
64 bytes from 192.168.99.12: icmp_seq=1 ttl=64 time=1.44 ms
64 bytes from 192.168.99.12: icmp_seq=2 ttl=64 time=0.123 ms
container2→container1
[root@container2 ~]# ping 192.168.99.11
PING 192.168.99.11 (192.168.99.11) 56(84) bytes of data.
64 bytes from 192.168.99.11: icmp_seq=1 ttl=64 time=3.43 ms
64 bytes from 192.168.99.11: icmp_seq=2 ttl=64 time=0.077 ms

最後に

はっきり言えば、ものすごく面倒でした。このあたりを綺麗に隠蔽するOpenStack Neutronのスゴさを改めて感じます。
とは言え設定方法さえ理解できれば、Neutronほど重い実装を持ち込まなくても、ネットワークの制約にとらわれないオーバーレイネットワークを構築できるのはかなりの魅力だと思います。

夢はさらに広がりますね!