AWS Fault Injection Service で EKS の障害テストを行う
この記事は 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 つのコンポーネントの障害を想定してテストを行うことにしました。
- fluentd: 各 Pod が出力するログを収集し、CloudWatch Logs に転送する役割
- 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 Fault の Partition
を検証しました。
Partition
を使用すると、ingress / egress の通信を遮断することができます。
また、externalTargets
を使用することで、FQDN による通信元 / 通信先の指定が可能なため、IP アドレスが明示できない通信先でも容易に指定ができます。
しかし、下記の理由から Chaos Mesh の Partition
では障害をシミュレートできないことがわかりました。
-
Partition
のexteranlTargets
で指定した FQDN は障害を発生させた時点で名前解決した IP アドレスを iptables の ipset に設定して通信を遮断するため、名前解決時にすべての IP アドレスを設定できない
実際に、CloudWatch Logs のエンドポイントである logs.ap-northeast-1.amazonaws.com
を externalTargets
に指定して障害を発生させた際、遅延はあるもののログは転送できていました。
また、ローカル端末や fluentd の Pod で nslookup を行ったら ipset に指定してある IP アドレス以外の IP アドレスが返ってきました。
FQDN で指定できないなら、AWS が使用している IP アドレスを指定してブロックしようとしましたが、今度は Partiion
の externalTargets
には 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) どちらの操作かを記載しています。
- 準備
- (AWS) FIS のサービスロール (IAM Role) を作成する
- (EKS) FIS が k8s リソースを操作する際の Service Account, Role, RoleBindings を用意する
- (EKS) サービスロールと Service Account を紐付けるために aws-auth を編集する
- テスト
- (AWS) 実験テンプレートとターゲットリソースの定義を作成する
- (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 のサービスロール作成のポイントは下記の二点です。
- 信頼関係で
fis.amazonaws.com
にsts:AssumeRole
アクションを許可する。 - 実施したいテストの内容に応じて、権限を付与する。
今回は 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 リソース作成のポイントは下記の点です。
- テスト対象のリソースと同じ 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-system
の aws-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
で検索した場合の表示結果の違いです。
FIS の画面に遷移したら、「実験テンプレート
」から、「実験テンプレートを作成
」をクリックします。
最初に実験タイプとして、同じ AWS アカウントをターゲットとするか、他の AWS アカウントを含む複数のアカウントをターゲットとするかを選択します。
今回は同じ AWS アカウントをターゲットとする前提で進めています。
任意で名前と説明を入力したら、アクションとターゲットを作成します。
それぞれの役割は下記のとおりです。
No. | リソース | 役割 |
---|---|---|
1 | アクション | どのような障害を注入するか、詳細を定義します。 |
2 | ターゲット | どのリソースに対して障害を注入するかを定義します。 |
アクション内で、どのターゲットと紐付けるかを定義するため、ターゲットを先に作成します。
今回は EKS 上の Pod に対して障害を注入するため、EKS クラスターや Namespace、Pod を識別するパラメーターを指定します。
今回は Pod 名を明示していますが、Deployment の名前や label により指定することも可能です。
その後、具体的な障害に関する設定を定義します。(画像が長くなってしまいすみません...)
ポイントは下記のとおりです。
- アクションタイプで具体的な障害の内容を選択する。
- Service Account は、
FIS が k8s リソースを操作する際の Service Account, Role, RoleBindings を用意する
で作成した Service Account を指定する。 - 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