Kubernetes(GKE)にお安く入門する

**※ この記事は2019年3月22日に作成した [Kubernetes(GKE)にお安く入門する - LiBz Tech Blog](https://tech.libinc.co.jp/entry/2019/03/22/105911) と同じ内容です**

# はじめに
こんにちは！今回が２回目の投稿になります。LiBのエンジニアの渡邊です！

前回は[GitのコミットメッセージにEmoji Prefixを使ってテンションをあげたい話🕺💃🕺💃](https://tech.libinc.co.jp/entry/2018/12/05/113103)を書きました。

今回はKubernetes(GKE)について書いていこうと思います！

![20190320155112.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/760714/65628944-19b1-b2a1-27d0-4e35e0eb3f0c.png)


# 経緯

自分の所属している部署では開発環境が完全にDocker化されており、プロダクション環境もAWSのECSで運用されています。

オープンソースのKubernetesについても最近の盛り上がりを受けて勉強しておかないとなぁとは思いつつ、なかなか触れることができていなかったので今回のブログを機に入門してみました。

とはいえ個人で使用するためには少々お高くなってしまうKubernetes。
なんとか費用を抑えて、**安くHello Worldしたい**というのが今回のブログの趣旨となります。

LiBでは1日の業務時間のうちの1時間を、こういった技術習得のための時間にあててもいいという**放課後制度**があるためエンジニアには嬉しい環境となっています。
(この放課後時間を使ってつくられた社内ツールもあったりします。)


# そもそもKubernetesとは

コンテナ化されたアプリケーションのデプロイ、管理、スケール(オーケストレーション)を行うための、**オープンソース**のコンテナオーケストレーションシステムです。
もともとはGoogleが設計したシステムでGo言語でつくられています。
略記は**k8s**です。

AWSならEKS, GCPならGKE, AzureならAKSというマネージドサービスがあるのでそれを利用しましょう。

# なぜEKS(AWS)ではなくてGKE(GCP)なのか

AWSでは、弊社でも使用しているECSというAmazonが独自に開発したコンテナオーケストレーションシステムがもともとあったため、k8sのマネージドサービスを提供しだしたのはかなり最近です。
(tokyoリージョンだと2018年12月末とか)

もともとGoogle発だったことによるサービスとの親和性や、ドキュメントの数や運用実績を考えると入門するならGCPのGKEが良いと思われます。

![20190320164716.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/760714/d5c3e6e3-8391-8ab7-cf1a-34c73422e0b9.png)


あと、GKE自体が無料というところもポイントです。
GKEクラスターを作成するとサーバーはGCEインスタンスとして作成されるのでそこに料金がかかってきます。
しかもGCEインスタンスはUSリージョン限定ですが f1-microインスタンス1つ + 30 GB の HDD が永久に無料です。この無料枠を使わない手はない！

 
# 無料のクラスタをつくる
※ GCPアカウントの取得と gcloud , kubectl のインストール・設定は省きます。

**gcloud**: AWSでいうaws-cliです。コマンドでGCPの操作ができます。
**kubectl**: k8sクラスタに対してコマンドを実行するためのcliツールです。


## とりあえずk8sクラスタをつくってみる

```
$ gcloud container clusters create <クラスター名> --zone us-west1-a --machine-type=f1-micro --num-nodes=3 --disk-size=10
```

**--num-nodesがインスタンスの数です。**
`--num-nodes=3` で3つもインスタンスたてたら無料じゃないじゃんってなりますが、f1-microだと最低3インスタンスないとエラーになるので今はおとなしく3つ立てます。

実は**f1-microでクラスタつくるときは最低3つ必要なのですが後から消す**ということができます。

ちなみに --zone を指定しないとマルチゾーンで作られてしまうので要注意。
us-west1-a, us-west1-b, us-west1-cにnum-nodesの数だけインスタンスができちゃいます。

`$ gcloud container clusters list` で 確認してみると**NUM_NODESが3**になってるはずです。

## ノード(インスタンス)が1つだけのノードプールをつくる

ノードプールとはインスタンスのグループのことです。
cluster作成時につくられた3つのノードは `default-pool` という名前でグループ化されています。
これとは別に1ノードのみのノードプールをつくり、あとからdefault-poolを消すことでサーバの数を1台だけします。

```
$ gcloud container node-pools create <プール名> --cluster <クラスター名> --zone us-west1-a --machine-type=f1-micro --num-nodes=1 --disk-size=10
```

## 3つのノードが登録されているノードプールを消す

```
$ gcloud container node-pools delete default-pool --zone us-west1-a --cluster <クラスター名>
```

`default-pool`を消しました。
`$ gcloud container clusters list` でノードの数を確認すると**1**になっています。

とりあえずこれで無料にはなりました！！
はたして最低スペックで動くのか...

# 作成したクラスタに接続

`$ gcloud container clusters get-credentials <クラスター名> --zone us-west1-a`

上記のコマンドを実行するとクラスターが設定ファイルに登録され、**kubectl** コマンドでクラスターを操作できるようになります。
ちなみに設定ファイルはデフォルトだと `~/.kube/config` です。

## 起動したクラスタのPodを確認してみる

### podとは？

node(インスタンス)の中で動いているdockerコンテナ、みたいなイメージです。
面白いのは、k8sは **node全てのパワーを全体リソース** として、podがいくつも立ち上がります。
f1-microのnodeだとメモリが 0.6GB なので、nodeが3つあれば1.8GBのメモリを使用でき、**どのnodeでどのpodを起動するかとかは全ていい感じに自動でやってくれます。**

あるnodeにはpodAとpodBが1つずつ、あるnodeにはpodBが３つ、といったようにnode全体としてpodsを共有しています。

pod = dockerコンテナ みたいなことを言いましたが **pod1つの中に、任意の数のdocker containerを詰め込むことも可能** です。nginxとapllicationとredisとか。
pod 1 つに対して内部IPが 1 つ割り当てられ、コンテナ間で共有できるvolumeも任意の数を持つことができます。

### kubectlコマンドでpodの状態を見てみる

```
$ kubectl get pod --all-namespaces
```

`--all-namespaces` をつけると、k8s-systemが動いているdefaultのpod含め全てのpodの状態をみることができます。
(オプションつけないと自分で立ち上げたpodだけ見れます)

![20190320135315.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/760714/3e3dbf0b-55f5-90ea-6d8c-cd6d2df7927b.png)


pendingが2つあります...やはりスペックが足りなくてデフォルトのpodすら立ち上がらない模様。
念の為原因を確認します。

```
$ kubectl describe pod --namespace kube-system kube-dns-548976df6c-hf67f
(長いので省略)
Events:
  Type     Reason            Age                   From               Message
  ----     ------            ----                  ----               -------
  Warning  FailedScheduling  42s (x8080 over 20h)  default-scheduler  0/1 nodes are available: 1 Insufficient memory.
```

**「Insufficient memory」** メモリ不足でした！＼(^o^)／

# preemptibleインスタンスのnodeを立ててスペックを確保する

## preemptibleインスタンスとは？
Google の膨大なデータセンターの余剰リソースを活用したインスタンスです。(要するに余ってるサーバー)
普通のインスタンスとなんら変わりはありませんが、 **必ず24時間以内にシャットダウンされます。**
そのため、通常のインスタンスの価格の数分の一の価格で提供されています。(**最大70%オフ**）

## **＼(^o^)／ここから有料です＼(^o^)／**

## preemptibleインスタンスが1つのnode-poolをつくってみる

```
$ gcloud container node-pools create <プール名> --cluster <クラスタ名> --zone us-west1-a --machine-type=f1-micro --num-nodes=1 --disk-size=10 --preemptible
```

しばらくたってからpodの状態を確認

![20190320140030.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/760714/47d781b1-26cd-591d-56b1-e3b81d85f2df.png)


全てrunningになった！！
GKEを動かそうとおもったら最低でもf1-microインスタンス2つ相当のスペックがいるようです。

# Datadogで監視してみる

**「ところでpreemptibleインスタンスってほんとに1日に1回死んでるの？」**
  
これを確認するために監視ツールのDatadogを入れてみます。
Datadogは5台以下のホスト構成ならFreeプラン(無償)でモニタリングしてくれます。(データの保管期間は1日)

Datadog公式で[KubernetesへのDatadog Agent導入方法](https://docs.datadoghq.com/agent/kubernetes/daemonset_setup/#create-manifest)が用意されているのでただ導入するだけなら簡単にできます。
Datadogにログインしている場合は[こちら](https://app.datadoghq.com/account/settings#agent/kubernetes)に書いてある `datadog-agent.yaml` そのままコピペで作成してcreateできます。

```
# 導入方法に従ってdatadog-agent.yamlを作成
$ vi datadog-agent.yaml

$ kubectl create -f datadog-agent.yaml
daemonset.extensions "datadog-agent" created
```

daemonsetで datadog-agentがcreateされました。

## ReplicaSetとDaemonSet

### ReplicaSetとは...
前述のnode全てのリソースを全体リソース として、podが立ち上がるための仕組みです。
(クラスタ上のPodを決められた数に維持してくれる仕組み。)
例えば、Aノードが死んでその上で動いていたコンテナ(Pod)が減っても、Bノードでまた同じ数のPodを起動してくれます。

### DaemonSetとは...

1ノードに1つだけPodを置いてくれる仕組みです。
例えば、ノードAでPodに異常が出て死んでしまっても、同じAノード上にPodが蘇るように調整してくれます。
クラスタにBノードが追加されたらそのノードにも自動でPodを配置してくれます。
DatadogやMackerel等でのコンテナクラスタの監視はこのDaemonSetでコンテナを建てるようです。

## Datadogをいれたらまたスペックがたりないと言われた、、

前述の通りDatadogはDaemonSetなのでノードを増やしたところでスペック不足は解消されません。
ノード自体のスペックを上げるしかないです、、
無料枠の活用は諦めてg1-smallのpreemptibleインスタンス2台構成にしました(T_T)

```
# めんどくさいので1度全て削除してからもう一度作成
$ kubectl delete -f datadog-agent.yaml
$ gcloud container node-pools delete <preemptibleプール> --zone us-west1-a --cluster <クラスタ名>
$ gcloud container node-pools delete <無料プール> --zone us-west1-a --cluster <クラスタ名>

# g1-smallのpreemptibleインスタンスのノードプール作成
$ gcloud container node-pools create <プール名> --cluster <クラスタ名> --zone us-west1-a --machine-type=g1-small --num-nodes=2 --disk-size=15 --preemptible
$ kubectl create -f datadog-agent.yaml
```

```
$ kubectl get pod
NAME                  READY     STATUS    RESTARTS   AGE
datadog-agent-77lwl   1/1       Running   0          4m
datadog-agent-tn4cs   1/1       Running   0          4m
```

ようやくDatadogのダッシュボードも見れるようになりました！

![20190320141242.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/760714/561a594b-defd-1425-6ec0-b84776e55062.png)



# nginxを立てる

続いて、nginxを動かしてみます。

nginx.deployment.yamlを作成

```nginx.deployment.yaml
apiVersion: extensions/v1beta1
kind: Deployment
metadata:
  name: nginx
spec:
  replicas: 1
  template:
    metadata:
      labels:
        app: nginx
    spec:
      containers:
      - name: nginx
        image: nginx:1.7.5
        ports:
        - containerPort: 80
```

deploymentを作成する

```
$ kubectl create -f nginx.deployment.yaml
deployment.extensions "nginx" created
```

```
$ kubectl get pod
NAME                     READY     STATUS    RESTARTS   AGE
datadog-agent-77lwl      1/1       Running   0          10m
datadog-agent-tn4cs      1/1       Running   0          10m
nginx-64c6b46884-zc9mq   1/1       Running   0          1m
```

続いてこのnginxにアクセスしたいのですが、この状態ではポートが外部に公開されていないためアクセスできません。
公開する方法としては、ロードバランサを用いる方法があるのですが**ロードバランサは高いです！！！！**
今回は安く抑えるためロードバランサを用いず、ノードのポートを直接公開します。

nginx.service.yamlを作成

```nginx.service.yaml
apiVersion: v1
kind: Service
metadata:
  name: nginx
spec:
  selector:
    app: nginx
  ports:
    - name: http
      protocol: TCP
      port: 80
      targetPort: 80
  externalIPs:
     - <ノードの内部IP>
     - <ノードの内部IP>
```

`<ノードの内部IP>` となっている箇所は適時書き換えてください。
内部IPはGCPのコンソールでインスタンス内部IPを見るか、下記のコマンドで取得できます。
`$ gcloud --format json compute instances list | jq -r '.[].networkInterfaces[].networkIP'`


serviceを作成

```
$ kubectl create -f nginx.service.yaml
service "nginx" created
```

最後にノードのポートを開放します。

```
$ gcloud compute firewall-rules create http-firewall --allow tcp:80
..省略..
Creating firewall...done.
NAME           NETWORK  DIRECTION  PRIORITY  ALLOW   DENY  DISABLED
http-firewall  default  INGRESS    1000      tcp:80        False
```
これでOKです！

あとはノードの外部IPにアクセスすればnginxのデフォルトページが表示されるはずです！！
外部IPもGCPコンソールで確認するか下記のコマンドで各自調べてください。
`$ gcloud --format json compute instances list | jq -r '.[].networkInterfaces[].accessConfigs[].natIP'`

![20190320142029.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/760714/a446f79c-7caf-b682-21f9-dbe41b85d08d.png)


🎉 🎉 🎉 🎉 🎉 🎉 🎉 🎉

### nginxのdeploymentとservice２つ作ってたけど何？

#### Deploymentとは？
複数のコンテナを並列起動しバージョン管理できる仕組みです。
Deploymentは前述した**ReplicaSetを生成・管理**し、ReplicaSetはPodを生成・管理します。

[Kubernetes: Deployment の仕組み - Qiita](https://qiita.com/tkusumi/items/01cd18c59b742eebdc6a)

#### Serviceとは？

Serviceは複数のコンテナへのアクセスを仲介してくれる仕組みです。
Serviceのおかげで個々のコンテナのアクセス方法を把握しなくても、Serviceの受付口だけ知っていれば通信できます。

[Kubernetesの Service についてまとめてみた - Qiita](https://qiita.com/kouares/items/94a073baed9dffe86ea0)

# アプリケーションをデプロイする

せっかくなので自分でつくったアプリケーションをデプロイしたいところですが、
今回はGKE自体を使うことが目的なので[Googleが公式で用意してくれているHelloWorldを表示するだけのアプリケーション](https://cloud.google.com/kubernetes-engine/docs/quickstart#code_review)をデプロイします！
ちなみにこのhello-appはGCR(GCPのコンテナレジストリ, AWSでいうECR)に用意されているのでそのままpullして使えます。

GCRのリポジトリは[こちら](https://console.cloud.google.com/gcr/images/google-samples/GLOBAL/hello-app@sha256:c62ead5b8c15c231f9e786250b07909daf6c266d0fcddd93fea882eb722c3be4/details?tab=info)


次にnginxと同じように hello.deployment.yamlとhello.service.yamlを作成します。

```hello.deployment.yaml
apiVersion: extensions/v1beta1
kind: Deployment
metadata:
  name: hello-app
  labels:
    app: hello
spec:
  replicas: 1
  selector:
    matchLabels:
      app: hello
  template:
    metadata:
      labels:
        app: hello
    spec:
      containers:
      - name: hello
        image: gcr.io/google-samples/hello-app:1.0
        ports:
        - containerPort: 8080
```

```hello.service.yaml
apiVersion: v1
kind: Service
metadata:
  name: hello-app
spec:
  type: NodePort
  selector:
    app: hello
  ports:
    - name: http
      port: 8080
      targetPort: 8080
  externalIPs:
    - <ノードの内部IP>
    - <ノードの内部IP>
```

```
$ kubectl create -f hello.deployment.yaml
$ kubectl create -f hello.service.yaml

# firewallで8080番も許可
$ gcloud compute firewall-rules create hello-firewall --allow tcp:8080
```

あとはノードの外部IPの8080番にアクセスすれば**「Hello, World!」**です！

![20190320143101.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/760714/b1eb84e2-c860-d4b2-986f-55b88c46ad94.png)


🎉 🎉 🎉 🎉 🎉 🎉 🎉 🎉

# nginxで80 -> 8080のプロキシの設定だけやってみる

アプリケーションとnginxのが動きだしたので、nginx.confで色々設定したくなってきました。
今回はお試しなので80番ポートにアクセスがきたら8080番に飛ばすプロキシの設定だけやってみます。

## kubernetesで動かすDockerコンテナ内にどうやって設定ファイルを差し込む?

今回でいえばnginx.confなどの設定ファイルをどうやって管理するか、です。
方法はいくつかあります。

```
1. Dockerイメージの中に入れておく
    - ADDした状態でdocker buildしておく
    - buildしたコンテナをコンテナレジストリにpushしておく
2. ファイルにしておいてコンテナにVolumeとしてマウントする
    - デプロイするホストのディレクトリをマウントする
    - 永続ディスクを作成してそこに配置しておいてマウントする
3. どこかのストレージに置いておいてダウンロードする
```

今回は勉強のためにkubernetesの**ConfigMap**というものを使ってみようと思います。

ConfigMapとは、**簡単にいうとkey-valueで値を持てるもの**らしいです。
イメージ的には環境変数に近いですが、設定ファイルも持つことが出来ます。
**volumeとしてもマウントもできる**ようになっています。

nginx.config.yamlを作成

```nginx.config.yaml
apiVersion: v1
kind: ConfigMap
metadata:
  name: nginx-conf
data:
  nginx.conf: |
    user nginx;
    worker_processes  3;
    error_log  /var/log/nginx/error.log;
    events {
      worker_connections  10240;
    }
    http {
      log_format  main
              'remote_addr:$remote_addr\t'
              'time_local:$time_local\t'
              'method:$request_method\t'
              'uri:$request_uri\t'
              'host:$host\t'
              'status:$status\t'
              'bytes_sent:$body_bytes_sent\t'
              'referer:$http_referer\t'
              'useragent:$http_user_agent\t'
              'forwardedfor:$http_x_forwarded_for\t'
              'request_time:$request_time';

      access_log    /var/log/nginx/access.log main;

      server {
          listen       80;
          server_name  <ノード1の外部IP>;

          location / {
              proxy_pass    http://<ノード1の外部IP>:8080/;
          }
      }

      server {
          listen       80;
          server_name  <ノード2の外部IP>;

          location / {
              proxy_pass    http://<ノード2の外部IP>:8080/;
          }
      }
    }
```

```
$ kubectl create -f nginx.config.yaml
configmap "nginx-conf" created
```

あとはconfigmapをnginxのdeploymentに適用します。

```nginx.deployment.yaml
apiVersion: extensions/v1beta1
kind: Deployment
metadata:
  name: nginx
spec:
  replicas: 1
  template:
    metadata:
      labels:
        app: nginx
    spec:
      containers:
      - name: nginx
        image: nginx:1.7.5
        ports:
        - containerPort: 80
        volumeMounts:
        - mountPath: /etc/nginx # /etc/nginxにvolumesのnginx-confをmountする
          readOnly: true
          name: nginx-conf
        - mountPath: /var/log/nginx
          name: log
      volumes:
      - name: nginx-conf # volumeMountsで/etc/nginxにmountするやつ
        configMap: 
          name: nginx-conf # ConfigMapのnginx-confを/etc/nginx以下に配置する
          items:
            - key: nginx.conf # nginx-confのkey
              path: nginx.conf # nginx.confというファイル名
      - name: log
        emptyDir: {}
```

では実際にnginxコンテナの中に入ってnginx.confがマウントされているか見てみます。

```
# kubectl get pod でnginxのコンテナ名を調べて中に入る
$ kubectl exec -it <nginxのコンテナ名> bash

# nginx.confをみてみる
$ cat etc/nginx/nginx.conf
```

`etc/nginx/nginx.conf` がConfigMapで設定したものになっているはずです！！
実際に80番ポートでnginxにアクセスするとhello-appのHello, Worldが表示されました！

![20190322112926.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/760714/24af5af7-7479-35d5-46b5-33929bb2822b.jpeg)


# 最後に
いかがでしたでしょうか？
今回はあくまで個人での検証/実験のためにつくっているので、本物のサービス運用を見据えたものとは全く違うと思います。
細かい仕様や制限についてもまだまだ調べきれていないので、考慮漏れや間違いなどが多々あると思うので、そこは注意していただきたいです。

料金に関しては、今回はDaemonSetでDatadogをいれたので無料枠のf1-microを諦めましたが、監視ツールをいれなければもっと安くk8s環境を構築できそうですね。
また、GCPは登録から12ヶ月は300$分を無料で試せるのでよっぽど贅沢な設定にしなければ財布には優しいかと思います。

昨今のマイクロサービスブームや[Google Cloud Next ’18](https://cloud.withgoogle.com/next18/sf/) でk8s上でServerlessを実現する[Knative](https://cloud.google.com/knative/?hl=ja)が発表される等、今一番Hotといっても過言ではない Kubernetes。
エンジニアとしてはこういった新しい技術はどんどん触れたいですね！


今回作成したyamlファイルは[こちらのリポジトリ]([https://github.com/kkwatanabe/hello-kube/tree/master])にまとめました。