GeekFactory

int128.hatenablog.com

本物のクラスタを利用してkubectl pluginをテストする

kubectlのプラグインを開発していると、ユニットテストだけでなく、本物のKubernetesクラスタを利用したテストが欲しくなります。プラグインの振る舞いが複雑な場合は自動テストがあると安心してリリースできます。

本稿では、本物のKubernetesクラスタを利用してkubectlプラグインをテストする方法を考えます。

テストの基本形

本物のクラスタを利用してプラグインをテストするための構成を下図に示します。

https://github.com/int128/kubectl-tree-e2e-test

必要なのは以下の3つです。

テストの流れは以下のようになります。

  1. クラスタを作成する。
  2. 必要なリソースをデプロイする。
  3. kubectlを実行する。
  4. 間接的にkubectl pluginが実行される。
  5. 実行結果が期待通りか検証する。

このようなテストはプラグインのリリース前に手動でやっていることが多いと思います。自動テストを導入することで、

  • リリース前ではなくPull Requestの契機で不具合を検出できる
  • クリーンな環境でテストできるので信頼性が高い
  • 手動では手間のかかる組合せテストが可能になる(複数のクラスタバージョンなど)

といったメリットがあります。

例: kubectl-treeのテストを書いてみる

ここでは、kubectl-treeというプラグインをテストする例を考えます。これはクラスタにあるリソースを木構造で表示してくれる便利なプラグインです。

簡単のため、テストシナリオはMakefileで書くことにします。Makefileをよく知らない方はシェルスクリプトだと思って読んでみてください。

クラスタの作成とリソースのデプロイ

まずは新しいクラスタを作成するターゲットを定義します。Kindを利用すると、Docker上に簡単にクラスタを作成できます。

CLUSTER_NAME := kubectl-tree-e2e-test
OUTPUT_DIR := $(CURDIR)/output

KUBECONFIG := $(OUTPUT_DIR)/kubeconfig.yaml
export KUBECONFIG

.PHONY: cluster
cluster: $(OUTPUT_DIR)/kubeconfig.yaml
$(OUTPUT_DIR)/kubeconfig.yaml:
  kind create cluster --name $(CLUSTER_NAME)
  kubectl cluster-info

.PHONY: delete-cluster
delete-cluster:
  kind delete cluster --name $(CLUSTER_NAME)
   -rm $(KUBECONFIG)

これでmakeを実行すると新しいクラスタが作成される仕組みができました。実行にはDockerとKindが必要です。

% make
kind create cluster --name kubectl-tree-e2e-test
Creating cluster "kubectl-tree-e2e-test" ...
 ✓ Ensuring node image (kindest/node:v1.17.0) 🖼
 ✓ Preparing nodes 📦
 ✓ Writing configuration 📜
 ✓ Starting control-plane 🕹️
 ✓ Installing CNI 🔌
 ✓ Installing StorageClass 💾
Set kubectl context to "kind-kubectl-tree-e2e-test"
You can now use your cluster with:

kubectl cluster-info --context kind-kubectl-tree-e2e-test

Not sure what to do next? 😅 Check out https://kind.sigs.k8s.io/docs/user/quick-start/
kubectl cluster-info
Kubernetes master is running at https://127.0.0.1:32771
KubeDNS is running at https://127.0.0.1:32771/api/v1/namespaces/kube-system/services/kube-dns:dns/proxy

To further debug and diagnose cluster problems, use 'kubectl cluster-info dump'.

% make delete-cluster
kind delete cluster --name kubectl-tree-e2e-test
Deleting cluster "kubectl-tree-e2e-test" ...
rm /kubectl-tree-e2e-test/output/kubeconfig.yaml

テストを行うにはクラスタだけでなく適当なリソースも必要です。ここではechoserverをデプロイすることにします。以下のようにkubectl applyコマンドを追記します。

.PHONY: cluster
cluster: $(OUTPUT_DIR)/kubeconfig.yaml
$(OUTPUT_DIR)/kubeconfig.yaml:
  # create a cluster
  kind create cluster --name $(CLUSTER_NAME)
  kubectl cluster-info
  # deploy the echoserver
  kubectl apply -f fixture.yaml
  # wait for the deployment
  kubectl -n echoserver rollout status deployment/echoserver

これで、テストに必要なクラスタとリソースが揃いました。

プラグインの実行

kubectl treeコマンドを実行すると以下のような結果が表示されます。

