排查 GKE 中的服务账号问题

Google Kubernetes Engine (GKE) 服务账号的权限配置错误或缺失可能会导致各种问题,例如节点无法注册或工作负载无法访问 Google Cloud 服务。

使用本文档可缓解因服务账号配置错误、停用或删除而导致的问题。

对于负责为 GKE 节点和核心 GKE 组件配置和管理项目级 IAM 权限的平台管理员、运维人员和安全工程师,此信息非常重要。如需详细了解我们在 Google Cloud 内容中提及的常见角色和示例任务,请参阅常见的 GKE 用户角色和任务

向节点服务账号授予 GKE 所需的角色

对于使用 Kubernetes 1.33 版或更低版本的 GKE 集群,GKE 节点使用的 IAM 服务账号必须具有 Kubernetes Engine Default Node Service Account (roles/container.defaultNodeServiceAccount) IAM 角色包含的所有权限。如果 GKE 节点服务账号缺少这些权限中的一项或多项,GKE 将无法执行以下系统任务:

节点服务账号可能因以下原因而缺少某些必需的权限:

如果您的节点服务账号缺少 GKE 所需的权限,您可能会看到类似于以下内容的错误和通知:

  • 在 Google Cloud 控制台的 Kubernetes 集群页面上,对于特定集群,通知列中显示 授予关键权限错误消息。
  • 在 Google Cloud 控制台中,特定集群的集群详情页面上显示以下错误消息:

    Grant roles/container.defaultNodeServiceAccount role to Node service account to allow for non-degraded operations.
    
  • 在 Cloud Audit Logs 中,如果节点服务账号缺少访问 monitoring.googleapis.com 等 Google Cloud API 的相应权限,则这些 API 的管理员活动日志会具有以下值:

    • 严重程度:ERROR
    • 消息:Permission denied (or the resource may not exist)
  • Cloud Logging 中缺少特定节点的日志,并且这些节点上 Logging 代理的 Pod 日志显示 401 错误。如需获取这些 Pod 日志,请运行以下命令:

    [[ $(kubectl logs -l k8s-app=fluentbit-gke -n kube-system -c fluentbit-gke | grep -cw "Received 401") -gt 0 ]] && echo "true" || echo "false"
    

    如果输出为 true,则表示系统工作负载发生 401 错误,这表明缺少权限。

如需解决此问题,请向导致错误的服务账号授予项目的 Kubernetes Engine Default Node Service Account (roles/container.defaultNodeServiceAccount) 角色。从下列选项中选择一项:

控制台

如需查找节点使用的服务账号的名称,请执行以下操作:

  1. 前往 Kubernetes 集群页面:

    转到 Kubernetes 集群

  2. 在集群列表中,点击您要检查的集群的名称。

  3. 查找节点服务账号的名称。您稍后需要使用此名称。

    • 对于 Autopilot 模式集群,在安全部分中,找到服务账号字段。
    • 对于 Standard 模式集群,执行以下操作:
    1. 点击节点标签页。
    2. 节点池表格中,点击节点池名称。此时会打开节点池详情页面。
    3. 安全部分中,找到服务账号字段。

    如果服务账号字段中的值为 default,则表示节点使用 Compute Engine 默认服务账号。如果此字段中的值不是 default,则表示节点使用自定义服务账号。

如需向服务账号授予 Kubernetes Engine Default Node Service Account 角色,请执行以下操作:

  1. 前往欢迎页面:

    前往“欢迎”页面

  2. 项目编号字段中,点击 复制到剪贴板

  3. 转到 IAM 页面:

    转到 IAM

  4. 点击 授予访问权限

  5. 新的主账号字段中,指定节点服务账号的名称。如果您的节点使用默认 Compute Engine 服务账号,请指定以下值:

    PROJECT_NUMBER-compute@developer.gserviceaccount.com
    

    PROJECT_NUMBER 替换为您复制的项目编号。

  6. 选择角色菜单中,选择 Kubernetes Engine Default Node Service Account 角色。

  7. 点击保存

如需验证是否已授予角色,请执行以下操作:

  1. IAM 页面中,点击按角色查看标签页。
  2. 展开 Kubernetes Engine Default Node Service Account 部分。此时会显示具有此角色的主账号的列表。
  3. 在主账号列表中找到您的节点服务账号。

