排查 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 服务代理,用于管理计算资源,例如实例、磁盘和负载均衡器。

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

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

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

    • Kubernetes Engine 服务代理:
    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 控制台中,前往 Kubernetes Engine API 页面。

    前往 Kubernetes Engine API 页面

  2. 选择您的项目。

  3. 如果您看到启用按钮,请点击该按钮以启用 Kubernetes Engine API。此流程会创建所需的服务代理,并向其授予项目所需的角色。此过程可能需要几分钟才能完成。

  4. 如果您看到管理按钮,则表示 Kubernetes Engine API 已启用。点击管理按钮。

    1. 如果您可以选择停用 API,则应先停用,然后再次启用。此操作会重新创建所需的服务代理,并向其授予项目所需的角色。

    2. 如果您无法停用 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

后续步骤