MinikubeでKubernetesクラスタが動く環境を作る

Minikubeではローカル環境にKubernetesクラスタを作成することができます。
GKE等のサービスと変わらない感覚でkubectlコマンドを叩けるので結構便利でした。
マルチノードに対応しているわけではないので、それなりに本番環境とはマニフェストの差異が出るとは思いますが、とりあえず動かしてみる目的であればローカルで動くというのは大きなメリットだと思います。

準備

  • VirtualBoxのインストール
    ここからインストールできる。
    6.0.4である程度の動作は確認済み。
  • minikubeのインストール
    Macなら以下で入る。その他はこちら
$ brew cask install minikube

minikubeの起動

$ minikube --vm-driver virtualbox start

vm-driverの指定が必須。面倒なのでaliasを指定してある。

alias 'minikube start'='minikube --vm-driver virtualbox start'

この時点ではリモートのDockerイメージを利用する設定になっているので、以下のコマンドを叩いておきます。

$ eval $(minikube docker-env)

これを行うことによって、ホストマシンの docker コマンドでminikube VM内のDockerデーモンと通信できるようになり、ローカルでビルドしたイメージがそのまま使えるようになります。

以下のコマンドで戻せます。

$ eval $(minikube docker-env -u)

停止

$ minikube stop

kubernetes contextの切り替え

# 一覧
$ kubectl config view
# minikubeに切り替え
$ kubectl config use-context minikube

参考

kubernetes/minikube

GKE上にMySQL/Redisを乗せて永続化させる

GKE上にMySQLとかRedisをわざわざ乗っけるならCloud SQLとかMemoryStoreとか使っといた方が耐障害性たけーよなとか思うんですが料金がお手軽じゃないです。
なので試しにGKEにのせてみることにします。 replicaは1台です。

Secret / ConfigMap の登録

---
apiVersion: v1
kind: Secret
metadata:
  name: mysql-secret
data:
  MYSQL_ROOT_PASSWORD: <BASE64_ENCODED_ROOT_PASSWORD>
  MYSQL_DATABASE: <BASE64_ENCODED_DATABASE>
  MYSQL_USER: <BASE64_ENCODED_USER>
  MYSQL_PASSWORD: <BASE64_ENCODED_PASSWORD>
---
apiVersion: v1
kind: ConfigMap
metadata:
  name: mysql-config-file
data:
  custom.cnf: |
    [mysqld]
    default_authentication_plugin=mysql_native_password
    character-set-server=utf8mb4

    [client]
    default-character-set=utf8mb4
$ kubectl apply -f mysql-config.yml

まずMySQLの設定に必要なConfigMapとSecretをapplyしておきます。
Secretの値にはbase64エンコードされた値を入れておく必要があります。
(一応エンコードが必要ないstringDataなるフィールドもある)

$ echo -n "<TEXT>" | openssl enc -e -base64

これでエンコードできる。

Mysql Serviceの登録

---
apiVersion: v1
kind: Service
metadata:
  name: mysql
spec:
  ports:
  - port: 3306
  selector:
    app: mysql
  clusterIP: None
---
apiVersion: apps/v1 # for versions before 1.9.0 use apps/v1beta2
kind: StatefulSet
metadata:
  name: mysql
spec:
  selector:
    matchLabels:
      app: mysql
  replicas: 1
  serviceName: mysql
  template:
    metadata:
      labels:
        app: mysql
    spec:
      containers:
      - image: mysql:8.0.15
        name: mysql
        ports:
        - containerPort: 3306
          name: mysql
        envFrom:
        - configMapRef:
            name: mysql-config-file
        - secretRef:
            name: mysql-config
        volumeMounts:
        - name: mysql-persistent-storage
          mountPath: /var/lib/mysql
          subPath: mysql
        - name: mysql-config-volume
          mountPath: /etc/mysql/conf.d/custom.cnf
          subPath: custom.cnf
      volumes:
      - name: mysql-config-volume
        configMap:
          name: mysql-config-file
  volumeClaimTemplates:
  - metadata:
      name: mysql-persistent-storage
    spec:
      accessModes: [ "ReadWriteOnce" ]
      resources:
        requests:
          storage: 20Gi
$ kubectl apply -f mysql.yml

volumeClaimTemplatesにClaimの情報を書き込んだ上で、そのClaimをvolumeMountsで指定してやります。 これで自動的にPersistentVolumeが立つようになります。
GKEではPersistent Volumeのデフォルトとして GCEPersistentDisk が作成されます。 デフォルトではHDDなので、SSDで作成したい場合はStorageClassを追加してspec.storageClassNameを指定する必要があります。

MySQLのデプロイでちょっとハマったんですが、マウントするストレージのディレクトリにはいくつかファイル(ディレクトリ)があるようで、MySQLは初回起動時にデータを保管するディレクトリが空でない場合にエラーを吐くため、Podが立ちませんでした。
対策として、subPath: mysql を指定することでPersistentVolumeに mysql/ ディレクトリを作成して、そのディレクトリを /var/lib/mysql にマウントするようにしています。

