はじめに
外出先から自宅にアクセスできるようにOpenVPNサーバーを動かしているのですが、気づいたら止まっていることが何度かあったため、HAクラスタ構成で稼働させてみました。
完成イメージ
完成イメージは次の通りです。
オーケストレーションツールで、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チカ」に記載の手順と同様です。
※ Dockerのインストール手順が変わっているため、Dockerは次の手順でインストールしています。
NASのマウント
今回、各ホスト間でOpenVPNサーバーの設定などを共有するためにNASを用いています。そのため、共有フォルダを/mnt/vpn
にマウントしました。
$ 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をインストールしました。
参考:インストール手順
GPGキーを追加します。
$ 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リポジトリを追加します。
$ 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をインストールします。
$ sudo apt-get install docker-engine
sudoなしでdocker
が実行できるように、ユーザーをdockerグループに追加します。
$ sudo groupadd docker
$ sudo gpasswd -a ${USER} docker
Dockerを起動します。
$ sudo service docker start
OpenVPNイメージの作成・設定
OpenVPNサーバーのDockerイメージを作ります。
ここでは、主要なファイルを記載します。その他のファイルや構成はGithubを参考にして下さい。
Dockerfileは次の通りです。鍵の生成などは、EasyRSAを用いるようにしました。
特に環境によって変更が必要な箇所はありません。
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接続先のアドレスなどが変更が必要な箇所です。
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サーバーの設定にあわせて、プロトコルやポートの変更も必要です。
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
各種設定ファイルを変更した後、イメージをビルドします。
$ docker build -t pi-openvpn:latest ./
ビルドが完了したのち、生成したイメージを使ってOpenVPNサーバーの初期化をします。初期化では、マウントした共有フォルダに対して設定ファイルなどを出力します。
※Raspberry piが非力なため非常に時間がかかりました。
$ docker run -v /mnt/vpn/openvpn:/etc/openvpn -v /mnt/vpn/EasyRSA:/usr/local/EasyRSA --rm -it pi-openvpn:latest ovpn_init_server
次にクライアントを追加します。クライアント名は適宜変更してください。
ここでは、先ほどと同様にマウントした共有フォルダに対してクライアントの設定ファイルなどを出力します。
$ 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を設定したいです。そのため、新たにブリッジを作成します[図の下側]。
なお、ブリッジを作成する手順の中で、ネットワークが切れるタイミングがあります。そのため、Raspberry piに直接キーボードとディスプレイを接続して作業する方が安全です。
bridge-utilsをインストールします。
$ sudo apt-get update
$ sudo apt-get install bridge-utils
ブリッジを作成します。
※ ここでネットワークの接続が切れます。
$ 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)をブリッジに接続します。
$ ifconfig eth0 0.0.0.0 promisc up
デフォルトゲートウェイを設定します。
$ brctl addif br0 eth0
ここでインターネットに接続できるはずですので、接続を確認します。
$ sudo apt-get update
接続できることが確認できたら、Dockerの起動/終了時に上記設定が実行されるようにシェルを作成します。なお、あまり良くはないですが、今回はこのシェルの中でNASのマウントも行っています。
#!/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
#!/bin/sh
br="br0"
eth="eth0"
ip=<ホストのIPアドレス(192.168.0.10)>
ifconfig $br down
brctl delbr $br
ifconfig $eth $ip
exit 0
上記シェルに実行権限を付与します。
$ chmod +x ~/startup.sh
$ chmod +x ~/shutdown.sh
Dockerの起動/終了時に実行されるよう設定します。
[Service]
ExecStartPost=/home/pi/startup.sh
ExecStopPost=/home/pi/shutdown.sh
設定を反映します。
$ sudo systemctl daemon-reload
これで、Dockerネットワークの変更は完了です。
再起動後、次のようなコンテナを立ち上げてPINGなどで接続を確認してもいいかもしれません。
$ docker run -d --net shared_nw --ip 192.168.0.90 resin/rpi-raspbian:jessie sleep 50
$ ping 192.168.0.90
クラスタの構築
Consul
Consulをインストールします。
ARM用のバイナリファイルをダウンロードするだけです。
$ 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
サーバーの設定ファイルを作成します。
{
"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"
}
上記設定ファイルでサーバーを起動します。
$ consul agent -config-dir /etc/consul.d >> /tmp/consul.log &
構築されたクラスタを確認します。
すべてのノードが表示されると思います。
$ 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の起動/終了時に起動されるよう設定します。
[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用のバイナリファイルをダウンロードするだけです。
$ 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
サーバーの設定ファイルを作成します。
# 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"
}
}
上記設定ファイルでサーバーを起動します。
$ sudo nomad agent -config=/etc/nomad.d/nomad.hcl >> /tmp/nomad.log &
構築されたクラスタを確認します。
すべてのノードが表示されると思います。
$ 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の起動/終了時に起動されるよう設定します。
[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
自動起動/終了の設定を反映します。
$ sudo systemctl daemon-reload
$ sudo systemctl enable nomad
これで、Nomadの設定は完了です。
OpenVPNサーバーの起動と接続
OpenVPNサーバーの起動
先ほどまでの手順で環境の構築は完了しました。
あとは、OpenVPNサーバーの起動です。
まず、起動に必要な設定ファイルを作成します。
ここで、OpenVPNホストのIPアドレス(192.168.0.99)
がルーターに設定する443ポートの転送先アドレスです。
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上でコンテナを起動します。
$ nomad run -address http://192.168.0.10:4646/ /opt/nomad/openvpn.nomad
起動したコンテナの状態は下記のコマンドで確認することができます。
動いていますね。
$ nomad status -address http://192.168.0.10:4646/
ID Type Priority Status
openvpn service 50 running
ちなみに、停止する際は下記のコマンドです。
$ nomad stop -address http://192.168.0.10:4646/ openvpn
これでクラスタ環境の構築は完了です。
確認
試しに、コンテナが稼働しているホストを落としてみます。
各端末でコンテナを表示した結果は下記のとおりです。現在はホスト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
$ docker ps -a
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
$ docker ps -a
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
この状態でホスト1
をシャットダウンします。
$ sudo shutdown now
しばらくするとホスト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
$ docker ps -a
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
クライアントからの接続
クライアントを作成したタイミングで、/mnt/vpn/openvpn/ovpn/<クライアント名>.ovpn
に接続用のファイルが出力されています。
$ 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」を使っています。
OpenVPN環境を作るにあたり、MyDNSに定期的にアクセスして、IPアドレスのアップデートを行うDockerコンテナを作りました。簡単なものなので説明は省きますが、Githubにアップロードしています。