gcloud

  1. 找到节点使用的服务账号的名称:

    • 对于 Autopilot 模式集群,请运行以下命令:
    gcloud container clusters describe CLUSTER_NAME \
        --location=LOCATION \
        --flatten=autoscaling.autoprovisioningNodePoolDefaults.serviceAccount
    
    • 对于 Standard 模式集群,请运行以下命令:
    gcloud container clusters describe CLUSTER_NAME \
        --location=LOCATION \
        --format="table(nodePools.name,nodePools.config.serviceAccount)"
    

    如果输出为 default,则表示节点使用 Compute Engine 默认服务账号。如果输出不是 default,则表示节点使用自定义服务账号。

  2. 找到您的 Google Cloud 项目编号:

    gcloud projects describe PROJECT_ID \
        --format="value(projectNumber)"
    

    PROJECT_ID 替换为您的项目 ID。

    输出类似于以下内容:

    12345678901
    
  3. 向服务账号授予 roles/container.defaultNodeServiceAccount 角色:

    gcloud projects add-iam-policy-binding PROJECT_ID \
        --member="SERVICE_ACCOUNT_NAME" \
        --role="roles/container.defaultNodeServiceAccount"
    

    SERVICE_ACCOUNT_NAME 替换为您在上一步中找到的服务账号的名称。如果节点使用 Compute Engine 默认服务账号,请指定以下值:

    serviceAccount:PROJECT_NUMBER-compute@developer.gserviceaccount.com
    

    PROJECT_NUMBER 替换为上一步中的项目编号。

  4. 验证是否已成功授予角色:

    gcloud projects get-iam-policy PROJECT_ID \
        --flatten="bindings[].members" --filter=bindings.role:roles/container.defaultNodeServiceAccount \
        --format='value(bindings.members)'
    

    输出是服务账号的名称。

识别节点服务账号缺少权限的集群

使用属于 NODE_SA_MISSING_PERMISSIONS Recommender 子类型的 GKE 建议可识别节点服务账号缺少权限的 Autopilot 集群和 Standard 集群。Recommender 仅识别 2024 年 1 月 1 日或之后创建的集群。如需使用 Recommender 查找并修复缺失的权限,请执行以下操作:

  1. 在项目中查找针对 NODE_SA_MISSING_PERMISSIONS Recommender 子类型的有效建议:

    gcloud recommender recommendations list \
        --recommender=google.container.DiagnosisRecommender \
        --location LOCATION \
        --project PROJECT_ID \
        --format yaml \
        --filter="recommenderSubtype:NODE_SA_MISSING_PERMISSIONS"
    

    替换以下内容:

    • LOCATION:查找建议的位置。
    • PROJECT_ID:您的 Google Cloud 项目 ID。

    输出会类似于以下内容,这表明某个集群的节点服务账号缺少权限:

    associatedInsights:
    # lines omitted for clarity
    recommenderSubtype: NODE_SA_MISSING_PERMISSIONS
    stateInfo:
      state: ACTIVE
    targetResources:
    - //container.googleapis.com/projects/12345678901/locations/us-central1/clusters/cluster-1
    

    建议最长可能需要 24 小时才会显示。如需了解详细说明,请参阅查看分析洞见和建议

  2. 对于上一步输出中的每个集群,找到关联的节点服务账号,并向这些服务账号授予所需角色。如需了解详情,请参阅向节点服务账号授予 GKE 所需的角色部分中的说明。

    向已识别的节点服务账号授予所需角色后,除非您手动忽略建议,否则建议可能会持续显示长达 24 小时。

识别所有缺少权限的节点服务账号

您可以运行一个脚本,在项目 Standard 集群和 Autopilot 集群的节点池中搜索任何不具备 GKE 所需权限的节点服务账号。此脚本使用 gcloud CLI 和 jq 实用程序。如需查看脚本,请展开以下部分:

查看脚本

#!/bin/bash

# Set your project ID
project_id=PROJECT_ID
project_number=$(gcloud projects describe "$project_id" --format="value(projectNumber)")
declare -a all_service_accounts
declare -a sa_missing_permissions

# Function to check if a service account has a specific permission
# $1: project_id
# $2: service_account
# $3: permission
service_account_has_permission() {
  local project_id="$1"
  local service_account="$2"
  local permission="$3"

  local roles=$(gcloud projects get-iam-policy "$project_id" \
          --flatten="bindings[].members" \
          --format="table[no-heading](bindings.role)" \
          --filter="bindings.members:\"$service_account\"")

  for role in $roles; do
    if role_has_permission "$role" "$permission"; then
      echo "Yes" # Has permission
      return
    fi
  done

  echo "No" # Does not have permission
}

# Function to check if a role has the specific permission
# $1: role
# $2: permission
role_has_permission() {
  local role="$1"
  local permission="$2"
  gcloud iam roles describe "$role" --format="json" | \
  jq -r ".includedPermissions" | \
  grep -q "$permission"
}

# Function to add $1 into the service account array all_service_accounts
# $1: service account
add_service_account() {
  local service_account="$1"
  all_service_accounts+=( ${service_account} )
}

# Function to add service accounts into the global array all_service_accounts for a Standard GKE cluster
# $1: project_id
# $2: location
# $3: cluster_name
add_service_accounts_for_standard() {
  local project_id="$1"
  local cluster_location="$2"
  local cluster_name="$3"

  while read nodepool; do
    nodepool_name=$(echo "$nodepool" | awk '{print $1}')
    if [[ "$nodepool_name" == "" ]]; then
      # skip the empty line which is from running `gcloud container node-pools list` in GCP console
      continue
    fi
    while read nodepool_details; do
      service_account=$(echo "$nodepool_details" | awk '{print $1}')

      if [[ "$service_account" == "default" ]]; then
        service_account="${project_number}-compute@developer.gserviceaccount.com"
      fi
      if [[ -n "$service_account" ]]; then
        printf "%-60s| %-40s| %-40s| %-10s| %-20s\n" $service_account $project_id  $cluster_name $cluster_location $nodepool_name
        add_service_account "${service_account}"
      else
        echo "cannot find service account for node pool $project_id\t$cluster_name\t$cluster_location\t$nodepool_details"
      fi
    done <<< "$(gcloud container node-pools describe "$nodepool_name" --cluster "$cluster_name" --zone "$cluster_location" --project "$project_id" --format="table[no-heading](config.serviceAccount)")"
  done <<< "$(gcloud container node-pools list --cluster "$cluster_name" --zone "$cluster_location" --project "$project_id" --format="table[no-heading](name)")"

}

# Function to add service accounts into the global array all_service_accounts for an Autopilot GKE cluster
# Autopilot cluster only has one node service account.
# $1: project_id
# $2: location
# $3: cluster_name
add_service_account_for_autopilot(){
  local project_id="$1"
  local cluster_location="$2"
  local cluster_name="$3"

  while read service_account; do
      if [[ "$service_account" == "default" ]]; then
        service_account="${project_number}-compute@developer.gserviceaccount.com"
      fi
      if [[ -n "$service_account" ]]; then
        printf "%-60s| %-40s| %-40s| %-10s| %-20s\n" $service_account $project_id  $cluster_name $cluster_location $nodepool_name
        add_service_account "${service_account}"
      else
        echo "cannot find service account" for cluster  "$project_id\t$cluster_name\t$cluster_location\t"
      fi
  done <<< "$(gcloud container clusters describe "$cluster_name" --location "$cluster_location" --project "$project_id" --format="table[no-heading](autoscaling.autoprovisioningNodePoolDefaults.serviceAccount)")"
}


# Function to check whether the cluster is an Autopilot cluster or not
# $1: project_id
# $2: location
# $3: cluster_name
is_autopilot_cluster() {
  local project_id="$1"
  local cluster_location="$2"
  local cluster_name="$3"
  autopilot=$(gcloud container clusters describe "$cluster_name" --location "$cluster_location" --format="table[no-heading](autopilot.enabled)")
  echo "$autopilot"
}


echo "--- 1. List all service accounts in all GKE node pools"
printf "%-60s| %-40s| %-40s| %-10s| %-20s\n" "service_account" "project_id" "cluster_name" "cluster_location" "nodepool_name"
while read cluster; do
  cluster_name=$(echo "$cluster" | awk '{print $1}')
  cluster_location=$(echo "$cluster" | awk '{print $2}')
  # how to find a cluster is a Standard cluster or an Autopilot cluster
  autopilot=$(is_autopilot_cluster "$project_id" "$cluster_location" "$cluster_name")
  if [[ "$autopilot" == "True" ]]; then
    add_service_account_for_autopilot "$project_id" "$cluster_location"  "$cluster_name"
  else
    add_service_accounts_for_standard "$project_id" "$cluster_location"  "$cluster_name"
  fi
done <<< "$(gcloud container clusters list --project "$project_id" --format="value(name,location)")"

echo "--- 2. Check if service accounts have permissions"
unique_service_accounts=($(echo "${all_service_accounts[@]}" | tr ' ' '\n' | sort -u | tr '\n' ' '))

