自宅のOpenVPNをHashiCorpのNomadを使ってHAクラスタ環境にした

# はじめに

外出先から自宅にアクセスできるようにOpenVPNサーバーを動かしているのですが、気づいたら止まっていることが何度かあったため、HAクラスタ構成で稼働させてみました。

# 完成イメージ

完成イメージは次の通りです。

![final_image.png](https://qiita-image-store.s3.amazonaws.com/0/129013/40c173f6-fd81-f041-afc7-47d711374259.png)

オーケストレーションツールで、Dockerコンテナ化したOpenVPNサーバーを実行することで構築しました。設定ファイルなどはNASを用いて各ホスト間で共有しています。
もし、OpenVPNサーバーが稼働しているホストがダウンした際には、自動的に他のホストでDockerコンテナが立ち上がることで、VPNサービスを継続します。

ルーターのポートマッピング設定の転送先が、IPでしか指定できなかったため、固定IPを割り当てたDockerコンテナを1並列で実行しています。

Raspberry pi上で、Docker、Consul、Nomadを使用して構築したのですが、次の要素が**無駄に**難しくしてしまった印象です。

* Dockerを使ってしまったこと  
* 冗長化をオーケストレーションツールで対応してしまったこと
* Raspberry pi 1（非力なARM）で構築してしまったこと  

なかなか、反省点が多い内容ではあるのですが、想定通りに動くものができましたし、面白かったので手順を残しておきます。

# 環境

構築に用いた環境です。

* Raspberry Pi 1 Model B+ ： 3台

各種バージョンです。

| | バージョン | 
|:-----------|------------:|
| OS | RASPBIAN JESSIE 2016-05-27 |
| Docker | version 1.13.0, build 49bf474 |
| Consul | v0.7.2-rc1-rc1 (3468995) |
| Nomad | v0.5.3-rc1 |

# 手順
## Raspberry pi
### OSインストールと固定IPの設定
Raspberry piのOSインストールと固定IPの設定を実施しました。
ここは「[Docker on Raspberry PiのインストールとLチカ](http://qiita.com/ykshr/items/c78eb72e3ee75664a5fe)」に記載の手順と同様です。
※ Dockerのインストール手順が変わっているため、Dockerは次の手順でインストールしています。

### NASのマウント
今回、各ホスト間でOpenVPNサーバーの設定などを共有するためにNASを用いています。そのため、共有フォルダを`/mnt/vpn`にマウントしました。

```shell-session
$ sudo service rpcbind start
$ sudo mkdir -p /mnt/vpn
$ sudo mount -t nfs <NASホスト名(192.168.0.2)>:/XXX /mnt/vpn
```

## Docker
### Dockerのインストール
公式HPに記載の手順でDockerをインストールしました。
参考：[インストール手順](https://docs.docker.com/engine/installation/linux/raspbian/)

GPGキーを追加します。  

```shell-session
$ sudo apt-get update  
$ sudo apt-get install apt-transport-https ca-certificates
$ sudo apt-key adv --keyserver hkp://p80.pool.sks-keyservers.net:80 --recv-keys 58118E89F3A912897C070ADBF76221572C52609D
```

ソースリストにDockerリポジトリを追加します。  

```shell-session
$ sudo vi /etc/apt/sources.list.d/docker.list
deb https://apt.dockerproject.org/repo raspbian-jessie main
$ sudo apt-get update
$ sudo apt-cache policy docker-engine
```

Dockerをインストールします。  

```shell-session
$ sudo apt-get install docker-engine
```

sudoなしで`docker`が実行できるように、ユーザーをdockerグループに追加します。  

```shell-session
$ sudo groupadd docker
$ sudo gpasswd -a ${USER} docker
```

Dockerを起動します。  

```shell-session
$ sudo service docker start
```

### OpenVPNイメージの作成・設定
OpenVPNサーバーのDockerイメージを作ります。
ここでは、主要なファイルを記載します。その他のファイルや構成は[Github](https://github.com/ykshr/pi-openvpn)を参考にして下さい。

Dockerfileは次の通りです。鍵の生成などは、EasyRSAを用いるようにしました。
特に環境によって変更が必要な箇所はありません。

```bash:Dockerfile
FROM resin/rpi-raspbian:jessie

RUN apt-get update
RUN apt-get install wget sed iptables openvpn

WORKDIR /tmp
RUN wget https://github.com/OpenVPN/easy-rsa/releases/download/v3.0.0-rc2/EasyRSA-3.0.0-rc2.tgz

RUN mkdir -p /etc/openvpn/ovpn
ADD ./template/template.ovpn /etc/openvpn/ovpn/template.ovpn
ADD ./conf/server.conf /etc/openvpn/server.conf
RUN cp -ar /etc/openvpn /tmp/

ADD ./bin /usr/local/bin
RUN chmod a+x /usr/local/bin/*

RUN sed -ien "s/.*net\.ipv4\.ip_forward.*$/net.ipv4.ip_forward = 1/g" /etc/sysctl.conf

EXPOSE 1194/udp
EXPOSE 443/tcp

VOLUME ["/etc/openvpn", "/usr/local/EasyRSA"]

CMD ["ovpn_run"]
```

OpenVPNサーバーの設定は、server.confに記載しています。
ポートやプロトコル、VPN接続先のアドレスなどが変更が必要な箇所です。

```bash:server.conf
port <ポート(443)>
proto <プロトコル(tcp)>
dev tun
ca /etc/openvpn/ca.crt
cert /etc/openvpn/server.crt
key /etc/openvpn/server.key
dh /etc/openvpn/dh2048.pem
server 10.8.0.0 255.255.255.0
ifconfig-pool-persist ipp.txt
push "route <転送先のアドレス(192.168.0.0)> 255.255.255.0"
keepalive 10 120
comp-lzo
user nobody
group nogroup
persist-key
persist-tun
status /var/log/openvpn-status.log
log-append  /var/log/openvpn.log
verb 3
```

Clientが接続する際に必要なovpnファイルのテンプレートです。
接続先（自宅）ドメインが変更が必要な箇所です。また、OpenVPNサーバーの設定にあわせて、プロトコルやポートの変更も必要です。

```bash:template.ovpn
dev tun
proto <プロトコル(tcp)>

remote <VPN接続先(example.com)> <ポート(443)>

;http-proxy-retry
;http-proxy [proxy server] [proxy port]

cipher BF-CBC
auth SHA1

resolv-retry infinite
nobind
persist-key
persist-tun
client
verb 3
comp-lzo
# auth-user-pass
```

各種設定ファイルを変更した後、イメージをビルドします。

```shell-session
$ docker build -t pi-openvpn:latest ./
```

ビルドが完了したのち、生成したイメージを使ってOpenVPNサーバーの初期化をします。初期化では、マウントした共有フォルダに対して設定ファイルなどを出力します。
※Raspberry piが非力なため非常に時間がかかりました。

```shell-session
$ docker run -v /mnt/vpn/openvpn:/etc/openvpn -v /mnt/vpn/EasyRSA:/usr/local/EasyRSA --rm -it pi-openvpn:latest ovpn_init_server
```

次にクライアントを追加します。クライアント名は適宜変更してください。
ここでは、先ほどと同様にマウントした共有フォルダに対してクライアントの設定ファイルなどを出力します。

```shell-session
$ docker run -v /mnt/vpn/openvpn:/etc/openvpn -v /mnt/vpn/EasyRSA:/usr/local/EasyRSA --rm -it pi-openvpn:latest ovpn_add_client <クライアント名(client)>
```

これで、OpenVPNイメージの作成と設定は完了です。

### Dockerネットワークの変更

Dockerでは、ブリッジ`docker0`によりコンテナには`172.17.0.X`が設定されます[図の上側]。今回は、ルーターにおけるポートマッピングをIPで設定するため、Dockerコンテナにホストと同じ`192.168.0.X`の固定IPを設定したいです。そのため、新たにブリッジを作成します[図の下側]。

![network.png](https://qiita-image-store.s3.amazonaws.com/0/129013/3146cbb0-80fc-8bea-121b-f843c034ae67.png)


なお、ブリッジを作成する手順の中で、ネットワークが切れるタイミングがあります。そのため、Raspberry piに直接キーボードとディスプレイを接続して作業する方が安全です。

bridge-utilsをインストールします。

```shell-session
$ sudo apt-get update
$ sudo apt-get install bridge-utils
```

ブリッジを作成します。
※ ここでネットワークの接続が切れます。

```shell-session
$ docker network create --driver bridge --subnet=<サブネット(192.168.0.0/24)> --gateway=<ホストのIPアドレス(192.168.0.10)> --opt "com.docker.network.bridge.name"="br0" shared_nw
```

ホストアダプタ（eth0）をブリッジに接続します。

```shell-session
$ ifconfig eth0 0.0.0.0 promisc up
```

デフォルトゲートウェイを設定します。

```shell-session
$ brctl addif br0 eth0
```

ここでインターネットに接続できるはずですので、接続を確認します。

```shell-session
$ sudo apt-get update
```

接続できることが確認できたら、Dockerの起動/終了時に上記設定が実行されるようにシェルを作成します。なお、あまり良くはないですが、今回はこのシェルの中でNASのマウントも行っています。

```bash:startup.sh
#!/bin/sh

br="br0"
eth="eth0"

ifconfig $eth 0.0.0.0 promisc up
brctl addif $br $eth

/bin/echo 1 > /proc/sys/net/ipv4/ip_forward

sudo service rpcbind start
sudo mkdir -p /mnt/vpn
sudo mount -t nfs <NASホスト名(192.168.0.2)>:/XXX /mnt/vpn

docker restart $(docker ps -q)

exit 0
```

```bash:shutdown.sh
#!/bin/sh

br="br0"
eth="eth0"

ip=<ホストのIPアドレス(192.168.0.10)>

ifconfig $br down
brctl delbr $br

ifconfig $eth $ip

exit 0
```

上記シェルに実行権限を付与します。

```shell-session
$ chmod +x ~/startup.sh
$ chmod +x ~/shutdown.sh
```

Dockerの起動/終了時に実行されるよう設定します。

```bash:/lib/systemd/system/docker.service
[Service]
ExecStartPost=/home/pi/startup.sh
ExecStopPost=/home/pi/shutdown.sh
```

設定を反映します。

```shell-session
$ sudo systemctl daemon-reload
```

これで、Dockerネットワークの変更は完了です。
再起動後、次のようなコンテナを立ち上げてPINGなどで接続を確認してもいいかもしれません。

```shell-session
$ docker run -d --net shared_nw --ip 192.168.0.90 resin/rpi-raspbian:jessie sleep 50
```

```shell-session
$ ping 192.168.0.90
```

## クラスタの構築
### Consul

Consulをインストールします。
ARM用のバイナリファイルをダウンロードするだけです。

```shell-session
$ wget https://releases.hashicorp.com/consul/0.7.2-rc1/consul_0.7.2-rc1_linux_arm.zip
$ unzip consul_0.7.2-rc1_linux_arm.zip
$ sudo mv ./consul /usr/local/bin/
$ rm consul_0.7.2-rc1_linux_arm.zip
$ sudo mkdir -p /opt/consul/data
$ sudo chown -R $(whoami) /opt/consul
$ sudo mkdir /etc/consul.d
```

サーバーの設定ファイルを作成します。

```bash:/etc/consul.d/consul.json
{
  "node_name":<node名(pi001)>,
  "datacenter":"dc1",
  "data_dir":"/opt/consul/data",
  "start_join": [ <MasterサーバーのIP(192.168.0.10)>, <MasterサーバーのIP(192.168.0.20)>, <MasterサーバーのIP(192.168.0.30)> ],
  "server":true,
  "bootstrap_expect":<Masterサーバーの台数(3)>,
  "bind_addr": <ホストのIPアドレス(192.168.0.10)>,
  "log_level":"INFO"
}
```

上記設定ファイルでサーバーを起動します。

```shell-session
$ consul agent -config-dir /etc/consul.d >> /tmp/consul.log &
```

構築されたクラスタを確認します。
すべてのノードが表示されると思います。

```shell-session
$ consul members
Node   Address           Status  Type    Build     Protocol  DC
pi001  192.168.0.10:8301  alive   server  0.7.2rc1  2         dc1
pi002  192.168.0.20:8301  alive   server  0.7.2rc1  2         dc1
pi003  192.168.0.30:8301  alive   server  0.7.2rc1  2         dc1
```

Raspberry piの起動/終了時に起動されるよう設定します。

```bash:/etc/systemd/system/consul.service
[Unit]
Description=consul agent
Requires=network-online.target
After=network-online.target

[Service]
Restart=on-failure
ExecStart=/usr/local/bin/consul agent -config-dir=/etc/consul.d
ExecReload=/bin/kill -HUP $MAINPID
KillSignal=SIGINT

[Install]
WantedBy=multi-user.target
```

自動起動/終了の設定を反映します。

```
$ sudo systemctl daemon-reload
$ sudo systemctl enable consul
```

これで、Consulの設定は完了です。

### Nomad

Nomadをインストールします。
こちらも、ARM用のバイナリファイルをダウンロードするだけです。

```shell-session
$ wget https://releases.hashicorp.com/nomad/0.5.3-rc1/nomad_0.5.3-rc1_linux_arm.zip
$ unzip nomad_0.5.3-rc1_linux_arm.zip
$ sudo mv ./linux_arm/nomad /usr/local/bin/
$ rm nomad_0.5.3-rc1_linux_arm.zip
$ sudo mkdir -p /opt/nomad/data
$ sudo chown -R $(whoami) /opt/nomad
$ sudo mkdir /etc/nomad.d
```

サーバーの設定ファイルを作成します。

```bash:/etc/nomad.d/nomad.hcl
# Increase log verbosity
log_level = "INFO"
bind_addr = <ホストのIPアドレス("192.168.0.10")>
datacenter = "dc1"
name = <node名("pi001")>
# Setup data dir
data_dir = "/opt/nomad/data"
# Enable the server
server {
    enabled = true
    bootstrap_expect = <Masterサーバーの台数(3)>
}
# Enable the client
client {
    enabled = true
    options = {
      "docker.privileged.enabled" = "true"
    }
}
```

上記設定ファイルでサーバーを起動します。

```shell-session
$ sudo nomad agent -config=/etc/nomad.d/nomad.hcl >> /tmp/nomad.log &
```

構築されたクラスタを確認します。
すべてのノードが表示されると思います。

```shell-session
$ nomad server-members -address http://192.168.0.10:4646/
Name          Address      Port  Status  Leader  Protocol  Build     Datacenter  Region
pi001.global  192.168.0.10  4648  alive   true    2         0.5.3rc1  dc1         global
pi002.global  192.168.0.20  4648  alive   false   2         0.5.3rc1  dc1         global
pi003.global  192.168.0.30  4648  alive   false   2         0.5.3rc1  dc1         global
```

Raspberry piの起動/終了時に起動されるよう設定します。

```bash:/etc/systemd/system/nomad.service
[Unit]
Description=nomad agent
Requires=consul.service
After=consul.service

[Service]
Restart=on-failure
ExecStart=/usr/local/bin/nomad agent -config=/etc/nomad.d/nomad.hcl
ExecReload=/bin/kill -HUP $MAINPID
KillSignal=SIGINT

[Install]
WantedBy=multi-user.target
```

自動起動/終了の設定を反映します。

```shell-session
$ sudo systemctl daemon-reload
$ sudo systemctl enable nomad
```

これで、Nomadの設定は完了です。

## OpenVPNサーバーの起動と接続
### OpenVPNサーバーの起動

先ほどまでの手順で環境の構築は完了しました。
あとは、OpenVPNサーバーの起動です。

まず、起動に必要な設定ファイルを作成します。
ここで、`OpenVPNホストのIPアドレス(192.168.0.99)`がルーターに設定する443ポートの転送先アドレスです。

```bash:/opt/nomad/openvpn.nomad
job "openvpn" {
  datacenters = ["dc1"]
  type = "service"
  update {
    stagger = "10s"
    max_parallel = 1
  }

  group "cache" {
    count = 1
    restart {
      attempts = 10
      interval = "5m"
      delay = "25s"
      mode = "delay"
    }
    ephemeral_disk {
      size = 300
    }
    task "openvpn" {
      driver = "docker"
      config {
        image = "pi-openvpn:latest"
        privileged = true
        network_mode = "shared_nw"
        volumes = [
          "/mnt/vpn/openvpn:/etc/openvpn",
          "/mnt/vpn/EasyRSA:/usr/local/EasyRSA"
        ]
        port_map {
          https = 443
        }
      }
      resources {
        cpu    = 500 # 500 MHz
        memory = 256 # 256MB
        network {
          mbits = 10
          port "https" {
            static = "443"
          }
        }
      }
      service {
        name = "global-openvpn-check"
        tags = ["global", "cache"]
        port = "https"
        check {
          name     = "alive"
          type     = "tcp"
          interval = "10s"
          timeout  = "2s"
        }
      }
      env {
        "CONTAINER_IP" = <OpenVPNホストのIPアドレス("192.168.0.99/24")>
        "DEFAULT_GW" = <OpenVPNホストのゲートウェイ("192.168.0.1")>
      }
    }
  }
}
```

設定ファイルをもとにNomad上でコンテナを起動します。

```shell-session
$ nomad run -address http://192.168.0.10:4646/ /opt/nomad/openvpn.nomad
```

起動したコンテナの状態は下記のコマンドで確認することができます。
動いていますね。

```shell-session
$ nomad status -address http://192.168.0.10:4646/
ID       Type     Priority  Status
openvpn  service  50        running
```

ちなみに、停止する際は下記のコマンドです。

```shell-session
$ nomad stop -address http://192.168.0.10:4646/ openvpn
```

これでクラスタ環境の構築は完了です。

### 確認

試しに、コンテナが稼働しているホストを落としてみます。
各端末でコンテナを表示した結果は下記のとおりです。現在は`ホスト1`で実行されています。

```shell-session:ホスト1
$ docker ps -a
CONTAINER ID        IMAGE               COMMAND                  CREATED             STATUS              PORTS                                                              NAMES
a69bfb6179c1        1685a2b93253        "/usr/bin/entry.sh..."   2 hours ago         Up 2 hours          192.168.0.10:443->443/tcp, 192.168.0.10:443->443/udp, 1194/udp   pi-openvpn-03bcc585-333b-7d24-2be9-2485608d7548
```

```shell-session:ホスト2
$ docker ps -a
CONTAINER ID        IMAGE               COMMAND                  CREATED             STATUS              PORTS                                                              NAMES
```

```shell-session:ホスト3
$ docker ps -a
CONTAINER ID        IMAGE               COMMAND                  CREATED             STATUS              PORTS                                                              NAMES
```

この状態で`ホスト1`をシャットダウンします。

```shell-session:ホスト1
$ sudo shutdown now
```

しばらくすると`ホスト2`で自動的に立ち上がりました。

```shell-session:ホスト2
$ docker ps -a
CONTAINER ID        IMAGE               COMMAND                  CREATED             STATUS              PORTS                                                              NAMES
d8aa46ff4a71        1685a2b93253        "/usr/bin/entry.sh..."   34 minutes ago      Up 34 minutes       192.168.0.20:443->443/tcp, 192.168.0.20:443->443/udp, 1194/udp   pi-openvpn-901b63f8-701b-6eca-098c-df1eaf8aee54
```

```shell-session:ホスト3
$ docker ps -a
CONTAINER ID        IMAGE               COMMAND                  CREATED             STATUS              PORTS                                                              NAMES
```

### クライアントからの接続

クライアントを作成したタイミングで、`/mnt/vpn/openvpn/ovpn/<クライアント名>.ovpn`に接続用のファイルが出力されています。

```shell-session
$ ls /mnt/vpn/openvpn/ovpn/
template.ovpn  <クライアント名>.ovpn
```

クライアント端末からは、このファイルを使って、VPN接続することができます。

# おわりに
## 反省点
冒頭に記載しましたが、下記が反省点です。

* __Dockerを使ってしまったこと__  
ホスト環境をきれいに保ちたいという思いからDockerを使いました。しかし、ネットワークを複雑にしてしまいました。  
* __冗長化をオーケストレーションツールで対応してしまったこと__  
オーケストレーションツールを使うことで、Dockerコンテナをよしなに管理できるかなと思い使いました。しかし、今回の利用に適さないオーケストレーションツールもあり、管理・設定が複雑になりました。また、今回は1並列でしか動かさないためオーケストレーションツールは必要はありませんでした。
* __Raspberry pi 1（非力なARM）で構築してしまったこと__  
使えるツールが限られてしまいました。

## オーケストレーションツール
オーケストレーションツールを選ぶうえで、次の点が課題でした。

* ARM対応（サクッと）
* ブリッジ接続

正しくはない可能性がありますが、次のような感じだと思われます。

| | ARM対応 | ブリッジ接続 | 
|:-----------|:------------:|:------------:|
| Swarm | ○ | × |
| Kubernetes | × | ○ |
| Mesos | × | ○ |
| Nomad | ○ | ○ |

Raspberry piでもサクッとつかえて、ブリッジ接続が可能な`Nomad`にしました。

## DDNS設定
OpenVPNは記載した手順で動きますが、それだけでは外部からアクセスはできません。
次のどちらかの設定が必要です。

* 固定IPアドレスの取得
* ダイナミックDNSサービスの利用

今回は`ダイナミックDNSサービスの利用`で対応しており、「[MyDNS](http://www.mydns.jp/)」を使っています。

OpenVPN環境を作るにあたり、MyDNSに定期的にアクセスして、IPアドレスのアップデートを行うDockerコンテナを作りました。簡単なものなので説明は省きますが、[Github](https://github.com/ykshr/pi-mydnsclient)にアップロードしています。