🧪

AWS Fault Injection Service で EKS の障害テストを行う

2023/12/14に公開

この記事は 3-shake Advent Calendar 2023 14 日目の記事です!

この記事に書いてあること

この記事では、AWS Fault Injection Service をつかって、EKS 上の Pod の障害テストを行う方法を説明します。

この記事を書こうと思ったモチベーションとして、EKS 上のアプリケーションで障害テストをするために AWS Fault Injection Service (以降、「FIS」と記載します) を使用しようとしたところ、導入手順がいまいち分からなかったため、残しておこうと思ったためです。
EC2 に障害を注入する場合は導入手順はシンプルかつ日本語記事も多かったのですが、Kubernetes のリソースに対して障害を注入する場合、すこし準備が複雑となります。

背景

やりたかったこと

私が参画しているサービスでは、EKS アプリケーションのログ基盤を CloudWatch Logs に移行しようとしています。

CloudWatch Logs への移行にあたり、各コンポーネントに障害が発生した際にログが欠損しないか、どのような挙動になるかを確認する必要があります。
移行プロジェクトにおいて、下記の 2 つのコンポーネントの障害を想定してテストを行うことにしました。

  1. fluentd: 各 Pod が出力するログを収集し、CloudWatch Logs に転送する役割
  2. CloudWatch Logs: fluend から転送されるログを受け取り中期的に保管する役割

上記のうち、1 の fluentd は EKS 上の Pod として起動しているため、簡単に障害をシミュレートできたのですが、2 の CloudWatch Logs に対して任意のタイミングで障害を起こすことはできません。
Network Firewall がいればアクセスを拒否することで障害をシミュレートことができたのですが、FQDN で通信を拒否するようなコンポーネントもいません。

そのような環境だったため、なんらかの Chaos Engineering Tool を導入しようと思いました。

他に検討した手段

検討した最初は Chaos Mesh を使用して障害をシミュレートしようとしました。

理由として、fluentd Pod の障害をシミュレートするために Chaos Mesh を使用していたためです。

CloudWatch Logs の障害をシミュレートするために、fluentd と CloudWatch Logs の通信を遮断しようと考え、Chaos Mesh の Network FaultPartition を検証しました。
Partition を使用すると、ingress / egress の通信を遮断することができます。
また、externalTargets を使用することで、FQDN による通信元 / 通信先の指定が可能なため、IP アドレスが明示できない通信先でも容易に指定ができます。

しかし、下記の理由から Chaos Mesh の Partition では障害をシミュレートできないことがわかりました。

  • PartitionexteranlTargets で指定した FQDN は障害を発生させた時点で名前解決した IP アドレスを iptables の ipset に設定して通信を遮断するため、名前解決時にすべての IP アドレスを設定できない

実際に、CloudWatch Logs のエンドポイントである logs.ap-northeast-1.amazonaws.comexternalTargets に指定して障害を発生させた際、遅延はあるもののログは転送できていました。
また、ローカル端末や fluentd の Pod で nslookup を行ったら ipset に指定してある IP アドレス以外の IP アドレスが返ってきました。

FQDN で指定できないなら、AWS が使用している IP アドレスを指定してブロックしようとしましたが、今度は PartiionexternalTargets には CIDR ブロックの形式での指定ができないことがわかり Chaos Mesh によるテストを諦めました。

上記の流れから、FIS の aws:eks:pod-netowrk-blackhole-port により TCP/443 をブロックすることで障害をシミュレートすることにしました。

概要

AWS Fault Injection Service とは

FIS とは AWS が提供するマネージドな Chaos Engineering Tool です。

Chaos Mesh と比べると、AWS に特化した障害のシミュレートが可能で、例えば RDS の再起動を行ったり、特定のサブネットの通信をできなくしたり、ssm を使用して EC2 インスタンスに任意のコマンドを実行させたりして障害をシミュレートすることができます。
また Chaos Mesh と同様に k8s 上のリソースに対して障害をシミュレートすることもでき、一部できることが重なります。

