排查 4xx 错误

本页面可帮助您解决使用 Google Kubernetes Engine (GKE) 时可能遇到的 400、401、403 和 404 错误。

问题:身份验证和授权错误

连接到 GKE 集群时,您可能会收到 HTTP 状态代码为 401 (Unauthorized) 的身份验证和授权错误。如果您尝试在本地环境的 GKE 集群中运行 kubectl 命令,可能会遇到此问题。

此问题可能是由以下某种原因造成的:

  • gke-gcloud-auth-plugin 身份验证插件未正确安装或配置。
  • 您无权连接到集群 API 服务器和运行 kubectl 命令。

如需诊断原因,请完成以下部分中的步骤:

  1. 使用 curl 连接到集群
  2. 在 kubeconfig 中配置插件

使用 curl 连接到集群

如需诊断身份验证和授权错误的原因,请使用 curl 连接到集群。使用 curl 可绕过 kubectl 命令行工具和 gke-gcloud-auth-plugin 插件。

  1. 设置环境变量:

    APISERVER=https://$(gcloud container clusters describe CLUSTER_NAME \
        --location=COMPUTE_LOCATION --format "value(endpoint)")
    TOKEN=$(gcloud auth print-access-token)
    
  2. 验证您的访问令牌是否有效:

    curl https://oauth2.googleapis.com/tokeninfo?access_token=$TOKEN
    

    当您拥有有效的访问令牌时,此命令会向 Google 的 OAuth 2.0 服务器发送请求,而服务器会回复令牌相关信息。

  3. 尝试连接到 API 服务器中的核心 API 端点:

    # Get cluster CA certificate
    gcloud container clusters describe CLUSTER_NAME \
        --location=COMPUTE_LOCATION \
        --format "value(masterAuth.clusterCaCertificate)" | \
        base64 -d > /tmp/ca.crt
    
    # Make API call with authentication and CA certificate
    curl -s -X GET "${APISERVER}/api/v1/namespaces" \
        --header "Authorization: Bearer $TOKEN" \
        --cacert /tmp/ca.crt
    

    如果 curl 命令成功,您将看到命名空间列表。请继续使用在 kubeconfig 中配置插件部分中的步骤检查插件是否是原因。

    如果 curl 命令失败,并且输出类似于以下内容,则表示您没有正确的权限来访问集群:

    {
    "kind": "Status",
    "apiVersion": "v1",
    "metadata": {},
    "status": "Failure",
    "message": "Unauthorized",
    "reason": "Unauthorized",
    "code": 401
    }
    

    如需解决此问题,请咨询您的管理员,以获取正确的权限来访问集群。

在 kubeconfig 中配置插件的使用

如果您在连接到集群时收到身份验证和授权错误,但能够使用 curl 连接到集群,请确保您无需 gke-gcloud-auth-plugin 插件也可访问集群。

如需解决此问题,请将您的本地环境配置为在向集群进行身份验证时忽略 gke-gcloud-auth-plugin 二进制文件。在运行 1.25 及更高版本的 Kubernetes 客户端中,gke-gcloud-auth-plugin 二进制文件是必需的,因此您需要使用 1.24 或更低版本的 kubectl 命令行工具。

请按照以下步骤操作,以便在不需要插件的情况下访问集群:

  1. 使用 curl 安装 1.24 版或更低版本的 kubectl 命令行工具。以下示例安装的是 1.24 版的工具:

    curl -LO https://dl.k8s.io/release/v1.24.0/bin/linux/amd64/kubectl
    
  2. 在文本编辑器中打开 shell 启动脚本文件。例如,针对 Bash shell,打开 .bashrc

    vi ~/.bashrc
    

    如果您使用的是 macOS,请在这些说明中使用 ~/.bash_profile 而非 .bashrc

  3. 将以下代码行添加到启动脚本文件中,然后保存:

    export USE_GKE_GCLOUD_AUTH_PLUGIN=False
    
  4. 运行启动脚本:

    source ~/.bashrc
    
  5. 获取集群的凭据,以设置 .kube/config 文件:

    gcloud container clusters get-credentials CLUSTER_NAME \
        --location=COMPUTE_LOCATION
    

    替换以下内容:

  6. 运行 kubectl 命令。例如:

    kubectl cluster-info
    

    如果您在运行这些命令后收到 401 错误或类似的授权错误,请确保您拥有正确的权限,然后重新运行返回错误的步骤。

错误 400:节点池需要重新创建

尝试执行重新创建控制平面和节点的操作时,可能会发生以下错误:

