Kubernetesで数百以上のDocker開発環境を運用する

## TL;DR

- KubernetesでDockerアプリケーションの開発環境を運用できる
- telepresenceを使い、開発したいコンテナだけをローカルで動かせる
- コンテナをローカルで動かしつつ、Kubernetes内のネットワークに透過的にアクセスできる
- 開発時以外は、Kubernetesクラスタ内で通常のコンテナが起動している
- Auto ScalingやALBの集約によって低コストで運用できる

インフラは全てTerraformでコードとして管理しているので、以下のリポジトリを参考に。
https://github.com/hareku/terraform-kubernetes-development

## 全体のアーキテクチャ図
### HTTP / HTTPSアクセスの流れ
![kubernetes-dev](https://www.plantuml.com/plantuml/png/0/ZLB1QiCm3Bq7yWyYT--oddOOTTbbh8oo7KifRYmQRwuT7BbiZFxxxBIhdQo5wcG_FUazoPvs91pvdOPFlq5daB1Z4lTb8Mgo3fvmZzgswFW_P5OMPL5hwoLKB-VY2dxA0a8OAn6gzzRI-ja45LFZEPVogTjwOIKVC68iNxVAVAt6OAYz--Wo-92EVGMmHpYneiqheloAA6z5tU1OzSrllYzB7lbHCt9omUD-efE5TPIPF270besI4irACXWjurOspzZ1r27gzuFEhP0peUFgNOG8mQbjG_00SzKH6fXCxc7dDIdMs8vYOUeABCmpR94CNaZcjpwTfax-0vosEuJ_NdgAHeOF-Gi0 "kubernetes-dev")

### 開発時の図
※Podを以下のような、Nginx,Go,MySQLの3コンテナとする。
![kubernetes-dev](https://www.plantuml.com/plantuml/png/0/SoWkIImgAStDKSWkJKejoaijIItMJKaju-NYvKhEoIzDKL3oXaieALJpSYaevEMdf2ee-HTbfcVc9UQuf1Rav5VcfkQL1EIdG5KeA4Jx0IKhOg4SztmOoxSoED0d5cGwY0jMfA0RdP6PafAQ5rkag-2U3v5npqSH1ci3oTJoSW6HGsfU2j3D0000 "kubernetes-dev")

#### Telepresenceによる開発時の図
![kubernetes-dev](https://www.plantuml.com/plantuml/png/0/XL8zJyCm4Dqj-J-Sibjqxe0gKQa3BGNALcKmzXEnSEt8SGeLwd_78I59HKGdw-vz-DtPYohA4Ek3fVVw5S4XeX8QHyuuaohwG4iSONs9m5bTGSaAlotEUep4xT3bp7aDojPUIRkHgZ0E6NrnHhHV-RmvLvw462k8bFMrfkm55cL01QTmPmrSp1hqRvj4SccbK-2-C5h3vRQbdHjcTyLzFscNgesx-v1b2Rr4QVtfa4mUlUu8AVZ3ct6VULTiJhkdzKzn7lJRO197sak35e-WX1QHvlISO9FMIBERMHj_KZXCe0LDHUjccbpCDF_YlKRkutVgETSP_xfmje3JwLzz0m00 "kubernetes-dev")

## 背景
組織の背景として、10~20ほどのDockerアプリケーションを運用しており、それらは全て**各エンジニアに与えられたEC2インスタンス上で開発**されている。
しかし開発したいアプリケーションの数が増えるにつれ、以下のような課題が出てきた。

- EC2インスタンスのリソース不足
  - メモリ不足やEBSの空きストレージ不足、CPUの高負荷など
- Chefのプロビジョニングとの非整合によるエラー
  - ほとんどのプロジェクトはChefが設定するDNSや環境変数に依存している
- ファイルがEC2上にある上での弊害
  -  SSHFSでのマウントには遅延があるため、ローカルからのGitなどはまともに扱えない
- 開発プロジェクトの自動更新
    - 今までは依存している他プロジェクトを`git pull`して最新にする必要があった
- デザイナーの開発環境への敷居
  - インスタンスへのSSHや各dockerコマンドなどを意識しなければならない

これらの課題を解決するため、Kubernetesで自律的な開発環境を構築することになった。

ちなみに移行前の開発環境は以下のようになっている。
![docker-ec-dev](https://www.plantuml.com/plantuml/png/0/VP9DJyCm38RlbVeVh9s9XiOaq0veAt3hWJj2KKWCxUZYolKc8RJ_JjCFr7Q5O_kyVc8xsLPYM8w70XpPJsI5LZaynP6nGWoFUCA2IcIzYwCuykGG5ayFA_rEh0Tm0Tzn1D3oz5QdP5nY2kCjSiS0B1rAykW5iXMrlpm90kSVcS06qhmIz71tVRLUtq1z2fJ4KeNaEPpU-v-PzcXbDo_MFPwoKBolHdXEc_olJQQCZszV8rXB6-zWFQ3KVP3KGsOYPRLPBiksGfaG3QYB_FRNPOTHtUnj50fL5uFrmadVmwz09vrVtddozustMpeFfqoQPcQYGJtpPa2zPBbpw5zPpPTjqRlwQVu0 "docker-ec-dev")

### なぜ全てローカルで開発しないのか
**「パブリックにアクセスできるIPが必要」「プロジェクト数が多くリソース不足」**という点にある。

新しいプロジェクトではDockerを用いているが、それ以前に作られた非Dockerプロジェクトの数は多く、そしてそれらはマイクロサービスではないがマイクロサービスのような複雑な依存性を持っている。より具体的には、**複数ドメインにまたがる通信やRewriteが数多く存在している。**

また全ての非DockerプロジェクトをDocker化し、1つのローカルマシンで動かすことは、リソース的にもコストパフォーマンス的にも妥当なものではない。そのため非Dockerプロジェクトが動いているEC2インスタンス上からアクセスするためのパブリックなIPが必要ということになる。

各開発環境へのHTTP/HTTPSアクセスは全て1つのApplication Load Balancerを通しており、証明書なども全てAWSのリソースを使用している。

## Telepresenceによるローカルでの開発
![telepresence-logo.png](https://qiita-image-store.s3.amazonaws.com/0/109606/fba2a93d-3aca-b1b0-d8d6-6ef514309de3.png)
https://www.telepresence.io

Telepresenceを使うことにより、Kubernetesクラスタにある**DeplyomentのPod中の1つのコンテナをローカルで起動することができる。**また同じPod内のコンテナや別Podからもローカルのコンテナと透過的に相互アクセスできる。

![why_telepresence.png](https://qiita-image-store.s3.amazonaws.com/0/109606/aa952a7d-213c-7015-2f3f-8925a911b823.png)

### Telepresenceの内部実装
Telepresenceの内部は`kubectl port-forward`を使っているだけである。要するに**HTTPSトンネリングによってKubernetesのDeploymentやPodにポートフォワードする機能**のため、例えば会社のネットワークにポート制限などがあってもHTTPSさえ通れば問題ない。

またPodの環境変数も全てローカルのコンテナにきちんと渡されるため、そのあたりも意識しなくて良い。例えば`my-app`というDeploymentのGoコンテナだけをローカルで起動したい場合、以下のようなコマンドを打つだけである。

`telepresence --swap-deployment my-app:go --docker-run --rm -it -v $(pwd):/var/www go:latest`

もちろんDockerを使わず、ローカルのGoプロセスを動かすこともできる。
`telepresence --swap-deployment my-app:go --method inject-tcp --expose 8080 --run go run main.go`

このコマンドで、Deployment(`my-app`)のReplicasは0になり、そしてTelepresenceが代わりのDeploymentを作成し、そのDeploymentの中のGoコンテナはローカルのものである。ちなみにMacBookはスリープしてもDockerのコンテナは終了しないため、閉じていても普通に動き続ける。
`exit`すれば代わりのDeploymentは削除され、そして元のDeployment(`my-app`)のReplicas値が戻る。

そのあたりを含めた内部的な実装はTelepresenceの「[How it works](https://www.telepresence.io/discussion/how-it-works)」で紹介されている。WindowsもWindows Subsystem for LinuxをインストールすればTelepresenceを使える。そちらもドキュメントの「[Windows Support](https://www.telepresence.io/reference/windows)」を参照して欲しい。

## Kubernetesによる開発環境の構築
ここでは具体的な開発環境の構築方法について解説していく。

### Amazon EKSの選択と料金
![eks_logo.png](https://qiita-image-store.s3.amazonaws.com/0/109606/fc7e4a40-9cd3-2cea-bce6-709954da8197.png)

Kubernetesの基盤として、**Amazon EKS**を選択している。これは既存の開発用DBやセキュリティグループを含めた様々な資産を活かすためである。

Amazon EKSではコントロールプレーンをマネージドで提供し、料金は東京リージョンで1時間あたり$0.20、**月に$144**となっている。その$144に加えて、リソースを動かすためのインスタンスやALBの料金が上乗せされる。ただしインスタンスに関してはスポットインスタンスを用いて安価に運用できる。

料金はどれほどのインスタンスを起動させるかで非常にブレてくるが、m4.2xlarge（8vCPU、32GBメモリ、\$0.1331/h）を1日12時間を6台と計算すると、およそ月$287である。それに**EKSやALB、EFSを加えるとおよそ合計で月$500**になるが、組織的な開発環境としては十分現実的なコストとして収まっている。

#### スポットインスタンスの注意点
Cluster AutoscalerはまだAWSのスポットフリートに対応しておらず、単一のインスタンスタイプを選択しなければならない。しかしm5やt3などの次世代のインスタンスタイプだとAWS側で余っている量が少なく、スポットリクエストが`capacity not available`エラーになってスケールアウトできない事態に陥りやすい。

対策として、m4などの旧世代のインスタンスタイプを利用する。m4などでも容量不足によるスケールアウトエラーが出てくる可能性はあるが、今のところ遭遇していないので良しとしている。

### 開発環境の起動はDashboardから
![kubernetes-ui-dashboard.png](https://qiita-image-store.s3.amazonaws.com/0/109606/49a2ab78-9b31-dde6-555c-993e91d81a14.png)
Kubernetesは公式にDashboardを提供しており、開発環境の起動もここから行う。**起動はとても簡単**で、起動したいプロジェクトのReplica数を0から1にするだけである。

ログイン方式は、Deploymentのスケールのみが可能なServiceAccountのトークンを配布する手段を取っている。

#### 開発環境の自動スケールイン
開発環境は勤務時間外に使われることはないため、CronJobにて一斉にDeploymentのReplica数を0にしている。もし指定の時刻に自動で起動したいのであれば、[hjacobs/kube-downscaler](https://github.com/hjacobs/kube-downscaler)で対応できる。

### Terraformによるインフラの管理
![terraform-logo.png](https://qiita-image-store.s3.amazonaws.com/0/109606/cf8f25e4-072f-3ace-46db-5d3135477096.png)

今回のインフラは全てTerraformで管理している。
https://github.com/hareku/terraform-kubernetes-development

また各エコシステムや開発環境のデプロイはHelmを用いて、Terraform上から操作している。[TerraformのHelm Providier](https://www.terraform.io/docs/providers/helm/index.html)はTerraform上のリソース値をvalueに直接渡せるため、手動で値を変えたりすることがない。

各プロジェクトの開発環境は、全てHelm Chartで管理している。Chartに関しては作るのが面倒であるが、[TerraformのKubernetes Providier](https://www.terraform.io/docs/providers/kubernetes/index.html)がIngressなどのbetaバージョンに対応しておらず、また[メンテナ不足のため、今後もGA以外は実装しない方針を執っている](https://github.com/terraform-providers/terraform-provider-kubernetes/issues/14)。そのため現状はHelm Chartで管理する手段しかない。

### Kubernetesの周辺システム
今回用いたKubernetesの各周辺システムを紹介する。

- **Kubernetes Dashboard**
    - ダッシュボード上から各プロジェクトを起動する
    - Deploymentのスケールのみが可能なServiceAccountのトークンを配布している
- **kube2iam**
    - Kubernetes上で動作する各Podに異なるIAMロールを付与する
- **ALB Ingress Controller**
    - Ingressリソースの作成を検知してALBを作成する
- **jakubkulhan/ingress-merge**
    - 各プロジェクトのIngressを1つのIngressにまとめる
- **External DNS**
    - 各ServiceやIngressのホスト名からRoute53にレコードを自動登録する
- **efs-provisioner**
    - Amazon EFSをKubernetes上で永続ボリュームとして扱えるようにする
- **Cluster Autoscaler**
    - 需要に応じてノードであるEC2インスタンス数を調整する
- **EC2 Spot Termination Notice Handler**
    - スポットインスタンスの監視とdrain処理をする

## まとめ
まだ完全に移行できている訳ではないため、これから何らかの問題が出てくるかも知れない。
細かい部分は省略しているため、何か疑問点などあればコメント欄や[Twitter(@hareku908)](https://twitter.com/hareku908)まで。