Mercari Engineering Blog

We're the software engineers behind Mercari. Check out our blog to see the tech that powers our marketplace.

マイクロサービスのTelepresenceを使ったローカル開発環境の話

f:id:bobchan1915:20190526143822p:plain
あいさつ返答サービスのアーキテクチャ図

今年、株式会社メルカリに入社し、希望であったメルペイ SREチームの配属になったSREのkeke(Twitter: @_k_e_k_e)です。

マイクロサービスは、サービスのスケーラビリティ、新技術への親和性、ベロシティ、モジュラティの観点から、より早く「価値」を届けることにつながり、ソフトウェア開発としても、ビジネスとしても非常に重要です。

本記事はKubernetesクラスタに構築されたマイクロサービスをより効率的に開発するためのツールであるTelepresenceの記事です。

Telepresenceを体験してもらえるように簡単なサービスのアプリケーションコードを用意したので「Telepresenceを使うためのサービスがない」という状況でも簡単に試してみることができます。

目次

本記事は以下のように構成されています。興味のあるセクションだけでもご覧ください。

マイクロサービスでのローカル開発環境の問題

知っている方は多いと思いますがメルカリではMicroservice化が進んでおり、Kubernetesクラスタの上でいくつものサービスが構築されています。

マイクロサービスを導入する理由としては、サービス毎の独立したスケーラビリティ、新技術への親和性、ベロシティ、モジュラティの高さなど挙げられます。それらはより早く価値を届けることにつながり、ビジネスとしても非常に重要です。

f:id:bobchan1915:20190525202318p:plain
https://github.com/warmchang/KubeCon-North-America-2018

それに伴って、サービスごとでチームの編成が、逆コンウェイの戦略と知られる、マイクロサービスのアーキテクチャを真似た開発の組織構造になりやすくなります。

サービスごとに疎な結合を目指す一方で、依存関係があるサービスももちろんあるので、開発をする、テストをする中でそれらがマイクロサービスでの障壁となります。一方で、モノリシックなサービスの場合は、3層アーキテクチャであり、バックエンドは基本的にはすべて単体で完結するのでそのような問題は少ないです。

f:id:bobchan1915:20190526222504p:plain
3層アーキテクチャ

これを解決するために、オープンソースなTelepresenceというリモートクラスタと透過的に双方向プロキシを行ってくれるツールがあります。 GitOpsや継続的デリバリーの概念を満たしながらも、サービス間または環境間での依存性を(透過的に)解決して、より効率よくローカル開発環境を構築できます。

繰り返しになりますが、今回の記事ではTelepresenceとはなにか、どのようなことができるのかを紹介します。

開発環境

本記事は以下のバージョンで開発をしています。

software version
macOS Mojave 10.14.4
Google Cloud SDK 246.0.0
Telepresence 0.99
kubectl 1.12.7
Helm 2.14.0

理論編: Telepresenceってなに

Telepresenceとはリモートクラスタに対してより早く、ローカルで開発することを支援するツールです。現時点(2019/05/28)での最新バージョンは0.99です。

「より早く、ローカルで開発するのを支援するツール」ができることは、大きくまとめると以下の2つです。

  1. クラスタのDeploymentをローカルサーバーと置き換える
  2. クラスタにローカルからアクセスをする

これらの恩恵として

  • ローカルサーバーのリモートクラスタの他サービスへアクセス
  • ローカルサーバーのKuberenetesクラスタのリソースであるsecretsやConfigMapのアクセス
  • リモートクラスタのローカルサーバーへのアクセス

が挙げられ、より簡単に開発をできるようになります。

0. 今回のサービス例

例えば以下のようなサービスがあるとします。このサービスがやることは以下の通りです。

  • クライアントからメッセージを受け取る
  • メッセージに対して返答を取得し、環境変数ENVとともにJSONでクライアントに返す

Secretが読み込まれているのを見せるためにSecretをレスポンスに含めて返していますが、本来の使い方ではないことに留意してください。また、本記事ではIngressを使いません。

f:id:bobchan1915:20190526143822p:plain
あいさつ返答サービスのアーキテクチャ図

つまり、SecretがYoでENVの値がtestだった場合は以下のようなリクエストを行って