Redis Serviceのデプロイ

apiVersion: v1
kind: Service
metadata:
  name: redis
spec:
  ports:
    - port: 6379
  clusterIP: None
  selector:
    app: redis
---
apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: redis
spec:
  selector:
    matchLabels:
      app: redis
  serviceName: redis
  replicas: 1
  template:
    metadata:
      labels:
        app: redis
    spec:
      containers:
      - name: master
        image: redis:alpine
        ports:
        - containerPort: 6379
        volumeMounts:
        - name: redis-persistent-storage
          mountPath: /data
          subPath: redis
  volumeClaimTemplates:
  - metadata:
      name: redis-persistent-storage
    spec:
      accessModes: [ "ReadWriteOnce" ]
      resources:
        requests:
          storage: 1Gi
$ kubectl apply -f redis.yml

MySQLとあんまり変わらないです。 確認してみます。

# keyをセットする
$ kubectl exec -it redis-0 redis-cli
127.0.0.1:6379> set key redis
OK
127.0.0.1:6379> exit

# podを落として再度作成されるまで待つ
$ kubectl delete pod redis-0
pod "redis-0" deleted

# 新しくできたpodに入ってkeyをgetしてみる
$ kubectl exec -it redis-0 redis-cli
127.0.0.1:6379> get key
"redis"

大丈夫そうです。
このままだとスケールしませんが節約にはなります。
でも節約考えるならGKE使わなくてもいいんだよなぁ…

Kubernetesにデプロイされているアプリケーションのデバッグ

雑にデバッグしたい時用。

Podのステータス確認

$ kubectl get pods

Podの詳細確認

$ kubectl describe pod <POD_NAME>

ログ確認
一番これを使う気がする。

$ kubectl logs <POD_NAME>
# マルチコンテナPodの場合
$ kubectl logs <POD_NAME> -c <CONTAINER_NAME>

コンテナが稼動していて中に入りたい場合

$ kubectl exec -it <POD_NAME> <COMMAND> # e.g. /bin/bash

リソース確認

# nodeの確認
$ kubectl top node <NODE>
# podの確認
$ kubectl top pod <POD>

Google Container Registry にイメージをアップロードする

公式がより詳しい。

準備

  • google-cloud-sdkのインストール
  • dockerのインストール
  • gcloudをdockerの認証ヘルパーに登録しておく
$ gcloud auth configure-docker

Dockerイメージにタグをつける

$ docker tag <SOURCE_IMAGE> <HOSTNAME>/<PROJECT_ID>/<IMAGE>:<TAG>
  • HOSTNAME
gcr.io us.gcr.io eu.gcr.io asia.gcr.io
米国(変更の可能性あり) 米国(gcr.ioとは別のバケット) 欧州 アジア

今回はアジア鯖に上げる。

ホストにpush

$ docker push asia.gcr.io/<PROJECT_ID>/<IMAGE>:<TAG>

確認

# Image確認
$ gcloud container images list --repository asia.gcr.io/<PROJECT_ID>
# Tag確認
$ gcloud container images list-tags asia.gcr.io/<PROJECT_ID>/<IMAGE>

gcloud command cheatsheet

基本的なところだけ。 こういうの devhints とか建てて書いといたほうがいいんだろうなぁ…

auth

# ログイン
$ gcloud auth login
# リスト
$ gcloud auth list

config

# リスト
$ gcloud config list
# アカウント変更
$ gcloud config set account <ACCOUNT>

project

# リスト
$ gcloud projects list
# 変更
$ gcloud config set project <PROJECT_ID>

compute

# リージョンリスト
$ gcloud compute regions list
# リージョン変更
$ gcloud config set compute/region <REGION>  # asia-northeast1
# リージョンリスト
$ gcloud compute zones list
# リージョン変更
$ gcloud config set compute/zone <REGION>  # asia-northeast1

components

$ gcloud components update  # アップデート
$ gcloud components install <COMPONENT> # インストール。kubectlとか。
$ gcloud components list  # リスト

address

# グローバルIP作成
$ gcloud compute addresses create <IP_NAME> --global
# 確認
$ gcloud compute addresses describe <IP_NAME> --global

GKEで組んだクラスタへのHTTPS接続を試みる

GKEではGAEやAWSロードバランサーのようにボタンポチーでHTTPS対応!みたいにはいかないようなのでメモ。

単純に実現しようとすると cert-manager でLet's Encryptの証明書を取得するのが最も手っ取り早そうなので、これを試してみることにします。
ちなみに 公式ドキュメント まんまなのでこっちを見たほうが早いと思います。

Helmのインストール

HelmはKubernetesのパッケージマネージャーです。
Macなら brew install kubernetes-helm で入る。
スクリプト叩くだけでもいい。詳しくは こちら

サービスアカウントの作成

サービスアカウントを作る

$ kubectl create serviceaccount tiller --namespace=kube-system

作成したサービスアカウントに権限をつける

$ kubectl create clusterrolebinding tiller-admin --serviceaccount=kube-system:tiller --clusterrole=cluster-admin

