LiBz Tech Blog

LiBの開発者ブログ

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

目次

はじめに

こんにちは!今回が2回目の投稿になります。LiBのエンジニアの渡邊です!

前回はGitのコミットメッセージにEmoji Prefixを使ってテンションをあげたい話🕺💃🕺💃を書きました。

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

f:id:kkwatanabe:20190320155112p:plain

経緯

自分の所属している部署では開発環境が完全に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が良いと思われます。

f:id:kkwatanabe:20190320164716p:plain

あと、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が3つ、といったように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だけ見れます)

f:id:kkwatanabe:20190320135315p:plain

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の状態を確認

f:id:kkwatanabe:20190320140030p:plain

全てrunningになった!!

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

Datadogで監視してみる

「ところでpreemptibleインスタンスってほんとに1日に1回死んでるの?」

これを確認するために監視ツールのDatadogを入れてみます。

Datadogは5台以下のホスト構成ならFreeプラン(無償)でモニタリングしてくれます。(データの保管期間は1日)

Datadog公式でKubernetesへのDatadog Agent導入方法が用意されているのでただ導入するだけなら簡単にできます。

Datadogにログインしている場合はこちらに書いてある 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のダッシュボードも見れるようになりました!

f:id:kkwatanabe:20190320141242p:plain

nginxを立てる

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

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を作成

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'

f:id:kkwatanabe:20190320142029p:plain

🎉 🎉 🎉 🎉 🎉 🎉 🎉 🎉

nginxのdeploymentとservice2つ作ってたけど何?

Deploymentとは?

複数のコンテナを並列起動しバージョン管理できる仕組みです。

Deploymentは前述したReplicaSetを生成・管理し、ReplicaSetはPodを生成・管理します。

Kubernetes: Deployment の仕組み - Qiita

Serviceとは?

Serviceは複数のコンテナへのアクセスを仲介してくれる仕組みです。

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

Kubernetesの Service についてまとめてみた - Qiita

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

せっかくなので自分でつくったアプリケーションをデプロイしたいところですが、
今回はGKE自体を使うことが目的なのでGoogleが公式で用意してくれているHelloWorldを表示するだけのアプリケーションをデプロイします!

ちなみにこのhello-appはGCR(GCPのコンテナレジストリ, AWSでいうECR)に用意されているのでそのままpullして使えます。

GCRのリポジトリはこちら↓ console.cloud.google.com

nginxと同じように hello.deployment.yamlとhello.service.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
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!」です!

f:id:kkwatanabe:20190320143101p:plain

🎉 🎉 🎉 🎉 🎉 🎉 🎉 🎉

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を作成

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に適用します。

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が表示されました!

f:id:kkwatanabe:20190322112926j:plain:w150

最後に

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

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

昨今のマイクロサービスブームやGoogle Cloud Next ’18 でk8s上でServerlessを実現するKnativeが発表される等、今一番Hotといっても過言ではない Kubernetes。
エンジニアとしてはこういった新しい技術はどんどん触れたいですね!

今回作成したyamlファイルはこちらのリポジトリにまとめました。

github.com