echo "Service accounts: ${unique_service_accounts[@]}"
printf "%-60s| %-40s| %-40s| %-20s\n" "service_account" "has_logging_permission" "has_monitoring_permission" "has_performance_hpa_metric_write_permission"
for sa in "${unique_service_accounts[@]}"; do
  logging_permission=$(service_account_has_permission "$project_id" "$sa" "logging.logEntries.create")
  time_series_create_permission=$(service_account_has_permission "$project_id" "$sa" "monitoring.timeSeries.create")
  metric_descriptors_create_permission=$(service_account_has_permission "$project_id" "$sa" "monitoring.metricDescriptors.create")
  if [[ "$time_series_create_permission" == "No" || "$metric_descriptors_create_permission" == "No" ]]; then
    monitoring_permission="No"
  else
    monitoring_permission="Yes"
  fi
  performance_hpa_metric_write_permission=$(service_account_has_permission "$project_id" "$sa" "autoscaling.sites.writeMetrics")
  printf "%-60s| %-40s| %-40s| %-20s\n" $sa $logging_permission $monitoring_permission $performance_hpa_metric_write_permission

  if [[ "$logging_permission" == "No" || "$monitoring_permission" == "No" || "$performance_hpa_metric_write_permission" == "No" ]]; then
    sa_missing_permissions+=( ${sa} )
  fi
done

echo "--- 3. List all service accounts that don't have the above permissions"
if [[ "${#sa_missing_permissions[@]}" -gt 0 ]]; then
  printf "Grant roles/container.defaultNodeServiceAccount to the following service accounts: %s\n" "${sa_missing_permissions[@]}"
else
  echo "All service accounts have the above permissions"
fi

此脚本适用于项目中的所有 GKE 集群。

识别缺少权限的服务账号的名称后,向这些账号授予所需的角色。如需了解详情,请参阅向节点服务账号授予 GKE 所需的角色部分中的说明。

将默认服务账号恢复到您的 Google Cloud 项目

GKE 的默认服务账号 container-engine-robot 可能会意外地从项目中解除绑定。Kubernetes Engine 服务代理角色 (roles/container.serviceAgent) 是一种 Identity and Access Management (IAM) 角色,它授予服务账号管理集群资源的权限。如果从服务账号中移除此角色绑定,则默认服务账号将从项目中解除绑定,这可能会阻止您部署应用和执行其他集群操作。

如需查看服务账号是否已从您的项目中移除,您可以使用 Google Cloud 控制台或 Google Cloud CLI。

控制台

gcloud

  • 运行以下命令:

    gcloud projects get-iam-policy PROJECT_ID
    

    PROJECT_ID 替换为您的项目 ID。

如果信息中心或该命令未针对服务账号显示 container-engine-robot,则表示角色已解除绑定。

如需恢复 Kubernetes Engine 服务代理角色 (roles/container.serviceAgent) 绑定,请运行以下命令:

PROJECT_NUMBER=$(gcloud projects describe "PROJECT_ID" \
    --format 'get(projectNumber)') \
gcloud projects add-iam-policy-binding PROJECT_ID \
    --member "serviceAccount:service-${PROJECT_NUMBER}@container-engine-robot.iam.gserviceaccount.com" \
    --role roles/container.serviceAgent

确认角色绑定已恢复:

gcloud projects get-iam-policy PROJECT_ID

如果您看到服务账号名称以及 container.serviceAgent 角色,则表示角色绑定已恢复。例如:

- members:
  - serviceAccount:service-1234567890@container-engine-robot.iam.gserviceaccount.com
  role: roles/container.serviceAgent

启用 Compute Engine 默认服务账号

用于节点池的服务账号通常是 Compute Engine 默认服务账号。如果此默认服务账号已停用,节点可能无法向集群注册。

如需查看服务账号是否已在您的项目中停用,您可以使用Google Cloud 控制台或 gcloud CLI。

控制台

gcloud

  • 运行以下命令:
gcloud iam service-accounts list  --filter="NAME~'compute' AND disabled=true"

如果服务账号已停用,请运行以下命令以启用服务账号:

  1. 找到您的 Google Cloud 项目编号:

    gcloud projects describe PROJECT_ID \
        --format="value(projectNumber)"
    

    PROJECT_ID 替换为您的项目 ID。

    输出类似于以下内容:

    12345678901
    
  2. 启用服务账号:

    gcloud iam service-accounts enable PROJECT_NUMBER-compute@developer.gserviceaccount.com
    

    PROJECT_NUMBER 替换为之前步骤的输出中的项目编号。

如需了解详情,请参阅排查节点注册问题

错误 400/403:缺少账号的修改权限

如果服务账号已删除,您可能会看到缺少修改权限的错误。如需了解如何排查此错误,请参阅错误 400/403:缺少账号的修改权限

后续步骤