開発用マシンからKubernetesのクラスタ内ネットワークに透過的にアクセスする

## モチベーション

* マイクロサービス化を進めたり、サービスで利用しているミドルウェアが多くなると、ローカル環境にすべての依存サービス、ミドルウェアを同居させることがリソース的にきびしくなります
  * 基本的にすべてをAWS等のクラウドプロバイダ上にデプロイして、開発対象のサービスだけ置き換えられるといいですよね。しかも高速に。
  * Kubernetesならできます。

## TL;DR;

* minikubeとそれ以外でハマりどころが違いました。
* ややこしいことに、K8Sと開発マシン、どちらからどちらへの通信を可能にするのか、もしくは両方なのか、によってやることがが変わります。
* 以下では特に説明できていませんが、通信だけでなく、K8Sクラスタ内にあるVolumeをマウントしてローカルプロセスから参照したい・・・というような場合は、Telepresence一択です

## この記事で説明しないこと

* ローカルでもIngressをテストしたいよ！という場合は @superbrothers さんの[「Minikube で快適に Ingress を利用する」](http://qiita.com/superbrothers/items/13d8ce012ef23e22cb74)がおすすめです

## 登場するOSS

* [telepresence](https://github.com/datawire/telepresence)
* [kube-openvpn](https://github.com/pieterlange/kube-openvpn)

## 基本的な考え方

### VPN

最もスタンダードなのは、K8Sクラスタ内ネットワークと開発用マシンのネットワークをつなぐためにVPNを使うというものです。

**minikube以外**

your-laptop <---vpn over internet---> K8S cluster-internal net

**minikube**

your-laptop <---vpn over private network---> K8S cluster-internal network

### ホストオンリーネットワーク

一方で、例えば、VirtualBoxの場合はVirtualBoxが作ったvboxnetに繋がる`vboxnet<N>`というインタフェース経由でminikubeVMのネットワークにアクセスできます。

your-laptop <---vboxnet---> VirtualBox machine

minikubeVMはkube-proxyによって設定されたiptablesによって、VM→K8Sクラスタ内ネットワークへのルートが設定されています。かつ、minikubeVMは`cat /proc/sys/net/ipv4/ip_forward`が`1`となることからわかるようにIPフォワードが有効化されています。

そのため、ホストマシンに、K8SクラスタのServiceやPod IPのCIDRへのパケットをminikubeVM経由でルーティングするようなルートを設定してやれば、結果的にホストマシンからK8Sクラスタ内ネットワークにアクセスすることができます。

macOS --route--> minikubeVM --route--> K8S

### iptables + sshポートフォワード = sshuttle

K8Sでは、kubectlでK8SのAPIサーバに繋がる状態でありさえすれば、`kubectl port-forward`コマンドでローカルポートフォワードができます。つまり、ローカルの特定ポートへのパケットをK8Sクラスタの特定podの特定ポートに転送することができます。

これを応用して、特定podをssh serverにして、開発用マシンからフォワードされたポート経由でSSHトンネルを作成し、iptablesを使ってK8Sネットワーク宛のパケットをSSHトンネル経由とpod経由でK8Sネットワークに送るようにすれば、あたかもローカルプロセスがK8Sクラスタのネットワークに繋がったような状態にできます。
[sshuttle](http://sshuttle.readthedocs.io/en/stable/)というOSSがこれを実装していて、telepresenceの`-m vpn-tcp`はこれを利用しています。

参考: [Telepresence - How it works](http://www.telepresence.io/discussion/how-it-works.html)

## minikube以外の場合

### 開発マシン→K8S, K8S→開発マシン,

#### telepresence

以下の欠点に目を瞑れるのであれば[telepresence](https://github.com/datawire/telepresence/)を使いましょう。

* telepresenceの設計思想的に、また実際telepresenceのスタンダードなメソッド(inject-tcp)は特に、特定プロセスからK8Sクラスタ内のIPアドレスやDNS名が見えるようにします。
    * 結果的に、「開発機で動いているWebアプリを、クラスタ内のDNS名でChromeからアクセス」というようなことはできません。
    * IngressなりNodePort and/or LoadBalancerを使うワークアラウンドもありですが、ちょっと面倒かもしれません

#### kube-openvpn

TODO

## minikubeの場合

VirtualBox前提で説明しますが、原理的にはVMWareなどでも一緒なはず…？

### kube-openvpn

開発マシン・K8S間の双方向通信ができます。

#### 開発マシン→K8S

~~K8Sクラスタ外のDNS名前解決ができなくなるのでNG~~

~~試してだめだったこと~~

~~- OpenVPNサーバ側の設定~~
    - ~~`push "redirect-gateway"`を追加してみた~~
    - ~~`push "block-outside-dns"`を追加してみた~~

~~おそらく、[telepresence同様](https://github.com/datawire/telepresence/issues/160)、DNSルックアップがループしている?~~

気をつけることとしては、あえてSplit DNSにすることと、K8Sクラスタ内ネットワークのドメイン配下の名前解決をするときだけK8SのDNSを使うように、開発用マシン側の設定を変更すること。

##### あえてSplit DNSにする

具体的には、OpenVPN Server側の設定で以下のように`push "block-outside-dns"`や`push "dhcp-options DNS ..."`を記述しないようにする。

前者はVPNクランアント側でホストに元々設定されていたDNSを使わないようにする(VPNサーバ側のDNSの使用を矯正する)、後者はDHCP Options経由でVPNクライアント側がVPN接続後に名前解決に使うDNSサーバの場所を強制するという設定。

どちらを設定しても、VPN接続後にホストOSからはminikubeVMから直接見えるホスト名しか解決できなくなってしまう。また、minikubeVMはminikubeVMから直接見えないホスト名についてはホストOSのDNSレゾルバーに委譲するので、ホストOSのDNSリゾルバーをこのように無効化してしまうと、minikubeVMから直接見えるホスト名以外解決できなくなってしまう。例えば、google.comがホストOSからもminikubeVMからも解決できなくなってしまう。

```
server ${OVPN_NETWORK_ROUTE}
topology subnet
verb ${OVPN_VERB}

# Filled by Secrets object. Use generic names
key ${EASYRSA_PKI}/private.key
ca ${EASYRSA_PKI}/ca.crt
cert ${EASYRSA_PKI}/certificate.crt
dh ${EASYRSA_PKI}/dh.pem
tls-auth ${EASYRSA_PKI}/ta.key

key-direction 0
keepalive 10 60
persist-key
persist-tun
#push "block-outside-dns"

proto ${OVPN_PROTO}
cipher ${OVPN_CIPHER}
tls-cipher ${OVPN_TLS_CIPHER}

# Rely on scheduler to do port mapping, internally always 1194
port 1194
dev tun0

user nobody
group nogroup

push "dhcp-option DOMAIN ${OVPN_K8S_DOMAIN}"
#push "dhcp-option DNS ${OVPN_K8S_DNS}"
```

参考: [Push DNS for only a domain OpenVPN - Server Fault](https://serverfault.com/questions/768749/push-dns-for-only-a-domain-openvpn)
[Mac OS X 10.8.4 で VPNを使うときに知っておきたいこと | hiro345](http://www.sssg.org/blogs/hiro345/archives/14625.html)

##### 開発用マシン側で、K8Sクラスタ内のDNS名を解決するときだけK８SのDNSを使う

ここまでの手順でK8S ServiceのCluster IPにVPN経由でアクセスできるようになる。
ただし、まだK8SのServiceのDNS名などは解決できる状態にははなっていない。あえてVPNサーバ側のDNS設定を利用しないようにしたから。

VPN経由でK8S ServiceのIPにアクセスできるということは、kube-dnsのIPにもアクセスできるということ。
この状態で、cluster.local以下の名前解決のためのDNSクエリがkube-dnsにフォワードされるようにすれば名前解決もできるようになる。

`/etc/resolver/cluster.local`

```
nameserver 10.0.0.10
```

#### K8S→開発マシン

[kube-openvpnのRouting back to the client参照](https://github.com/pieterlange/kube-openvpn#routing-back-to-the-client)

### 開発マシン→K8S

telepresenceのようにLD_PRELOADを乗っ取ったりVPN over SSHを使わなくてもできる。

### KubernetesのService CIDRへの静的ルート追加

```
sudo route -n add 10.0.0.0/24 $(minikube ip)
```

これでkube-dnsはじめ、K8S ServiceのCluster IPにminikubeVM経由でアクセスできるようになる。
この状態で、cluster.local以下の名前解決のためのDNSクエリがkube-dnsにフォワードされるようにすればよい。

`/etc/resolver/cluster.local`

```
nameserver 10.0.0.10
```

### テスト

nslookupはcurlなどが使うMacOSのDNSスタックを使わないので、このアプローチのテストには使えない。以下のようにdns-sdを使う。

```
$ dns-sd -G v4 nginx.default.svc.cluster.local
DATE: ---Tue 13 Jun 2017---
11:30:07.937  ...STARTING...
Timestamp     A/R Flags if Hostname                               Address                                      TTL
11:30:07.939  Add     2  0 nginx.default.svc.cluster.local.       10.0.0.242                                   39
```

dns-sdが通るなら、curlなどでも通るはず。
試しにkubectl run & exposeで起動したnginxにアクセスしてみる。

```
$ curl nginx.default.svc.cluster.local
<!DOCTYPE html>
<html>
<head>
<title>Welcome to nginx!</title>
<style>
    body {
        width: 35em;
        margin: 0 auto;
        font-family: Tahoma, Verdana, Arial, sans-serif;
    }
</style>
</head>
<body>
<h1>Welcome to nginx!</h1>
<p>If you see this page, the nginx web server is successfully installed and
working. Further configuration is required.</p>

<p>For online documentation and support please refer to
<a href="http://nginx.org/">nginx.org</a>.<br/>
Commercial support is available at
<a href="http://nginx.com/">nginx.com</a>.</p>

<p><em>Thank you for using nginx.</em></p>
</body>
</html>
```

Chromeでアクセスすることも可能。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/5912/8998979e-46d6-63e4-b9c9-e48aba10537f.png)

## 参考リンク集

* [minikube - Routing an internal Kubernetes IP address to the host system - Stack Overflow](https://stackoverflow.com/questions/42268814/routing-an-internal-kubernetes-ip-address-to-the-host-system)
* [networking - Dev machine as part of Minikube's network? - Stack Overflow](https://stackoverflow.com/questions/42652781/dev-machine-as-part-of-minikubes-network)
* [domain name system - dig & nslookup can't resolve local hosts unless I specify the (local) DNS server to use - Server Fault](https://serverfault.com/questions/607908/dig-nslookup-cant-resolve-local-hosts-unless-i-specify-the-local-dns-server)
    * OS標準のDNSではなく、dnsmasqを使ったケース。dnsmasqでupstreamのDNSサーバを変えたのに、nslookupしてもそのDNSサーバが使われないという問題
    * この場合、基本的にDNSクエリの経路はOSのDNS→dnsmasq→upstream DNSとなるが、nslookupやpingはこの経路を通らないから。
これはCentOSの例だが、実はmacOSも一緒
* [Using Dnsmasq for local development on OS X - Passing Curiosity](https://passingcuriosity.com/2013/dnsmasq-dev-osx/)
    * .devでおわるあらゆるドメイン名を127.0.0.1に解決させる方法
    * 今回のトピックとはあまり関係しませんが
* [linux - How to enforce dnsmasq to use dns for some hosts? - Server Fault](https://serverfault.com/questions/420689/how-to-enforce-dnsmasq-to-use-dns-for-some-hosts)
* [dns - Route complete TLD (*.dev for example) to 127.0.0.1 - Ask Ubuntu](https://askubuntu.com/questions/233195/route-complete-tld-dev-for-example-to-127-0-0-1)
* [Use different DNS servers for different domains - Mac OS X Hints](http://hints.macworld.com/article.php?story=2004062902195410)
* [macos - Do /etc/resolver/ files work in Mountain Lion for DNS resolution? - Ask Different](https://apple.stackexchange.com/questions/74639/do-etc-resolver-files-work-in-mountain-lion-for-dns-resolution)