ERROR: (gcloud.container.clusters.update) ResponseError: code=400, message=Node pool "test-pool-1" requires recreation.

例如,当您完成正在进行的凭证变换时,可能会发生此错误。

在后端,节点池会标记为要进行重新创建,但实际的重新创建操作可能需要一些时间才能开始。因此,该操作会失败,因为 GKE 尚未重新创建集群中的一个或多个节点池。

如需解决此问题,请选择以下解决方案之一:

  • 等待重新创建进行。这可能需要几个小时、几天或几周的时间,具体取决于现有维护窗口和排除项等因素。
  • 通过启动与控制平面相同的版本升级,手动开始创建受影响的节点池。

    如需开始重新创建,请运行以下命令:

    gcloud container clusters upgrade CLUSTER_NAME \
        --node-pool=POOL_NAME
    

    升级完成后,请重试该操作。

错误 401:未授权

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

如需识别节点服务账号缺少关键权限的集群,请使用 NODE_SA_MISSING_PERMISSIONS Recommender 子类型GKE 建议

  • 使用 Google Cloud 控制台。 前往 Kubernetes 集群页面,然后在特定集群的通知列中查看授予关键权限建议。
  • 通过指定 NODE_SA_MISSING_PERMISSIONS Recommender 子类型来使用 gcloud CLI 或 Recommender API。

    如需查询建议,请运行以下命令:

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

请注意,建议最多可能需要 24 小时才能显示。如需查看详细说明,请参阅如何查看分析洞见和建议

如需实施此建议,请向节点的服务账号授予 roles/container.defaultNodeServiceAccount 角色。

您可以运行一个脚本,在项目 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 使用关联到节点的 IAM 服务账号来运行日志记录和监控等系统任务。这些节点服务账号必须至少拥有项目的 Kubernetes Engine Default Node Service Account (roles/container.defaultNodeServiceAccount) 角色。默认情况下,GKE 会将 Compute Engine 默认服务账号(在您的项目中自动创建)用作节点服务账号。

如果您的组织强制执行 iam.automaticIamGrantsForDefaultServiceAccounts 组织政策限制,则项目中的默认 Compute Engine 服务账号可能无法自动获得 GKE 所需的权限。

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

    控制台

    1. 前往 Kubernetes 集群页面:

      转到 Kubernetes 集群

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

    如果服务账号字段中的值为 default,则表示节点使用 Compute Engine 默认服务账号。如果此字段中的值不是 default,则表示节点使用自定义服务账号。如需向自定义服务账号授予所需角色,请参阅使用最小权限 IAM 服务账号

    gcloud

    对于 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,则表示节点使用自定义服务账号。如需向自定义服务账号授予所需角色,请参阅使用最小权限 IAM 服务账号

  2. 如需向 Compute Engine 默认服务账号授予 roles/container.defaultNodeServiceAccount 角色,请完成以下步骤:

    控制台

    1. 前往欢迎页面:

      前往“欢迎”页面

    2. 项目编号字段中,点击 复制到剪贴板
    3. 转到 IAM 页面:

      转到 IAM

    4. 点击 授予访问权限
    5. 新的主账号字段中,指定以下值:
      PROJECT_NUMBER-compute@developer.gserviceaccount.com
      PROJECT_NUMBER 替换为您复制的项目编号。
    6. 选择角色菜单中,选择 Kubernetes Engine Default Node Service Account 角色。
    7. 点击保存

    gcloud

    1. 找到您的 Google Cloud 项目编号:
      gcloud projects describe PROJECT_ID \
          --format="value(projectNumber)"

      PROJECT_ID 替换为您的项目 ID。

      输出内容类似如下:

      12345678901
      
    2. roles/container.defaultNodeServiceAccount 角色授予 Compute Engine 默认服务账号:
      gcloud projects add-iam-policy-binding PROJECT_ID \
          --member="serviceAccount:PROJECT_NUMBER-compute@developer.gserviceaccount.com" \
          --role="roles/container.defaultNodeServiceAccount"

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

错误 403:权限不足

当您尝试使用 gcloud container clusters get-credentials 连接到 GKE 集群,但该账号无权访问 Kubernetes API 服务器时,会发生以下错误:

ERROR: (gcloud.container.clusters.get-credentials) ResponseError: code=403, message=Required "container.clusters.get" permission(s) for "projects/<your-project>/locations/<region>/clusters/<your-cluster>".

