排查 GKE 中的服务账号问题


本页面介绍了如何排查 Google Kubernetes Engine (GKE) 服务账号的问题。

向节点服务账号授予 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 Service Agent 角色 (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 Service Agent 角色 (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:缺少账号的修改权限

后续步骤