FIS の概念

FIS には、実験テンプレート (Experience template) と実験 (Experience) という 2 つの種類のリソースがあります。

実験テンプレートは、どのリソースに対して、どのような障害を発生させるかを定義する設計図のようなものです。
実験は、実験テンプレートをもとに実際に実行された障害です。

FIS をどうやって使用するかは記事の後半で実際に設定しながら説明します。

FIS の概念について詳細を知りたい場合は下記の URL を参照してください。
AWS Fault Injection Service とは? - AWS Fault Injection Service

テストまでの流れ

FIS で障害テストを実施する場合の準備およびテストの流れは下記のとおりです。
括弧書きは、AWS と EKS (k8s) どちらの操作かを記載しています。

  1. 準備
    1. (AWS) FIS のサービスロール (IAM Role) を作成する
    2. (EKS) FIS が k8s リソースを操作する際の Service Account, Role, RoleBindings を用意する
    3. (EKS) サービスロールと Service Account を紐付けるために aws-auth を編集する
  2. テスト
    1. (AWS) 実験テンプレートとターゲットリソースの定義を作成する
    2. (AWS) 実験テンプレートから、実験を開始する

やってみた

それでは、実際に Pod の障害テストを実施してみます。

今回は fis-test という Namespace 内の nginx-fault という Pod が外部の TCP/443 宛の egress 通信ができないようにします。

k8s 上のテスト対象のリソースは下記のとおりです。
nginx-nonfault には障害を注入せず、実験中に egress 通信することで障害の原因が FIS であることを確認します。

$ kubectl get namespaces
NAME              STATUS   AGE
default           Active   5h33m
fis-test          Active   5h11m
kube-node-lease   Active   5h33m
kube-public       Active   5h33m
kube-system       Active   5h33m

$ kubectl -n fis-test get pods
NAME             READY   STATUS    RESTARTS   AGE
nginx-fault      1/1     Running   0          4h50m
nginx-nonfault   1/1     Running   0          4h50m

準備

(AWS) FIS のサービスロールを作成する

まず、FIS のサービスロールを作成します。

FIS のサービスロール作成のポイントは下記の二点です。

  1. 信頼関係で fis.amazonaws.comsts:AssumeRole アクションを許可する。
  2. 実施したいテストの内容に応じて、権限を付与する。

今回は terraform でサービスロールを作成したため、terraform のサンプルコードを記載します。
上記のポイントに関するコードにコメントを記載しています。

data "aws_caller_identity" "current" {}

data "aws_iam_policy_document" "fis_trust_relationship" {
  statement {
    sid     = "FISAssumeRole"
    effect  = "Allow"
    actions = ["sts:AssumeRole"]

    // ポイント 1: 信頼関係で `fis.amazonaws.com` に `sts:AssumeRole` アクションを許可する。
    principals {
      type        = "Service"
      identifiers = ["fis.amazonaws.com"]
    }

    condition {
      test     = "StringEquals"
      variable = "aws:SourceAccount"
      values   = [data.aws_caller_identity.current.account_id]
    }
  }
}

resource "aws_iam_role" "fis" {
  name               = "saikyo-fisdemo-fis"
  assume_role_policy = data.aws_iam_policy_document.fis_trust_relationship.json
}

// ポイント 2: 実施したいテストの内容に応じて、権限を付与する。
// 今回は EKS に対するテストとなるため、AWS 管理ポリシーの `AWSFaultInjectionSimulatorEKSAccess` を使用した。
resource "aws_iam_role_policy_attachment" "fis" {
  role       = aws_iam_role.fis.name
  policy_arn = "arn:aws:iam::aws:policy/service-role/AWSFaultInjectionSimulatorEKSAccess"
}

(EKS) FIS が k8s リソースを操作する際の Service Account, Role, RoleBindings を用意する

続いて FIS が k8s リソースを操作する際の RBAC 関連のリソースを作成します。