如需解决此问题,请完成以下步骤:

  1. 确定存在访问权限问题的账号:

    gcloud auth list
    
  2. 按照向 Kubernetes API 服务器进行身份验证中的说明,向该账号授予所需的访问权限。

错误 403:重试预算用尽

在尝试创建 GKE 集群时,可能会出现以下错误:

Error: googleapi: Error 403: Retry budget exhausted: Google Compute Engine:
Required permission 'PERMISSION_NAME' for 'RESOURCE_NAME'.

在此错误消息中,以下变量适用:

  • PERMISSION_NAME:权限的名称,例如 compute.regions.get
  • RESOURCE_NAME:您尝试访问的 Google Cloud资源的路径,例如 Compute Engine 区域。

如果关联到集群的 IAM 服务账号没有创建集群所需的最低权限,则会发生此错误。

如需解决此问题,请执行以下操作:

  1. 创建或修改 IAM 服务账号,以拥有运行 GKE 集群所需的所有权限。如需查看相关说明,请参阅使用最小权限 IAM 服务账号
  2. 使用 --service-account 标志在集群创建命令中指定更新后的 IAM 服务账号。如需查看相关说明,请参阅创建 Autopilot 集群

或者,省略 --service-account 标志,让 GKE 使用项目中的 Compute Engine 默认服务账号,该账号默认具有所需权限。

错误 404:找不到资源

如果您在调用 gcloud container 命令时收到错误 404(找不到资源),请通过重新对 Google Cloud CLI 进行身份验证来解决此问题:

gcloud auth login

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

缺少账号的修改权限错误(错误 400 或 403)表示以下任一项已被手动删除或修改:

启用 Compute Engine 或 GKE API 时, Google Cloud会创建以下服务账号和代理:

  • 您项目中的 Compute Engine 默认服务账号。默认情况下,GKE 会将此服务账号关联到节点,以用于日志记录和监控等系统任务。
  • Google 管理的项目中具有项目修改权限的 Google API 服务代理。
  • Google 管理的项目中具有项目的 Kubernetes Engine Service Agent 和 Kubernetes Engine Default Node Service Agent 角色的 Google Kubernetes Engine 服务代理。

如果有人在任何时候修改了这些权限、移除了项目上的角色绑定、完全移除了服务账号或停用了 API,则集群创建和所有管理功能都将失败。

验证 GKE 服务代理的权限

Google Kubernetes Engine 具有以下服务代理:

  • Kubernetes Engine Service Agent,用于管理计算资源,例如实例、磁盘和负载均衡器。

  • Kubernetes Engine 默认节点服务代理,由 Google Kubernetes Engine 节点系统工作负载用于支持日志记录和监控等标准功能。

如需验证 Google Kubernetes Engine 服务代理是否已在项目中分配了 Kubernetes Engine Service Agent 角色,请完成以下步骤:

  1. 确定您的 Google Kubernetes Engine 服务代理的名称。服务代理的格式如下:

    • Kubernetes Engine Service Agent:
    service-PROJECT_NUMBER@container-engine-robot.iam.gserviceaccount.com
    
    • Kubernetes Engine Default Node Service Agent:
    service-PROJECT_NUMBER@gcp-sa-gkenode.iam.gserviceaccount.com
    

    PROJECT_NUMBER 替换为您的项目编号

  2. 验证您的 Google Kubernetes Engine 服务代理是否已在项目中分配了 Kubernetes Engine Service Agent 和 Kubernetes Engine Default Node Service Agent 角色:

    gcloud projects get-iam-policy PROJECT_ID
    

    PROJECT_ID 替换为您的项目 ID。

如需解决此问题,如果有人从您的 Google Kubernetes Engine 服务代理中移除了 Kubernetes Engine Service Agent 或 Kubernetes Engine Default Node Service Agent,请按照以下说明操作,确保 Kubernetes Engine API 已启用,并向您的服务代理授予必要的角色:

控制台

  1. 进入 Google Cloud 控制台中的 API 和服务页面。

    转到“API 和服务”

  2. 选择您的项目。

  3. 点击启用 API 和服务

  4. 搜索 Kubernetes,然后从搜索结果中选择 API。

  5. 点击启用。如果您之前已启用 API,则必须先停用,然后再次启用。API 和相关服务的启用可能需要几分钟才能完成。

gcloud

在 gcloud CLI 中运行以下命令:

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 add-iam-policy-binding PROJECT_ID \
    --member "serviceAccount:service-${PROJECT_NUMBER?}@gcp-sa-gkenode.iam.gserviceaccount.com" \
    --role roles/container.defaultNodeServiceAgent

后续步骤