※この辺から訂正と追記しました(10/8)
前回でKubernetesへのアクセスができるようになりましたが、ここで一歩引いて EC2 Node を起動するための Autoscaling Group を作成します。
LaunchTemplates と Autoscaling Group だけじゃ味気ないので、UserData でこんなことやってますってのを濃いめに解説するところまで、いきたいと思います。
Launch Template
LaunchConfiguration はもう使わないので、LaunchTemplateにお世話になります。UserData
UserDataのシェルスクリプトについては、最後に記載と説明をします。ここのチョットしたポイントとしては、template_file において template = file(“script.sh”) と vars を使わず、templatefile にしているところです。vars では配列やハッシュは使えないので、そのへんを自由に扱える templtefile を採用しています。
1 2 3 4 5 6 7 8 9 10 11 12 13 |
data "template_file" "eks_node_userdata" { count = local.on_eks ? 1 : 0 template = templatefile("eks_template/eks_node_userdata.sh.template", { eks_endpoint = aws_eks_cluster.main[0].endpoint eks_certificate_authority = aws_eks_cluster.main[0].certificate_authority[0].data cluster_name = local.eks_cluster_name role_label_key = local.eks_role_label_key taints_key_prefix = local.eks_taints_key_prefix hostname_prefix_key = local.eks_asg_hostname_prefix_key node_sysctl = local.eks_node_sysctl }) } |
Launch Template
特に変わったことはしていないです。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
resource "aws_launch_template" "eks_node" { count = local.on_eks ? 1 : 0 name_prefix = format("%s-%s-eks-nodes", local.env, local.service_name) image_id = local.eks_config["node_image_id"] key_name = data.terraform_remote_state.system.outputs.aws_key_pair_admin_key_name user_data = base64encode(element(data.template_file.eks_node_userdata.*.rendered, 0)) network_interfaces { associate_public_ip_address = true security_groups = [aws_security_group.base_ssh[0].id, aws_security_group.eks_node[0].id] } iam_instance_profile { name = data.terraform_remote_state.common.outputs.aws_iam_instance_profile_eks_node_id } monitoring { enabled = true } lifecycle { create_before_destroy = true } } |
Autoscaling Group
こちらも大したことはしていないです。新しい dynamic を使ってコードをスッキリさせたくらいでしょうか。あとはオンデマンド/スポットを扱えるようにしています。また、CloudWatchによる増減機能は使用しません。
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 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 |
resource "aws_autoscaling_group" "eks_scaler" { count = local.on_eks ? 1 : 0 name = format("%s-%s-eks-scaler-node", local.env, local.service_name) target_group_arns = [aws_alb_target_group.eks-web[0].arn] vpc_zone_identifier = slice(aws_subnet.public.*.id, 0, local.eks_config["az_num"]) health_check_grace_period = 300 health_check_type = "EC2" enabled_metrics = local.autoscaling_metrics termination_policies = ["OldestInstance"] max_size = local.eks_config["scaler_max_size"] min_size = local.eks_config["scaler_min_size"] desired_capacity = local.eks_config["scaler_min_size"] mixed_instances_policy { launch_template { launch_template_specification { launch_template_id = aws_launch_template.eks_node[0].id version = "$Latest" } dynamic "override" { for_each = local.eks_config["instance_types"] content { instance_type = override.value } } } instances_distribution { on_demand_base_capacity = local.eks_config["scaler_on_demand_base_capacity"] on_demand_percentage_above_base_capacity = local.eks_config["scaler_on_demand_percentage_above_base_capacity"] spot_instance_pools = length(local.eks_config["instance_types"]) } } lifecycle { ignore_changes = [ max_size, min_size, desired_capacity, mixed_instances_policy[0].instances_distribution[0].on_demand_base_capacity, ] } dynamic "tag" { for_each = local.eks_asg_scaler_tags content { key = tag.key value = tag.value propagate_at_launch = true } } } |
で、これと全く同じ内容で master 用のグループをもう1つ作っています。コードとしては scaler を master に置換しただけのような内容で、count でまとめることもできましたが、明確に役割で分けたほうが扱いやすそうだったので、分けてやりました。
scaler / master の構想については前の記事を参照です。
Notifications
Group に Notification をつけておきます。Group と同じく master にもつけていますが省略します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
locals { eks_autoscaling_notifications = [ "autoscaling:EC2_INSTANCE_LAUNCH_ERROR", "autoscaling:EC2_INSTANCE_TERMINATE_ERROR", ] } resource "aws_autoscaling_notification" "eks_scaler" { count = local.on_eks ? 1 : 0 group_names = [aws_autoscaling_group.eks_scaler[0].name] notifications = local.eks_autoscaling_notifications topic_arn = local.mail-arns["mail-warning"] } |
UserData スクリプトと解説
LaunchTempalate で指定した UserData のスクリプトと、何をやっているかの解説をします。まず根本的な、何故 UserData を利用するかですが、そもそも公式のEKS Node用AMIを利用すると、UserData で /etc/eks/bootstrap.sh を実行するように定められています。実行することで、Kubernetes の Node として登録する、という重要な処理を完了させるからです。この辺のベースとなる内容は AWS EKS Introduction | Terraform – HashiCorp Learn をみるとよいでしょう。
基本的に、公式AMIはそのまま使いたいという方針でいます。そのため、Node の OS に手を加えたいことがあれば、この UserData のタイミングで行うことがベターになります。ですが、よほど大きな変更を入れたければ、手を加えた状態で手元のアカウントにAMIを保存して使うというのもアリでしょう。
スクリプト
今のところ、こんな感じです。
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 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 |
#!/bin/bash set -o xtrace METADATA_URL="http://169.254.169.254/latest/meta-data" INSTANCE_ID=$(curl -s $METADATA_URL/instance-id/) REGION=$(curl -s $METADATA_URL/placement/availability-zone | sed -e 's/.$//') LOCAL_ADDR=$(curl -s $METADATA_URL/local-ipv4) ROLE_VALUE="None" # # Hostname # FILTERS="Name=resource-id,Values=$INSTANCE_ID" QUERY='Tags[?Key==`${hostname_prefix_key}`].[Value]' TAG_VALUE=$(aws ec2 describe-tags --filters "$FILTERS" --output text --query "$QUERY" --region "$REGION") if [ ! -z "$TAG_VALUE" -a "$TAG_VALUE" != " " ]; then HOSTNAME_SUFFIX=$(echo -n $LOCAL_ADDR | sed -e 's/\./x/g') HOSTNAME="$TAG_VALUE-$HOSTNAME_SUFFIX" aws ec2 create-tags --resources $INSTANCE_ID --tags Key=Name,Value=$HOSTNAME --region "$REGION" ## Don't change local hostname. Because metrics-server can not get metrics. # hostnamectl set-hostname $HOSTNAME # systemctl restart rsyslog fi # # sysctl # %{ for k,v in node_sysctl ~} sysctl -w "${k}=${v}" echo "${k} = ${v}" >> /etc/sysctl.d/eks-node.conf %{ endfor ~} # # Kubernetes # FILTERS="Name=resource-id,Values=$INSTANCE_ID" QUERY='Tags[?Key==`${role_label_key}`].[Value]' TAG_VALUE=$(aws ec2 describe-tags --filters "$FILTERS" --output text --query "$QUERY" --region "$REGION") if [ ! -z "$TAG_VALUE" -a "$TAG_VALUE" != " " ]; then ROLE_VALUE=$TAG_VALUE fi KUBELET_EXTRA_ARGS="--node-labels=${role_label_key}=$ROLE_VALUE" if [ "$ROLE_VALUE" != "None" ]; then TAINTS="${taints_key_prefix}/$ROLE_VALUE=:NoSchedule" TAINTS+=",CriticalAddonsOnly=:NoSchedule" KUBELET_EXTRA_ARGS+=" --register-with-taints=$TAINTS" fi /etc/eks/bootstrap.sh \ --apiserver-endpoint '${eks_endpoint}' \ --kubelet-extra-args "$KUBELET_EXTRA_ARGS" \ --b64-cluster-ca '${eks_certificate_authority}' \ '${cluster_name}' |
Hostname
EC2でホスト名を扱うのは通常、Nameタグと、OS内の hostname になります。Nameタグは、AutoscalingGroup につけたタグから propagate_at_launch = true することでEC2に伝播する方法がありますが、それだと固定値になってしまいます。クラスタの機能を狭い目で見れば機能的にはそれでも動きますが、EC2一覧でユニークな情報がないとミスが起きたり無駄な判断時間を要するので、Name = ユニーク値 をつけるのがベターです。
で、ここでは Group名 + IPアドレスのドットを x に置換した値を hostname として create-tags しています。この x置換手法は昔に自分で編み出したのですが、長く色々みてると、わりと同じことしているシステムやプラットフォームを見かけたので、それなりに一般的な手法っぽいです。
もう1つ、OS内の hostname ですが、これはあえて処理をコメントアウトしてわかるとおり、変更してしまうとEKS内の情報と食い違うことになり、メトリック取得に失敗したり不都合が出るのでやめています。SSHログインしての調査時のことを考えると変更したいのですが、仕方ありません。いまのところオプションでの辻褄合わせなどできなさそうですが、こちらは変更しないほうが無難なように判断したので深追いはしません。
sysctl
Node にも Pod にも sysctl 設定はあるのですが、一部設定は Node でしか変更できないものがあります。例えば、Pod にて fs.inotify.max_user_watches が不足するとスクリプトとしては、Terraform のEKS設定としてsysctl のKeyValueハッシュを置いといて、templatefile から渡して、設定変更&設定ファイル記述 をループさせています。追記してUserDataを更新すれば、次のNodeから反映されるので、このくらいだとAMI更新より良い方法だと思えますね。
Kubernetes –kubelet-extra-args
EKS に登録するための /etc/eks/bootstrap.sh の基本的使い方では、–kubelet-extra-args オプションは出てこないと思いますが、ここでは2つの項目を扱うためにオプション行を入れています。1つは、–node-labels=kubernetes.io/role=master (or scaler) を指定するためです。Kubernetesのラベルについては別記事で触れますが、Nodeに任意のラベルKeyValue を入れるのはこのタイミングしかありません。
もう1つは、–register-with-taints=node-role.kubernetes.io/master=:NoSchedule,CriticalAddonsOnly=:NoSchedule を指定するためで、Taints機能も同じく別記事&このタイミングです。
FQDNっぽいキー名は、EKS内で固定なのですが、一応Terraform設定値に書いて、templatefile から渡すように書いています。
ぶっちゃけ、このあたりはクソ複雑なので、いきなりこれらを扱おうとしても無意味どころか害になるので、最初は入れないでください。こういう方法があるんだ程度に済ませておいて、後で取りにくる感じがよいと思います。
下記訂正!とタグについて追記します。
まず、この時点ではまだNodeの状態がReadyにならないので、Podは起動できません。次の記事の作業が終われば、Node が使える状態になります。
AutoscalingGroupのタグとNode追加の関係
大事なことを忘れていたので追記します。AutoscalingGroup の Terraformコードで、dynamic “tag” { とカッコつけて簡略化したタグ付け部分がありますが、この中にEKS Nodeとして仲間入りするための大切なタグがあります。
”kubernetes.io/cluster/${local.eks_cluster_name}” = “owned”
という内容です。このタグが存在することで、EKS が EC2 一覧からクラスタへ利用するノードである、ということを認識します。locals 変数の部分を抜き出してくると、こんな感じのコードになっています。
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 31 32 |
locals { eks_tag_key = "kubernetes.io/cluster/${local.eks_cluster_name}" eks_asg_common_tags = { "service" = local.service_name "env" = local.env "${local.eks_tag_key}" = "owned" } eks_asg_auto_discovery_tags = { "k8s.io/cluster-autoscaler/enabled" = "true", "k8s.io/cluster-autoscaler/${local.eks_cluster_name}" = "true", } eks_asg_scaler_tags = merge( local.eks_asg_common_tags, local.eks_asg_auto_discovery_tags, { "Name" = "${local.env}-${local.service_name}-eks-scaler-node", "${local.eks_role_label_key}" = "scaler", "inspector" = "False" } ) eks_asg_master_tags = merge( local.eks_asg_common_tags, { "Name" = "${local.env}-${local.service_name}-eks-master-node", "${local.eks_role_label_key}" = "master", "inspector" = "True" } ) } |
まだ話に出していない cluster-autoscaler 用のタグもありますが、ご愛嬌で。scaler / master の役割のために、細分化して merge していますが、結果的に dynamic が使えるようになったおかげで、綺麗に書けた、つもりでいます。
次からようやくKubernetes に踏み込むので、ちっとは楽しくなっていきます。
先は長い・・・!