Chaos Toolkit Sample using Azure Kubernetes Service/Spring Boot

Azure Kubrnetes Service上で動くSpring Bootアプリケーションに対してChaos Toolkitを使ったカオス挿入を行います。

Azure Kubrnetes Serviceクラスタの作成

次のコマンドでクラスタを作成します。

RG_NAME=aks-sample
AKS_NAME=aks-sample
az group create \
    --name $RG_NAME \
    --location japaneast

az aks create \
    --resource-group $RG_NAME \
    --name $AKS_NAME \
    --node-count 2 \
    --enable-addons monitoring \
    --generate-ssh-keys

クラスタが作成できたら、次のコマンドでクラスタの接続に必要なクレデンシャルを取得します。

az aks get-credentials \
    --resource-group $RG_NAME \
    --name $AKS_NAME

kubectlは、Kubernetesのリソースを操作するためのCLIです。次のコマンドを実行してkubectlをインストールし、クラスタのノード一覧を表示します。

az aks install-cli
kubectl get node

Spring Bootによるコンテナアプリケーションの開発

Build/Share

サンプルアプリは次の構成です。

overview

chaos-frontendおよびchaos-backendをビルド/コンテナイメージを作成し、コンテナレジストリで共有します。 は環境に合わせて変更してください。

ACR_NAME=<your container registry name>

backendアプリ

backendアプリのコンテナイメージを作成し、レジストリにPushします。

cd apps/backend
./mvnw spring-boot:build-image -Dspring-boot.build-image.imageName=$ACR_NAME/chaos-backend:v1
docker push $ACR_NAME/chaos-backend:v1

frontendアプリ(v1)

frontendのv1アプリのコンテナイメージを作成し、レジストリにPushします。

cd apps/frontend.before
./mvnw spring-boot:build-image -Dspring-boot.build-image.imageName=$ACR_NAME/chaos-frontend:v1
docker push $ACR_NAME/chaos-frontend:v1

frontendアプリ(v2)

frontendのv2アプリのコンテナイメージを作成し、レジストリにPushします。v2はResilience4jを利用してサーキットブレーカーを実装しています。

cd apps/frontend.after
./mvnw spring-boot:build-image -Dspring-boot.build-image.imageName=$ACR_NAME/chaos-frontend:v2
docker push $ACR_NAME/chaos-frontend:v2

Kubernetesクラスタへのデプロイ

次のコマンドでクラスタの構成を確認します。

kubectl get svc,deployment,pod

クラスタにはまだアプリケーションが何もデプロイされていません。

backendアプリのデプロイ

Kubernetesマニフェストからbackendアプリをクラスタにデプロイします。

cd manifest/backend

kubectl apply -f service.yaml -f deployment.yaml 

DeploymentとServiceを確認します。

kubectl get deploy,service

frontendアプリのデプロイ

Kubernetesマニフェストからfrontアプリをクラスタにデプロイします。ここではv1のイメージを使います。

cd manifest/frontend

kubectl apply -f service.yaml -f deployment.yaml 
spec:
  containers:
    - image: asashiho/chaos-frontend:v1
      imagePullPolicy: Always
      name: front

DeploymentとServiceを確認します。

kubectl get deploy,service

動作確認

frontendアプリのエンドポイントを確認します。

kubectl get svc

Webブラウザから次のURLにアクセスします。

http://<frontend External IP>/

backend

動いているアプリケーションのログを確認するには、次のコマンドを実行します。

kubectl logs -f <pod name>

Chaos Toolkitによるカオス挿入

Chaos Toolkitを使って、Kubernetesクラスタにカオスを挿入します。

環境構築

次のコマンドを実行し、Chaos Toolkitの環境を作成します。

python3 -m venv ~/.venvs/chaostk
source ~/.venvs/chaostk/bin/activate

pip install -U chaostoolkit
chaos --version

export KUBECONFIG=~/.kube/config

1. Podの削除

動作しているfrontend Podを削除し、システムの状態を確認します。

cd chaos
chaos run experiment.pod.json
chaos run experiment.pod.json 
[2021-10-25 10:15:24 INFO] Validating the experiment's syntax
[2021-10-25 10:15:44 INFO] Experiment looks valid
....
[2021-10-25 10:15:47 INFO] Playing your experiment's method now...
[2021-10-25 10:15:47 INFO] Action: stop-backend-service
[2021-10-25 10:15:47 INFO] Pausing after activity for 10s...
[2021-10-25 10:15:57 INFO] Probe: all-services-are-healthy
[2021-10-25 10:15:57 INFO] Steady state hypothesis: Services are all available and healthy
[2021-10-25 10:15:57 INFO] Probe: all-services-are-healthy
[2021-10-25 10:15:58 INFO] Probe: front-service-must-still-respond
[2021-10-25 10:15:58 INFO] Steady state hypothesis is met!
[2021-10-25 10:15:58 INFO] Let's rollback...
[2021-10-25 10:15:58 INFO] No declared rollbacks, let's move on.
[2021-10-25 10:15:58 INFO] Experiment ended with status: completed

