Telepresence で Microservices on Kubernetes の開発環境構築

この記事は以下の 3 点について書いています。

- Microservices on Kubernetes の開発環境の課題と Telepresence の概要
- Telepresence を使い、ローカルのコンテナから Kubernetes 上のサービスにアクセスするサンプル
- Telepresence を利用した、Spring Bootの開発環境のサンプルと解説

# 背景

## Microservices on Kubernetes の開発環境の課題

Kubernetes 上で動作するマイクロサービスの開発では、あるサービスを開発する際に依存関係をどのように用意するかが問題になります。

ローカルにすべての依存関係を用意しようとすると、クラウドが提供するリソースのエミュレートや PC のリソース不足などの問題が発生します。

一方、Kubernetes 上ですべて開発しようとすると、開発中のデバッグが困難になります。
適切にデバッグするためには、開発者全員が Kubernetes をよく理解している必要があります。

その他のメリット・デメリットを含め、この問題について Telepresence の開発元 Datawire のマイクロサービスアーキテクチャガイド [Development environments for Kubernetes](https://www.datawire.io/guide/development/development-environments-microservices/) にまとめられています。

## Telepresence とは

上記の問題の解決策として、 PC と Kubernetes を接続し、手元で開発しつつ依存関係は Kubernetes を見にいくという手段があります。
PC と Kubernetes を VPN で接続するイメージです。

それを実現してくれるのが Telepresence です。
Telepresenceは OSS であり、CNCF Sandbox プロジェクトの 1 つです。

# 検証環境

今回は以下の環境で検証しました。

```bash
$ telepresence --version
0.97
$ docker --version
Docker version 18.09.2, build 6247962
$ kubectl version
Client Version: version.Info{Major:"1", Minor:"13", GitVersion:"v1.13.1", GitCommit:"eec55b9ba98609a46fee712359c7b5b365bdd920", GitTreeState:"clean", BuildDate:"2018-12-13T19:44:10Z", GoVersion:"go1.11.2", Compiler:"gc", Platform:"darwin/amd64"}
Server Version: version.Info{Major:"1", Minor:"11+", GitVersion:"v1.11.5-eks-6bad6d", GitCommit:"6bad6d9c768dc0864dab48a11653aa53b5a47043", GitTreeState:"clean", BuildDate:"2018-12-06T23:13:14Z", GoVersion:"go1.10.3", Compiler:"gc", Platform:"linux/amd64"}
```

Kubernetes は Amazon EKS を利用しています。

# サンプル実行

[Telepresence のドキュメント](https://www.telepresence.io/tutorials/docker) のサンプルを実行していきます。

## サンプルアプリケーションデプロイ

まずは [Telepresence のドキュメント](https://www.telepresence.io/tutorials/docker) に記載の通り、サンプルアプリケーションをデプロイしておきます。

```bash
$ kubectl run qotm --image=datawire/qotm:1.3 --port=5000 --expose
```

## Telepresence 実行サンプル

Telepresence では、`--docker-run` というオプションを利用して、ローカルで Kubernetes 上と通信可能なコンテナを起動することができます。

例えば、以下のように起動します。

```bash
$ telepresence --docker-run --rm -it alpine sh
```

curl を使い、先ほどデプロイしたサンプルアプリケーションへのアクセスを確認します。

```
# apk add --no-cache curl
# curl http://qotm:5000/
{"hostname":"qotm-5969b5f959-vv82j","ok":true,"quote":"The light at the end of the tunnel is interdependent on the relatedness of motivation, subcultures, and management.","time":"2019-02-26T07:13:41.112912","version":"1.3"}
```

このように、非常に簡単に Kubernetes 上のアプリケーションにアクセスできました。

`telepresence --docker-run` の `--docker-run` 以降の部分は、通常の `docker run` コマンドのオプションを与えるようになっています。

実際に Telepresence のヘルプを確認すると...

```bash
$ telepresence -h
usage: telepresence [-h] [--version] [--verbose] [--logfile LOGFILE]
                    [--method {inject-tcp,vpn-tcp,container}]
                    [--new-deployment DEPLOYMENT_NAME | --swap-deployment DEPLOYMENT_NAME[:CONTAINER]
                    | --deployment EXISTING_DEPLOYMENT_NAME]
                    [--context CONTEXT] [--namespace NAMESPACE]
                    [--expose PORT[:REMOTE_PORT]]
                    [--also-proxy CLOUD_HOSTNAME] [--mount PATH_OR_BOOLEAN]
                    [--env-json FILENAME] [--env-file FILENAME]
                    [--run-shell | --run ... | --docker-run ...]
    :
    :
    :
  --docker-run ...      Run a Docker container, by passing the arguments to
                        'docker run', e.g. '--docker-run -i -t ubuntu:16.04
                        /bin/bash'. Requires --method container.
```

このように、`docker run` に渡す引数を与えるようにと書かれています。

`telepresence --docker-run` は内部で `docker run` コマンドを実行しており、その内容は ./telepresence.log で確認できます。

```bash
$ cat ./telepresence.log
    :
    :
    :
 256.9 TEL | Main process (docker run --name=telepresence-1551165091-314365-90696 --network=container:telepresence-1551165070-841074-90696 -e=TELEPRESENCE_POD -e=TELEPRESENCE_CONTAINER -e=KUBERNETES_SERVICE_PORT -e=KUBERNETES_PORT_443_TCP_PROTO -e=QOTM_SERVICE_HOST -e=QOTM_PORT_5000_TCP -e=QOTM_PORT_5000_TCP_PORT -e=QOTM_PORT_5000_TCP_ADDR -e=KUBERNETES_SERVICE_HOST -e=KUBERNETES_PORT_443_TCP -e=KUBERNETES_PORT_443_TCP_PORT -e=KUBERNETES_PORT_443_TCP_ADDR -e=QOTM_PORT -e=QOTM_PORT_5000_TCP_PROTO -e=KUBERNETES_SERVICE_PORT_HTTPS -e=KUBERNETES_PORT -e=QOTM_SERVICE_PORT -e=TELEPRESENCE_ROOT -e=TELEPRESENCE_METHOD --volume=/tmp/tel-5i3c8wyy/fs:/tmp/tel-5i3c8wyy/fs --init --rm -it alpine sh)
    :
    :
    :
```

Telepresence が環境変数などのオプションを加えた上で `docker run` を実行していることが分かります。

# Spring Boot の開発環境構築

ここまでで Telepresence の概要がつかめたので、実際に Spring Boot の開発環境を構築します。

## シェルスクリプト作成

開発環境構築といっても、以下のシェルスクリプトを用意するだけです。

```bash
#!/bin/bash
#
# 開発環境のコンテナを起動するスクリプト
#
# 引数1. 開発対象のプロジェクトホームディレクトリ
#
# <実行例>
# ./develop.sh .
#

set -o errexit
set -o nounset
set -o pipefail
set -o xtrace

readonly DEVELOP_PROJECT_DIR="$1"
readonly DEVELOP_PROJECT_ABSOLUTE_DIR=$(cd ${DEVELOP_PROJECT_DIR}; pwd)

readonly IMAGE='openjdk:8u191-jdk-alpine3.8'
readonly APP_PORT='8080'
readonly DEBUG_PORT='5005'

readonly CONTAINER_WORKING_DIR='/opt/work'

telepresence \
  --docker-run \
  --rm \
  -p "${APP_PORT}":"${APP_PORT}" \
  -p "${DEBUG_PORT}":"${DEBUG_PORT}" \
  -v "${DEVELOP_PROJECT_ABSOLUTE_DIR}":"${CONTAINER_WORKING_DIR}" \
  -v ~/.m2:/root/.m2 \
  -w "${CONTAINER_WORKING_DIR}" \
  "${IMAGE}" \
  ./mvnw spring-boot:run \
  -Drun.jvmArguments="-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=n,address=${DEBUG_PORT}"
```

## 解説

上記のシェルスクリプトのポイントを順に解説していきます。

### spring-boot-devtools によるホットデプロイ

コードを書き、コンテナにビルドしてから動作確認するのではあまりに時間がかかります。
そこで、spring-boot-devtools を使っています。

pom.xml に spring-boot-devtools を追加し、`./mvnw spirng-boot:run` というコマンドで起動することで、ソースコードの変更が即座に反映されるようになります。

このとき、 `-v "${DEVELOP_PROJECT_ABSOLUTE_DIR}":"${CONTAINER_WORKING_DIR}"` の部分で、プロジェクトホームディレクトリをコンテナのワーキングディレクトリにマウントしていることがポイントです。

これにより、コンテナ内で開発せずとも、ローカルで IDE を使っての開発が可能になります。

※ IntelliJ で spring-boot-devtools を利用する際は IntelliJ 側に追加の設定が必要なのでご注意ください

### --rm オプション

私はローカルで実行するコンテナには基本的に `--rm` オプションをつけるようにしています。
`--rm` オプションをつけることで、コンテナは停止時に削除されるようになります。
これにより、PC に不要なコンテナがたまっていかないようにします。

### .m2 ディレクトリのマウントによる Maven キャッシュの利用

`--rm` で毎回コンテナを作り直すことにすると、そのままでは .m2 ディレクトリの Maven キャッシュが効かず、毎回起動に長時間かかるようになってしまいます。

`-v ~/.m2:/root/.m2` で .m2 ディレクトリをマウントすることで、この問題を解決しています。

`--rm` オプションを使わずコンテナを使いまわすという解決策は、コンテナを削除しないモチベーションを発生させてしまい、再現性の低い開発環境を生み出してしまう可能性があるため推奨しません。

### デバッガの実行

コンテナ上でアプリケーションが動いていても、デバッガは問題なく利用できます。
起動コマンドに `-Drun.jvmArguments="-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=n,address=${DEBUG_PORT}"` というオプションを付与し、 `-p "${DEBUG_PORT}":"${DEBUG_PORT}"` のようにしてデバッガ用のポートをホストにフォワードするだけです。
これで IDE のデバッガが通常通り動作します。

# まとめ

Telepresence は `--docker-run` を使うだけであれば非常に低い学習コストで利用可能でした。
Kubernetes 上の Deployment の入れ替えなど、応用的な利用方法もあるようですが、まずは `--docker-run` だけでも使ってみるとよさそうです。

今回は Spirng Boot の開発環境を例にしましたが、コンテナ上で開発環境を構築するには結構なコツを知っている必要があります。
例えば、webpack-dev-server をコンテナ上で使用する場合、設定を変更しないとライブリロードしてくれません。

Kubernetes やその周辺ツールを使いこなすためには、実際に色々構築して知見をためていく必要がありそうです。

# 余談 (Telepresence をコンテナ化するべきか)

Amazon EKS + Telepresence on Mac を利用する場合、以下の通り依存関係となるツールが非常に多いです。

- Docker
- AWS CLI
- kubectl
- aws-iam-authenticator
- osxfuse
- Telepresence

Docker はいずれにせよインストールが必要ですが、その他 5 つのツールを全ての開発者にインストールしてもらうのは少し手間になります。
そこで、Docker 以外の 5 つのツールをコンテナ化して配布することも考えられます。

コンテナ化するメリットは

- 開発環境セットアップの手間が減る
- 開発者から Kubernetes を隠蔽できる

といった点だと思われます。

一方、コンテナ化しないメリットは

- 興味を持った開発者にとって開発環境を理解するコストが下がる
- 開発者が少しずつ Kubernetes にふれ、学んでいく可能性がある

といった点だと思います。

Telepresence までコンテナ化することにより、ポータビリティは向上しますが、Dev - Ops の分離のような現象が発生する可能性があります。
何でもコンテナにすれば良い訳ではないという一例でした。

# 参考
- [Telepresence](https://www.telepresence.io/)
- [開発用マシンからKubernetesのクラスタ内ネットワークに透過的にアクセスする](https://qiita.com/mumoshu/items/12e452ce74d957bf137a)
- [Telepresence で EKS 上の k8s クラスタをデバッグする](https://qiita.com/yuzumikan15/items/5e5949f3058535f5e5ec)