KubernetesとNode.jsでマイクロサービスを作成する 6/6 Kubernetes with minikube

# 第6章 Kubernetes with minikube
いよいよKubernetesを使ってマイクロサービスを構築していきます。

本章では、ローカル環境で簡単にKubernetes環境を構築できる[minikube](https://github.com/kubernetes/minikube)を使ってKubernetes上にマイクロサービスを構築します。

## チュートリアル全体
- [第1章 概要](https://qiita.com/reireias/items/6a78d6b8df781a76f94f)
- [第2章 Tweetサービス](https://qiita.com/reireias/items/82cfbb285130caf3e091)
- [第3章 Userサービス](https://qiita.com/reireias/items/27e21896b701360bc152)
- [第4章 Webサービス](https://qiita.com/reireias/items/204c080cd5ec1bf432fd)
- [第5章 Dockerを使ったサービス構築](https://qiita.com/reireias/items/efefe718d74ed8447931)
- [第6章 Kubernetes with minikube](https://qiita.com/reireias/items/084888abb3a757c0c5e2)

## 構成
![microservice-tutorial01.png](https://qiita-image-store.s3.amazonaws.com/0/214526/36201231-6030-9ac3-03ad-aaa99619eed2.png)

## minikubeとkubectlの準備
まずは、Kubernetesをローカル環境で再現できる[minikube](https://github.com/kubernetes/minikube)を準備します。
[公式ドキュメント](https://kubernetes.io/docs/tasks/tools/install-minikube/)にしたがってインストールを行います。

インストールが完了したら下記コマンドでminikubeを起動します。

```sh
minikube start
```

※ 私の環境はLinux Mintなのですが、VirtualBox版の起動がうまくいかないため、`minikube start --vm-driver kvm`でKVM版を起動しています。

ingressアドオンを有効にします。(少し時間がかかります)

```sh
minikube addons enable ingress
```

minikubeでは、下記のコマンドを実行することで、ダッシュボードにブラウザでアクセスできます。

```sh
minikube dashboard
```

ブラウザで自動で下記のダッシュボードが開かれます。（画像では既に本章で構築するマイクロサービスが稼働している状態です）

![k8s_01.png](https://qiita-image-store.s3.amazonaws.com/0/214526/277d4cba-3a73-24ac-efb4-ce9287b9ccd4.png)



続いて、Kubernetesをコマンドラインから操作する`kubectl`コマンドをインストールします。
[公式ドキュメント](https://kubernetes.io/docs/tasks/tools/install-kubectl/)の説明に沿ってインストールします。

インストールできたら、minikubeを認識しているか、下記のコマンドで確認してみましょう。
currentの`*`がminikubeについていれば問題ありません。
（既にGKE等の別のKubernetes環境を利用している場合は`kubectl config use-context minikube`で切り替えましょう）

```sh
kubectl config get-contexts
CURRENT   NAME       CLUSTER    AUTHINFO   NAMESPACE
*         minikube   minikube   minikube
```

以上でminikubeとkubectlの準備は完了です。

## minikubeにDockerイメージを登録する

`docker`コマンドが利用するdocker enginをminikubeのものに変更します。

```sh
eval $(minikube docker-env)
```

次に前章で作成した`microservice-sample-integration`リポジトリの`make build`でDockerイメージを作成し、minikubeのイメージリポジトリへ登録します。

```sh
cd path/to/microservice-sample-integration
make build
```

以上でminikube上へDockerイメージをpushできました。

## yamlファイルによるKubernetesの設定
それでは、Kubernetesの設定を記述していきます。
Kubernetesは`kubectl`コマンドやダッシュボードを利用した操作の他に、マニュフェストファイルというyaml形式で記述されたファイルを利用した操作が可能と成っています。
おそらく、ほとんどの現場ではこのマニュフェストファイルでの管理を実施しているはずだと思います。

今回作成する各マニュフェストファイルと、それぞれの役割について紹介します。

- tweet-db.yml
  - Tweetサービスのデータベースです
  - PersistentVolumeを使いデータの永続化を行います
  - StatefulSetを使い、MongoDBのクラスター構成を構築できるようにします
- tweet.yml
  - Tweetサービスです
  - k8sのServiceとDeploymentを記述します
- user-db.yml
  - Userサービスのデータベースです
  - tweet-dbとほぼ同じ設定です
- user.yml
  - Userサービスです
  - k8sのServiceとDeploymentを記述します
- web.yml
  - Webサービスです
- ingress.yml
  - ロードバランサーの設定です
- cluster-role.yml
  - Kubernetes APIのRoleを設定します
  - MongoDBのside carからAPIを利用するために作成します


それでは、詳細に1ファイルずつ確認していきます。

### tweet-db.yml
Tweetサービスのデータベース用のサービスの設定です。

複数のノードからなるデータベースはステートフルのため、少し工夫が必要です。
公式ブログの[Running MongoDB on Kubernetes with StatefulSets](https://kubernetes.io/blog/2017/01/running-mongodb-on-kubernetes-with-statefulsets/)を参考に実装しています。

```yml:manifests/tweet-db.yml
---
apiVersion: v1
kind: Service
metadata:
  name: tweet-db
  labels:
    name: tweet-db
spec:
  ports:
    - port: 27017
      targetPort: 27017
  clusterIP: None
  selector:
    app: tweet-db
---
apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: tweet-db
spec:
  selector:
    matchLabels:
      app: tweet-db
  serviceName: tweet-db
  replicas: 3
  template:
    metadata:
      labels:
        app: tweet-db
    spec:
      terminationGracePeriodSeconds: 10
      containers:
        - name: tweet-db
          image: mongo
          command:
            - mongod
            - "--bind_ip"
            - 0.0.0.0
            - "--replSet"
            - rs0
            - "--smallfiles"
            - "--noprealloc"
          ports:
            - containerPort: 27017
          volumeMounts:
            - name: tweet-db-pvc
              mountPath: /data/db
        - name: tweet-db-sidecar
          image: cvallance/mongo-k8s-sidecar
          env:
            - name: MONGO_SIDECAR_POD_LABELS
              value: "app=tweet-db"
            - name: KUBERNETES_MONGO_SERVICE_NAME
              value: tweet-db
  volumeClaimTemplates:
    - metadata:
        name: tweet-db-pvc
      spec:
        accessModes:
          - "ReadWriteOnce"
        resources:
          requests:
            storage: 100Mi
```

### tweet.yml
TweetサービスのNode.jsで実装したAPI部分を動かす部分です。コンテナを動かすDeploymentとそれをクラスター内へ公開するServiceを定義しています。

```yml:manifests/tweet.yml
---
apiVersion: v1
kind: Service
metadata:
  name: tweet-service
spec:
  ports:
    - port: 3000
      targetPort: 3000
  selector:
    app: tweet
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: tweet-deployment
  labels:
    app: tweet
spec:
  replicas: 1
  selector:
    matchLabels:
      app: tweet
  template:
    metadata:
      labels:
        app: tweet
    spec:
      containers:
        - name: tweet
          image: microservice_tweet:1.0
          ports:
            - containerPort: 3000
          env:
            - name: MONGODB_URL
              value: mongodb://tweet-db:27017/tweet?replicaSet=rs0
            - name: WAIT_HOSTS
              value: tweet-db:27017
```

### user-db.yml
UserサービスのDB部分です。`tweet-db.yml`とほぼ同じ構成です。

```yml:manifests/user-db.yml
---
apiVersion: v1
kind: Service
metadata:
  name: user-db
  labels:
    name: user-db
spec:
  ports:
    - port: 27017
      targetPort: 27017
  clusterIP: None
  selector:
    app: user-db
---
apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: user-db
spec:
  selector:
    matchLabels:
      app: user-db
  serviceName: user-db
  replicas: 3
  template:
    metadata:
      labels:
        app: user-db
    spec:
      terminationGracePeriodSeconds: 10
      containers:
        - name: user-db
          image: mongo
          command:
            - mongod
            - "--bind_ip"
            - 0.0.0.0
            - "--replSet"
            - rs0
            - "--smallfiles"
            - "--noprealloc"
          ports:
            - containerPort: 27017
          volumeMounts:
            - name: user-db-pvc
              mountPath: /data/db
        - name: user-db-sidecar
          image: cvallance/mongo-k8s-sidecar
          env:
            - name: MONGO_SIDECAR_POD_LABELS
              value: "app=user-db"
            - name: KUBERNETES_MONGO_SERVICE_NAME
              value: user-db
  volumeClaimTemplates:
    - metadata:
        name: user-db-pvc
      spec:
        accessModes:
          - "ReadWriteOnce"
        resources:
          requests:
            storage: 100Mi
```

### user.yml
UserサービスのAPI部分です。こちらも`tweet.yml`とほぼ同じ構成です。

```yml:manifests/user.yml
---
apiVersion: v1
kind: Service
metadata:
  name: user-service
spec:
  ports:
    - port: 3000
      targetPort: 3000
  selector:
    app: user
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: user-deployment
  labels:
    app: user
spec:
  replicas: 1
  selector:
    matchLabels:
      app: user
  template:
    metadata:
      labels:
        app: user
    spec:
      containers:
        - name: user
          image: microservice_user:1.0
          ports:
            - containerPort: 3000
          env:
            - name: MONGODB_URL
              value: mongodb://user-db:27017/user?replicaSet=rs0
            - name: WAIT_HOSTS
              value: user-db:27017
```

### web.yml
Webサービスでは、GitHubのOAuth用環境変数をSecretから取得する設定にしています。
Secretの作成は後述します。

```yml:manifests/web.xml
---
apiVersion: v1
kind: Service
metadata:
  name: web-service
  labels:
    app: web-service
spec:
  ports:
    - port: 3000
      targetPort: 3000
  selector:
    app: web
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: web-deployment
  labels:
    app: web
spec:
  replicas: 1
  selector:
    matchLabels:
      app: web
  template:
    metadata:
      labels:
        app: web
    spec:
      containers:
        - name: web
          image: microservice_web:1.0
          ports:
            - containerPort: 3000
          env:
            - name: GITHUB_CLIENT_ID
              valueFrom:
                secretKeyRef:
                  name: github-oauth
                  key: GITHUB_CLIENT_ID
            - name: GITHUB_CLIENT_SECRET
              valueFrom:
                secretKeyRef:
                  name: github-oauth
                  key: GITHUB_CLIENT_SECRET
            - name: CALLBACK_URL
              value: http://microservice.test/callback
            - name: BASE_URL
              value: http://microservice.test
            - name: NUXT_HOST
              value: 0.0.0.0
            - name: USER_SERVICE
              value: http://user-service:3000
            - name: TWEET_SERVICE
              value: http://tweet-service:3000
      dnsConfig:
        nameservers:
          - 8.8.8.8
```

### ingress.yml
Ingressを利用してWebサービスを公開します。
Ingressを利用することで、AWSならALB、GCPならLoadBalancerを作成してくれます。
k8sでのサービスの公開方法はいくつかありますが、[この記事](http://chidakiyo.hatenablog.com/entry/2018/09/10/Kubernetes_NodePort_vs_LoadBalancer_vs_Ingress%3F_When_should_I_use_what%3F_%28Kubernetes_NodePort_%E3%81%A8_LoadBalancer_%E3%81%A8_Ingress_%E3%81%AE%E3%81%A9%E3%82%8C%E3%82%92%E4%BD%BF%E3%81%86)が参考になります。

```yml:manifests/ingress.yml
---
apiVersion: extensions/v1beta1
kind: Ingress
metadata:
  name: ingress
spec:
  rules:
    - host: microservice.test
      http:
        paths:
          - backend:
              serviceName: web-service
              servicePort: 3000
```

### cluster-role.yml

```yml:manifests/cluster-role.yml
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
  name: admin
rules:
- apiGroups: [""]
  resources: ["pods", "nodes"]
  verbs: ["get", "watch", "list"]

---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
  name: rbac
subjects:
- kind: ServiceAccount
  name: default
  namespace: default
roleRef:
  kind: ClusterRole
  name: admin
  apiGroup: rbac.authorization.k8s.io
```

## サービスを構築する
それでは、作成したマニュフェストファイルを使い、サービスが稼働するように構築していきます。

minikubeはローカルPC上のVMで稼働しているため、エンドポイントがminikubeのIPとなってしまいます。
Ingressの設定をhttpで作成したので、この場合は、`http://<minikubeのIP>`がwebサービスのエンドポイントになっています。
今回はこのIPアドレスにhostsで`microservice.test`という名前をつけ、エンドポイントを`http://microservice.test`という形でアクセスするように設定しましょう。

最終的には下図のような構成となります。
![minikube.png](https://qiita-image-store.s3.amazonaws.com/0/214526/a17f4b8d-3ee5-e9f2-3c42-36258e4f3c25.png)


### OAuth設定の作成
エンドポイントが変更になったので、前章までの`localhost:3000`用のOAuth設定は利用できません。
新しいOAuth設定を作成します。

[こちら](https://qiita.com/reireias/items/2bc1b33dfb0e63f31517#github%E8%AA%8D%E8%A8%BC%E3%81%AE%E6%BA%96%E5%82%99)を参考に、GitHubでOAuth用のClientIDとClientSecretを取得してください。
その際、callbackURLは`http://microservice.test/callback`とします。

作成したOAuth設定のIDとSecretは`.env.minikube`に以下の形式で設定しておきましょう。

```:.env.minikube
GITHUB_CLIENT_ID=xxxxxxxxxxxxxx
GITHUB_CLIENT_SECRET=xxxxxxxxxxxxxxxxx
```

### hostsの設定
minikubeのIPをmicroservice.testという名前でhostsに設定します。
minikubeのIPは`minikube ip`コマンドで取得できます。

```:/etc/hosts
# 略
192.168.42.242 microservice.test
```

### Secretの作成
先ほど作成したGitHubのOAuth設定をSecretというKubernetesの機能を利用して、Kubernetes上から参照できるようにします。

以下のコマンドでファイルを指定してSecretが作成できます。

```sh
kc create secret generic github-oauth --from-env-file .env.minikube
```

作成できたらダッシュボードから確認してみましょう。
ダッシュボードは`minikube dashboard`で開くことができます。

### kubectlで構築
それでは、kubectlコマンドでマニュフェストファイルを利用しサービスを構築していきます。

```sh
kubectl apply -f manifests/cluster-role.yml
kubectl apply -f manifests/tweet-db.yml
kubectl apply -f manifests/tweet.yml
kubectl apply -f manifests/user-db.yml
kubectl apply -f manifests/user.yml
kubectl apply -f manifests/web.yml
kubectl apply -f manifests/ingress.yml
```

作成できたら、ブラウザで`http://microservice.test`へアクセスしてみましょう。
userやtweetを何も作成していないため、データは表示されませんが、下記コマンドで前章と同様にデータを投入できます。

```sh
kubectl exec <user/tweetのpod名> node scripts/initialize.sh
# deployment名は下記コマンドで確認可能
kubectl get pods
```

無事にサービスが動いていれば、本章のチュートリアルは完了です。

## その他デバッグに役立つコマンド

```sh
# ターミナル接続
kubectl -it exec <pod名> sh
# ログの確認(ダッシュボード上でも確認可能)
kubectl logs -f <pod名>
```


## まとめ
本章ではminikubeを使い、ローカルPC上に構築したKubernetesに対し、マイクロサービスを構築しました。

以上で本チュートリアルは終了です。
いかがだったでしょうか？
不足している点などは、今後追記していこうかと思います。

ありがとうございました。