FIS 用の RBAC リソース作成のポイントは下記の点です。

  1. テスト対象のリソースと同じ Namespace に作成する

今回は k8s マニフェスト (YAML) でリソースを作成します。
上記のポイントに関するコードにコメントを記載しています。

kind: ServiceAccount
apiVersion: v1
metadata:
  namespace: fis-test # ポイント 1: テスト対象のリソースと同じ Namespace に作成する
  name: fis-test

---
kind: Role
apiVersion: rbac.authorization.k8s.io/v1
metadata:
  namespace: fis-test # ポイント 1: テスト対象のリソースと同じ Namespace に作成する
  name: role-experiments
rules:
- apiGroups: [""]
  resources: ["configmaps"]
  verbs: [ "get", "create", "patch", "delete"]
- apiGroups: [""]
  resources: ["pods"]
  verbs: ["create", "list", "get", "delete", "deletecollection"]
- apiGroups: [""]
  resources: ["pods/ephemeralcontainers"]
  verbs: ["update"]
- apiGroups: [""]
  resources: ["pods/exec"]
  verbs: ["create"]
- apiGroups: ["apps"]
  resources: ["deployments"]
  verbs: ["get"]

---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
  name: bind-role-experiments
  namespace: fis-test # ポイント 1: テスト対象のリソースと同じ Namespace に作成する
subjects:
- kind: ServiceAccount
  name: fis-test
  namespace: fis-test # ポイント 1: テスト対象のリソースと同じ Namespace に作成する
- apiGroup: rbac.authorization.k8s.io
  kind: User
  name: fis-experiment
- apiGroup: rbac.authorization.k8s.io
  kind: Group
  name: fis-experiment-group
roleRef:
  kind: Role
  name: role-experiments
  apiGroup: rbac.authorization.k8s.io

(EKS) サービスロールと Service Account を紐付けるために aws-auth を編集する

準備フェーズの最後として、サービスロールと RBAC リソースを紐づけます。

紐づけには Namespace kube-systemaws-auth で定義します。
今回は検証なので kubectl edit で直接編集してしまいます。(本来は設定をコードに残すことが望ましいです...)

$ kubectl -n kube-system edit configmap aws-auth

mapRoles 内に下記を追記します。
ポイントは、groups の名称を、FIS が k8s リソースを操作する際の Service Account, Role, RoleBindings を用意する の RoleBindings の subjects に定義したグループ名と同じものを指定します。

apiVersion: v1
data:
  mapRoles: |
    - groups:
      - system:bootstrappers
      - system:nodes
      rolearn: arn:aws:iam::926742232207:role/eksctl-saikyo-fisdemo-nodegroup-ng-NodeInstanceRole-gdanzDIHNIC0
      username: system:node:{{EC2PrivateDNSName}}
    ## ここから
    - groups:
      - "fis-experiment-group"
      rolearn: "arn:aws:iam::926742232207:role/saikyo-fisdemo-fis"
      username: "role:{{AccountID}}/saikyo-fisdemo:fis-experiment:{{SessionName}}"
    ## ここまで
kind: ConfigMap
metadata:
  creationTimestamp: "2023-12-10T08:41:24Z"
  name: aws-auth
  namespace: kube-system
  resourceVersion: "38682"
  uid: e635e287-abf8-4345-b00e-e75829610379

テスト

準備が完了したら実際に FIS の操作をして障害テストの定義と実行を行っていきます。

ここからはマネジメントコンソールを使って操作していきます。

(AWS) 実験テンプレートとターゲットリソースの定義を作成する

まずはマネジメントコンソールで FIS の画面に遷移していくのですが、検索ウィンドウにサービス名称の一部である「Fault」といれても表示されないので「FIS」と入力します。
下記の画像は Fault で検索した場合と FIS で検索した場合の表示結果の違いです。

Fault で検索
FIS で検索

FIS の画面に遷移したら、「実験テンプレート」から、「実験テンプレートを作成」をクリックします。

