Kubernetes環境についてTerratestでテストを書く

28 Oct 2021, 23:11 By mosuke5

こんにちは、もーすけです。
今回はTerratest を用いたKubernetes環境のテストについて検討します。

TerratestはGruntwork.ioが作成しているインフラのテスティングソフトウェアです。 もともとは、Terraformで作成したクラウド環境のテストとして発達がしましたが、いまの時代となってKubernetes環境やコンテナイメージもテストできるようになっています。

Kubernetesマニフェストにより、宣言的にインフラ環境を表現できるようになってきているととはいえ、その結果が期待通りに動作しているのかは日々の悩みのタネであることはかわりません。 Terratestがこの悩みを解消するのにイケてそうなので調査してみます。

かつて仮想サーバでアプリケーションを運用している時代に、Serverspecを用いてテスト駆動のインフラ構築を行っていてとても気持ちがよかったので、そのレイヤーが移ってきているとも考えられます。

環境

本検証を行ったGoとKubernetesのバージョンは下記のとおりです。

% go version
go version go1.17.2 darwin/amd64

% kubectl version --short
Client Version: v1.22.2
Server Version: v1.22.0-rc.0+894a78b

Quick Start

なにごともはじめはあります。まずはドキュメントのサンプルをみながら試してみます。 参考にしたのは公式ドキュメントのExample #4: Kubernetesです。 最終的に以下のようなファイル構成で進めました。

% tree .
.
├── hello-world-deployment.yaml
└── test
    ├── go.mod
    ├── go.sum
    └── kubernetes_hello_world_example_test.go

サンプルマニフェスト

まずは、サンプルのKubernetesマニフェストを理解しましょう。 このブログの読者であれば解説は不要かと思いますが、かんたんに残しておきます。

---
# Pythonで動くかんたんなWebサーバです。ポート5000でリッスンすると"Hello, World!"を返します。
# コンテナイメージはすでに用意されているものです。
apiVersion: apps/v1
kind: Deployment
metadata:
  name: hello-world-deployment
spec:
  selector:
    matchLabels:
      app: hello-world
  replicas: 1
  template:
    metadata:
      labels:
        app: hello-world
    spec:
      containers:
        - name: hello-world
          image: training/webapp:latest
          ports:
            - containerPort: 5000
---
# 上のアプリケーションを外部公開します。
# Kubernetesにおけるアプリケーションの公開方法はいくつかありますが、SeviceのType: LoadBalancerで公開します。
kind: Service
apiVersion: v1
metadata:
  name: hello-world-service
spec:
  selector:
    app: hello-world
  ports:
    - protocol: TCP
      targetPort: 5000
      port: 5000
  type: LoadBalancer

試した環境は、AWS上に構築したKubernetesクラスタであり、Type: LoadBalancerのServiceリソースを作成するとELBが作成されてエンドポイントとなります。

% kubectl apply -f hello-world-deployment.yaml
deployment.apps/hello-world-deployment created
service/hello-world-service created

% kubectl get pod,service
NAME                                          READY   STATUS    RESTARTS   AGE
pod/hello-world-deployment-698bfbc4fc-pcvfl   1/1     Running   0          24s

NAME                          TYPE           CLUSTER-IP      EXTERNAL-IP                              PORT(S)          AGE
service/hello-world-service   LoadBalancer   172.30.205.11   xxxxx.ap-southeast-1.elb.amazonaws.com   5000:30287/TCP   24s

% curl http://xxxxx.ap-southeast-1.elb.amazonaws.com:5000
Hello world!

Terratestのテストコード

マニフェストが確認できたので、次にテストコードを確認していきましょう。 コメントアウトを日本語でいれてみました。

package test

import (
  "fmt"
  "testing"
  "time"

  http_helper "github.com/gruntwork-io/terratest/modules/http-helper"
  "github.com/gruntwork-io/terratest/modules/k8s"
)