% kubectl tree -n echoserver deployment echoserver
NAMESPACE   NAME                                  READY  REASON  AGE
echoserver  Deployment/echoserver                 -              59s
echoserver  └─ReplicaSet/echoserver-5d8cc8d48   -              48s
echoserver    └─Pod/echoserver-5d8cc8d48-bcvs4  True           46s

ここでは簡単のため、grepで必要な文字列が表示されているかチェックします。もっと複雑な条件判定が必要な場合はスクリプトを書く方がよいでしょう。

.PHONY: test
test: cluster
  # run kubectl-tree
  kubectl tree -n echoserver deployment echoserver | tee $(OUTPUT_DIR)/actual
  # make sure the output contains the expected lines
  egrep --color '^echoserver +Deployment/echoserver' $(OUTPUT_DIR)/actual
  egrep --color '^echoserver +└─ReplicaSet/echoserver-' $(OUTPUT_DIR)/actual
  egrep --color '^echoserver +└─Pod/echoserver-' $(OUTPUT_DIR)/actual

makeを実行してみましょう。以下のようにテストに合格するはずです。

% make
# run kubectl-tree
kubectl tree -n echoserver deployment echoserver | tee /kubectl-tree-e2e-test/output/actual
NAMESPACE   NAME                                  READY  REASON  AGE
echoserver  Deployment/echoserver                 -              5m4s
echoserver  └─ReplicaSet/echoserver-5d8cc8d48   -              4m56s
echoserver    └─Pod/echoserver-5d8cc8d48-nk47g  True           4m55s
# check actual output
egrep '^echoserver +Deployment/echoserver' output/actual
echoserver  Deployment/echoserver                 -              5m4s
egrep '^echoserver +└─ReplicaSet/echoserver-' output/actual
echoserver  └─ReplicaSet/echoserver-5d8cc8d48   -              4m56s
egrep '^echoserver +└─Pod/echoserver-' output/actual
echoserver    └─Pod/echoserver-5d8cc8d48-nk47g  True           4m55s

これでローカルでのテストは完了です。

CIで実行する

品質を継続的に向上させるにはCIが不可欠です。ここではGitHub Actionsでテストを実行します。

GitHub ActionsのUbuntu 18.04にはすでにKindがインストールされていますが、バージョンが古いので改めて最新版をインストールすることにします。CIの最初のステップでは以下のツールをインストールします。

  • Kind
  • krew
  • kubectl-tree

それからmakeを実行します。

最終的に、workflowは以下のようになります。

name: test
on: [push]
jobs:
  build:
    name: test
    # https://help.github.com/en/actions/automating-your-workflow-with-github-actions/software-installed-on-github-hosted-runners#ubuntu-1804-lts
    runs-on: ubuntu-18.04
    steps:
      - uses: actions/checkout@v1
      # https://kind.sigs.k8s.io/docs/user/quick-start/
      - run: |
          wget -q -O ./kind "https://github.com/kubernetes-sigs/kind/releases/download/v0.7.0/kind-linux-amd64"
          chmod +x ./kind
          sudo mv ./kind /usr/local/bin/kind
          kind version
      # https://github.com/kubernetes-sigs/krew
      - run: |
          (
            set -x; cd "$(mktemp -d)" &&
            curl -fsSLO "https://github.com/kubernetes-sigs/krew/releases/download/v0.3.3/krew.{tar.gz,yaml}" &&
            tar zxvf krew.tar.gz &&
            KREW=./krew-"$(uname | tr '[:upper:]' '[:lower:]')_amd64" &&
            "$KREW" install --manifest=krew.yaml --archive=krew.tar.gz &&
            "$KREW" update
          )
      # https://github.com/ahmetb/kubectl-tree
      - run: PATH=$PATH:$HOME/.krew/bin kubectl krew install tree
      - run: PATH=$PATH:$HOME/.krew/bin make

ここまでの内容は下記のリポジトリにまとめてあります。CIの実行時間や実行結果が気になる方はぜひご覧ください。

github.com

例:kubeloginの受け入れテスト

拙作のkubeloginでは、本物のKubernetesクラスタOpenID Connect OPを利用したテストを下図の構成で行っています。

https://github.com/int128/kubelogin

テストを支える裏方の仕組みは https://github.com/int128/kubelogin/tree/master/acceptance_test をご覧ください。

まとめ

DockerとKindを利用すると、本物のクラスタを用いたkubectl pluginのテストを実現できます。

