SREの林です。本記事では最近知ったAmazon Elastic Kubernetes Service (以後はEKS)で利用する AWS Load Balancer(以後ALB) Controller の IngressGroup 機能でALBを集約してコスト削減を行なった事例を紹介します。
最初にまとめ
ALB ControllerのIngressGroupを利用すると、EKSクラスタ内で共通の一つのALBに対して複数のIngressを管理することができます。これによって「各Ingress毎にALBを作成した場合のコスト」を削減できます。*1
IngressGroupを有効にするのはとても簡単です。既存の ingress.yaml
への変更点は以下の3点でした。
alb.ingress.kubernetes.io/group.name
の追記alb.ingress.kubernetes.io/group.order
の追記ingresses.spec.rules.host
の追記
これによって以下のようにALBを一つにまとめることができました。
Before
After
抱えていた課題
さて、私たちは複数のKubernetes(以後はK8s)クラスタをEKSで管理しています。そのクラスタの一つに”SREチーム検証用&社内向けのミッションクリティカルではないアプリケーション”専用のEKSクラスタがあります。
そのEKSクラスタのマニフェストの構成は以下の通りです。各アプリケーション毎にディレクトリを作成し、その中にYAMLファイルを配置しています。
. ├── awx ├── guacamole │ ├── base │ ├── dev │ ├── imacrea │ └── init ├── netbox_ldap │ ├── base │ ├── dev │ ├── init │ └── prod ├── omnidb ├── other_api_services │ ├── alb-alb-controller │ └── metrics_server ├── pgadmin ├── portal ├── redmine │ ├── 100_namespace │ ├── 200_external_dns │ └── 300_manifests ├── shlink ├── trac └── volumes
各アプリケーションディレクトリの中のファイル構成は基本的に以下の通りです。どれもEKSを運用する中で基本的なリソースですので一つ一つをここでは説明しませんが、今回焦点となる ingress.yaml
ファイルがHTTPSで利用する各アプリケーションのディレクトリの中に作成されています。
trac/ ├── README.md ├── deployment.yaml ├── external-dns.yaml ├── ingress.yaml ├── namespace.yaml ├── pvc.yaml ├── sc.yaml ├── service.yaml └── serviceaccount.yaml
図にするとこんなイメージです。*2
ポイントは以下の最後にある”1つのアプリケーションに対して1つのALBが作成される”点です。
- 1つのアプリケーションは1つのIngressを持つ
- 1つのIngressは1つのALBを作成する
- 結果として1つのアプリケーションに対して1つのALBが作成される
ALB一つのお値段っておいくら?
上述した構成でもアプリケーションは問題なく利用できていました。しかしチームで定期的に行っているクラウドコスト費用削減運動の中で、私はALBの乱立に心を痛め続けていました。
さて、ここで参考として”東京リージョンのALB1台の月額費用”を確認してみましょう。*3
上記のAWSのドキュメントによると以下とあります。
- Application Load Balancer 時間(または 1 時間未満)あたり、0.0243USD
- LCU 時間(または 1 時間未満)あたり、0.008USD
- 新しい接続: 1 秒あたりの新たに確立された接続の数。通常、接続ごとに多くのリクエストが送信されます。
- アクティブ接続: 1 分あたりのアクティブな接続の数。
- 処理タイプ: ロードバランサーによって処理された HTTP(S) リクエストと応答のバイト数 (GB 単位)。
- ルール評価: ロードバランサーにより処理されたルールの数とリクエストレートの積。最初に処理される 10 個のルールは無料 (ルール評価 = リクエスト率 * (処理されたルールの数 - 無料分の 10 個のルール))。
今回対象としているのは”SREチーム検証用&社内向けのミッションクリティカルではないアプリケーション”群ですので、LCUに関しては誤差程度のコストとし、ALB時間についてのみ着目します。
0.0243USD * 24h * 30d = 17.496USD * 約127円(レート) = 約2,222円/月
となりました。LCUの変動分もざっくり加算するとALBにつき多く見積もって 3,000円/月
程度のコストはかかると見込んで良さそうです。
コンテナ化してEKSへ集約したのにコストメリットが出ない問題
今回対象のEKSクラスタは”SREチーム検証用&社内向けのミッションクリティカルではないアプリケーション”用と説明しました。この中の"ミッションクリティカルではないアプリケーション"は以下の特性を持ちます。
- そのアプリケーションの停止はお客様へ影響する業務に直接関わらない
- ほとんど使用しないが昔の情報があるのでアプリケーションを廃止することはできない
- 以前は国産クラウドのVMで月額1,500円程度で動いていた
問題となるのは最後の特性です。VM環境では 1,500円/月
で動いていたものが、コンテナに移行してEKSに集約してもALBを作ると 3,000円/月
となってしまいます。私たちはこれまでVMで動いていたアプリケーションをコンテナにマイグレーションをし集約していく取り組みを行なっています。それがコスト改善につながる期待もあるからです。
コンテナの特性についてGoogle Cloudのドキュメント what are containers? より以下の文を引用します。
Efficient operations
Containers are lightweight and allow you to use just the computing resources you need. This lets you run your applications efficiently.
引用文の通り、私たちの”ミッションクリティカルではない”アプリケーションについてもVMからコンテナへ移行したことでアプリケーション自体のリソースは lightweight になりました。 使用するリソースが少なくなればそれにかかる費用も安くなるのがクラウドですが、"ALBが1つ作られるだけで移行前よりもコストが増額する状況"になっていたのです。
これは喜ばしい状況ではありませんでした。
IngressGroupで解決
本来であれば解決策は難しいものではありません。私たちが管理するEC2で構成されるアプリケーションの多くは一つのALBを利用して複数のアプリケーションへルーティングを行なっており、それをIngressでも行えれば良いのです。
ALBの持つルーティング機能は非常に強力です。ALBドキュメントのRule condition typesが詳しいですが、以下の要素をルーティングの条件として利用できます。
- host-header
- http-header
- http-request-method
- path-pattern
- query-string
- source-ip
私たちのケースではアプリケーションそれぞれに異なるホスト(URL)を付与していますので、1つのALBで host-header
を条件に各アプリケーションへのルーティングができれば問題は解決します。しかしそれをEKSに対してマニフェストファイルで表現する方法を知りませんでした...
それを可能としたのが冒頭で紹介したALB ControllerのIngressGroup 機能です。以下に概要を引用します。
IngressGroup feature enables you to group multiple Ingress resources together. The controller will automatically merge Ingress rules for all Ingresses within IngressGroup and support them with a single ALB. In addition, most annotations defined on an Ingress only apply to the paths defined by that Ingress.
support them with a single ALB
!!これぞ正しく私が求めていた機能でした。
IngressGroupを有効にするためのingress.yamlの変更点
IngressGroupを有効にするのはとても簡単です。変更点は以下の3点でした。
- alb.ingress.kubernetes.io/group.name の追記
- alb.ingress.kubernetes.io/group.order の追記
- ingresses.spec.rules.host の追記
参考としてFiles changedも載せておきます。*4
実際の ingress.yaml
についても紹介しておきます。今回の変更点にはコメントを付与しています。
apiVersion: networking.k8s.io/v1 kind: Ingress metadata: annotations: alb.ingress.kubernetes.io/actions.ssl-redirect: '{"Type": "redirect", "RedirectConfig": { "Protocol": "HTTPS", "Port": "443", "StatusCode": "HTTP_301"}}' alb.ingress.kubernetes.io/auth-idp-cognito: '{"UserPoolArn":"arn:aws:cognito-idp:ap-northeast-1:123456789012:userpool/ap-northeast-1_hoge","UserPoolClientId":"hoge","UserPoolDomain":"hoge"}' alb.ingress.kubernetes.io/auth-on-unauthenticated-request: authenticate alb.ingress.kubernetes.io/auth-scope: openid alb.ingress.kubernetes.io/auth-session-cookie: AWSELBAuthSessionCookie alb.ingress.kubernetes.io/auth-session-timeout: "604800" alb.ingress.kubernetes.io/auth-type: cognito alb.ingress.kubernetes.io/certificate-arn: arn:aws:acm:ap-northeast-1:123456789012:certificate/hoge-piyo-fuga alb.ingress.kubernetes.io/listen-ports: '[{"HTTP": 80}, {"HTTPS": 443}]' alb.ingress.kubernetes.io/scheme: internet-facing alb.ingress.kubernetes.io/success-codes: "200" alb.ingress.kubernetes.io/group.name: sre.k8s-middleware-common # Cluster共通のALBを使う。コスト削減 alb.ingress.kubernetes.io/group.order: '110' # 1-1000 の範囲 external-dns.alpha.kubernetes.io/hostname: trac.example.com kubernetes.io/ingress.class: alb creationTimestamp: null labels: app: trac name: ingress-trac namespace: trac spec: rules: - host: trac.example.com # Cluster共通のALBを使う場合は必須 http: paths: - backend: service: name: ssl-redirect port: name: use-annotation path: /* pathType: ImplementationSpecific - backend: service: name: trac-service port: number: 80 path: /* pathType: ImplementationSpecific status: loadBalancer: {}
そしてこれによって以下のように一つのALBで複数のアプリケーションに対応することができました!スッキリです!!
最後に
以上がALB Controllerの IngressGroup 機能の紹介でした。
”塵も積もればなんとやら”です。EKSでALB Controllerをお使いの皆様でIngressとALBが一対一となっている場合、ミッションクリティカルなアプリケーションでなければコスト重視でIngressGroupを利用するのも良いかもしれません。
参考情報
group.orderが重複するとエラー
group.order
に複数のIngressで同じ値をセットした場合どうなるかを検証したところ、エラーとなりましたのでログを記載しておきます。クラスタ用リポジトリのREADMEに一覧を記載して管理するなどした方が良さそうですね。
ingress-awx-prod
という名前のIngressgroup.name
はsre.k8s-middleware-common
と他と同じ名前を指定group.order
は '100' を指定して既に利用済みだった
aws-load-balancer-controller-7657f8b975-xbpkj controller {"level":"info","ts":1653900236.4752069,"logger":"controllers.ingress","msg":"successfully built model","model":"{\"id\":\"awx/ingress-awx-prod\",\"resources\":{}}"} aws-load-balancer-controller-7657f8b975-xbpkj controller {"level":"error","ts":1653900236.4759805,"logger":"controller-runtime.manager.controller.ingress","msg":"Reconciler error","name":"sre.k8s-middleware-common","namespace":"","error":"conflict Ingress group order: 100"} aws-load-balancer-controller-7657f8b975-xbpkj controller {"level":"error","ts":1653900236.481908,"logger":"controller-runtime.manager.controller.ingress","msg":"Reconciler error","name":"sre.k8s-middleware-common","namespace":"","error":"conflict Ingress group order: 100"} ... 以下同じエラーの繰り返し
Command to list group.order in a k8s cluster
(2022-06-14追記)
上述した通り、 group.order
が重複するとエラーになることもあり、当初はリポジトリのREADME.mdで表形式で手動管理していましたが当然のように記載漏れがあったため、group.order重複エラーに辛酸を舐めさせられたSREメンバたちがコマンドを組み立ててくれました。それがこちらです!
クラスタ内のgroup.orderを簡単に確認するコマンド
$ kubectl get ingress -A -o=custom-columns="Namespace:metadata.namespace,IngressName:metadata.name,IngressGroupName:metadata.annotations.alb\.ingress\.kubernetes\.io/group\.name,GroupOrder:metadata.annotations.alb\.ingress\.kubernetes\.io/group\.order" --sort-by=".metadata.annotations.alb\.ingress\.kubernetes\.io/group\.order"
結果としては以下のようになり、大変見やすいです。
--sort-by=".metadata.annotations.alb\.ingress\.kubernetes\.io/group\.order"
によって見やすさに拍車がかかっていると感じています。
$ kubectl get ingress -A -o=custom-columns="Namespace:metadata.namespace,IngressName:metadata.name,IngressGroupName:metadata.annotations.alb\.ingress\.kubernetes\.io/group\.name,GroupOrder:metadata.annotations.alb\.ingress\.kubernetes\.io/group\.order" --sort-by=".metadata.annotations.alb\.ingress\.kubernetes\.io/group\.order" Namespace IngressName IngressGroupName GroupOrder kotslack ingress-kotslack <none> <none> guacamole-imacrea guacamole-imacrea <none> <none> guacamole-dev guacamole-dev <none> <none> envoy-proxy ingress-proxy <none> <none> trac ingress-trac sre.k8s-middleware-common 100 portal ingress-portal sre.k8s-middleware-common 110 netbox ingress-netbox sre.k8s-middleware-common 115 netbox-dev dev-ingress-netbox sre.k8s-middleware-common 116 shlink ingress-shlink sre.k8s-middleware-common 120 shlink ingress-shlink-web sre.k8s-middleware-common 121 awx ingress-awx-prod sre.k8s-middleware-common 200 redmine-test ingress-redmine sre.k8s-middleware-common 300 pgadmin ingress-pgadmin sre.k8s-middleware-common 310 omnidb ingress-omnidb sre.k8s-middleware-common 320