Telepresence で EKS 上の k8s クラスタをデバッグする

この記事は [Kubernetes2 Advent Calendar 2018](https://qiita.com/advent-calendar/2018/kubernetes2) 一日目の記事です。

# 概要

ローカルにある docker image を特定の k8s Deployment と差し替えてくれる便利な OSS、[Telepresence](https://www.telepresence.io/) を紹介します。コードの変更箇所をリモートにある k8s クラスタにデプロイしなくても、ローカルで動作確認ができるようになるライブラリです。

※ 今回 k8s は AWS EKS 上に構築していますが、Telepresence の情報量は GCP の方が圧倒的に多いくらい、ほぼ GCP 向けに作られていると思います。

# 環境など
## リモート
すべて AWS リソースを使用しています。
- EKS (`us-west-2`) 上に k8s クラスタを構築
- 他に Dynamo DB, Fluentd, SQS などを作っています

## ローカル
- macOS High Sierra (v10.13.2)
- Telepresence (v0.94)
- Docker for Mac (v18.09.0)
    - k8s 入りですが今回は使いません
- kubectl (v1.10.3)
    - [Amazon EKS が発行した kubectl バイナリ](https://docs.aws.amazon.com/ja_jp/eks/latest/userguide/install-kubectl.html) も選択肢にはありますが、今回は `brew` で入れたものを使っています
- [AWS IAM Authenticator](https://github.com/kubernetes-sigs/aws-iam-authenticator) (v0.4.0-alpha.1)
- eksctl (v0.1.11)
- helm (v2.11.0)

## 使用したソースコード
今年の re:Invent のワークショップで使用された [EKS を Cloudwatch と X-Ray に連携するためのサンプルプロジェクト](https://github.com/aws-samples/reinvent2018-dev303-code) を使用します。

- サービス数がそこそこある
    - サービスが少ないとスペック :money_with_wings: で殴れば minikube や Docker for Mac with Kubernetes が使えてしまうため
- Dynamo DB や SQS など AWS EKS 以外のリソースを使っている
- MIT License :raised_hands: :raised_hands: :raised_hands: (念のため…)

:zap: **このサンプルは EC2 の m5.large インスタンスを4台も使用しているため、無料枠では収まりません。ご注意ください。** :zap:
※ 請求書によると、今回だけで EC2 (61h) + EKS (75h) で合計 $10.54 かかりましたが、それ以外は無料枠に収まっていました。

使い終わったら [cleanup 方法](https://github.com/aws-samples/reinvent2018-dev303-code/blob/master/docs/cleanup.md) の通りにリソースを削除していってください。VPC を削除できなくなります… (私です)。

# k8s クラスタの構築
基本的には上記の [サンプルプロジェクト](https://github.com/aws-samples/reinvent2018-dev303-code) の `docs/` 以下を順番にやっていけば問題ないです。ただし、

- ローカルから Telepresence を使うので [Prerequisites](https://github.com/aws-samples/reinvent2018-dev303-code/blob/master/docs/prerequisites.md) の **Manual Setup - Expert** で各ライブラリをインストールしてください
    - Cloud9 Setup ではなく :point_right: :point_right: [Manual setup](https://github.com/aws-samples/reinvent2018-dev303-code/blob/master/docs/manual-mode.md) :point_left: :point_left: を使います
- X-Ray 連携まではやらないので Prerequisites のあとは [lab1](https://github.com/aws-samples/reinvent2018-dev303-code/blob/master/docs/lab1.md) + [lab2: Collecting logs using Fluentd](https://github.com/aws-samples/reinvent2018-dev303-code/blob/master/docs/lab2.md) までで大丈夫です
- region は `us-west-2` (オレゴン) にするとすべてコピペで完成します

`frontend` service から URL を取得し、ブラウザでこんなページが出てくれば OK です
![screencapture-a0f89d6ddf3c211e8b3020abf6600db1-522586172-us-west-2-elb-amazonaws-2018-11-29-19_37_36.png](https://qiita-image-store.s3.amazonaws.com/0/67357/961630b5-0103-81d0-09cc-a0a727d3027d.png)

## Cloud9 を使わなかった理由
主旨と離れるので細かくは書きませんが、EKS は `aws-auth` という configmap でそのクラスタにアクセスできるロールやユーザを制御できるようになっており、`eksctl` でクラスタを作成すると、最初はクラスタを作成した端末の default credential だけが許可されています。そのため Cloud9 以外 (今回はローカル) からクラスタに (`kubectl` で) アクセスすると `Access denied` が返ってきてしまいます。

また `aws-auth` 自体を変更するのにはなんらかの credentials が必要なようなのですが、このサンプルプロジェクトでは Cloud9 から AWS credentials を全く使わず EKS にクラスタを構築しているため、Cloud9 から編集しようとしても `Please login to the server` というエラーになってしまいます。

`eksctl` や `aws eks` も `aws-auth` にロールやユーザを追加するコマンドは現時点では存在せず、結局 Cloud9 からしかクラスタにアクセスできませんでした。今回はローカル端末から Telepresence を使って EKS にアクセスしたかったので、Cloud9 を使わない方法を選びました。

※ もし解決法をご存知の方がいらっしゃったらぜひご教示ください :bow:

# Telepresence を使う
Telepresence は k8s にある特定の deployment を proxy に置き換え、そのサービスに来た通信をローカルのコンテナに向けてくれるライブラリです (Document の [Why Telepresence?](https://www.telepresence.io/discussion/why-telepresence) にある図が分かりやすいです)。[How it works](https://www.telepresence.io/discussion/how-it-works) にあるように、Telepresence は内部的には `kubectl port-forward` と `os port-forward` を使って proxy を実現しています。
[Proxying methods](https://www.telepresence.io/reference/methods) を見ると3種類の差し替え方法がありますが、今回は docker :whale2: を使った方法のみ使用します。

```zsh
$ telepresence --swap-deployment <Deployment name on your k8s cluster> --docker-run \
$ --rm -it <Docker image name>:<tag>
```

これだけです。`--docker-run` 以降は任意の docker run option を使えます。

ちなみに、今回は考慮しなくてよかったのですが [Troubleshooting & Workarounds](https://www.telepresence.io/reference/limitations) を読むと気付きが得られることがあります (特に `localhost` の扱い)。

## Frontend Service
一番わかりやすいので最初は view をいじってみます。
[src/frontend/views/index.hbs](https://github.com/aws-samples/reinvent2018-dev303-code/blob/master/src/frontend/views/index.hbs) の `AnyCompany Shop` を `Yuzuco's Shop` に置換してみます :rabbit: 

```src/frontend/views/index.hbs
...
<div class="container">
  ...
      <h2>Yuzuco's Shop</h2>
      <p>The Yuzuco's Shop is an online store for luxury products.</p>
      <p><a class="btn btn-secondary" href="/static" role="button">View details &raquo;</a></p>
  ...
```

ローカルに docker image を作成し、Telepresence で EKS 上にある `frontend` deployment と swap します。

```zsh
$ cd reinvent2018-dev303-code/src/frontend
$ docker build -t dev303-frontend .
$ telepresence --swap-deployment frontend --docker-run \
$ --rm -it dev303-frontend:latest
```

ここで大量に出てくるログは `./telepresence.log` にも書き込まれるので、issue を投げるときにたくさん活用しましょう :bookmark: 

先ほどと同じ URL で、きちんとローカルの image が反映されているのを確認できます :rabbit: :tada: :tada:
![screencapture-a0f89d6ddf3c211e8b3020abf6600db1-522586172-us-west-2-elb-amazonaws-2018-11-29-21_08_51.png](https://qiita-image-store.s3.amazonaws.com/0/67357/4dfc31ed-4e79-e8ed-a91e-03156b4027cf.png)

### 反映までの流れ
反映までの Deployment と Pod の状態を見てみます。Telepresence の仕組みをよく知っている方は読み飛ばしてください :pray:

```zsh
# オリジナルの frontend deployment がなくなり、変な deployment が作成されています
$ kgd
 NAME                                        DESIRED   CURRENT   UP-TO-DATE   AVAILABLE   AGE
 ...
 frontend                                    0         0         0            0           1h
 frontend-89842e0a82b144c48181ff262f0575de   1         1         1            1           12s
 ...
# Pod の方も変な pod が起動しています
$ kgp
 NAME                                                         READY     STATUS        RESTARTS   AGE
 ...
 frontend-5769984958-46f9w                                    0/1       Terminating   0          1h
 frontend-5769984958-nwkfh                                    0/1       Terminating   0          1h
 frontend-89842e0a82b144c48181ff262f0575de-85955c7555-qbtrp   1/1       Running       0          39s
 ...
# オリジナルの frontend deployment は desired replicasets が 0 になっています
$ kdd frontend
 Name:                   frontend
 ...
 Replicas:               0 desired | 0 updated | 0 total | 0 available | 0 unavailable
 ...
# 代わりに telepresence によって作成された deployment は desired replicasets が 1 になっています
$ kdd frontend-89842e0a82b144c48181ff262f0575de
 Name:                   frontend-89842e0a82b144c48181ff262f0575de
 ...
 Replicas:               1 desired | 1 updated | 1 total | 1 available | 0 unavailable
 ...
# 変な pod の image は telepresence proxy になっています
$ kdp frontend-89842e0a82b144c48181ff262f0575de-85955c7555-qbtrp
 Name:           frontend-89842e0a82b144c48181ff262f0575de-85955c7555-qbtrp
 ...
 Containers:
   frontend:
     Container ID:   docker://632c14c9118f0e67dfe5264606cf664c4438dedfb7610c1800543ec640d1f31e
     Image:          datawire/telepresence-k8s:0.94
     Image ID:       docker-pullable://datawire/telepresence- 
 k8s@sha256:2a8485d8a27b4e84751f714ba2f84753a2684aced4abdcc0894e63ece8013a7d
 ...
```

前述した「 [Why Telepresence?](https://www.telepresence.io/discussion/why-telepresence) にある図」の状態を垣間見ることができました :wink: 

ちなみに今回のサンプルでは Cloudwatch logs でコンテナイメージに telepresence proxy が使われていることを確認できます。

![Screen Shot 2018-11-30 at 5.56.51.png](https://qiita-image-store.s3.amazonaws.com/0/67357/85a35254-266e-7803-a643-55c5e9c7d41e.png)

ログが流れている状態で `Ctrl-C` を押すと telepresence proxy deployment が削除され、元の image を使った deployment と pods が再び起動します。

## Catalog Service
AWS EKS (EC2) 以外のリソースにアクセスしている場合を試してみます。実は先ほどと同じように単に swap するだけでは Products ページは `500` エラーが返ってきてしまいます。

Catalog Service は Python の [boto3](https://boto3.amazonaws.com/v1/documentation/api/latest/index.html) という AWS SDK を使って DynamoDB にアクセスしていますが、DynamoDB は `CatalogserviceDDB-Policy` がアタッチされたリソースからのアクセスのみを許可しています。そのため EKS (EC2) の外にある (ローカルの) コンテナからは DynamoDB にアクセスできません。

※ `500` が返ってくるのは Catalog Service がレスポンスをそのようにまとめてしまっているからです。`telepresence.log` を見るときちんと `CredentialsError` が返ってきています。

```text
File "/usr/local/lib/python3.7/site-packages/botocore/auth.py", line 357, in add_auth
     raise NoCredentialsError
 botocore.exceptions.NoCredentialsError: Unable to locate credentials
```

### Credentials 情報を与える
一番簡単な解決法は [`boto3` が DynamoDB にアクセスするところ](https://github.com/aws-samples/reinvent2018-dev303-code/blob/master/src/catalogservice/catalog/app.py#L32) で DynamoDB の read 権限以上のアクセスキーを与えてあげることです。

```catalogservice/catalog/app.py
# DO NOT DO THIS!!!
ddb = boto3.resource('dynamodb',
    region_name=app.config['AWS_REGION'],
    aws_access_key_id='YourAccessKeyId'
    aws_secret_access_key='YourSecretAccessKey'
    )
```

しかしこれはとても危険な方法ですので **絶対に行わないでください！** :no_good: :no_good: :no_good: 
(何かあっても責任持てませんよ〜)

せっかく k8s を使っているので [Secrets](https://kubernetes.io/docs/concepts/configuration/secret/) を使う方法や、今回は AWS を使っているので [KMS](https://aws.amazon.com/jp/kms/) を使う方法など、ぜひ安全なやり方でアクセスキーを秘匿した上で `boto3` に渡してあげてください :secret: :thumbsup: :thumbsup: 

[KMS を使う方法](https://medium.com/@mtreacher/using-aws-kms-for-application-secrets-in-kubernetes-149ffb6b4073) も見つけましたが、今回は Secrets を使って環境変数としてアクセスキーを渡すことにしました。

※ Deployment.yml などに元々入れている環境変数は、telepresence で proxy に swap されたあとも使えます。


```deploy/eks/prep.yml
apiVersion: v1
kind: Secret
...
data:
  # Random secret data, replace with your own.
  dynamouserkey: <base64 encoded aws access key>
  dynamouservalue: <base64 encoded aws secret key>
```
```deploy/services/catalogservice.yml
apiVersion: apps/v1beta1
kind: Deployment
...
      containers:
        - name: catalogservice
       ...
          env:
            - name: SECRET_USERNAME
              valueFrom:
                secretKeyRef:
                  name: microservices-aws
                  key: dynamouserkey
            - name: SECRET_PASSWORD
              valueFrom:
                secretKeyRef:
                  name: microservices-aws
                  key: dynamouservalue
...
```
```catalogservice/catalog/app.py
import os

ddb = boto3.resource('dynamodb',
    region_name=app.config['AWS_REGION'],
    aws_access_key_id=os.environ.get('SECRET_USERNAME'),
    aws_secret_access_key=os.environ.get('SECRET_PASSWORD')
    )
```

あとは新しい Secret と Deployment を `$ kubectl apply -f path/to/file` で反映させれば準備は完了です :muscle: 

### DynamoDB にアクセスする箇所を変更
もっと面白いことができればよかったのですが、私が Python に全く慣れていないため至極単純な変更です… :disappointed: 
DynamoDB から `product` の情報を取得している部分を固定値にします。

```catalogservice/catalog/routes.py
@api.route('/product/<string:product_id>')
def get_product(product_id):
    try:
        res = current_app.config['db'].get_item(
            Key={
                # 'id': product_id
                'id': '1051094507639'
            }
        )
```

あとはいつもの swap コマンドで telepresence proxy が起動するのを待ちます。

```zsh
$ cd reinvent2018-dev303-code/src/catalogservice
$ docker build -t dev303-catalog:dynamo .
$ telepresence --swap-deployment catalogservice --docker-run \
$ --rm -it dev303-catalog:dynamo
```

今回はうまく DynamoDB にアクセスすることができました！ :clap: :clap: 
サイトの方は…何を選択しても一番高い Rolex を買わせるサイトになってしまった… :metal: :metal: 
![rolex_g.gif](https://qiita-image-store.s3.amazonaws.com/0/67357/8ee0eac2-be6c-d0bf-e81d-45863b736c1f.gif)

# まとめ
Telepresence を使って EKS 上にある k8s クラスタをデバッグ (?) してみました。
複数の deployments を同時に telepresence で swap することはできませんでしたが、今回のようなちょっとした変更であればいちいちリモートにデプロイしたり、ローカルに k8s クラスタを構築しなくても動作確認を行うことができそうです。

[今回使ったソースコード](https://github.com/yuzumikan15/reinvent2018-dev303-code/tree/feature/telepresence) を載せるので自由に試してみてください :angel: 
(Secrets の base64 エンコードされたアクセスキーの箇所だけダミーになっています)

それでは、Happy Kubernetes Life :christmas_tree: :santa: 

次回の担当は @tanaka_733 さんです :rabbit: :rabbit: