[Kubernetes] クラスタ外から ClusterIP の Service にいい感じにアクセスする

## はじめに

Kubernetes を使っていて、Service (ClusterIP) 経由でアプリケーションにアクセスしたいこと、ありますよね。ClusterIP はクラスタ内の Pods からしかアクセスできないので、ちょっと手元のブラウザからアクセスしたいというときに困ります。

ここでは、ハンズオン形式で手を動かしながら Kubernetes クラスタの外から ClusterIP の Serivce を使ってアプリケーションにいい感じにアクセスする方法を学びます。

## TL;DR

- Service の ClusterIP にはクラスタ外からアクセスできない
- `kubectl port-forward svc/<svc-name>` コマンドは実際には ClusterIP を使っていないので注意
- 実は `kubectl proxy` コマンド + Service proxy サブリソースを使うとクラスタ外から Service にアクセスできる
- `kubectl open-svc` プラグインを使うと ClusterIP の Service にいい感じにアクセスできる

## 事前準備

ここでは minikube を使用してクラスタを構築します。minikube および kubectl のインストールは次のドキュメントを参照してください。

- [Minikubeのインストール \- Kubernetes](https://kubernetes.io/ja/docs/tasks/tools/install-minikube/)
- [kubectlのインストールおよびセットアップ \- Kubernetes](https://kubernetes.io/ja/docs/tasks/tools/install-kubectl/)

```
$ minikube version
minikube version: v1.6.1
commit: 42a9df4854dcea40ec187b6b8f9a910c6038f81a
$ minikube start --kubernetes-version=v1.17.0
$ kubectl version
Client Version: version.Info{Major:"1", Minor:"17", GitVersion:"v1.17.0", GitCommit:"70132b0f130acc0bed193d9ba59dd186f0e634cf", GitTreeState:"clean", BuildDate:"2019-12-07T21:20:10Z", GoVersion:"go1.13.4", Compiler:"gc", Platform:"linux/amd64"}
Server Version: version.Info{Major:"1", Minor:"17", GitVersion:"v1.17.0", GitCommit:"70132b0f130acc0bed193d9ba59dd186f0e634cf", GitTreeState:"clean", BuildDate:"2019-12-07T21:12:17Z", GoVersion:"go1.13.4", Compiler:"gc", Platform:"linux/amd64"}
```

## Service の ClusterIP にはクラスタ外からアクセスできない

Service の ClusterIP にはクラスタ外からアクセスできないとはどういうことでしょうか。ここでは、`nginx` Service を作成してその Service の ClusterIP にクラスタの外からアクセスしてみます。

まず `nginx` Service を作成します。

```
$ kubectl create deploy nginx --image=nginx
deployment.apps/nginx created
$ kubectl expose deploy/nginx --port=80
service/nginx exposed
$ kubectl get svc nginx
NAME    TYPE        CLUSTER-IP      EXTERNAL-IP   PORT(S)   AGE
nginx   ClusterIP   10.96.225.250   <none>        80/TCP    12s
```

ここでは `nginx` Service の ClusterIP は `10.96.255.250` になりました。ではこのアドレスにアクセスしてみます。

```
$ curl 10.96.225.250
curl: (7) Failed to connect to 10.96.225.250 port 80: No route to host
```

正しくアクセスできませんでした。それは ClusterIP がクラスタ内部でのみ有効な仮想的な IP アドレスだからです（Service には ClusterIP 以外に外部 IP を持たせる ExternalIP などがあります）。この仕組みを提供しているのがノードコンポーネントの1つである kube-proxy です。Service（と Endpoints）が作成されると全ノードの kube-proxy が各ノードの設定を変更します（デフォルトは iptables を使用）。この設定変更によって、ノードから出て行くパケットの宛先アドレスが ClusterIP と一致すると、その ClusterIP を持つ Service のバックエンドである Pod グループのアドレスのいずれかに転送されるようになります。

![kube-proxy: iptablesプロキシーモード](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3780/76bd2aa2-9cd1-1fdf-c5ee-294b3be1e574.png)
*[Kubernetes 公式ドキュメント: kube-proxy iptablesプロキシーモード](https://kubernetes.io/ja/docs/concepts/services-networking/service/#proxy-mode-iptables)から引用*


ここから分かる通り、これは Kubernetes ノードのなかだけで有効な設定であるため、クラスタの外からでは ClusterIP 宛てのパケットは宛先不明としてエラーになります（クラスタ内で実行される Pod から送信されるパケットはそれが実行されているノードを経由するため、ClusterIP を使用できます）。

## ClusterIP にアクセスする（クラスタ内編）

クラスタ外からアクセスできないのであればクラスタ内からアクセスすればいいということで、ここではクラスタ内からアクセスする方法を紹介します。

まず最初は `kubectl exec` コマンドを使用する方法です。このコマンドはクラスタ内で実行されている Pod のコンテナのなかで任意のコマンドを実行します。ここでは debian コンテナを実行して、そこからアクセスしてみます。

```
$ kubectl run debian --image debian --command tail -f /dev/null
kubectl run --generator=deployment/apps.v1 is DEPRECATED and will be removed in a future version. Use kubectl run --generator=run-pod/v1 or kubectl create instead.
deployment.apps/debian created
$ kubectl exec debian-5f56977f6b-sx7p2 -it /bin/sh
# apt update -qq && apt install curl
# curl --silent 10.96.225.250 | head -n 3
<!DOCTYPE html>
<html>
<head>
# exit
```

もう1つは `kubectl run` コマンドを使って Pod を作成し、その Pod からアクセスする方法です。`kubectl exec` コマンドとほぼ同じですが、こちらのほうが使い勝手がいいかもしれません。

```
$ kubectl run busybox --restart=Never -it --image=busybox --rm /bin/sh
If you don't see a command prompt, try pressing enter.
/ # wget -q -O- 10.96.225.250 | head -n 3
<!DOCTYPE html>
<html>
<head>
/ # exit
```

TIP としてこのコマンドを `kbb` のようなエイリアスに設定しておくと便利です。

```
alias kbb="kubectl run busybox --restart=Never -it --image=busybox --rm /bin/sh"
```

2つのコマンドを使ってクラスタ内から ClusterIP にアクセスする方法を紹介しました。これでもよさそうですが、クラスタ外から、例えばブラウザから確認したい場合にこれらの方法では難しいです。では次にクラスタ外いからアクセスする方法を紹介します。

## クラスタ外から ClusterIP に紐づく Pod にアクセスする

そもそも ClusterIP ではなく Pod に直接アクセスするのでも構わないかもしれません。それには `kubectl port-forward` コマンドが使えます。このコマンドはローカルのポートを任意の Pod のポートに転送します。ここではローカルの8080番ポートを `nginx` コンテナの80番ポートに転送します。

```
$ kubectl port-forward nginx-86c57db685-glcnv 8080:80 &
$ curl --silent 127.0.0.1:8080 | head -n 3
<!DOCTYPE html>
<html>
<head>
```

また、`kubectl port-forward` コマンドには Service 名を指定することもできます。

```
$ kubectl port-forward svc/nginx 8080:80
```

ここで注意しなければならないのは、`kubectl port-forward` コマンドで Service 名を指定したとしても ClusterIP を使用してアクセスしているではなく、Service のラベルセレクタに一致する Pod の1つに対してポート転送しているということです。本当にそうなのかを確認するのにもっとも簡単なのは `kubectl port-forward service/<service>` コマンドで内部的にどの Kubernetes API を使用しているかを見ることです。

```
$ kubectl port-forward svc/nginx 8080:80 -v=6 2>&1 | grep portforward
I1217 17:07:07.307619   30292 round_trippers.go:443] POST https://192.168.99.100:8443/api/v1/namespaces/default/pods/nginx-86c57db685-glcnv/portforward 101 Switching Protocols in 19 milliseconds
```

上記のログから Service 名を指定していても内部的には単一の Pod の `portforward` サブリソースを使用していることが分かります（サブリソースが何かについては次の節で説明します）。

結果として、この方法では ClusterIP を使用しているわけではないため、もし Service のバックエンドに複数の Pod が存在していたとしても、この方法ではそれらに対してアクセスが分散するわけではありません。

## ClusterIP にアクセスする（クラスタ外編）

ここからは、本丸であるクラスタ外から ClusterIP にアクセスする方法を説明します。

実をいうと Service にはクラスタ外からアクセスするための `proxy` という機能を持っているのです。これと `kubectl proxy` コマンドを組み合わせることでクラスタ外から ClusterIP でアプリケーションにアクセスできます。さっそくアクセスする方法を紹介します。

```
$ kubectl proxy &
Starting to serve on 127.0.0.1:8001
$ curl 127.0.0.1/api/v1/namespaces/default/services/nginx/proxy/
/ # wget -q -O- 10.96.225.250 | head -n 3
<!DOCTYPE html>
<html>
<head>
```

### Service `proxy` サブリソース

このプロキシ機能は、Service の `proxy` サブリソースとして用意されています。そもそもサブリソースとは何でしょうか。サブリソースとは、一般的なリソースの HTTP パスに対して追加でサフィックスを付与した特別な HTTP パスのことです。ここでは Pod を例として説明します。Kubernetes API ではリソースの取得、作成、更新、削除は REST として次のように表現されます。

- `/api/v1/namespaces/<namespace>/pods/<pod>`

これに対して、`kubectl logs/port-forward/exec` といった操作は REST では表現できません。そのため、サブリソースをパスとして標準パスのサフィックスに追加することでこれを表現しています。

- Pod のログを取得する: `/api/v1/namespaces/<namespace>/pods/<pod>/logs`
- Pod のポートを転送する: `/api/v1/namespaces/<namespace>/pods/<pod>/portforward`
- Pod で任意のコマンドを実行する: `/api/v1/namespaces/<namespace>/pods/<pod>/exec`

またプロトコルも標準のパスとは異なるものが使われる場合があります。例えば `logs/portforward/exec` サブリソースでは WebSocket が使用されてます。このほかに、オブジェクトの `status` フィールドのみを更新するための `status` サブリソースやスケールさせるための `scale` サブリソースなどがあります。

ここまでで分かる通り、Service の `proxy` サブリソースは、Service に対するアクセスをプロキシするための機能です。

- `/api/v1/namespaces/<namespace>/services/<scheme>:<service>:<port>/proxy/`

`kubectl proxy` コマンドと同時に使用しているのは、開発者の権限でエンドポイントにアクセスするためです（`kubectl proxy` コマンドは、ローカルでプロキシサーバを実行し、kubectl の設定ファイルにあるトークンや証明書を自動的に付与してくれます）。また、今回はあらゆる操作が可能なクラスタ管理者（cluster-admin）の権限で操作しているため、何の権限付与もなしに Service `proxy` サブリソースが使用できています。権限を持っているかどうかは次のコマンドで確認できます（標準では ClusterRole `edit` 以上の権限で使用できるようになっています）。

```
kubectl auth can-i '*'  services --subresource=/proxy/
```

## クラスタ外から ClusterIP の Service にいい感じにアクセスする

Service `proxy` サブリソースを使うことでクラスタ外からアクセスできることは分かりましたが、いい感じかと言われると長々とした URL をわざわざ入力しなければなりませんし、もっといい感じにアクセスする方法はないのでしょうか。

### kubectl open-svc プラグイン

そんなあなたに `kubectl open-svc` プラグインです。これは、内部的に `kubectl proxy` と同じことをやりつつ Service `proxy` サブリソースの URL を自動的にブラウザで展開してくれるという優れものです。

- [superbrothers/kubectl\-open\-svc\-plugin: kubectl open\-svc plugin makes services accessible via their ClusterIP from outside your cluster](https://github.com/superbrothers/kubectl-open-svc-plugin)

使い方はとっても簡単でブラウザで展開したいサービス名を引数として指定するだけです。

```
kubectl open-svc <service>
```

実際にどのように動作するかは次の GIF をご覧ください。

![kubectl open-svc プラグイン](https://github.com/superbrothers/kubectl-open-svc-plugin/raw/master/screenshots/kubectl-open-svc-plugin.gif)

このコマンドは kubectl プラグイン（kubectl の拡張機能として任意のサブコマンドを kubectl に追加します）として実装されており、追加でインストールが必要です。

### kubectl open-svc プラグインをインストールする

kubectl プラグインをインストールするのに簡単な方法は Krew を使う方法です。Krew は kubectl プラグインマネージャでレジストリに登録された50以上のプラグインを簡単にインストールして使いはじめることができます。

- [kubernetes\-sigs/krew: 📦 Package manager for kubectl plugins](https://github.com/kubernetes-sigs/krew)

まず最初に Krew をインストールします。

bash または zsh を使用しているなら次のコマンドを実行します。

```bash
(
  set -x; cd "$(mktemp -d)" &&
  curl -fsSLO "https://github.com/kubernetes-sigs/krew/releases/download/v0.3.3/krew.{tar.gz,yaml}" &&
  tar zxvf krew.tar.gz &&
  KREW=./krew-"$(uname | tr '[:upper:]' '[:lower:]')_amd64" &&
  "$KREW" install --manifest=krew.yaml --archive=krew.tar.gz &&
  "$KREW" update
)
```

次に、`.bashrc` または `.zshrc` に次の行を追加してシェルを再起動させてください。

```
export PATH="${KREW_ROOT:-$HOME/.krew}/bin:$PATH"
```

fish または Windows を使用している場合は、[ドキュメントの手順](https://github.com/kubernetes-sigs/krew#installation)にしたがってインストールしてください。

正しくインストールされていれば次のコマンドで実行可能なプラグイン一覧が出力されます。`kubectl-krew` が含まれていれば成功です。Krew 自体もプラグインとして実装されています。

```
kubectl plugin list | grep kubectl-krew
```

最後に `kubectl open-svc` プラグインを次のコマンドでインストールします。

```
kubectl krew install open-svc
```

正しくインストールされていれば次のコマンドでヘルプメッセージが出力されます。

```
kubectl open-svc -h
```

### `kubectl open-svc` プラグインで ClusterIP にいい感じにアクセスする

では、早速インストールした `kubectl open-svc` プラグインを使用して ClusterIP にいい感じにアクセスしてみてください。

```
kubectl open-svc nginx
```

## おわりに

いかがだったでしょうか。このエントリは [Cloud Native Days Kansai 2019 前夜際での LT](https://speakerdeck.com/superbrothers/how-to-connect-to-clusterip-service-from-the-outside-of-kubernetes-cluster) として話した内容をハンズオン形式で試していただけるようにしてみました。また合わせて、各要素について少し踏み込んだ説明を入れてみました。少しでも楽しんでもらえていれば幸いです。