curl -X POST クラスタのIP  -d '{"message": "Hi"}'

ENVとメッセージの返答が返ってくるサービスです。

{
     "env": "test"
     "message": "Yo Hi"
}

また、このサービスを作っているチームは継続的デリバリーの観点から手動でイメージの更新は禁止されていて、Git pushしてCircleCIが走るのを待たないといけないチームです。

f:id:bobchan1915:20190525202126p:plain
ワークフロー

1. クラスタのDeploymentをローカルサーバーと置き換える

先ほどのService Aをチームで機能を追加したいとなったときに、どのように開発したらいいでしょうか。もちろん、Service AとService Bをローカルで別プロセスで起動してやることもできますが、スケールしないので、現実味の観点から一つ考えられることはDocker Composeなどでリモートクラスタを再現することです。

f:id:bobchan1915:20190525203621p:plain
Docker Composeによる構築

しかし、いくつも問題があります。

  • Secretを開発者が全員持っていなければならない
  • Service Aの開発中にService Bの変更があった時にPullしなければならない
  • サービスが増える度に、Docker Composeも設定しなければならない
  • SecretやconfigMapなどの値が変わると対応できない

この時に、Telepresenceを使えば以下のように既存のDeployment(=Service A)を置き換えることができます。ローカルでサーバーを立ち上げてTelepresenceを使うと以下のように双方向プロキシを構成してくれます。

f:id:bobchan1915:20190526143744p:plain
TelepresenceでDeploymentを置き換える

これによって、先ほどの問題を解決することができます。

2. クラスタにローカルからアクセスをする

新しくサービスを作りたいけど、ひとまず他のサービスを試しにアクセスしてみたいというときはどのようにしましょう。

一つは以下のようにPodに対してのPort forwardingです。

f:id:bobchan1915:20190526173750p:plain
ServiceBのPodとのPort Forwarding

これによってlocalhostでアクセスをできるようになります。しかし、単一Podへのアクセスはできたにしろ、例えば内部DNSなどKubernetes内部のリソースにはアクセスをすることができません。

Telepresenceを使うと透過的にアクセスをできるようになるため以下のように内部DNSへ問い合わせてServiceへアクセスをすることが可能になります。

f:id:bobchan1915:20190526174354p:plain
Telepresenceによる透過的なアクセス

内部DNSへ問い合わせてClusterIPへアクセスすることはKubernetesクラスタ内で一つのサービスがすることなので、本来の形で試せることになります。

実践編: やってみよう

0. 準備

以下の準備物が既にインストールされていることを前提にしています。

  • Google Cloud SDK
  • Helm
  • kubectl
  • Docker

0.1 アプリケーション側の準備

今回は皆さんが簡単にTelepresenceのユースケースを確認できるように、この簡単なサービスのソースコードを用意しました。

以下のrepositoryをcloneしてください。

github.com

$ git clone git@github.com:KeisukeYamashita/greet-service-api.git
$ cd greet-service-api

今回はGoogle Cloud Platformを使用します。まずGKEクラスタを作ります。

$ make init

デフォルトでは以下のspecになっています。

  • クラスタ名: greeting-cluster
  • ゾーン: asia-northeast1-c
  • Node数: 1
  • プリエンティティブノード

そして、まずService AとServiceBのDocker Imageを作成してCloud RegistoryへPushします。

$ make build

次にHelmを使ってDeploymentをクラスタへデプロイします。

$ make deploy

あとはそれを確認できたら準備は終了です。

$ kubectl get svc, pods

0.2 Telepresenceの準備

準備といえど、インストールするだけです。macOSの場合はHomebrewを使って、以下のようにインストールしてください。

$ brew cask install osxfuse
$ brew install datawire/blackbird/telepresence

1. クラスタのDeploymentをローカルサーバーと置き換える

今回はクライアント、ServiceBとKubernetesのリソースであるconfigMapに板挟みになっているServiceAを置き換えてみましょう。

