みんな大好き Autoscaling の時間です☆ 少し長めになっちゃったのですが、分割するとわかりづらくなりそうだったので1つでぶっ込んでいきます。
さてここで新たに、helm という仕組みが登場します。Kuberntes におけるパッケージ管理みたいなもので、これも Terraform でやっつけちゃうので安心して・・・うーん、最後までいければいいですね!くらいな感じです。
EKS における Autoscaling の仕組み
まずおさらいとして、ALB + EC2 の時はどうやっていたかというと、Autoscaling Group に CloudWatchAlarm を組み合わせて、例えばCPU使用率を条件に、何%を超えたら既存台数の何%を増加、何%より低くなったら既存台数の何%を減少、のように実現していました。これがガラリと変わって CloudWatchAlarm とオサラバします。そして、EC2 Node と Pod という2種類のリソースがあるため、Autoscaling の仕組みも2段階になります。
まず、Pod のスケーリングですが、helm の metrics-server でCPU等のリソース使用率を取得し、Horizontal Pod Autoscaler(※以下、HPA)がリソース使用状況からPod数増減の判断をして、Deployment のPod数を編集します。
Nodeリソースが不足して、Pod数を増やせなくなった場合、helm の cluster-autoscaler が AutoscalingGroup の desired を編集し、Nodeを増やすことで HPA の詰まりを解消します。Nodeリソースが不要になった場合は、減少してくれます。
ここで特徴的なのは、Pod は旧来通り、リソース使用率によって増減しますが、Node は Pod の増減状況に依存するという点です。Pod のリソース状況は、EC2 のようには CloudWatch では管理できないので、Kubernetesクラスタ内で解決することになるのが自然な成り行きなのかな、と思います。また、Nodeの増減は CloudWatchAlarm によるグループの平均CPU使用率でもできないことはないのですが、おそらく、cluster-autoscaler に任せた方が素早く適切な対応となる、と思われます。
と、いうことで、
metrics-server でPodリソース監視をし、
HPA がPodリソース状況を見てPodを増減し、
cluster-autoscaler がNode過不足ならAutoscalingGroupイジってNode増減する!
な感じで、早速 mertics-server を動かす!
・・・ための、helm の準備から入りましょう。
helm の準備
まず、helm とは。下記ページをご参考あれ。ザッとみて雰囲気でわかると思うのですが、yum みたいに install したりなんだりできて、その結果としてそのパッケージ機能を動かすための Pod が起動されたりします。
なので、Kubernetesへのアクセスが整っている環境であれば、手元で helm install や search をできます。そのための手順はドキュメントがあって、
例えばスクリプトを実行して、初期化するとコマンドで遊べるようになります。helm のインストール自体は Terraform に任せてしまいますが、helm としての情報を色々みてみたり、Terraform に不整合が起きた時に帳尻合わせたりするのに必要になることもありますので、準備はしておくとよいでしょう。
1 2 3 4 |
curl -L https://git.io/get_helm.sh | bash # ServiceAccount は後述 Terraform でつくります helm init --service-account tiller --history-max 200 --upgrade |
helm 用の変数
Terraform にて aws, kubernetes と扱ってきたように、helm も provider を用意します。と、その前に local 変数で色々くくりだしておきます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
locals { eks_namespace = "kube-system" eks_role_label_key = "kubernetes.io/role" eks_taints_key_prefix = "node-role.kubernetes.io" } locals { on_helm = true on_eks_helm = local.on_eks && local.on_helm ? true : false helm_name = "tiller" helm_namespace = local.eks_namespace helm_tiller_version = "2.14.1" helm_image = "gcr.io/kubernetes-helm/tiller:v${local.helm_tiller_version}" helm_master_specs = { "nodeSelector.${replace(local.eks_role_label_key, ".", "\\.")}" : "master", "tolerations[0].key" : "${replace(local.eks_taints_key_prefix, ".", "\\.")}/master", "tolerations[0].effect" : "NoSchedule", "tolerations[1].key" : "CriticalAddonsOnly", "tolerations[1].effect" : "NoSchedule", } } |
前半は、次の provider用の情報ですが、最後の specs は、前の記事で書いた Pod 配置のための記述です。インストールした helm パッケージが起動する Pod は管理系なので、安定したNodeへ配置するよう、ここで指定する内容をまとめています。見ての通り、キーの配列やエスケープが、かなり特殊な書き方しているので、離脱危険地帯です。
helm provider
helm_*** を扱えるようにするため、provider を記述するのですが、ここから先を実行する前に、AutoscalingGroup にて(私の場合は role=master の方)、1台以上のNodeを起動しておく必要があります。これは、パッケージインストールうんぬん以前に、helm を利用するために tiller-deploy という Pod を起動するためです。この Pod がないと、helm は操作できません。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
provider "helm" { namespace = local.helm_namespace service_account = local.helm_name tiller_image = local.helm_image install_tiller = true insecure = false enable_tls = false debug = false home = "./.helm" override = [for k, v in local.helm_master_specs : "spec.template.spec.${k}=${v}"] kubernetes { host = aws_eks_cluster.main[0].endpoint token = data.aws_eks_cluster_auth.main[0].token cluster_ca_certificate = base64decode(aws_eks_cluster.main[0].certificate_authority.0.data) load_config_file = false } } |
ここで特徴的な部分を補足説明しておきます。
kubernetes
見たとおり、kubernetes への接続情報です。provider kubernetes の記述内容とほぼ同じです。override
前述の locals 変数で定義した、Pod配置関連の情報を上書きしています。全Node に Taints が登録されているので、こんな感じで master Node の方へ強制的に案内してあげることで、tiller-deploy Pod が任意のNodeへ起動されます。後述の helm_release の記述と合わせて、とてもお上手にコードを書けたと思いマッスル。
home
helm を実行すると、実行したことによって出来上がるファイルがあり、Terraform でうまいことヤリクリするには、コード直下で管理する必要がありました。そのために少々特殊なことをしていて、確か repositories.yaml をローカルの helm で実行して作られたものをコピーしました。この辺は、apply して怒られるたびに調整した感じです。また、Terraformコードは Git で管理しているので、.helm/.gitignore に不要なキャッシュを除くよう /cache を記述しています。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
$ find .helm/ .helm/ .helm/repository .helm/repository/repositories.yaml .helm/repository/cache .helm/repository/cache/stable-index.yaml .helm/repository/local .helm/.gitignore .helm/cache .helm/cache/archive .helm/cache/archive/metrics-server-2.8.0.tgz .helm/cache/archive/cluster-autoscaler-0.13.2.tgz .helm/plugins .helm/starters |
Service Account 作成
さきほども書いたとおり、helm の管理のために tiller-deploy というPodが起動されます。その稼働に必要な情報として、この ServiceAccount と、次の RoleBinding が必要になります。
1 2 3 4 5 6 7 8 9 10 |
resource "kubernetes_service_account" "tiller" { count = local.on_eks ? 1 : 0 metadata { name = local.helm_name namespace = local.helm_namespace } depends_on = [aws_eks_cluster.main] } |
Cluster Role Binding 作成
次はこれ。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
resource "kubernetes_cluster_role_binding" "tiller" { count = local.on_eks ? 1 : 0 metadata { name = kubernetes_service_account.tiller[0].metadata.0.name } role_ref { api_group = "rbac.authorization.k8s.io" kind = "ClusterRole" name = "cluster-admin" } subject { kind = "ServiceAccount" name = kubernetes_service_account.tiller[0].metadata.0.name namespace = kubernetes_service_account.tiller[0].metadata.0.namespace } # Waiting for helm_release. # Otherwise, an error occured. # (Error: error creating tunnel: "could not find tiller") provisioner "local-exec" { command = "sleep 2" } depends_on = [aws_eks_cluster.main] } |
わざわざ sleep をしているのは、コメントの通りですが、Terraform でガーッとリソースを作ると、tiller-deploy Pod が起動しきる前に helm_release でのパッケージインストールが走ってしまい、エラーが発生してしまいます。そのため、ウチの環境の場合はココで2秒待てば大丈夫だったので、仕方なく入れています。Terraform でやっていると、こういうのは慣れたものです。
metrics-server 作成
Node/Pod達のメトリクス管理をしてくれる、helm の metrics-server を入れます。完了すると、metrics-server という Pod が起動します。もちろん、このPodは管理系なので、前述のPod配置管理変数を dynamic – for_each を使って set をループ設定しています。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
resource "helm_release" "metrics-server" { count = local.on_eks_helm ? 1 : 0 namespace = kubernetes_service_account.tiller[0].metadata.0.namespace name = "metrics-server" chart = "stable/metrics-server" dynamic "set" { for_each = local.helm_master_specs content { name = set.key value = set.value } } depends_on = [kubernetes_cluster_role_binding.tiller] } |
これがちゃんと動かないと、その後いろいろダメなので、軽くコマンドを記載しておきます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
# パッケージの状態確認 helm status metrics-server # metrics-server の稼働確認 kubectl get pods -n kube-system # metrics-server のログ確認 kubectl logs metrics-server-abcde12345-67890 -n kube-system # 設定内容の確認 kubectl describe deployment metrics-server -n kube-system # Nodeのリソース確認 kubectl top node # Podのリソース確認 kubectl top pod --all-namespaces |
HPA 作成
HPA は helm ではなく、Kubernetes 付属の機能になっています。metrics-server が稼働していること前提のシステムなので、前述のコマンドなどで動作確認をキチンとしてから進むとよいでしょう。公式ドキュメントはこちら。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
resource "kubernetes_horizontal_pod_autoscaler" "main" { count = local.on_eks ? 1 : 0 metadata { name = local.eks_cluster_name } spec { max_replicas = local.eks_config["pod_max_size"] min_replicas = local.eks_config["pod_min_size"] scale_target_ref { api_version = "apps/v1" kind = "Deployment" name = local.eks_cluster_name } target_cpu_utilization_percentage = local.eks_config["pod_target_cpu"] } depends_on = [aws_eks_cluster.main] } |
CPU条件
CPU条件については別記事でまた追う予定ですが、旧来のCPU条件の扱いとは感覚がだいぶ異なるので、十分な検証が必要なところです。max_replicas
基本的にEC2リソースの量や費用のコントロールは AutoscalingGroup の方でやるので、こちらで上限に引っかかって増加できなくなる、ということは避けたほうがよいと思います。1Node, 1Pod あたりのリソース設定によりますが、Nodeリソースをフルに使えるような超過的な数値が安定的になる、と思われます。api_version
ドキュメントにいくつか書いてあって、例えば autoscaling/v2beta2 だとCPU以外に複数のメトリクス条件を組み込めたりするのですが、EKSでは autoscaling/v1 しか使えないようなので、apps/v1 を指定するしかありません。PodのCPUリソース指定が必須
かなり大事なヤツです。Autoscaling対象となるDeployment にて、前の記事で、なにげなくCPUリソース量を指定していましたが、HPAを扱うためには、resources > requests > cpu
を必ず指定する必要があります。指定しないと、PodのCPU使用率が不明になり、当然HPAも動きません。
その記述をコチラから抜粋しておきます。
Please note that if some of the pod’s containers do not have the relevant resource request set, CPU utilization for the pod will not be defined and the autoscaler will not take any action for that metric.
cluster-autoscaler 作成
cluster-autoscaler は helm で入れます。入れたら、管理Podが起動します。一部クセがありまして、要所は locals に括りだして指定しています。その上で、また同じように master用Node へ配置されるように、dynamic “set” のところで、merge して一括りのコードにしています。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 |
locals { helm_ca_sets = { "cloudProvider" : "aws", "awsRegion" : local.region, "rbac.create" : "true", "rbac.pspEnabled" : "true", "extraArgs.expander" : "most-pods", "extraArgs.balance-similar-node-groups" : "false", "sslCertPath" : "/etc/kubernetes/pki/ca.crt", "autoDiscovery.clusterName" : local.eks_cluster_name, } } resource "helm_release" "cluster-autoscaler" { count = local.on_eks_helm ? 1 : 0 namespace = kubernetes_service_account.tiller[0].metadata.0.namespace name = "cluster-autoscaler" chart = "stable/cluster-autoscaler" dynamic "set" { for_each = merge(local.helm_master_specs, local.helm_ca_sets) content { name = set.key value = set.value } } depends_on = [kubernetes_cluster_role_binding.tiller] } |
sslCertPath
EKS では、この項目を上記値にしないとエラーになります。chart
stable/cluster-autoscaler という名前です。検索すると stable/aws-cluster-autoscaler というのも出てきますが、既に非推奨となっているので注意してください。参考ページ
と、こんな感じで構築が完了です。helm を扱うために、helm関連のPodが起動するための Node が1台以上起動していないとダメっていうのが開発中は少々鬱陶しいのですが、それ以外はキレイに収まっていると思います。
これだけでも、わりと苦労した部類なんですけど、大事なのはこれらの挙動なので、それについてはまた別記事で書いていきたいと思います。気力がもてば。