Gunosy Tech Blog

Gunosy Tech Blogは株式会社Gunosyのエンジニアが知見を共有する技術ブログです。

EKS Managed Node Groupでカーネルパラメータを変更する

21卒広告技術部のyamaYuです。 マトリックスの新作が楽しみです。 過去作を見返さなくてはと思いつつこの記事を書いています。 個人的には第一作が一番好きです。

さて、こちらの記事はGunosy Advent Calendar 2021の15日目の記事になっています。 昨日は村田さんの『AdKDD & KDD 2021 に参加しました』という記事でした。

今回は、EKS Managed Node Groupでカーネルパラメータを変更する必要があったのですが、一筋縄ではいかなかったのでその話を書こうと思います。

背景: net.core.somaxconnを増やしたい

ここ最近の担当業務として、GunosyAds管理画面のOpsWorksからEKS*1への移行を進めています。 旧環境ではアプリケーションサーバーはNginx+Unicorn+Railsという構成になっています。 これをEKSに乗せるためにDockerコンテナ化し、NginxコンテナとRailsコンテナのSidecarパターンでPodを構成しました。 しかしながら、実際に動かしてみるとCPU等のリソースは十分にあるにもかかわらず、しばしば502が発生しており、Nginxのログには下記のエラーが出力されていました。

connect() to unix:/usr/src/app/shared/sockets/unicorn.sock failed (11: Resource temporarily unavailable) while connecting to upstream

調べてみたところ、net.core.somaxconnというカーネルパラメータがあり、これがTCPソケットが受け付けた接続要求を格納するキューの最大長を定義しているのですが、 主要なDockerイメージではこの値はデフォルトで128になっており、それを超えるリクエストについてはキューから溢れてしまっていることが原因でした。 因みにこの設定値は下記コマンドで確認できます。

$ cat /proc/sys/net/core/somaxconn
128

設定値を変更するためには下記のsysctlの実行が必要になりますが、Dockerコンテナではデフォルトで許可されていません。

$ sysctl -w net.core.somaxconn=1024
sysctl: setting key "net.core.somaxconn": Read-only file system

Kubernetesでsysctlを実行する

Dockerであればdocker runコマンドの--sysctlオプションを指定すれば変更可能です。 Kubernetesの場合にどうすれば良いかは下記ドキュメントに書かれています。

sysctlはsafeとunsafeの2つに分類され、Kubernetesで定められたいくつかのsysctlだけが前者に属します。 今回設定したいnet.core.somaxconnを含め、それ以外の多くのsysctlは後者に属し、実行するには次に示す手順が必要になります。

  1. kubeletコマンドのオプション--allowed-unsafe-sysctlsで実行したいunsafe sysctlを許可
  2. unsafe sysctlを許可したPod Security Policy*2を作成
  3. 上記Pod Security Policyを設定したCluster Roleを作成
  4. 上記Cluster RoleをbindしたService Accountを作成
  5. 上記Service AccountをPodに設定
  6. PodのsecurityContextでsysctlと設定値を指定

1.のkubeletの設定については後ほど触れます。 一旦ここではその後の部分の設定例を載せます。 広告技術部ではCluster RoleなどのKubernetesのリソースはTerraformで管理しているためTerraformでの設定例になります。

# Pod Security Policy
resource "kubernetes_pod_security_policy" "allow_unsafe_sysctls" {
  metadata {
    name = "allow-unsafe-sysctls"
  }

  spec {
    privileged                 = false
    allow_privilege_escalation = false
    volumes                    = ["*"]

    fs_group {
      rule = "RunAsAny"
    }

    run_as_user {
      rule = "RunAsAny"
    }

    se_linux {
      rule = "RunAsAny"
    }

    supplemental_groups {
      rule = "RunAsAny"
    }

    allowed_unsafe_sysctls = ["net.core.somaxconn"]
  }
}

# Cluster Role
resource "kubernetes_cluster_role" "example" {
  metadata {
    name = "example"
  }

  rule {
    verbs          = ["use"]
    api_groups     = ["policy"]
    resources      = ["podsecuritypolicies"]
    resource_names = ["allow-unsafe-sysctls"]
  }
}

