iT邦幫忙

2025 iThome 鐵人賽

DAY 11
1
Cloud Native

駕馭商用容器叢集,汪洋漂流術系列 第 11

【Day 11】 實戰篇 - 憑證快要過期了 (中) / 記錄 Pod Mount

  • 分享至 

  • xImage
  •  

不重要的前言

  • 接續前一回,預計之後會把有用到的東西的參考連結補上。

前言

前一回,在理解了 OpenShift Router 的轉導方式後,

調查手法

  1. 找出叢集內的所有使用中的路由
    https://ithelp.ithome.com.tw/upload/images/20250828/20130149VmNdw4KDLL.png
  2. 找出這些路由,理解 Termination 後,區別加解密發生的地點,才能知道憑證被套在哪些地方
  3. 在得知上面四種模式後,還需要知道憑證可能被附掛的形式
    • 關注下列資源
      • Secret
      • ConfigMap
      • 容器 Hard-Coding (用這種做法除了管理是個麻煩問題之外,也可能外洩)
  4. 從 Service / Namespace 取得 Pod 的 Label Selector
    • 因為並沒有直接「從 Service 找 Pods」的指令,所以要拆分步驟
      oc get service <服務名稱> -n <命名空間名稱> -o yaml
      
    • 看上述指令的 selector 區塊,例如...
      app.kubernetes.io/instance: mgmt
      app.kubernetes.io/managed-by: aaaaa-bbbcccc
      app.kubernetes.io/name: cvbnm
      
    • 因為這個服務中可能有一個以上的 Pod(s),所以還要先抓出 Pods
      oc get pods -n apiconnect --selector app.kubernetes.io/instance=mgmt,app.kubernetes.io/managed-by=aaaaa-bbbcccc,app.kubernetes.io/name=cvbnm
      
      https://ithelp.ithome.com.tw/upload/images/20250828/20130149YAPJ5XIDRl.png
  5. 以上三個 Pod 分別用 Describe 的方式查看 Pod 怎麼被叫起來的,到底是棒棒,壞壞,還是頑劣份子⋯⋯
    • 先做其中第一個...
      oc describe pod mgmt-wwwwwwwwwww -n apiconnect
      
    • 可以發現這個 Pod 用掛載(Mount)的方式,把 SecretConfigMap 置入!
      https://ithelp.ithome.com.tw/upload/images/20250828/201301499n7Lb37iTO.png
    • 從上圖可以知道,這一系列的流程,可以快速地抓出,整個叢集裡,所有命名空間中的,各項服務內的 Pods 分別掛載了哪些 Secret 和 ConfigMap 唷!
  6. 如果採用 工人智慧 逐項撈取,那肯定沒日沒夜,所以你需要的是一個 script 去幫你進行這堆流程。

Script / 善用 AI 幫你做苦工

  • 這個年代用點 LLM 幫你修改生成 script 在所難免,別當義和團用肉身對抗 LLM 洋槍洋砲。
  • 以前在手工寫 Bash 我都會挑上午剛上班的時候,沒人找的時候,寫到中午,或是下班後回家夜深人靜的時候產出。 耗費時間通常都是用幾個小時起跳。 現在有了好用的工具,清楚地告訴 AI 你要怎麼搞。
  • 生成出來的腳本,建議不要直接呼叫,至少要先看過一輪,檢查,檢查很重要,一定要檢查!! 大概花十幾分鐘就行了。
  • 確定沒問題(避免可能造成資源異動或毀壞的操作)後,再跑。

Source

#!/usr/bin/env bash
set -euo pipefail

# Usage/help
usage() {
  cat <<'EOF'
Usage:
  ocp-pod-inventory.sh [--namespace <ns1,ns2,...>]

Description:
  Inventory all Pods and show:
    - Top-level owner (Deployment/StatefulSet/DaemonSet/CronJob/Job/ReplicaSet/Pod)
    - Services that select the Pod (selector ⊆ labels)
    - "oc describe pod $pod_name -n $ns 2>/dev/null"
  Output: pods_inventory.tsv (TSV table)

Requirements:
  - oc (logged in), jq
EOF
}

# Parse args (simple long option parser)
NS_FILTER=""
while [ $# -gt 0 ]; do
  case "$1" in
    --namespace|-n)
      [ $# -lt 2 ] && { echo "ERROR: --namespace requires value"; usage; exit 1; }
      NS_FILTER="$2"
      shift 2
      ;;
    -h|--help)
      usage; exit 0
      ;;
    *)
      echo "Unknown option: $1"; usage; exit 1
      ;;
  esac
