LIFULL Creators Blog

「株式会社LIFULL(ライフル)」の社員によるブログです。

KubernetesクラスタのE2Eテスト

技術開発部の川合です。

この記事ではアプリケーション実行基盤開発を支える視点での取り組みについて紹介したいと思います。

キーワードはKubernetesクラスタE2Eテストと自動化です。

背景

私が所属するチームではKubernetesを利用しアプリケーション実行基盤を開発しています。 エンドユーザへの安定したサービス提供はもちろんのことアプリケーション開発者が開発に集中するための様々な機能提供を行っています。

一例を紹介すると以下のような機能が提供されています。

  • アラート・メトリクスを可視化するGrafanaダッシュボードの自動構築
  • OPAを利用したmanifestがベストプラクティスに沿っているかどうかの検証
  • 実行基盤への移行を容易にするテンプレートの開発

他にも多数の機能を提供していますがそれらをさらにシンプルに導入する取り組みとして、CRDとして定義された設定ファイルを書くだけでまとめて手に入るという仕組みを開発しています。 これが完成すればnamespaceの作成からデプロイ環境の構築、アラート・メトリクスなど多岐に渡る機能が統合的に導入できるようになります。 こちらについてはまた別の機会に紹介できればと思います。

チームでは「可観測性の向上」「アプリケーション移行支援」「コスト削減」といったテーマを決定し それぞれに求められる改善・機能開発を継続して提供することを目標としています。 こういった活動に対しスピード感を損なわず安定して価値を提供するための仕組みづくりが必要でした。

また、実行基盤(クラスタ)にはマルチテナンシーを採用しており多種多様なアプリケーションがデプロイされます。 多くの裁量を開発者へ移譲できるような思想で運用していますが、基盤自体を安定稼働させる責任としての調査・不具合等対応等が求められます。 テナントが増えることで観測範囲も増加し基盤への開発・運用はより複雑なものになっていく傾向がありました。

提供機能の保証

提供機能も増え、それを利用するアプリケーションも増え影響範囲も増加していきます。 さらにKubernetes本体や周辺エコシステムのアップデートはとても早く、さらなる価値提供のためにもいち早く適用していかなれければいけません。

そこで実行基盤開発の「守り」を強化することを目指しテストを実装しました。 Sonobuoyというツールを利用してクラスタのE2Eテストを実行しています。 github.com

このテストによって機能が正しく動作することを保証し、 機能開発のデグレチェック・クラスタバージョンアップによる影響の確認が機械的に行えるようになります。

Conformance E2Eテストの実行

KubernetesにはCNCFが実施している認定プログラムがあり、 Certified Kubernetesとして認定されたアプリケーションはSonobuoyによる適合試験をクリアしていている必要があります。

※認定済みのアプリケーションはこちらのリポジトリに登録されています。 github.com

テスト実装についてはKubernetesリポジトリに格納されておりKubernetesのE2Eの実行方法や、ラベルの種類などはこちらから確認することができます。 github.com

Sonobuoyはこれらのテストを便利に実行してくれるテストツールとして利用することが出来ます。 実行されるテストの一部を紹介すると以下のようにConformanceラベルがついたテストを実行していく様子が見られます。

Sonobuoy namespaceを始め独自のnamespaceが作成され実行ログやnodeの様子、テスト実行結果などはSonobuoyが提供するコマンドを通じて確認することが出来ます。

"[sig-network] DNS should provide DNS for pods for Subdomain [Conformance]"
"[sig-api-machinery] AdmissionWebhook [Privileged:ClusterAdmin] listing mutating webhooks should work [Conformance]"
"[sig-storage] Projected secret should be consumable from pods in volume with defaultMode set [LinuxOnly] [NodeConformance] [Conformance]"
"[sig-storage] Projected downwardAPI should provide container's memory limit [NodeConformance] [Conformance]"
"[sig-node] Downward API should provide pod UID as env vars [NodeConformance] [Conformance]"

認定済みのアプリケーションを利用しているわけですが組み合わせによる不具合やクラウド固有の設定による挙動変化などがあった場合にそれを検知したいという意図がありLIFULLでは構築後のクラスタでConformanceテストを実行するようにしています。

カスタムE2Eテストの実行

また、前述の通りLIFULLのアプリケーション実行基盤として多くの機能開発を行っています。それらが期待通りに動作するかについてConformanceテストとは別にE2Eテストを実装しています。 これらはKubernetes本家同様にテストフレームワークであるGinkgoとGomegaを使いSonobuoyのカスタムプラグインとして実行できるようにテストを実装しています。例として以下のようなテストを実装しています。

  • cluster-autoscalerを稼働させインスタンス増減が機能するかを確認する
  • デプロイされたpodがすべてreadyとなっているかupstreamから検証する
  • prometheusがscrapeするjobがすべて成功しているかどうか
  • 実行基盤にデプロイされているアプリケーションにおいてSIGTERMの挙動が期待しているものとなっているかrolling restartさせてみる
  • spinnakerのpipelineを設定したアプリケーションがすべてデプロイされているかを確認する
  • fluentdによるログ収集が機能しているか

カスタムプラグインについては公式のドキュメントが充実しています。作成手順は簡単です手順通りに実行すればすぐに導入可能と思います。

sonobuoy.io

前述した内容についてテストを実装してimageをbuildし、 Sonobuoy pluginのcommandが以下のスクリプトを参照するようにしてGinkgoを実行するようにしています。

#!/bin/bash

set -x

results_dir="${RESULTS_DIR:-/tmp/results}"
mkdir -p $results_dir

saveResults() {
    cd ${results_dir}
    tar czf results.tar.gz *
    printf ${results_dir}/results.tar.gz > ${results_dir}/done
}