# Cluster Role Binding
resource "kubernetes_cluster_role_binding" "example" {
  metadata {
    name = "example"
  }

  subject {
    kind      = "ServiceAccount"
    name      = kubernetes_service_account.example.metadata[0].name
    namespace = kubernetes_service_account.example.metadata[0].namespace
  }

  role_ref {
    api_group = "rbac.authorization.k8s.io"
    kind      = "ClusterRole"
    name      = kubernetes_cluster_role.example.metadata[0].name
  }
}

# Service Account
resource "kubernetes_service_account" "example" {
  metadata {
    name      = "example"
    namespace = "example"
  }
  automount_service_account_token = false
}

EKS Managed node groupでkubeletの設定を変更する

先程後回しにしたkubeletの設定についてです。 EKS Managed Node Groupで設定する場合について書きます。

EKS Managed Node Groupはワーカーノードのプロビジョニングや管理をしてくれる便利なしくみで、Launch templateやカスタムAMIを用いることで細かい設定もできるようになっています。 但しこれには少しクセがあり、Managed Node GroupのLaunch templateでUser dataを設定する場合、multipart形式で指定しなければならず、開発者が作成したUser dataとEKSが生成したUser dataがマージされるようになっています。

そのあたりの話は以下の記事でも紹介されています。 tech.gunosy.io

今回やりたいkubeletの設定は/etc/eks/bootstrap.sh--kubelet-extra-argsに記述しなければならない*3のですが、この部分はEKSが生成する側のUser dataに含まれるためそのままでは設定できません。 そこでドキュメントに従いカスタムAMIを指定します。 カスタムAMIといってもUser dataのみの変更なので実際に使うAMIは同じもので問題ないです。 これでマージしない通常のUser dataを記述できるようになります。 但し、本来Managed node groupで自動でやってくれていた部分を開発者が記述しないといけないので必須の設定*4を漏らさないように注意しないといけません。 以下Terraformでの設定例です。

locals {
  eks_k8s_version ="your_eks_k8s_version"
  userdata = <<-USERDATA
    #!/bin/bash
    ︙
    B64_CLUSTER_CA="your_b64_cluster_ca"
    API_SERVER_URL="your_api_server_url"
    K8S_CLUSTER_DNS_IP="your_k8s_cluster_dns_ip"
    /etc/eks/bootstrap.sh example-cluster \
      --b64-cluster-ca $B64_CLUSTER_CA \
      --apiserver-endpoint $API_SERVER_URL \
      --dns-cluster-ip $K8S_CLUSTER_DNS_IP \
      --kubelet-extra-args '--allowed-unsafe-sysctls=net.core.somaxconn' \
  USERDATA
}

# EKS Optimized AMI
data "aws_ssm_parameter" "eks_ami" {
  name = "/aws/service/eks/optimized-ami/${local.eks_k8s_version}/amazon-linux-2/recommended/image_id"
}

# Launch template
resource "aws_launch_template" "example" {
  image_id    = data.aws_ssm_parameter.eks_ami.value
  ︙
  user_data   = base64encode(local.eks_ads_admin_autoscale_managed_userdata)
}

# EKS node group
resource "aws_eks_node_group" "example" {
  ami_type       = "CUSTOM"
  ︙
  launch_template {
    id      = aws_launch_template.example.id
    version = aws_launch_template.example.latest_version
  }
}

おわりに 🐾

これで無事net.core.somaxconnを変更でき、また502エラーを解消することができました。 他のカーネルパラメータを変更する際にも同様の手順を踏むことになると思いますので参考になれば幸いです。

*1:執筆時点でKubernetes 1.18を使用しています。

*2:Kubernetes 1.21時点でdeprecatedになっています。代替についてはこちらに書かれています。

*3:kubeletの設定はUser dataの他の箇所に記述せず、bootstrap.shの引数として与えることがこちらで推奨されています。

*4:必須の設定はこちらに書かれています。