Podの状態を確認します。 ChaosToolkitによって、動作しているfront-54758ddbc9-vxk7qが削除されていますが、それを検知したReplicaSet Controllerが新たなPodであるfront-54758ddbc9-rj2c9を起動しています。

front-54758ddbc9-vxk7q   1/1     Terminating         0          44s
front-54758ddbc9-rj2c9   0/1     Pending             0          0s
front-54758ddbc9-rj2c9   0/1     Pending             0          0s
front-54758ddbc9-rj2c9   0/1     ContainerCreating   0          0s
front-54758ddbc9-vxk7q   0/1     Terminating         0          47s
front-54758ddbc9-rj2c9   1/1     Running             0          3s
front-54758ddbc9-vxk7q   0/1     Terminating         0          49s
front-54758ddbc9-vxk7q   0/1     Terminating         0          49s

Kubernetesの自己修復機能によりシステムが復旧し、frontendアプリがエラーなくサービスを継続出来ていることが分かります。

2. Deploymentの削除

サーキットブレーカーのないケース

次にChaos Toolkitを使ってDeploymentリソースを削除して、システムの状態を確認します。Deploymentリソースが喪失するケースとしては、CI/CD Pipeline実行時のトラブルや、オペレーションミスなどが想定されます。また、今回のサンプルではfrontendアプリが内部でbackendアプリを呼び出しデータを取得しているため、例えばfrontendがクラスタ外で提供されている外部APIを呼び出しているものの、通信先サービスのネットワークエラー/システム障害/メンテナンスなどで、一時的に応答を返せなくなったケースなどもあてはまります。