決済手段を選択するビジネスルールを考える

お店やネットで買い物する時のビジネスルールが複雑になってきたので書き出してみました。

以下の順に評価して条件を満たす決済手段で支払います。

  1. USD/EUR建ての場合:Sony Bank WALLET
  2. ANA FESTAの場合:ソラチカカードVisa(5%割引)
  3. ビックカメラの場合:ビックカメラSuicaカード(10%)
  4. アトレやJR定期券の場合:JREカード(5%)
  5. 投資信託の場合:楽天カード(1%)→楽天証券
  6. コード決済が使える場合:Origami Pay(1%割引)→Kyash Visa(1%)→EPOS(1.5%)
  7. コード決済が使える場合:楽天Pay(1%)→Kyash Visa(1%)→EPOS(1.5%)
  8. Visa決済が使える場合:Kyash Visa(1%)→EPOS(1.5%)
  9. プリペイドVisaがダメな場合:EPOS (1.5%)
  10. 東京メトロ乗車の場合:ソラチカPASMO
  11. Suicaが使える場合:SuicaJREカードオートチャージ (3%)
  12. PayPayが使える場合:PayPay (0.5%)
  13. 現金

ただし、以下の例外条件があります。

  • コード決済のキャンペーン期間中は還元率の高い決済手段を優先的に選択します。例えば、2/4時点では一部店舗でPayPay(40%)があります。
  • Kyash Visaカードのポイント付与対象外(航空券や鉄道など)の場合はEPOSカード(or 楽天カード)で支払います。
  • Kyash Visaカードの月間支払いが12万円を超えた場合はEPOSカード(or 楽天カード)で支払います。
  • EPOSカードの年間支払いが100万円を超えた場合は楽天カードで支払います。

また、支払い前に以下を考慮します。

  • 決済とは別にdポイントを付与できる場合があります。
  • 西友ネットスーパーやApple Storeなどの場合はRebatesを経由してから発注します。
  • ポイントサイトのキャンペーンがある場合があります。(最近あまり見ていない)

上記はFeliCa/NFC決済を考慮していません。FeliCa/NFC決済が使える場合はビジネスルールが変わってくると思います。

もし、幅広い決済手段に対応したプロキシがあったとしても、これらのビジネスルールを実装してテストするのは大変そうです。

kindでクラスタが起動しない原因を調べる

kind create cluster コマンドでKubernetesクラスタが起動しない場合、以下のようなメッセージが表示されます。

 ✗ Starting control-plane 🕹️
ERROR: failed to create cluster: failed to init node with kubeadm: command "docker exec --privileged kind-control-plane kubeadm init --ignore-preflight-errors=all --config=/kind/kubeadm.conf --skip-token-print --v=6" failed with error: exit status 1

クラスタが起動しない原因を調査するには以下の方法があります。

  • kindコマンドのログレベルを上げる
  • kindノードコンテナの内部に入って、Control Planeのログを調査する

kindコマンドのログ

kindコマンドに -v オプションを渡すと詳細なログが表示されるようになります。例えば -v10 を渡すと以下のようなログが表示されます。

Creating cluster "kubelogin-acceptance-test" ...
DEBUG: docker/images.go:70] Pulling image: kindest/node:v1.17.0@sha256:9512edae126da271b66b990b6fff768fbb7cd786c7d39e86bdf55906352fdf62 ...
 ✓ Ensuring node image (kindest/node:v1.17.0) 🖼
 ✓ Preparing nodes 📦
DEBUG: config/config.go:90] Using kubeadm config:
apiVersion: kubeadm.k8s.io/v1beta2
clusterName: kind
controlPlaneEndpoint: 172.17.0.3:6443
controllerManager:
  extraArgs:
    enable-hostpath-provisioner: "true"
kind: ClusterConfiguration
kubernetesVersion: v1.17.0
(以下略)

kindコマンドのログからは、kubeadmに渡す設定ファイルの内容、Control Planeで使うイメージ、Control Planeの各設定ファイルのパスなどが分かります。kindコンテナの内部に入って調査する時の材料になります。

kindノードコンテナの内部にあるログ

kindではノードコンテナの内部でKubernetesクラスタが動いています。ノードコンテナのIDや名前は以下のようにdocker psコマンドで確認できます。

% docker ps
CONTAINER ID        IMAGE                        COMMAND                  CREATED              STATUS              PORTS                       NAMES
38ea3c1f766a        kindest/node:v1.17.0         "/usr/local/bin/entr…"   About a minute ago   Up About a minute   127.0.0.1:32768->6443/tcp   kind-control-plane