done

OUT_FILE="pods_inventory.tsv"
echo -e "Namespace\tPod\tPhase\tNode\tOwnerKind\tOwnerName\tServices\tMounts" > "$OUT_FILE"

# Resolve top-level owner (chase ownerReferences)
resolve_owner() {
  local ns="$1" kind="$2" name="$3"
  local safety=0
  while [ -n "${kind:-}" ] && [ -n "${name:-}" ] && [ $safety -lt 6 ]; do
    safety=$((safety+1))
    case "$kind" in
      Deployment|StatefulSet|DaemonSet|CronJob)
        echo -e "${kind}\t${name}"
        return 0
        ;;
    esac
    local json
    if ! json="$(oc get "$kind" "$name" -n "$ns" -o json 2>/dev/null)"; then
      echo -e "${kind}\t${name}"
      return 0
    fi
    local next_kind next_name
    next_kind="$(echo "$json" | jq -r '.metadata.ownerReferences[0].kind // empty')"
    next_name="$(echo "$json" | jq -r '.metadata.ownerReferences[0].name // empty')"
    if [ -z "$next_kind" ] || [ -z "$next_name" ]; then
      echo -e "${kind}\t${name}"
      return 0
    fi
    kind="$next_kind"
    name="$next_name"
  done
  echo -e "${kind:-Pod}\t${name:-<unknown>}"
}