public Mono<String> getApiData() {
    return webClient.get()
            .uri("http://backend:8081/message")
            .retrieve()
            .bodyToMono(String.class);

まず、現在のPodの状態を確認します。

kubectl get pod

NAME                     READY   STATUS    RESTARTS   AGE
back-6cc87c6f6f-g2ck4    1/1     Running   0          19m
front-54758ddbc9-rj2c9   1/1     Running   0          18m

次のコマンドを実行し、Chaos Toolkitを使って動作しているfrontend Deploymentを削除し、システムの状態を確認します。

chaos run experiment.deployment.json
[2021-10-25 10:37:52 INFO] Validating the experiment's syntax
[2021-10-25 10:38:10 INFO] Experiment looks valid
[2021-10-25 10:38:10 INFO] Running experiment: System is resilient to backend failures
....
[2021-10-25 10:38:10 INFO] Playing your experiment's method now...
[2021-10-25 10:38:10 INFO] Action: stop-backend-service
[2021-10-25 10:38:10 INFO] Pausing after activity for 10s...
[2021-10-25 10:38:20 INFO] Probe: all-services-are-healthy
[2021-10-25 10:38:21 INFO] Steady state hypothesis: Services are all available and healthy
[2021-10-25 10:38:21 INFO] Probe: all-services-are-healthy
[2021-10-25 10:38:21 INFO] Probe: front-service-must-still-respond
[2021-10-25 10:38:22 CRITICAL] Steady state probe 'front-service-must-still-respond' is not in the given tolerance so failing this experiment
[2021-10-25 10:38:22 INFO] Experiment ended with status: deviated
[2021-10-25 10:38:22 INFO] The steady-state has deviated, a weakness may have been discovered

Podを確認すると、backend Deploymentが削除されたため、frontendのみ動作している状態となります。

kubectl get pod
NAME                     READY   STATUS    RESTARTS   AGE
front-54758ddbc9-rj2c9   1/1     Running   0          23m

これにより、frontendアプリが通信先のbackendサービスからデータを取得できなくなり、サービスが継続出来ていることが分かります。

[2021-10-25 10:38:22 CRITICAL] Steady state probe 'front-service-must-still-respond' is not in the given tolerance so failing this experiment

サーキットブレーカーのあるケース

このサンプルでは、Deploymentリソースの喪失でシステムが停止することがわかりました。これを是正するためサーキットブレーカーを導入します。

circuitbreaker

サーキットブレーカーの詳細はMicrosoft Azureのサーキット ブレーカー パターンにまとまっています。


まず、停止したbackend Deploymentを復旧します。

kubectl apply -f manifest/backend/deployment.yaml
kubectl get po
NAME                     READY   STATUS    RESTARTS   AGE
back-6cc87c6f6f-x27fd    1/1     Running   0          4s
front-54758ddbc9-rj2c9   1/1     Running   0          39m

frontendアプリのv2の実装を確認します。Resilience4jは、Javaによるフォールトトレランスライブラリです。サーキットブレーカーだけでなくリトライ処理やハルクヘッドなどをサポートしています。このResilience4jは、Spring MVCとSpring Webfluxで使用可能です。

たとえば、JavaのClassで設定する場合、slidingWindowで設定したcallの成功/失敗を保存し、失敗の確率がfailureRateThresholdに達すると、サーキットブレーカーをOPENにし、アプリケーションに例外を返します。

この例では、10回中の80%のリクエストに失敗すると、例外を返します。

@Configuration
public class CircuitConfig {

    @Bean
    public CircuitBreakerRegistry circuitBreakerRegistry() {
        return CircuitBreakerRegistry.of(
                CircuitBreakerConfig.custom()
                    .slidingWindowSize(10)
                    .failureRateThreshold(80)
                    .build()
        );
    }
}

circuitBreakerメソッドは、引数で指定したサーキットブレーカーが存在しない場合は新しく作成し、既に存在する場合は、そのオブジェクトを返します。

ここでは、サーキットブレーカーレジストリを介してfrontbreakerという名前のサーキットブレーカーを作成します。

@Bean
public CircuitBreaker circuitBreaker(CircuitBreakerRegistry circuitBreakerRegistry) {
    return circuitBreakerRegistry.circuitBreaker("frontbreaker");
}

backendのサービスを呼び出すときは、@CircuitBreakerアノテーションを使用するか、Operetorを使って指定します。

transformメソッドチェーンを利用してCircuitBreakerOperator.of(circuitBreaker)を利用すると、サーキットブレーカーが適応されます。 ここで、api.getApiDataの処理でサーキットブレーカーがOPENになると、onErrorResumeで指定したfallbackメソッドを呼び出します。

public Mono<String> getData() {
    CircuitBreaker circuit = circuitBreakerRegistry.circuitBreaker("frontbreaker");

    return api.getApiData()
            .transform(CircuitBreakerOperator.of(circuit))
            .onErrorResume(this::fallback);
}

public Mono<String> fallback(Throwable t) {
    log.error("Fallback : " + t.getMessage());
    return Mono.just("Sorry...I'm frontend.");
}

このアプリが有効になるよう、frontend Deploymentのコンテナイメージのバージョンをv1からv2に変更します。

code manifest/frontend/deployment.yaml
# 変更前
containers:
- image: <your container registry name>/chaos-frontend:v1

# 変更後
containers:
- image: <your container registry name>/chaos-frontend:v2
kubectl apply -f manifest/frontend/deployment.yaml

次のコマンドを実行し、Chaos Toolkitを使って動作しているfrontend Deploymentを削除し、システムの状態を確認します。

chaos run experiment.deployment.json
chaos run experiment.deployment.json
[2021-10-25 11:08:07 INFO] Validating the experiment's syntax
[2021-10-25 11:08:15 INFO] Experiment looks valid
....
[2021-10-25 11:08:16 INFO] Playing your experiment's method now...
[2021-10-25 11:08:16 INFO] Action: stop-backend-service
[2021-10-25 11:08:17 INFO] Pausing after activity for 10s...
[2021-10-25 11:08:27 INFO] Probe: all-services-are-healthy
[2021-10-25 11:08:27 INFO] Steady state hypothesis: Services are all available and healthy
[2021-10-25 11:08:27 INFO] Probe: all-services-are-healthy
[2021-10-25 11:08:27 INFO] Probe: front-service-must-still-respond
[2021-10-25 11:08:28 INFO] Steady state hypothesis is met!
[2021-10-25 11:08:28 INFO] Let's rollback...
[2021-10-25 11:08:28 INFO] No declared rollbacks, let's move on.
[2021-10-25 11:08:28 INFO] Experiment ended with status: completed

Podを確認すると、backend Deploymentが削除されたため、frontendのみ動作している状態ですが、frontendがリクエストをクライアントに返しているため、サービスが提供されつづけていることが分かります。

kubectl get pod
NAME                     READY   STATUS    RESTARTS   AGE
front-55bd94d9c8-8dkjz   1/1     Running   0          3m38s

Webブラウザから次のURLにアクセスします。

http://<frontend External IP>/

backend

クリーンアップ

検証が終わりクラスタが不要になったらクラスタのリソースグループを削除します。

RG_NAME=aks-sample
az group delete --name $RG_NAME 

Enjoy! 🍖🍺