やることは、TelepresenceでServiceAに機能を追加したServiceA`をローカルで開発することです。

既存のサービスはクライアントに対して以下のようなリクエスト

$ HOST=$(kubectl get svc service-a -o jsonpath="{.status.loadBalancer.ingress[0].ip}:{.spec.ports[0].port}")
$ curl $HOST/api/serviceA -d '{"message": "Hi"}'

に対して以下のように返していました。

{
     "env": "test"
     "message": "Yo Hi"
}

しかし、チームで機能を追加しないといけないため、以下のようにしたいです。

{
     "env": "test"
     "message": "Yo Hi"
     "dateTime": 1558850051
}

新機能としてはdateTimeフィールドにタイムスタンプを返します。図にすると以下のような感じです。

f:id:bobchan1915:20190526145244p:plain
ServiceAの機能追加

開発段階なので、ローカルでさくさく開発したいです。そのようなときはServiceAをServiceA'に置き換えます。

手元でソースコードを以下のように変更しました。

res := &Response{
-        Env:     env,
-        Message: msg.Message,
+        Env:      env,
+        Message:  msg.Message,
+        DateTime: time.Now().Unix(),
}

そして、置き換えるために、以下のコマンドを実行します。

$ docker build . -t service-a
$ telepresence --swap-deployment service-a --docker-run --rm -it service-a

再度、実行します。

$ curl $HOST/api/serviceB -d='{"message": "Hi"}'

すると以下のように返ってきました。

{
     "env": "test"
     "message": "Yo Hi"
     "dateTime": 1558866769
}

見事にローカルのDocker Imageと入れ替わっています。

もちろんプロセスならなんでも良いので、

telepresence --swap-deployment service-a  --expose 5050 \
--run go run main.go 

でも可能です。

ここでSecretを変更してみます。

$ kubectl apply -f serviceB/templates/secret.yaml

そしてアクセスすると以下のようになります。

$ telepresence --swap-deployment service-a --docker-run --rm -it service-a
$ curl $HOST/api/serviceB -d='{"message": "Hi"}'

すると以下のように変わっていました。

{
     "env": "test"
     "message": "Yeah Hi"
     "dateTime": 1558866769
}

2. クラスタにローカルからアクセスをする

f:id:bobchan1915:20190526191019p:plain
Telepresenceでアクセスをする

PublicIPを持たないServiceBがあったとし、自分たちのチームはそのAPIを使ってServiceAを作るとします。

試しにServiceBへアクセスしてみましょう。内部DNSを引いてアクセスするため以下のように実行します。

$ telepresence --run curl -X POST http://service-b:5100/api/serviceB -d '{"message": "Bye"}'

付録

Multi Containerのとき

Podにいくつもコンテナがあるときにはどのようにしましょう。

f:id:bobchan1915:20190526214323p:plain
Multi containerのアプリケーション例

特に、サービスメッシュや何かしらのSide carがコンテがあることが多いと思います。そのようなときは[Deployment名]:[コンテナ 名]を指定し以下のコマンドを実行すると置き換えることができます。

$ telepresence --swap-deployment service-a:app --docker-run --rm -it service-a:0.6.0  

図的には以下のようになっています。

f:id:bobchan1915:20190526223411p:plain
Multi containerでも置き換える

Multi containerでも問題はありません。

ContextやNamespaceの指定

以下のようにcontextnamespaceを指定することができます。

$ telepresence --context=[コンテキスト] --namespace=[ネームスペース] --swap-deployment service-c --run-docker service-c

制限

注意点としては、単一サービスしか置き換えることができないところです。

まとめ

Telepresenceはマイクロサービスを構成するサービスをより効率的に開発するためにはうってつけのツールであると感じました。

デバックがしたいだけなのに、継続的デリバリーに反するからという理由でGit pushしてdev環境へデリバリーすることを待ってからフィードバックを受けるのは時間の無駄です。

このツールを使うことによってローカル環境でなるべくリモートクラスタのリソースを使うことにより、開発環境とクラスタ間の乖離を少なくして、効率よくアプリケーションを書くことができるのではないかと思います。

また、単一のデプロイメントがローカルサーバーにトラフィックが流れるだけで、環境的には本番もしくは開発環境と同等なのでシークレットや環境変数をKubernetes上で共通化でき、開発機が持つ必要がなくなるという面も重要でしょう。

これからKubernetesを触る人はぜひ使ってみてください。最後までありがとうございました!

参考文献