この例では管理者権限を付与してますが本番環境で使うときはしっかり権限のハンドリングをしておいたほうが良さそうです。参考

Tillerのインストール
サービスアカウントを渡してhelmをイニシャライズします。
これでKubernetesクラスタにTillerが立ちます。
Tillerはhelmのクライアントから送られてくる情報を元にうまいことk8sAPIを叩いてくれるいい奴です。

$ helm init --service-account=tiller

nginx-ingressを立てる

$ helm install stable/nginx-ingress

これで各クラウドベンダのロードバランサーも立ちます。お金がかかります。しばらくすると外部IPが生えます。

外部IPにドメインを紐付ける

DNSの設定を行う。

アプリケーションのデプロイ

アプリケーションのServiceを立てておく。

apiVersion: extensions/v1beta1
kind: Ingress
metadata:
  name: exmaple-ingress
  annotations:
    kubernetes.io/ingress.class: "nginx"    
    # certmanager.k8s.io/issuer: "letsencrypt-staging"
    # certmanager.k8s.io/acme-challenge-type: http01

spec:
  tls:
  - hosts:
    - example.example.com
    secretName: example-tls
  rules:
  - host: example.example.com  # ドメイン名
    http:
      paths:
      - path: /
        backend:
          serviceName: example-service  # アプリケーションのService名
          servicePort: 80

これを適当なファイル(今回はingress.yml)に保存して

$ kubectl apply -f ingress.yml

ここまでで外部からの通信が可能になる。

cert-managerのインストール

$ kubectl apply -f https://raw.githubusercontent.com/jetstack/cert-manager/release-0.6/deploy/manifests/00-crds.yaml

Issuer の登録

Let's Encryptには証明書の取得制限があります。
基本的に気にしなくてもいいレベルの制限ですが、同じドメインの証明書の取得は一週間に5度までです。
作り直したりする過程で制限に入っても困るのでstagingで試してからにします。
なお、DNS-01方式はだるそうだったので、HTTP-01方式でドメインの認証を行います。

apiVersion: certmanager.k8s.io/v1alpha1
kind: Issuer
metadata:
  name: letsencrypt-staging
spec:
  acme:
    # The ACME server URL
    server: https://acme-staging-v02.api.letsencrypt.org/directory
    # Email address used for ACME registration
    email: user@example.com
    # Name of a secret used to store the ACME account private key
    privateKeySecretRef:
      name: letsencrypt-staging
    # Enable the HTTP-01 challenge provider
    http01: {}
$ kubectl apply -f staging-issuer.yaml

本番用のIssuerも作成しておく。

apiVersion: certmanager.k8s.io/v1alpha1
kind: Issuer
metadata:
  name: letsencrypt-prod
spec:
  acme:
    # The ACME server URL
    server: https://acme-v02.api.letsencrypt.org/directory
    # Email address used for ACME registration
    email: user@example.com
    # Name of a secret used to store the ACME account private key
    privateKeySecretRef:
      name: letsencrypt-prod
    # Enable the HTTP-01 challenge provider
    http01: {}
$ kubectl apply -f production-issuer.yaml

再度ingress.ymlを開いてコメントアウトを外した後にapplyする。

apiVersion: extensions/v1beta1
kind: Ingress
metadata:
  name: exmaple-ingress
  annotations:
    kubernetes.io/ingress.class: "nginx"    
    certmanager.k8s.io/issuer: "letsencrypt-staging"
    certmanager.k8s.io/acme-challenge-type: http01

spec:
  tls:
  - hosts:
    - example.example.com
    secretName: example-tls
  rules:
  - host: example.example.com
    http:
      paths:
      - path: /
        backend:
          serviceName: example-service
          servicePort: 80
$ kubectl apply -f ingress.yml

applyすることでOrderが動いてLet's Encryptに対してリクエストが飛びます。
証明書の発行状態は kubectl describe certificate example-tls で表示されるEventから見れる。

完了していた場合はSecretが作成されている。

$ kubectl describe secret example-tls

で確認できる。

問題なければIssuerを本番のものに切り替える。

apiVersion: extensions/v1beta1
kind: Ingress
metadata:
  name: exmaple-ingress
  annotations:
    kubernetes.io/ingress.class: "nginx"    
    certmanager.k8s.io/issuer: "letsencrypt-prod"  # changed
    certmanager.k8s.io/acme-challenge-type: http01

spec:
  tls:
  - hosts:
    - example.example.com
    secretName: example-tls
  rules:
  - host: example.example.com
    http:
      paths:
      - path: /
        backend:
          serviceName: example-service
          servicePort: 80
$ kubectl apply -f ingress.yml

Issuerを変更した後に、Secretを削除することで変更後のIssuerで証明書を取得します。

$ kubectl delete secret example-tls

ちょっとだけ時間がかかる。 状態はOrderから見れる。

$ kubectl get order
NAME                    AGE
example-tls-2000613234   2h
$ kubectl describe order example-tls-2000613234

以上でHTTPS接続が確認できた。
Helmとか知ってればあんまり手間じゃないのかもしれないけど結構面倒くさかったです…