最初に実験タイプとして、同じ AWS アカウントをターゲットとするか、他の AWS アカウントを含む複数のアカウントをターゲットとするかを選択します。
今回は同じ AWS アカウントをターゲットとする前提で進めています。

実験タイプを選択

任意で名前と説明を入力したら、アクションとターゲットを作成します。
それぞれの役割は下記のとおりです。

No. リソース 役割
1 アクション どのような障害を注入するか、詳細を定義します。
2 ターゲット どのリソースに対して障害を注入するかを定義します。

アクション内で、どのターゲットと紐付けるかを定義するため、ターゲットを先に作成します。

今回は EKS 上の Pod に対して障害を注入するため、EKS クラスターや Namespace、Pod を識別するパラメーターを指定します。

Pod を指定する

今回は Pod 名を明示していますが、Deployment の名前や label により指定することも可能です。

その後、具体的な障害に関する設定を定義します。(画像が長くなってしまいすみません...)

障害の内容を定義する

ポイントは下記のとおりです。

  1. アクションタイプで具体的な障害の内容を選択する。
  2. Service Account は、FIS が k8s リソースを操作する際の Service Account, Role, RoleBindings を用意する で作成した Service Account を指定する。
  3. Port は送信先のポートを指定する。
    • egress の場合は接続先のポート、ingress の場合は Pod がうけるポートを指定する。

その後、サービルロールの設定とログの出力先設定、タグを指定してから「実験テンプレートを作成」をクリックします。

サービスロールは、FIS のサービスロールを作成する で作成した IAM Role を指定します。
ログの出力先は S3 か CloudWatch Logs のいずれか (またはいずれとも) を指定できます。

(AWS) 実験テンプレートから、実験を開始する

最後に実験テンプレートを指定して、実際に障害テストを実行します。

作成した実験テンプレートを選択して、「実験を開始」をクリックします。

実験テンプレートから実験開始

確認画面が表示されます。また。実験自体にもタグをつけることができます。

実験開始の確認

実験を開始」をクリックすると、さらに確認を求められます。
確認を終え、開始を行うと実験のステータス画面が表示されます。
状態が Running の場合は実際に障害が注入されています。

実験中の画面

実験中に障害が発生しているはずの Pod にアクセスし、外部通信をしてみます。

$ kubectl -n fis-test exec pods/nginx-fault -it -- /bin/bash
Defaulted container "nginx" out of: nginx, c48db930-62fb-3331-b038-0bcb3dd51574 (ephem), 126b7b8a-67f6-353b-9f32-4613c004d799 (ephem)
root@nginx-fault:/# curl https://www.google.com -I
curl: (28) Failed to connect to www.google.com port 443 after 129242 ms: Couldn't connect to server
root@nginx-fault:/# 
root@nginx-fault:/# exit

Google への HTTPS アクセスがタイムアウトしており、アクセスできていないことがわかります。

一応、障害が発生していない Pod でも同様に確認してみます。