trap saveResults EXIT
ginkgo_args=()
if [[ -n ${E2E_DRYRUN:-} ]]; then
    ginkgo_args+=("--dryRun=true")
fi

ginkgo_args+=(
    "-ginkgo.focus=${E2E_FOCUS}"
    "-ginkgo.skip=${E2E_SKIP}"
    "-ginkgo.noColor=true"
    "-ginkgo.v=false"
    "-ginkgo.reportFile=${results_dir}/junit.xml"
)

e2e_args=()
e2e_args+=(
    "-kubeconfig=${E2E_KUBECONFIG}"
    "-disable-log-dump=true"
    "-dump-logs-on-failure=false"
)

extra_args=(${E2E_EXTRA_ARGS:-})

target="${E2E_TARGET:-apps}"
cd /go/src/github.com/lifull/cluster/test/e2e
ginkgo $target -- "${ginkgo_args[@]}" "${e2e_args[@]}" "${extra_args[@]}" | (tee ${results_dir}/e2e.log) && ret=0 || ret=$?
saveResults
exit ${ret}

実はこのタスクは私が今のチームにJOINした際のオンボーディングの一環として担当させてもらったタスクでした。 そういった経緯のためにクラスタ挙動の理解が浅く、運用開始当初は稼働中のクラスタでは動作するけど構築直後には動作しないといった不十分な実装になっていたりnodeの状態によって失敗するといった不安定さも多く残っていました。

例えばこのコードは安定したdeploymentにおいては稼働しますが、spotインスタンスに配置されたpodがspotのterminationによってEvictされたり、deschedulerによってPodが移動されている最中に実行されたりすると失敗してしまいます。

Context("Deployment", func() {
    var dList *appsv1.DeploymentList
    BeforeEach(func() {
        deployments, err := cs.AppsV1().Deployments("").List(metav1.ListOptions{})
        if err != nil {
            panic(err.Error())
        }
        dList = deployments
    })
    It("should be fullfill specified number of replica", func() {
        errList := make([]error, 0)
        for _, d := range dList.Items {
            if deployment.IsFulfilledDesiredReplica(&d) != true {
                e := errors.New(fmt.Sprintf("Failure deployment ns:%s name:%s check replicas (%d desired | %d updated | %d total | %d available | %d unavailable)", d.GetNamespace(), d.GetName(), *(d.Spec.Replicas), d.Status.UpdatedReplicas, d.Status.Replicas, d.Status.AvailableReplicas, d.Status.UnavailableReplicas))
                errList = append(errList, e)
            }
        }
        Expect(errList).To(BeEmpty())
    })
})

そういったケースを考慮してpolling処理を行いながらdeploymentを満たすかどうかを判定するような処理に修正しています。 Kubernetesの挙動を知る意味でもテストを書くのは有益ですし、Kubernetes本体に実装されているテストを参考にしつつ挙動を理解していくのは学習方法としても良かったと感じています。

interval := 5 * time.Second
duration := 3 * time.Minute
err := wait.PollImmediate(interval, duration, func() (bool, error) {
    return deployment.IsFulfilledDesiredReplica(&d), nil
})
if err == wait.ErrWaitTimeout {
    e := rrors.New(fmt.Sprintf("Failure deployment ns:%s name:%s check replicas (%d desired | %d updated | %d total | %d available | %d unavailable)", d.GetNamespace(), d.GetName(), *(d.Spec.Replicas), d.Status.UpdatedReplicas, d.Status.Replicas, d.Status.AvailableReplicas, d.Status.UnavailableReplicas))
    errList = append(errList, e)
}

実行したテスト結果はslackに通知し、失敗時のログが後から確認できるようにS3に保存するように設定してあります。 Sonobuoyで実行されたテストは実行前のnodeの状態や実行時のログを残すことが出来、どういう状態で失敗したかの詳細を確認することが出来ます。 f:id:LIFULL-kawaih:20200924000412p:plain

こうしてテストコードで提供機能の正常稼働を保証しバージョンアップにおいてもそれらが正しく機能するかが判定できるテスト基盤が用意できました。

クラスタ構築の不安を軽減する

LIFULLではいつ誰が実行しても期待するクラスタが手に入る状態を一つの目標としていました。 気軽に稼働中のクラスタのクローンが手に入り破壊的変更を試すことができるのは開発を支える視点でも優位なものとなります。 そのための材料として構築の自動化と構築後のクラスタにおいてE2EテストがGreenになることは重要でした。

現在、構築手順についてはすべてコードベースで定義されスクリプト実行に依存し手作業は一切排除されています。 さらにリソースを操作するための権限差異、OS依存の環境差異などにより構築が失敗する状態を避けるため AWS CloudFormationでAWS Systems Manager Automationを構築し固定されたマシンイメージ・権限に基づいてスクリプトが実行できる環境として整備してあります。

この構築環境を利用し「毎日、EC2インスタンスからクラスタ構築スクリプトと一緒にE2Eテストを実行する」ことにしています。

  • 毎日構築を試すことにより構築スクリプトの健全性が定期的に保証される
  • 構築されたクラスタはE2Eテストによって期待する動作を保証される

この2つが担保されていることで構築スクリプトの風化やデグレの不安から開放されポジティブに開発に取り組むことが出来ます。

おわりに

以上、アプリケーション実行基盤開発を支える視点での取り組みについて紹介してきました。 目新しいものではありませんがこういった取り組みによりスピード感を損なわずチャレンジングな開発に取り組むことができます。

今後はテストの拡充を行うことでさらなる安心を得て、一切手作業が発生しないクラスタの完全自動切り替えなどにも挑戦して行きたいと思います。

E2Eテスト導入の参考になれば幸いです。

また、LIFULLではメンバーを募集しております! カジュアル面談もありますのでご興味ある方は是非ご参加ください!

hrmos.co