Pairs(Eureka)のEKS Production環境の設計と運用のお話
この記事は Eureka Advent Calendar 2022 の16日目の記事です。
こんにちは、こんにちは。eureka SREチームのEngineer/Managerの @marnie です。
今年はre:Inventに行けなかったので、来年こそはre:Inventにいきたいと今からイメトレに勤しんでいます。
今年はSREチームとしても、ECS Fargateを中心としたインフラからEKS on EC2(ManagedNodeGroup)への移行という大きな変更を実施した年でもありました。
この記事では、EKSへの移行に際して、どのようにEKS環境を設計して行ったか、どのようなプラクティスを実践したり参考にしたのかなどをご紹介していけたらと思います。
頭の整理がてら書いてたら、とても長く長くなってしまったのですが、年末ということで… 🐼
Table of Contents
− 対象読者
− 前提
− AWS ECS FargateからEKSへの移行
− テナンシーポリシーの策定とテナントの適切な分離
−− Data Account/Computing Accountの分離
− 構成管理とデプロイパイプライン
−− クラスタ構築
−− terraform/manifest管理の境界をどこにするか?
−− 変更の反映とアプリケーションのデプロイ
− クラスターセキュリティ
−− Identity And Access Management
−− PodSecurity
−− NetworkPolicy
−− Policy as Code + GateKeeper
−− Audit
−− DetectiveControl,DataEncryption,Audit,etcetc…
− スケーラビリティの担保と配置戦略の工夫
−− Priority based expander for cluster-autoscaler
−− coreDNSなどの重要なPodのオートスケール
−− deschedulerの重要性
−− IPアドレスとスケーラビリティ
− ロギング・モニタリング
− まとめ
− 謝辞 (Special Thanks)
対象読者
- これからEKSの導入を検討している方
- EKSを利用しており、他社の運用や設計指針に興味がある方
前提
- EKS version1.21時点に移行作業をおこなったので、以降の記事におけるEKS versionは1.21が前提となります。
AWS ECS FargateからEKSへの移行
Eurekaでは元々、3年近くECS Fargateを中核にしたインフラ運用を行っていました。
2019年初頭にEC2からECS Fargate移行当時もEKSとECSどちらを選ぶかは大層悩んだのですが、当時の時点ではEKSはまだまだリリースしたてで、様々な事を自前で運用しないといけなかった事と実質2名で全てのインフラを運用していたので、なるべく運用コストのかからないECS Fargateを採用しました。
ECS Fargateはノードやオーケストレーターの面倒を見ることなく、非常に簡単にスケーラビリティも担保できると、Webサービスのホスティング環境としては非常に良いサービスだったのですが、移行から数年経った今の社内の現実を改めて見てみると
- GPUインスタンスを利用する処理や、データ処理などにおけるハイスペックなコンピューティングが必要なワークロード
- ECS Fargateをbaseに構築したBatch処理における諸課題
- 別チームが機械学習・データ基盤などがGCPを利用している事による別クラウド上でのワークロード(cf.GKE)のニーズ
など、ECS Fargateだけでは対応しきれないパターンのユースケースもチラホラ出てきており、結局、ECS on EC2や一部チームではGKEなどを部分的に利用していたりと、異なる技術スタックに対して守備を張り続けている状態が生まれていました。
コンテナオーケストレーションの技術スタックを複数守備範囲に置くことに自分を含め魅力を感じていなかったので、技術スタックを統一したいという気運が高まり、改めて検討を行い、結果、
- エコシステムの充実具合や作り込みの柔軟性の魅力
- クラウドプラットフォームに依存しない幅広いニーズに応えられる
- 一部とはいえ社内で運用実績が既にあってゼロスタートではない
- EKSはリリース以降、頻繁にアップデートがなされており、Managed NodeGroupを始めとする導入・運用難易度を下げるアップデートや公式ドキュメントの情報量の強化もなされている
という点から、”社内で採用するコンテナオーケストレーションの技術スタックをkubernetesで統一をする” という事を決めてEKSへの移行を行いました。
テナンシーポリシーの策定とテナントの適切な分離
まず、EKSの設計において最初に決めたのは、クラスター上のアプリケーションをどんな粒度で配置し、どんなポリシーでクラスターを分けるのか?でした。
代表的な分け方は以下のような2つかと思います。
- マルチテナント・シングルクラスタ
リソースの集約性・メンテナンス効率に優れるが、内部での権限管理や分離が必要になる、テナント間で影響を受ける。
- シングルテナント・マルチクラスタ
独立性が高いので、テナント間の影響を考慮しなくて済む。クラスタ数が多くなるので、アップデートなどのメンテナンスの負担は大きくなる。
前者は共存のための分離、後者はメンテナンス・管理の効率向上と、どちらにしても運用する際は異なる努力が必要になると考えています。
Eurekaではリソースの集約性とメンテナンス効率を重視しマルチテナント・シングルクラスタを選択しました。
また、社内の組織が以下のようにシンプルだったという事もあります。
- 複数事業部制ではなく、開発組織と事業組織が1つしかない
- 利用ユーザーが社内のユーザーに限られる
また運用が大変な時は、あまり固執せず積極的に分離すればいいのかなと考えています。例えば、以下のようなケースの場合は積極的に別クラスタへの切り出しを検討します。
実際に今時点では4.に該当するAIチーム専用クラスタなどが存在しています。
1.協業他社/グループ会社との共用など、クラスタに変更を行う際の意思決定や責任範囲が自社で完結しない可能性が出るもの
2.法律を含む特殊な管理・コントロールが必要なもの(例:金融系等)
3.外部向けに約束した固有のSLAがある
4.組織デザイン上、選任で管理を任せられる・専門性が高いサブシステム(例:AI系など)
eurekaのEKS上は自社利用&&自社運用のサービスのみが存在するソフトテナンシーであると言っても、色々なサービスが1つのクラスタ上に共存するという現実から向き合わなければならない事が増えたのも事実です。
各テナントに脆弱性があった場合などにおける影響範囲を考慮すると、以下のようなリスク低減の対策が必要になってきます。
- 適切なアクセスコントロールと認証認可の管理
- ネットワークレベルでの分離
- 特権昇格の防止やNode侵害の防止
こちらについては、後述するアカウント分離とクラスターセキュリティの項で行なった対策の具体を記載していきます 🐼
(*)マルチテナンシーのモデルについては3種類がよく語られていますが、eurekaでは社内ユーザーかつ規模も限られている事から、各アプリケーションごとにNameSpaceを細かく分離して開発者に提供する Namespace as a Service が該当します。
Data Account/Computing Accountの分離
マルチテナンシーにおける独立性を考えた際にまず考慮をすることになったのは、データをどこに所有するかについてです。
上述のようにマルチテナンシー構成を採用しましたが、データ面について、全てのサービスの利用するデータストアがアカウント上に共存する事になる場合、アカウント内の権限管理はシングルアカウントの場合より複雑煩雑を極めそうだなぁ…. 😢という悩みがありました。
この悩みへの答えとして、eurekaのEKSでは以下のようにクラスターの所属するアカウントと各サービスのデータストア(RDS,DynamoDB,etc)が所属するアカウントを分離しました。
このアカウント分離と併せて、クラスターセキュリティの項で後述するAWS Pod Security GroupとIRSAを利用する事で同一クラスタ上に所属しながらも、クラスタ上の各サービス・コンポーネントのデータはアカウントという強い分離レベルで分離する事が管理する事が可能になりました。
(*) この手法は、筆者もいくことができた唯一のre:inventである re:Invent 2019 Amazon EKS under the hood (CON421-R1) で紹介されていたものです 、ありがとう re:Invent 2019 👍
構成管理とデプロイパイプライン
eurekaにおけるデプロイと構成管理の関係性を示す全体像を以下に図で示します。
構成管理はApplication,Manifest,Infraの3つのRepositoryで基本的に構成しています。それぞれの役割は以下の通りです。
- Application Repository
アプリケーションコードの格納されたRepositoryです(まんま)。アプリケーションのビルド、プッシュまでを責務にしています。アプリケーションのデプロイについては後述します。
- Manifest Repository
各クラスタのKubernetes manifestを管理するレポジトリで、kustomizeを利用したレイヤ構造で構築しています。CIの整備などのコストと規模感から、今時点では全clusterを1個で管理してます。アプリケーションとclusterはn:1の構成になることもある事や、Kubernetesリソースのみの変更などが起きるといった点、CI/変更履歴やmergeなど権限の管理は分けたいetc…etc..などで煩雑になるというという点からApplicationとは分けて管理をしています。(*) Argo CD best Practiceでも紹介されています。
- Infra Repository
terraformを用いた全体のインフラをコード管理するRepository。EKS ClusterやApplicationLoadBalancerなどはこちらで作成しています。
クラスタ構築
EKS ClusterとClusterの動作に必要な関連リソースは、他のインフラ同様にterraform moduleを自社で作成して再作成や横展開を容易にすると共に、完全にコードによる管理下に置いて管理しています。イメージはこんな感じ(実際のものとは異なります)
module "sample_cluster" {
source = "../../modules/eks_cluster"
cluster_k8s_version = "1.24"
cluster_name = "sample"
node_suffix = "suffix"
node_subnet_1a_id = aws_subnet.app_1a.id
node_subnet_1c_id = aws_subnet.app_1c.id
node_subnet_1d_id = aws_subnet.app_1d.id
cluster_description = "sample cluster"
cluster_secret_administrators = [
"arn",
"arn2",
]
cpu_ondemand_node_instance_types = ["c5.large"]
cpu_spot_node_instance_types = [
"c5.large",
"c5a.large",
"c5n.large",
"m5.large",
"m5a.large",
]
cpu_ondemand_for_essential_pod_node_instance_types = ["c5.large"]
cpu_node_ami_type = "AL2_x86_64"
cpu_ondemand_node_min_size = 1
cpu_ondemand_node_max_size = 1
cpu_ondemand_for_essential_pod_node_min_size = 1
cpu_ondemand_for_essential_pod_node_desired_size = 1
cpu_ondemand_for_essential_pod_node_max_size = 1
cpu_spot_node_min_size = 1
cpu_spot_node_max_size = 1
cpu_node_disk_size = 1
cluster_accessible_security_group_id = hoge
}
クラスタ構築については、最適化AMIで構成されてAutoScalingGroupも併せて組んでくれるManaged node groupsが既にあるので、Kubernetes周りのセットアップなどはする必要がなく、主にネットワーク周りでの許可やIAMがメインになってきます。
terraform-aws-eksのようなモジュールも公開されていますので、terraformを利用したい方は参考にできるのかなと思います ⛱
terraform/manifest管理の境界をどこにするか?
Kubernetesを導入する際に一度は社内で議論になる話題かなーと思うので、ピックアップしてみました。
eurekaの場合は以下のようなシンプルなルールで運用しています。
- AWSのリソースは terraform を管理する Infra Repository で作成
- KubernetesのリソースはManifest Repository
AWS Load Balancer ControllerやExternalDNSなどを利用すると、terraformを書くことなくダイナミックにAWSリソースを追加する事も可能なのですが、以下のようなメリットを見出して、このように運用しています。
- terraform上で管理する事でAWSリソース間の参照や関連付けやSecretStoreなどの参照等の管理が容易。
- 基本的にInfra repository側をManifest Repository側が一方的に参照する関係性でわかりやすい
変更の反映とアプリケーションのデプロイ
基本的なコンセプトは、gitopsの考え方を参考にしており
- 基本的アプリケーションのデプロイを含む全てのKubernetes上の変更はmanifestファイルの変更によってのみ、反映される
- 誰がいつどの変更を行ったかなどがわかりやすい
- manifestファイルの状態とクラスタの状態が一致する
と言うシンプルでわかりやすいメリットを発揮してくれます 😃
上記を円滑に実現するためにKubernetes上のリソースとmanifestのsyncには以下のようなOSSをセットで採用しています。
Argo CDはGitHub上でOAuth Applicationの設定を行なう+Applicationリソースを作成するだけでmanifestの変更をKubernetes上にsyncしてくれますし、Argo Rolloutsと組み合わせる事でcanary Releaseやblue greenデプロイなども簡単に実現する事ができます。
(*) Argo CD ApplicationSetも併せて使うと、Applicationリソースの作成をより省力化できるのでおすすめです 🐼
Application RepositoryとManifest Repositoryの接続
アプリケーションのデプロイについても、Application RepositoryからManifest RepositoryRepository上で以下の動作をするGithubActionをdispatchする事で、ArgoCDに反映を任せる事ができてシンプルになります。わーい
- gitコマンドでブランチを作成する
- 作成したブランチ上でApplication Repositoryから渡されたImageIDと対象サービス名をもとにyqなりでManifestファイルを編集する
- ghコマンドでPRを作成する
- ghコマンドでMerge
- ArgoCDが反映してくれるぞ!やったぜ!
(*) argocd-image-updaterでもうちょいおしゃれにできるんだろうか?とか思いつつ、大した処理でもないので自前でサクッと書いちゃいました。
クラスターセキュリティ
EKSでManagedNodeGroupを採用した場合の責任境界モデルは以下の通りです。
WorkerNodeのOSや設定については、AWSの責任範疇ですが、Kubernetes内のセキュリティ(コンテナの脆弱性やクラスタ内の権限管理などetc)はユーザーの責任範囲となります。
また、Kubernetes公式のドキュメントでは、以下のようにレイヤリングされています。
Code,Container部分のセキュリティ対策は今回の記事の主旨と異なるので、割愛して、以降はこのクラスターのセキュリティについて記載していきます。
Identity And Access Management
まず、全ての大前提となるIdentity And Access Management について述べます。
EKSでは以下の2つの機構が非常に強力です。
- Kubernetes内の情報の役割ベースのアクセス制御機構であるRBACとIAM Roleをリンクするaws-authConfigMap
- PodにIAM Roleを適用するIRSA
上図のような構成を取る事で以下のメリットを享受できました。
- Okta & AWS IAM Roleの連携の延長でKubernetes上の権限を管理する
- Pod単位でEC2,ECSのアプリケーションと同様にIAM Role baseでのアクセス制御ができる
特に後者のPod単位でのIAM Role のアタッチの実現は、テナント単位で細かく権限を管理したいマルチテナントにおいては必須の機能です(デフォルトはノードのRoleが利用される)
PodSecurity
PodのSecurityについては、Kubernetes公式でPod Security Standards が定義されています。
PSSはPriviledged,Baseline,Restrictedの3つレベルがあり、Baseline以上に準拠することで、特権昇格やHostPathマウントによるHost侵害などに対するリスクを低減する事が出来ます。
EurekaではBaselineレベルは既に適用しておりますがBaselineでもaws-nodeやロギングエージェントのような一部のPodは動作上どうしても特権が必要なので、対象から除外をしたりして運用しています。
PodのSecurityについてはEKS BestPracticeも非常に参考になりますので、一読ください。
またPSSは後述するOPA/Conftest, GateKeeperなどを利用して実際の適用を検査することが可能です。
NetworkPolicy
Kubernetesはデフォルトでは、所属するNamespaceに関わらず、Pod間が自由に通信可能な状態になりますので、テナント同士で通信させたくない(ハードテナンシー)などでは対策が必要です。
具体的にはCalico AddOnを利用する事でNamespace間の通信などを細かく制御設定することできます。
例えば、kustomizeと組み合わせて、下記のようなbaseルールを敷く事で、自身のNamespaceとkube-system以外を遮断するような事も可能です。
# allow,kube-system,owned traffic Ingress Access (BaseRule)
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: allow-owned-traffic
spec:
podSelector: {}
policyTypes:
- Ingress
ingress:
- from:
- namespaceSelector:
matchLabels:
name: yourNamespace
- namespaceSelector:
matchLabels:
name: kube-system
eurekaでは、ソフトテナンシーなので、現時点ではそこまで厳密な独立性は必要ないのですが、後で入れるのが大変+各テナントで脆弱性が発生した場合のリスク低減として、下図のようにポリシーを敷いて基本的に必要のない通信を許可しないように設定しています。
また、SecurityGroup for Pods を利用する事で、Podにセキュリティグループを付与できるので、AWSリソースへのアクセスについてもセキュリティグループ単位での制御を継続する事が可能です。
Policy as Code + GateKeeper
どんなポリシーを定めても、レビューでの見落としなどが発生する可能性などがありますし、遵守されなければ意味がありません。また複雑なルールは理解コストが高くなりがちです。
eurekaではPSSや独自ポリシーを検査をOPA/Rego/ConftestでCIベースでチェックする事でセキュリティや設定の一貫性を担保しています。
PSSについては、PSS Rego Policyを参考に実装して利用しています。
またGatekeeperを用いる事で実際のクラスタ上に対するチェックや変更を禁止する事も可能です。
GateKeeper はKubernetesのAdmission Controllerとして動作するカスタムコントローラーでOPAに設定されたPolicyに反するmanifestsのデプロイを禁止したり、動作中のアプリケーションのチェックが可能です。Code Scanningで使用しているOPA/ConftestのRego policyをkonstraintでGatekeeper用のConstraintTemplateに変換して共通利用する形で利用しています。
図示すると以下の通りです。
Audit
cluster設定から変更するだけでControl Planeのログなどを含むログがcloudwatchに出力可能です。terraformだと1行追加するだけで実現可能なので、忘れずにやっておくと良いかなと思います。
DetectiveControl,DataEncryption,Audit,etcetc…
上記以外にも、セキュリティ周りについてはその他にDetectiveControl,DataEncryption,etc…と多岐に渡って対応をしていますが、ここではマルチテナンシーの文脈に沿ったものに留め割愛します。(ただでさえもう長
同チームの@ogadyさんが以前に登壇した資料で詳しく紹介されていますので、興味がある方は是非↓もご参照ください。
スケーラビリティの担保と配置戦略の工夫
EKSにおけるスケーラビリティは、以下の技術の組み合わせで実現されます。
- Horizontal Pod Autoscaling(+MetricServer)
- Cluster AutoScaler
- EC2 AutoScaling
関係性としては以下の通りです。
- hpaはmetric serverが収集するクラスタ内のリソース使用率などを基にPod全体の使用率を計算し、閾値に応じてPodの数を増減する
- Cluster AutoScalerは、hpaがPodを配置できない場合にScaleが必要と判断してScale依頼をAutoScalingGroupに発行する
- AutoScalingGroupはEC2のScale In/Outを行う
注意としては、hpaがpodを設置する際に考慮するのはpodのrequest値となります。
従ってrequest値があまりに小さいと実際には負荷が高いのだが、Pod自体は配置できてしまうので、ノードに対してオーバーコミットな配置がされてしまい、負荷がうまく分散しないというような現象が発生します。
大きすぎるとリソース集約率が下がるというトレードオフの関係にあります。eurekaでは、リソース集約率よりは安定性を重視して、request値は起動に必要な最小の値ではなく、実運用値を参考にした上でやや悲観的に設定するように運用しています。
Priority based expander for cluster-autoscaler
また、Cluster AutoScalerには、スケールするノードグループにPriorityを設定できる拡張も用意されており、設定された時間以内にspot nodeが用意できなければondemandのスケールを開始するような設定を作る事も可能です。以下は例です。
apiVersion: v1
kind: ConfigMap
metadata:
name: cluster-autoscaler-priority-expander
namespace: kube-system
data:
priorities: |-
100:
- .*spot-nodes.*
99:
- .*ondemand-nodes.*
coreDNSなどの重要なPodのオートスケール
eurekaではアプリケーション以外にもcoreDNSのようなkubernetes上での重要なPodについてもオートスケール設定を適用しています。
deschedulerの重要性
また、KubernetesのschedulerはNodeにPodを割り当てるという仕事はしてくれるのですが、起動した後のスケジュールの見直しはしないので、Nodeが増えても再配置・taintの再評価はしてくれないので、Pod数などが偏った場合、その状態が継続してしまうことになります。これによる一部リクエストの応答速度の劣化などを解決するためにdeschedulerを導入しました。deschedulerはリソース使用率をベースにした再配置戦略などを提供してくれます。以下は設定例です。
apiVersion: "descheduler/v1alpha1"
kind: "DeschedulerPolicy"
evictLocalStoragePods: true
strategies:
"LowNodeUtilization":
enabled: true
params:
nodeResourceUtilizationThresholds:
# 50~80を正常と見做す。80~は使用率が高いとみなされて~50の使用率が低いNodeへの再配置候補になる。
thresholds:
"cpu" : 50
targetThresholds:
"cpu" : 80
その他にArgoCDやAWS Load Balancer Controllerのような統括的な仕事をするPodについては、ondemand側の特定のNodeGroupに設置するようにnodeAffinityを設定するなども行なっています。
apiVersion: apps/v1
kind: Deployment
metadata:
labels:
k8s-app: sample
name: sample
namespace: sample
spec:
template:
spec:
affinity:
nodeAffinity:
preferredDuringSchedulingIgnoredDuringExecution:
- weight: 100
preference:
matchExpressions:
- key: eks.amazonaws.com/nodegroup
operator: In
values:
- ondemand-nodes-for-essential-pod
IPアドレスとスケーラビリティ
スケーラビリティについては、IP数や利用するインスタンスタイプによる制約などにも注意が必要です。
- クラスタが設置されているSubnetIP数がPodに付与できるIP数
こちらは対策としては、そもそも余裕を持ったsubnetを作成する事もそうですが、難しい場合は、EKS BestPracticeで紹介されているようにWARM_IP_TARGETの設定を見直して調整をしたり、それでも難しければ、”Run worker nodes and pods in different subnets” の項で紹介されているような別のsubnetでPodを動かすなどの対応が必要になります。
(*) 実Pod数より多くのIPアドレスを確保してしまうWARM_ENI_TARGETの仕様を知らずEurekaでも一度枯渇しかけました..
- Nodeに利用するInstanceTypeのENI数と付与できるIP制限
見落としがちなのですが、ENI数と付与できるIPアドレス数はインタンス別で定まっているので、事前に確認が必要です。(*) NitroベースかどうかでIP Prefix機能が使える使えないなどもありますが、オプションでそれらも確認可能です。
こちらについてはAWS公式リファレンスで簡単に調べるシェルコマンドが用意されています。以下は実行例です。
% ./max-pods-calculator.sh --instance-type m5.large --cni-version 1.11.4-eksbuild.1
29
% ./max-pods-calculator.sh --instance-type m5.large --cni-version 1.11.4-eksbuild.1 --cni-custom-networking-enabled
20
% ./max-pods-calculator.sh --instance-type m5.large --cni-version 1.11.4-eksbuild.1 --cni-custom-networking-enabled --cni-prefix-delegation-enabled
110
また、kubenetes公式の記載ではkubernetes上の制約として、 110 pods per nodeの記載があります。
ロギング・モニタリング
ロギングに関しては、公式で利用し易いfluent-bitのイメージや設定手順を用意してくださっているので、基本的に標準出力から/var/log/containerに出力されたものやdataplaneのsystemdのログ等等をそこまで難しい設定なく収集が可能です。(*)流量が多いサービスの場合はフィルタする設定がコスト的に必要です 💰
監視についてはECS環境から引き続きDatadogAgentを利用しており、DaemonSetを活用してMetricsを収集しています。
監視するメトリクスについては、今まで同様に、ユーザー影響を表現し易いLoadBalancerのステータスコードやアプリケーションのエラーレート監視が主になることは変わりませんが、crashloopbackoffやcontainer restartはアラート対象にしておりますし、coredns latencyやkube-api serverのようなメトリクスはFYIとしてdashboardで閲覧できるようにしていますが、このレイヤーが関わるような問題も起きていないので、今の所小慣れていないですね… 😢
まとめ
eurekaにおけるEKSへの移行で考慮・検討した事や導入した技術などについての大枠をとりとめもなく書いてみました。
Kubernetesは学習コストが高いという事で敬遠されがちですが、出来る事が多いので多少は仕方ないかなぁと思いますし、EKSリリース当時に比べて、だいぶ情報量も利便性も上がったなぁと思いました 👍
色々紹介しきれなかったのですが、istiodの採用やcanaryリリースなどについては@ogadyが記事を書いてくれるのでお楽しみに!
そういえばアドベントカレンダーでしたので、この文章が誰かの幸せなクリスマスに少しでも役に立てば幸いです。
それでは皆様良いクリスマスを 🎅
謝辞 (Special Thanks)
EKSへの移行に際しては、AWS社公式のEKS Best Practiceに記載されている内容も非常に参考になりました。一部古い情報もあるという事でしたが、考慮している項目自体がとても有用だと思いますので、このドキュメントについては、とても感謝しています :)
また、EKSへの移行において積極的に相談に乗ってくださった弊社の担当SAである@kazuya_iwamiさんにも様々な面で情報提供などの支援を頂き心強かったです。ありがとうございます!