Kubernetesを実戦投入してマイクロサービス化するまでに四苦八苦している話

忙し過ぎて書けなかった宿題などを消化中・・・

Kubernetes入門して結構経ちましたが、やっと色々怖くなく使いこなせることが増えてきて、と思って変わった構成をやって見ると、事故ってみたいなことを繰り返してますが、せっかくなので色々書いて置こうと思いました。

[gcpug](https://gcpug.jp/join)の方に聞いて見たところ、とても快く教えてくださる方がいて、なんとか実戦投入することができました。

勉強は[公式のドキュメント](https://kubernetes.io/)と[Kubernetes: Up and Running: Dive into the Future of Infrastructure](https://www.amazon.co.jp/dp/B075G373MJ/ref=dp-kindle-redirect?_encoding=UTF8&btkr=1)+github上のIssueやネット記事を参考にして行なっていました。
後者の本は、kubernetesの全体像を掴まずにいきなり読むと、割と概念がわからずつまづく箇所が多く、公式ドキュメントをスッと入ってくることが多かったのでドキュメントである程度概念を知っておくことをおすすめします。出版されてからもうすでにマイナーバージョンが2上がっています。進化早過ぎですね。。。

色々と作業中に実行結果からの推測などが入ってしまっているので、間違ってたら、まさかりどんどんください。

# 最低限知っておくと良いこと
## 用語など
Node: podをホストするマシンのこと
Cluster: Nodeのグループのこと
Pod: コンテナのグループのこと、Node上に同種のPodや別種のPodを複数配置可能

## Kube Objectについて(kindキーで設定される値)
全部覚える必要はなく最低限以外のものは必要になった都度覚えるとよいです。以下主要なものの抜粋。クラウドによってクラウド特有のオプションをmetadataなどで設定できる。

pod : 単一または複数のコンテナからなるグループの単位、鯨などの群を表す(pod)という単語からきてるらしい。
service : podへの接続設定を保持しルーティングする。typeによって、接続IPなどの扱いが異なる。
deployment : podの設定とReplicaSetの設定を同時に行うオブジェクト、deploymentによって作成されたpodは、ローリングアップデートなどの設定ができる
ReplicaSet: podの生成についてのルールを作成する。ReplicaControllerの上位互換ラベルセレクタに条件式が使える
ConfigMap: 環境変数などの設定値を設定するためのオブジェクト
Secret: 特に機密情報に関わるデータを設定するためのオブジェクト

kubeobjectは基本service+deploymentで外部からの接続が可能になる

##　kubectlについて

Kube Objectの設定を反映させるために使うCLI
クラスタに関する設定と全てのKubeObjectに対してのほとんどの設定を
kubectlから行うことが可能なので、色々叩いて何が起こるか勉強するのが
理解の近道だと思います。

*今回は単純にマイクロサービス型のクラスタを構成するまでの必要最低限のコマンドしか叩いていません。

# Kubernetes初期設定と操作クラスタの切り替え

1. 前提としてクラスタを管理しているマシン上で[master components](https://kubernetes.io/docs/concepts/overview/components/)が起動している必要がある(minikubeを使うとVM上に,GKE上ではGUI・CUIでクラスタを作ると勝手に立つと思われる。これは簡潔性のために同ホストで全てのmaster componentsを立ち上げるが個別の起動や自作サービスとの共存も可能(ただしやらない方がいいと書いてある))。
参考:
The Kubernetes Master is a collection of three processes that run on a single node in your cluster, which is designated as the master node. Those processes are: kube-apiserver, kube-controller-manager and kube-scheduler.

2. kubectlコマンドはmaster componentsのうちkube-apiserverを指定・経由して操作したいホスト(クラスタ)に変更を加える(REST)
(各ホストではkubeletとkube-proxyというデーモンが操作を待ち受けている or 新たに作成されたnode上でkubele, kube-proxyを起動させる「GCPの場合は、おそらくGKEクラスタの作成時にそれぞれのnode上でkubeletを起動させている」)

3. kubectlで操作されるホスト群(クラスタ)の設定は(デフォルトでは)$HOME/.kube/configから読まれる

4. 以下のコマンド
```
gcloud container clusters get-credentials $CLUSTER_NAME --zone=$ZONE_OF_CLUSTER
```
を実行するとkubectlの操作対象をGKE上のクラスタに設定できる - ここの設定は、.kubeを見る限りkubectlのREST先を切り替えている。minikube startを使うとリクエスト先がVMになる。)

5.  .kube/configに一度設定したクラスタはcontextというkeyのデータに格納される。
```
kubectl config use-context $CLUSTER_NAME
```
で操作対象のクラスタを自由に変えることができる。

## 参考(https://github.com/kubernetes/kubernetes/issues/25383)
### 手動でkubectlの操作先を決定する最小の設定方法
```
$ kubectl config set-cluster --server=xxx
$ kubectl config set-credentials xx --username=xxx password=xxxx
$ kubectl config set-context xxx --cluster=xxx --user=xxx --namespace=xxxx
$ kubectl config use-context xxxx
```
`gcloud container clusters get-credentials` は、以上のコマンドを簡略化してくれていると思われる
### 現在kubectl実行環境に設定されているcontextのリストを確認する方法
```
kubectl config view -o jsonpath='{.contexts[*].name}'
```

また、kubectlのcontextの向き先を設定すればgoogle cloud console上のkuberctlでも操作が可能

## 設定情報を見る

dash_boardを使うと一覧できる。
gcp上のkubenetesの設定を確認したい場合は認証情報が必要なため、クレデンシャルを設定した端末でkubectl proxyを実行し、

```
$host:8001/ui
```
でアクセスすることにより本番環境の設定情報にもアクセスできる。gcp上の端末だとIPなどで弾かれるのでローカルマシンのkubectl proxyからlocalhostで接続するのが無難。ただGKEの場合これを使わずGKEのダッシュボード見ろとの噂・・・。

# Kubernetes Ingressとは

簡単にいうと,Kubernetes上に(L7)ロードバランサーを作成する機能


## 複数のサービスにルーティングする

```kubectl get ing```

[kind : Ingress で、設定ファイルを作成する](https://kubernetes.io/docs/concepts/services-networking/ingress/)
pathsでL7ロードバランサーからルーティングができる。

```yaml
apiVersion: extensions/v1beta1
kind: Ingress
metadata:
  name: web-ingress
  annotations:
    kubernetes.io/ingress.global-static-ip-name: hoge-ip
spec:
  tls:
    - secretName: tls-certs-1709
  rules:
  - http:
      paths:
      - path: / 
        backend:
          serviceName: hoge-service
          servicePort: 80
```

## 複数のサービスをホスティングする

Service Type:LoadBalancerで指定されるロードバランサーを使用する場合、パスによる、ルーティングはおそらくできない(L4ロードバランサーなので),portフォワーディングによるサービス振り分けは可能。

よって、複数サービスを単一ホストのサービスに見せかけるにはIngress(L7ロードバランサー)を使うのが良い。

# 固定IPを作成

```shell
# 設定
gcloud compute addresses create ${ipName} --global

# 確認 
# kubctl create(or apply or replace ) -f hoge-ingress.ymlを使用したときに
# 以下のコマンドで IN USE担っていることを確認する
gcloud compute addresses list
# 後表示されているaddressからブラウザアクセスできるか確認
# backendに対応するservice objectとpodsが正しく設定されていれば、global static ipでサービスに接続できる
```

ingress-> nginx-service -> nginx-pod -> nuxt-service->nuxt-podを自己署名証明書で、https接続するところまでできたので、手順をまとめておく。
また、なぜうまくいくのかを理解しておくことで、この構成を再現できる人を増やしたい。
なお、kubernetes objectの作成順番が変わってもいいもの、悪いものにがあるは、手順に従っていれば、大丈夫なように書かれている。

## やった手順 

前提:あらかじめ、GKEクラスタをGCPコンソールあるいは、gcloudコマンドから作成しておく。

```shell
gcloud container clusters create $clusterName --zone $region --project $gcp-project
```

クラスタ作成初回は時間がかかるので注意。
get-credentialsでgkeの権限とgcrの権限を手に入れておく。

1. nginxのdeployment,serviceを作成
2. nuxtアプリケーションのdeployment,serviceを作成
3.  static ipを作成し、ingressに紐づける
4. この状態でcurlなどでingressにアクセスし、nuxtサービスからレスポンスが帰ってくるか確かめる
5. secrets.yamlにbase64で暗号化したパラメータを設定し、ingressのtlsにsecretsオブジェクトの識別子(name)を入れる

## nginxの設定

nginxはどの環境でもほぼ同じように使えるため、
抽象度を高めて汎用化するのが良い。

nginx-service.yml

```yaml
kind: Service
apiVersion: v1
metadata:
  labels:
    name: nginx 
  name: nginx-service
  namespace: default
spec:
  type: NodePort
  ports:
    - port: 80
      protocol: TCP
  # Label keys and values that must match in order to receive traffic for this service.
  selector:
      app: nginx
```

ここで、deploymentに使うイメージをbuildする。

./docker-nginxなどわかりやすいディレクトリ名を作り、Dockerfileと
imageに含めたいファイルなどを用意しておく。
シンプルにnginxの設定ファイルだけ用意すれば良いので、

```
FROM nginx:latest

ADD server.conf /etc/nginx/conf.d/server.conf
```

server.conf

```shell
# for staging
server {
  listen 80 default_server;
  location / {
      proxy_pass "http://hoge-service.default.svc.cluster.local:8080";
  }
}
```

を用意し、
```
$ docker build . -t gcr.io/$gcp_project/hoge-nginx:$label
$ gcloud docker -- push gcr.io/$gcp_project/hoge-nginx:$label
```
でgcrにpushする、タグは gcr.io/$projectName/$image:$versionの
命名規則に従う必要がある。

- server.confの設定について　

ingressからnginxに接続した時に、kubeDNSのデフォルトの
命名規則から、サービスへとルーティングを行う。

Service Objectを生成した時にkubeDNSは

`{$serviceName}.{$nameSpace}.svc.cluster`

というdnsレコードを自動で作成する。これはクラスタのど
こからでも、参照できるため、ロードバランサを通って来た
リクエストをnginxで振り分ければ、好きなkubernetesサービスへ、
ルーティングすることができる。なお、ingress-nginxの
設定は、この処理を自動化しているように見える。

nginx-service.yml

```yaml
apiVersion: extensions/v1beta1
kind: Deployment
metadata:
  annotations:
    deployment.kubernetes.io/revision: "1"
  labels:
    name: nginx 
  name: nginx-deployment
  namespace: default
spec:
  replicas: 3 
  selector:
    matchLabels:
      name: nginx 
  strategy:
    type: RollingUpdate
    rollingUpdate:
      maxSurge: 100%
      maxUnavailable: 50%
  template:
    metadata:
      creationTimestamp: null
      labels:
        name: nginx 
        app: nginx
    spec:
      containers:
      - image: gcr.io/$gcpproject/nginx:latest
        name: nginx
        ports:
        - containerPort: 80
          name: nginx 
          protocol: TCP
```

nginxから接続するService Objectの設定は以下の通り

```yaml
kind: Service
apiVersion: v1
metadata:
  labels:
    name: hoge
  name: hoge-service
  namespace: default
spec:
  type: NodePort
  ports:
    - port: 8080
      protocol: TCP
      targetPort: 8080
  # Label keys and values that must match in order to receive traffic for this service.
  selector:
      app: hoge
```

`{$serviceName}.{$nameSpace}.svc.cluster`の命名規則から
resolverとしてkube-dns.kube-system.svc.cluster.localを使用することで、hoge-service.default.svc.cluster.localというDNでアクセス可能。deploymentはnameがhogeのコンテナで8080ポートでアプリケーションを待ち受けていれば、どんなWebサービスでも接続可能なので省略。

ingressを接続する前に以上のオブジェクトを生成しておく。

```shell
$ kubectl create -f nginx-deployment.yml  -f nginx-service.yml # -f ...other objects setting
```

## ingressの接続

ingressに対するstatic-ipを作成しておく。


```shell
$ gcloud compute addresses create hoge-ip --global   
```

以下のように、annotations.kubernetes.io/ingress.global-static-ip-name
に設定しておくことで、作成されたstatic-ipを指定することで、ingressに
httpアクセスができるようになる。

nginx-ingress.yml

```yaml
apiVersion: extensions/v1beta1
kind: Ingress
metadata:
  name: nginx-ingress
  annotations:
    kubernetes.io/ingress.global-static-ip-name: hoge-ip
spec:
  tls:
  - secretName: test-secret
  rules:
    - http:
        paths:
        - backend:  
            serviceName: nginx-service
            servicePort: 80
```

httpだけで運用するつもりであれば、tlsはなくてもよい、疎通ができるまでに不安がある場合
tlsパラメータを設定する前にcurlでingressにアクセスし、nuxtサービスからレスポンスが帰って
くるか確かめることもできる。

  - loadbalancerで接続できていない場合、
     curlを行っても、404が帰ってくる
  - nginxで接続できていない場合、
    backend service not foundが返ってくる
  - nginxからアプリケーションに接続できていな い場合
    curlで301が返ってくることで確認できる
  - nginxからアプリケーションに接続できてるばあい(http)
    curlで普通にコンテンツサーブされる、アプリケーション側でTLSを有効にしていなければ、ブラウザだとhttpsにリダイレクトされて接続できない

なお、ingressの反映はダウンタイムがないが、rolling updateは遅いため,アクセスしてうまくいかないなーと思っていても、時間をおくとうまくいってることがあるため、5,6分は様子を見ること。

https対応にする場合、アプリケーション側のミドルウェアに証明書をおく、nginxにおく、ロードバランサーにおくなど、いくつかの方法がありますが、ロードバランサーに設定するのが一番簡単かつ汎用性が高い。

```shell
$ kubectl create secret generic test-secret --from-file=tls.crt=$cert_path --from-file=tls.crt=$key_path
```
で、証明書を包含したSecretObjectを設定し、前述のnginx-ingress.ymlのtls.secretsName[*]にsecretの識別子を設定して、
ingressに反映させると、443でもingressに接続できるようになる。また、以上の設定は、secrets.ymlでも設定を行うことができる。

ここでは、練習のために自己署名証明書を発行して、設定ファイルで行う方法も記述する。

## 自己署名証明書で、動作確認を行う

2048bit RSAで自己署名証明書を作成する。期限は適当で良い。

```shell
$ openssl genrsa 2048 > server.key;
$ openssl req -new -key server.key > server.csr
$ openssl x509 -days 3650 -req -signkey server.key < server.csr > server.crt
```

```shell
$ cat server.crt | base64  
$ cat server.key | base64  
```
の出力結果を以下の$server.crt 
$server.keyにセットする。

```yaml
apiVersion: v1
data:
  tls.crt: $server.crt 
  tls.key: $server.key
metadata:
  name: test-secret
  namespace: default
type: Opaque
```

なお、これらのファイルはバージョン管理に含めない方が良い。

```shell
$ kubectl replace -f nginx-service.yml
```
などで、ingressに反映させ,数分置いたのち、 curl $ingress-ip -kLあるいは、ブラウザからhttps でアクセスを行うと、サービスに接続できることを確認できれば、めでたく、ルーティングでマイクロサービスを自由に振り分けられる状態になったことになる。
なお振り分けられているipは
 
```shell
$ gcloud compute addresses list
```

```shell
$ kubectl get ing
```

で確認が可能。

## urlの負荷分散について
マイクロサービスを増やした際にnginx側の設定に追加が必要なのは構成管理の手間的に推奨されない。通常はGCPUGの方に教えてもらったようにサブドメインで対応して

```
proxy_pass "http://${version}.{env}.svc.cluster.local:80";
```

汎用化する方がよい。もしくは/ルートを使わず第一urlをキャプチャすればいけるかも。proxyの設定は必要に応じて。

```
resolver kube-dns.kube-system.svc.cluster.local valid=5s;

# for staging
server {
  listen 80 default_server;
  location /hoge/ {
      proxy_pass "http://hoge-service.default.svc.cluster.local:8080/";
  }
  location / {
      proxy_pass "http://http-test-service.default.svc.cluster.local:8000";
      proxy_set_header        Referer                 $http_referer;
      proxy_set_header        Host                    $host;
      proxy_set_header        X-Real-IP               $remote_addr;
      proxy_set_header        X-Forwarded-Host        $host;
      proxy_set_header        X-Forwarded-Server      $host;
      proxy_set_header        X-Forwarded-For         $proxy_add_x_forwarded_for;
      proxy_set_header        X-Forwarded-Ssl         on;
      proxy_set_header        X-Forwarded-Proto       https;
  }
}
```

*色々作業中のものが残ってしまっていますが、DockerImageはlatestタグは管理しにくいため都度ラベルを割り振った方がいいようです。

# 一回デプロイした後は

イメージプッシュするたび

```
kubectl set image deployment/hoge-deployment hoge=hoge:$label
```

でデプロイメントで指定されているimageをすげ替えるだけでdeploymentに指定されている通りのupdateの仕方で更新が走ります。簡単にblue-green canary リリースができて便利、auto healingももちろん設定できます。

# CDを使う

CIの機能に組み込まれているものや単体のCDツールを使って,image pushおよび、kubectl set imageまでの作業もリリースタグを切るなどのトリガーを用いて自動化できます。
Circle CI, Wercker, Spinnaker あたりが良いようです。Spinnakerは特に推されている印象があります。