func TestKubernetesHelloWorldExample(t *testing.T) {
  t.Parallel()

  // Kubernetesマニフェストのパス。ディレクトリ構成に合わせて編集しましょう
  kubeResourcePath := "../hello-world-deployment.yaml"

  // Kubectlのオプション指定
  // 引数は左から順番に、"contextName, configPath, namespace"
  // この例だとdefault namespaceにKubernetesリソースが作られます
  // 仕様を確認したいときはドキュメントを見る https://pkg.go.dev/github.com/gruntwork-io/[email protected]/modules/k8s#NewKubectlOptions
  options := k8s.NewKubectlOptions("", "", "default")

  // テストの終了後にリソースの削除を行う
  // deferは、上位ブロックの関数がreturnするまで遅延するもので、つまりテスト終了後に削除が走る
  defer k8s.KubectlDelete(t, options, kubeResourcePath)

  // `kubectl apply`の実行
  k8s.KubectlApply(t, options, kubeResourcePath)

  // Serviceが利用可能になるまで待つ
  k8s.WaitUntilServiceAvailable(t, options, "hello-world-service", 10, 1*time.Second)

  // Serviceリソースを取得
  service := k8s.GetService(t, options, "hello-world-service")

  // HTTPリクエストを投げる先のURLを取得
  url := fmt.Sprintf("http://%s", k8s.GetServiceEndpoint(t, options, service, 5000))

  // 取得したURLへリクエスを送付し、HTTPステータス200で"Hello world!"が返るまでリトライ
  // ドキュメントの記載のリトライ数ではタイムアウトすることがあったので調整しました。
  http_helper.HttpGetWithRetry(t, url, nil, 200, "Hello world!", 60, 3*time.Second)
}

Terratestのモジュール

Terratestでは、KubernetesのリソースやHTTPリクエストをテストするためやの便利なモジュールを用意してくれています。 上のソースコードの例では k8s.*http_helper.HttpGetWithRetryは、すべてTerratestライブラリが用意したものです。

Terratestが用意した関数の一覧や仕様はこちらのドキュメントから確認できます。

applyとdeleteを行わないでテストだけしたい

前述の例では、テストを実行するたびにマニフェストをApplyして、テストして、テスト後に削除するというステップで行いました。 CI環境などではそのような実行ステップでまったく問題ないですが、個人的に望んでいるのはもうすこしTDD的な使い方です。 テストを書いてそれを満たすマニフェストを書いていく、というマニフェスト作成時にもっとカジュアルにテストしたいとも思っています。 その際には、時間がかからずすぐにテストできることが重要です。

ApplyやDeleteはせずに、テストだけ単純にするというケースについても考えてみます。
次のようにかなりシンプルなコーディングができますね。

package test

import (
  "fmt"
  "testing"
  "time"

  http_helper "github.com/gruntwork-io/terratest/modules/http-helper"
  "github.com/gruntwork-io/terratest/modules/k8s"
)

func TestKubernetesHelloWorldExampleNoApply(t *testing.T) {
  t.Parallel()

  options := k8s.NewKubectlOptions("", "", "default")
  service := k8s.GetService(t, options, "hello-world-service")
  url := fmt.Sprintf("http://%s", k8s.GetServiceEndpoint(t, options, service, 5000))

  http_helper.HttpGetWithRetry(t, url, nil, 200, "Hello world!", 60, 3*time.Second)
}

go testで実行するとわかりますが、1.973秒でテストが完了しています。 開発中においては、テストの都度にリソースの作成をする必要はないことが多いかなと思います。

% go test -v
=== RUN   TestKubernetesHelloWorldExample
=== PAUSE TestKubernetesHelloWorldExample
=== CONT  TestKubernetesHelloWorldExample
TestKubernetesHelloWorldExample 2021-10-30T18:44:31+09:00 client.go:42: Configuring Kubernetes client using config file /Users/shinyamori/.kube/config with context
TestKubernetesHelloWorldExample 2021-10-30T18:44:31+09:00 node.go:33: Getting list of nodes from Kubernetes
TestKubernetesHelloWorldExample 2021-10-30T18:44:31+09:00 client.go:42: Configuring Kubernetes client using config file /Users/shinyamori/.kube/config with context
TestKubernetesHelloWorldExample 2021-10-30T18:44:32+09:00 retry.go:91: HTTP GET to URL http://af5ba2bb2822c4c68a6d1b629a7d3178-1669677222.ap-southeast-1.elb.amazonaws.com:5000
TestKubernetesHelloWorldExample 2021-10-30T18:44:32+09:00 http_helper.go:32: Making an HTTP GET call to URL http://af5ba2bb2822c4c68a6d1b629a7d3178-1669677222.ap-southeast-1.elb.amazonaws.com:5000
--- PASS: TestKubernetesHelloWorldExample (1.37s)
PASS
ok    github.com/mosuke5/terratest-kubernetes-practice  1.973s