# Which services select this pod (selector ⊆ pod.labels)
services_selecting_pod() {
  local ns="$1" pod_labels_json="$2"
  local svc_cache_file="/tmp/_svc_${ns}.json"
  if [ ! -f "$svc_cache_file" ]; then
    oc get svc -n "$ns" -o json > "$svc_cache_file" 2>/dev/null || echo '{"items":[]}' > "$svc_cache_file"
  fi
  # Load services and compare selectors
  jq -r --argjson labels "$pod_labels_json" '
    .items[]
    | {name: .metadata.name, selector: (.spec.selector // {})}
    | select((.selector | length) > 0)
    | select(
        (.selector | to_entries) as $req
        | all($req[]; ($labels[.key] // null) == .value)
      )
    | .name
  ' "$svc_cache_file"
}

# Build namespace list
NAMESPACES=""
if [ -n "$NS_FILTER" ]; then
  # Split comma-separated into lines
  NAMESPACES="$(echo "$NS_FILTER" | tr ',' '\n' | sed '/^$/d')"
else
  NAMESPACES="$(oc get ns -o jsonpath='{range .items[*]}{.metadata.name}{"\n"}{end}')"
fi

# Iterate namespaces
echo "$NAMESPACES" | while read -r ns; do
  [ -z "$ns" ] && continue

  pods_json="$(oc get pods -n "$ns" -o json 2>/dev/null || echo '{"items":[]}')"

  # Loop through pod names
  echo "$pods_json" | jq -r '.items[].metadata.name' | while read -r pod; do
    [ -z "$pod" ] && continue

    pod_item="$(echo "$pods_json" | jq --arg name "$pod" -c '.items[] | select(.metadata.name == $name)')"
    [ -z "$pod_item" ] && continue

    phase="$(echo "$pod_item" | jq -r '.status.phase // ""')"
    node="$(echo "$pod_item" | jq -r '.spec.nodeName // ""')"
    labels_json="$(echo "$pod_item" | jq -c '.metadata.labels // {}')"

    owner_kind="$(echo "$pod_item" | jq -r '.metadata.ownerReferences[0].kind // ""')"
    owner_name="$(echo "$pod_item" | jq -r '.metadata.ownerReferences[0].name // ""')"
    if [ -z "$owner_kind" ] || [ -z "$owner_name" ]; then
      top_kind="Pod"
      top_name="$pod"
    else
      read -r top_kind top_name <<EOF
$(resolve_owner "$ns" "$owner_kind" "$owner_name")
EOF
    fi

    # Services selecting this pod (comma joined)
    svc_joined="$(services_selecting_pod "$ns" "$labels_json" | paste -sd, -)"
    # Mounts: robust extraction (handles indentation and multiple containers)
    mounts_raw="$(
      oc describe pod "$pod" -n "$ns" 2>/dev/null \
      | awk 'BEGIN{f=0} /^[[:space:]]*Mounts:/{f=1; next} f && NF==0{f=0} f{print}'
    )"
    # Normalize whitespace and squash to one line
    mounts_one_line="$(echo "$mounts_raw" \
      | sed 's/^[[:space:]]\+//' \
      | tr '\t' ' ' \
      | tr '\n' ' ' \
      | sed 's/  \+/ /g; s/^ *//; s/ *$//')"

    printf "%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\n" \
      "$ns" "$pod" "$phase" "$node" "$top_kind" "$top_name" "${svc_joined}" "${mounts_one_line}" >> "$OUT_FILE"
  done
done

echo "Done. Output -> $OUT_FILE"

分段解說

前面

#!/usr/bin/env bash
set -euo pipefail

# Usage/help
usage() {
  cat <<'EOF'
Usage:
  ocp-pod-inventory.sh [--namespace <ns1,ns2,...>]

Description:
  Inventory all Pods and show:
    - Top-level owner (Deployment/StatefulSet/DaemonSet/CronJob/Job/ReplicaSet/Pod)
    - Services that select the Pod (selector ⊆ labels)
    - "oc describe pod $pod_name -n $ns 2>/dev/null"
  Output: pods_inventory.tsv (TSV table)

Requirements:
  - oc (logged in), jq
EOF
}
  • 在上面腳本 1 行,這邊只是告訴你的作業系統,若不指定的話預設用哪個 shell 來執行。
  • 2 行的設定,避免你腳本中,存在了一些回傳值不為零的命令,引發整串腳本結束時噴錯。 這個設定下去後,只需關注最後一個指令正不正常結束即可以他為主。

    the return value of a pipeline is the status of the last command to exit with a non-zero status, or zero if no command exited with a non-zero status.

  • 在上面腳本 4 ~ 20 行,用來當作提示文字,吐給呼叫者,可能是未來某天你要呼叫這腳本做事,但是忘記當初在裡面埋了什麼,就備註在這吧!

處理參數

# Parse args (simple long option parser)
NS_FILTER=""
while [ $# -gt 0 ]; do
  case "$1" in
    --namespace|-n)
      [ $# -lt 2 ] && { echo "ERROR: --namespace requires value"; usage; exit 1; }
      NS_FILTER="$2"
      shift 2
      ;;
    -h|--help)
      usage; exit 0
      ;;
    *)
      echo "Unknown option: $1"; usage; exit 1
      ;;
  esac
done
  • 這邊 $# 表示前景一片看好,意思是要去抓取命令列、接在這串後面的參數數量,greater than 0 的話才執行迴圈內的東西。
  • switch case 用來定義 sub-command

模仿人力調查的操作

# Resolve top-level owner (chase ownerReferences)
resolve_owner() {
  local ns="$1" kind="$2" name="$3"
  local safety=0
  while [ -n "${kind:-}" ] && [ -n "${name:-}" ] && [ $safety -lt 6 ]; do
    safety=$((safety+1))
    case "$kind" in
      Deployment|StatefulSet|DaemonSet|CronJob)
        echo -e "${kind}\t${name}"
        return 0
        ;;
    esac
    local json
    if ! json="$(oc get "$kind" "$name" -n "$ns" -o json 2>/dev/null)"; then
      echo -e "${kind}\t${name}"
      return 0
    fi
    local next_kind next_name
    next_kind="$(echo "$json" | jq -r '.metadata.ownerReferences[0].kind // empty')"
    next_name="$(echo "$json" | jq -r '.metadata.ownerReferences[0].name // empty')"
    if [ -z "$next_kind" ] || [ -z "$next_name" ]; then
      echo -e "${kind}\t${name}"
      return 0
    fi
    kind="$next_kind"
    name="$next_name"
  done
  echo -e "${kind:-Pod}\t${name:-<unknown>}"
}
  • 這邊提到一個觀念 「owner kind」。 簡單地說就是,由誰、生出來的資源。
  • 這一段先寫一個 function 去拆解內容。 一邊看內容一邊注意是否符合他的函式名稱,真的在解析 owner 才給過。
  • 通常使用 kubectl get <resource> <name> -o jsonpath="{.metadata.ownerReferences[*].kind}"

提取 Selector / Label

# Which services select this pod (selector ⊆ pod.labels)
services_selecting_pod() {
  local ns="$1" pod_labels_json="$2"
  local svc_cache_file="/tmp/_svc_${ns}.json"
  if [ ! -f "$svc_cache_file" ]; then
    oc get svc -n "$ns" -o json > "$svc_cache_file" 2>/dev/null || echo '{"items":[]}' > "$svc_cache_file"
  fi
  # Load services and compare selectors
  jq -r --argjson labels "$pod_labels_json" '
    .items[]
    | {name: .metadata.name, selector: (.spec.selector // {})}
    | select((.selector | length) > 0)
    | select(
        (.selector | to_entries) as $req
        | all($req[]; ($labels[.key] // null) == .value)
      )
    | .name
  ' "$svc_cache_file"
}
  • 這段也是一個 function,透過 service 去找 pod。

尋找開始

# Build namespace list
NAMESPACES=""
if [ -n "$NS_FILTER" ]; then
  # Split comma-separated into lines
  NAMESPACES="$(echo "$NS_FILTER" | tr ',' '\n' | sed '/^$/d')"
else
  NAMESPACES="$(oc get ns -o jsonpath='{range .items[*]}{.metadata.name}{"\n"}{end}')"
fi

# Iterate namespaces
echo "$NAMESPACES" | while read -r ns; do
  [ -z "$ns" ] && continue

  pods_json="$(oc get pods -n "$ns" -o json 2>/dev/null || echo '{"items":[]}')"

  # Loop through pod names
  echo "$pods_json" | jq -r '.items[].metadata.name' | while read -r pod; do
    [ -z "$pod" ] && continue

    pod_item="$(echo "$pods_json" | jq --arg name "$pod" -c '.items[] | select(.metadata.name == $name)')"
    [ -z "$pod_item" ] && continue

    phase="$(echo "$pod_item" | jq -r '.status.phase // ""')"
    node="$(echo "$pod_item" | jq -r '.spec.nodeName // ""')"
    labels_json="$(echo "$pod_item" | jq -c '.metadata.labels // {}')"

    owner_kind="$(echo "$pod_item" | jq -r '.metadata.ownerReferences[0].kind // ""')"
    owner_name="$(echo "$pod_item" | jq -r '.metadata.ownerReferences[0].name // ""')"
    if [ -z "$owner_kind" ] || [ -z "$owner_name" ]; then
      top_kind="Pod"
      top_name="$pod"
    else
      read -r top_kind top_name <<EOF
$(resolve_owner "$ns" "$owner_kind" "$owner_name")
EOF
    fi

    # Services selecting this pod (comma joined)
    svc_joined="$(services_selecting_pod "$ns" "$labels_json" | paste -sd, -)"
    # Mounts: robust extraction (handles indentation and multiple containers)
    mounts_raw="$(
      oc describe pod "$pod" -n "$ns" 2>/dev/null \
      | awk 'BEGIN{f=0} /^[[:space:]]*Mounts:/{f=1; next} f && NF==0{f=0} f{print}'
    )"
    # Normalize whitespace and squash to one line
    mounts_one_line="$(echo "$mounts_raw" \
      | sed 's/^[[:space:]]\+//' \
      | tr '\t' ' ' \
      | tr '\n' ' ' \
      | sed 's/  \+/ /g; s/^ *//; s/ *$//')"

    printf "%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\n" \
      "$ns" "$pod" "$phase" "$node" "$top_kind" "$top_name" "${svc_joined}" "${mounts_one_line}" >> "$OUT_FILE"
  done
done

echo "Done. Output -> $OUT_FILE"
  • 列出所有命名空間
  • 依序勘查所有 pod
  • 找出所有 pod 的各項資訊
    • Namespace
    • Pod
    • Phase
    • Node
    • OwnerKind
    • OwnerName
    • Services
    • Mounts

結論

TODO

  • 還沒盤點憑證
  • 找出所有憑證的有效期限

小結

  • 這時只能算整理完所有的 Pod 所在的位置,並且知道 Pod 怎麼被生出來的,還有、他有沒有掛載 Secret / ConfigMap

參考資料

  1. 鳥哥私房菜 / 第十二章、學習 Shell Scripts
    連結: https://linux.vbird.org/linux_basic/centos7/0340bashshell-scripts.php
  2. GitHub / kodekloudhub/community-faq
    連結: https://github.com/kodekloudhub/community-faq/blob/main/docs/jsonpath.md

上一篇
【Day 10】 實戰篇 - 憑證快要過期了 (上) / 認識憑證 / OCP 轉送機制
下一篇
【Day 12】 實戰篇 - 憑證快要過期了 (下) / 挖出 TLS / CA
系列文
駕馭商用容器叢集,汪洋漂流術14
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言