# kubectl -n fis-test exec pods/nginx-nonfault -it -- /bin/bash
root@nginx-nonfault:/# curl https://www.google.com -I
HTTP/2 200 
content-type: text/html; charset=ISO-8859-1
content-security-policy-report-only: object-src 'none';base-uri 'self';script-src 'nonce-n1IOHU5U1KwYs0uI0dUXqQ' 'strict-dynamic' 'report-sample' 'unsafe-eval' 'unsafe-inline' https: http:;report-uri https://csp.withgoogle.com/csp/gws/other-hp
p3p: CP="This is not a P3P policy! See g.co/p3phelp for more info."
date: Sun, 10 Dec 2023 15:23:23 GMT
server: gws
x-xss-protection: 0
x-frame-options: SAMEORIGIN
expires: Sun, 10 Dec 2023 15:23:23 GMT
cache-control: private
set-cookie: 1P_JAR=2023-12-10-15; expires=Tue, 09-Jan-2024 15:23:23 GMT; path=/; domain=.google.com; Secure
set-cookie: AEC=Ackid1SeRrz7nhofEA1nnNapry7-TMMHD3rw3_reGWVX3kx6yHCiEklEprI; expires=Fri, 07-Jun-2024 15:23:23 GMT; path=/; domain=.google.com; Secure; HttpOnly; SameSite=lax
set-cookie: NID=511=i0deV_axnUj3hEF3IVHhayiFYQ9xG_Mgyb-p-yGFy7Pm842uNeTqSeSIiLAxUsjedHuVOje6k0dn4bDuMF_ab7UuMC6IoRBRvkxjWPjEP-aWG-mruOLujFQxTNFdtMXwI_q1G-4qU7qEScipZYTKnrIwCj8NECK5ITcP01LAD_8; expires=Mon, 10-Jun-2024 15:23:23 GMT; path=/; domain=.google.com; HttpOnly
alt-svc: h3=":443"; ma=2592000,h3-29=":443"; ma=2592000

root@nginx-nonfault:/# exit

こちらはちゃんとアクセスできています。

今回は 5 分間の障害だったので、5 分間で実験は完了します。
完了すると、状態が Completed に変わります。

実験完了

最後に、障害が終わったあとにアクセスできるようになっているかを確認します。

# kubectl -n fis-test exec pods/nginx-fault -it -- /bin/bash   
Defaulted container "nginx" out of: nginx, c48db930-62fb-3331-b038-0bcb3dd51574 (ephem), 126b7b8a-67f6-353b-9f32-4613c004d799 (ephem)
root@nginx-fault:/# curl https://www.google.com -I
HTTP/2 200 
content-type: text/html; charset=ISO-8859-1
content-security-policy-report-only: object-src 'none';base-uri 'self';script-src 'nonce-qfp_2tndS22x2xEWyug2qw' 'strict-dynamic' 'report-sample' 'unsafe-eval' 'unsafe-inline' https: http:;report-uri https://csp.withgoogle.com/csp/gws/other-hp
p3p: CP="This is not a P3P policy! See g.co/p3phelp for more info."
date: Sun, 10 Dec 2023 15:31:50 GMT
server: gws
x-xss-protection: 0
x-frame-options: SAMEORIGIN
expires: Sun, 10 Dec 2023 15:31:50 GMT
cache-control: private
set-cookie: 1P_JAR=2023-12-10-15; expires=Tue, 09-Jan-2024 15:31:50 GMT; path=/; domain=.google.com; Secure
set-cookie: AEC=Ackid1S_X4mcUhcn1o1-oo0_XN5K4NOPDi2Ejcs4l5qqaOFtsStJdJ_ccZc; expires=Fri, 07-Jun-2024 15:31:50 GMT; path=/; domain=.google.com; Secure; HttpOnly; SameSite=lax
set-cookie: NID=511=qwzQto89SptCsSt_0g6hQGC_Ld7U6izgINg42dncZbOgtfxDj1Zg5qs4zkZkkvJhFn91LbGa_RpOAUmB78C-bvl_fvSwZUuyhOyJFEicvEwBZNrME5geyoMRcCCCW96Cae0_k_wHZjJAxiHWOkK_sUAdw-GNMT2KzdmX8p8kCmM; expires=Mon, 10-Jun-2024 15:31:50 GMT; path=/; domain=.google.com; HttpOnly
alt-svc: h3=":443"; ma=2592000,h3-29=":443"; ma=2592000

root@nginx-fault:/# exit

ちゃんとアクセスできるようになっていました!

これで FIS の検証は終わりです。

まとめ

FIS を使用して EKS ポッドのネットワーク障害をシミュレートすることができました。

FIS では今回使用した Network Blackhole Port 以外にも様々な障害を注入することができるので必要に応じて試してみてください!
(参考) FIS の EKS に関する障害タイプ

この記事が役に立てば幸いです!

Discussion