assertion

Goのテスティングフレームワークの話になるのですが、assertionを書きたいことがあります。 Terratestが用意したモジュールのみでテスト可能であればいいのですが、取得したなんらかの値が期待通りかどうかを確認したいこともあります。その場合には、testifyをインストールして使いましょう。

モジュール外のテストを検討する

Terratestのモジュールは便利ですが、実際のテストシナリオを考えるとTerratestのモジュールだけでは足りないことが出てくると思います。 ここからさきはGoのプログラミングとして考えていく必要がありますが、実際に遭遇したシナリオの例に簡単に実装してみます。

ふたつのシナリオを追加しました。※これらのシナリオが必要というわけではないです。
ひとつめは、作成されたPodのQoSClassのテストです。LimitRangeを設定していて、Resource設定を行わなかった場合もBestEffortではなくBurstableに設定されていることを確認したかったケースです。また、testifyを用いたAssertionでテストしています。

もうひとつが、作成したServiceをPod内から名前解決できるかどうかのテストです。 レアなケースかも知れませんが、KubernetesのDNS機能を外だししていて設定がうまくいっていないと名前解決できないことがあったため確認した例です。IPアドレスの妥当性というよりは、単純にレコードが登録されたかどうかだけのテストですが、デバッグ用のコンテナを用いて確認しました。

package test

import (
  "fmt"
  "testing"
  "time"

  http_helper "github.com/gruntwork-io/terratest/modules/http-helper"
  "github.com/gruntwork-io/terratest/modules/k8s"
  "github.com/gruntwork-io/terratest/modules/shell"
  "github.com/stretchr/testify/assert"
  metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)

func TestKubernetesHelloWorldExampleAssertion(t *testing.T) {
  t.Parallel()

  // Kubectlのオプション指定
  options := k8s.NewKubectlOptions("", "", "default")

  // Serviceリソースを取得
  service := k8s.GetService(t, options, "hello-world-service")

  // HTTPリクエストを投げる先のURLを取得
  url := fmt.Sprintf("http://%s", k8s.GetServiceEndpoint(t, options, service, 5000))

  // 取得したURLへリクエスを送付し、HTTPステータス200で"Hello world!"が返るまでリトライ
  // ドキュメントの記載のリトライ数ではタイムアウトすることがあったので調整しました。
  http_helper.HttpGetWithRetry(t, url, nil, 200, "Hello world!", 60, 3*time.Second)


  // 作成されたPodのQoSClassをassertionで確認
  pods := k8s.ListPods(t, options, metav1.ListOptions{})
  for _, pod := range pods {
    assert.Equal(t, "Burstable", string(pod.Status.QOSClass))
  }

  // Pod内からServiceを名前解決できるか確認
  // 名前解決できないとexit 1で返すのでエラーでテストに失敗する
  command := shell.Command{
    Command: "bash",
    Args: []string{
      "-c",
      "kubectl run --restart=Never --rm -it debug --image registry.gitlab.com/mosuke5/debug-container:latest -- nslookup hello-world-service",
    },
  }
  shell.RunCommandAndGetStdOut(t, command)
}

さいごに

Terratestでテストを書いていくともっともっと実践的なトピックはでてきますが、はじめの一歩ということで共有します。 なかなか書いている人が少ないネタでありながらも、Kubernetesのテストは非常に重要なトピックなので、新しい知見がでてき次第更新したいと思います。

記事の内容に関連した相談、仕事依頼したい New

記事の内容やクラウドネイティブ技術に関する相談、仕事依頼を開始しました。
仕事依頼、相談をしてみる

記事へのフィードバック

本記事に対して、フィードバックあればこちらのフォームからご記入ください。
記事の内容にフィードバックしてみる

このエントリーをはてなブックマークに追加