kindノードコンテナの内部ではsystemdが動いています。systemdのログは以下のようにdocker logsコマンドで確認できます。

% docker logs kind-control-plane
INFO: ensuring we can execute /bin/mount even with userns-remap
INFO: remounting /sys read-only
(中略)

Welcome to Ubuntu 19.10!

[  OK  ] Started Dispatch Password …ts to Console Directory Watch.
[  OK  ] Listening on Journal Socket.
         Mounting FUSE Control File System...
         Starting Remount Root and Kernel File Systems...

(以下は抜粋)
         Starting containerd container runtime...
[  OK  ] Started kubelet: The Kubernetes Node Agent.
[  OK  ] Started containerd container runtime.

このように、ノードコンテナの起動時にsystemdがcontainerdやkubeletを実行して、Control Planeの各コンポーネントが実行される流れになっています。

コンポーネントのログを確認するには、以下のようにノードコンテナの内部に入る必要があります。

% docker exec -it kind-control-plane /bin/bash

kindノードコンテナの内部では以下のプロセスが動いています。

  • apiserver(コンテナとして動作)
  • kube-controller-manager(コンテナとして動作)
  • kube-scheduler(コンテナとして動作)
  • etcd(コンテナとして動作)
  • kubelet(プロセスとして動作)

これらが立ち上がると以下のコンテナも動き始めます。

  • kube-proxy
  • coredns
  • kindnet
  • local-path-provisioner

各コンテナのマニフェスト/etc/kubernetes/manifests/ にあります。

# ls -la /etc/kubernetes/manifests/
total 24
drwxr-xr-x 1 root root 4096 Jan 24 11:53 .
drwxr-xr-x 1 root root 4096 Jan 24 11:53 ..
-rw------- 1 root root 1805 Jan 24 11:53 etcd.yaml
-rw------- 1 root root 3204 Jan 24 11:53 kube-apiserver.yaml
-rw------- 1 root root 3090 Jan 24 11:53 kube-controller-manager.yaml
-rw------- 1 root root 1120 Jan 24 11:53 kube-scheduler.yaml

また、各コンテナのログは /var/log/containers/ にあります。

# ls -la /var/log/containers/
total 28
drwxr-xr-x 2 root root 4096 Jan 24 12:45 .
drwxr-xr-x 4 root root 4096 Jan 24 11:53 ..
lrwxrwxrwx 1 root root  114 Jan 24 11:53 etcd-...
lrwxrwxrwx 1 root root  136 Jan 24 12:45 kube-apiserver-...
lrwxrwxrwx 1 root root  152 Jan 24 11:53 kube-controller-manager-...
lrwxrwxrwx 1 root root  134 Jan 24 11:53 kube-scheduler-...

kubeletのログはどこに出力されるか分かりませんでした。詳しい人教えてください。 kubeletのログは下記のコマンドで確認できます。

# journalctl -u kubelet
Jan 27 01:25:58 kubelogin-acceptance-test-control-plane systemd[1]: Started kubelet: The Kubernetes Node Agent.
(以下略)

これらの設定ファイルやログを参照すると、クラスタが起動しない原因が分かると思います。例えば、apiserverの引数が間違っている場合はapiserverコンテナに以下のログが出ます。

# cat /var/log/containers/kube-apiserver-...
2020-01-24T11:56:18.6797274Z stderr F I0124 11:56:18.679446       1 server.go:596] external host was not specified, using 172.17.0.3
2020-01-24T11:56:18.6807181Z stderr F I0124 11:56:18.680394       1 server.go:150] Version: v1.17.0
2020-01-24T11:56:18.9807349Z stderr F Error: invalid authentication config: 'oidc-issuer-url' ("http://localhost") has invalid scheme ("http"), require 'https'
2020-01-24T11:56:18.9827157Z stderr F Usage:
2020-01-24T11:56:18.9827978Z stderr F   kube-apiserver [flags]
2020-01-24T11:56:18.9828244Z stderr F
2020-01-24T11:56:18.9828494Z stderr F Generic flags:
2020-01-24T11:56:18.9828729Z stderr F
(以下略)

kindにはログを一括エクスポートする機能があります。詳しくは https://kind.sigs.k8s.io/docs/user/quick-start/#exporting-cluster-logs を参照してください。

See Also