:從YAML編寫到部署排查全解析)
做運維和開發(fā)這些年Kubernetes以下簡稱 k8s已經(jīng)成了一個繞不開的話題。接觸 k8s 的人大多會有同樣的感受命令記了一堆文檔翻了不少但真正動手時還是發(fā)懵。尤其是剛?cè)腴T時看到滿屏的 YAML 文件心里多少會犯嘀咕——這些東西到底是什么、每個字段有什么用、為什么寫完之后集群沒反應(yīng)。我自己就是從這個階段過來的踩過的坑比寫過的配置文件還多。今天這篇內(nèi)容就把“k8s 資源文件”這件事從頭到尾捋一遍用一個最小可用的實踐路徑講清楚它是怎么組織、怎么寫、怎么用的以及我實際操作中遇到過的問題和排查思路。這篇文章適合正在學(xué) k8s 基礎(chǔ)、準備把服務(wù)往集群里部署的開發(fā)者也適合那些能用 kubectl 跑命令但對 YAML 文件還停留在“能復(fù)制、不能改”階段的運維同學(xué)。讀完你至少能獨立寫出一份簡單的 DeploymentService 資源配置并且知道報錯之后應(yīng)該從哪里入手排查。我不會一股腦堆術(shù)語盡量用實際場景把概念講透因為光看懂概念離能上手干活還差得很遠差的那部分就是今天要補齊的。1. 資源文件到底是什么為什么要從它入手1.1 資源文件與 kubectl 命令行的關(guān)系很多人入門時的第一個動作是敲 kubectl run、kubectl create這沒有錯但只靠命令行的方式管理集群很快會碰壁。舉個例子你手動創(chuàng)建了一個 Pod運行得好好的但是節(jié)點一重啟這個 Pod 可能就沒了因為沒有任何機制替你把它拉起來。如果你想要“服務(wù)掛了自動恢復(fù)”的效果就得寫一個 Deployment 資源讓控制器替你維護副本數(shù)和生命周期。這時候你會發(fā)現(xiàn)命令行雖然能創(chuàng)建資源卻很難完整表達這些復(fù)雜的聲明意圖。資源文件也叫資源清單、Manifest本質(zhì)上是一份用 YAML 或 JSON 格式寫成的聲明式配置。它的核心思想是“告訴集群我想要的最終狀態(tài)是什么”而不是“告訴集群每一步去做什么”。這兩者的差別是理解 k8s 一切實踐的分水嶺。kubectl apply 是資源文件最常用的交互入口我會在后面實際操作部分詳細介紹它的用法。1.2 聲明式 API 的核心邏輯聲明式 API 可以這樣理解你去餐廳吃飯不是告訴廚師“先切菜、再熱鍋、然后放油”而是直接說“我要一盤番茄炒蛋”。之后怎么切、用什么火候、放多少鹽由廚房自己決定廚師只保證端上來的菜符合你的要求。k8s 里也是這樣你寫好一份 YAML聲明“我要 3 個副本、鏡像版本是 v1.2.0、容器端口是 8080”剩下的調(diào)度、拉起、健康檢查、滾動更新這些動作全部由集群內(nèi)部的各種控制器完成。這種模式的好處非常明顯文件即配置、配置即代碼。同一份資源文件放到測試環(huán)境和生產(chǎn)環(huán)境只要鏡像地址、副本數(shù)這些參數(shù)改一改跑出來的行為基本一致。版本管理、代碼評審、回滾這些成熟的開發(fā)流程也都可以直接套用到集群配置上。從學(xué)習(xí)和工程化兩個角度看資源文件都是 k8s 入門階段必須先啃下的一塊硬骨頭。2. 資源文件的基礎(chǔ)結(jié)構(gòu)與必備字段2.1 apiVersion、kind、metadata、spec 四件套隨便打開一份 k8s 資源文件大概率會看到四個頂層字段apiVersion、kind、metadata、spec。這四個字段是所有資源文件的骨架明白了它們剩下的細節(jié)都是在往里填肉。apiVersion 表示資源所屬的 API 版本。不同版本的 k8s 對同一資源可能提供不同的 API 組比如 Deployment 常見的寫法是 apps/v1Pod 是 v1Job 是 batch/v1。版本選錯了kubectl 會直接報錯告訴你找不到對應(yīng)資源。kind 表示資源類型比如 Pod、Deployment、Service、ConfigMap、Secret。這兩個字段決定了“這是一個什么對象、走哪個 API 路徑去處理它”。metadata 是元數(shù)據(jù)最重要的兩個子字段是 name 和 labels。name 是資源在命名空間內(nèi)的唯一標識labels 則是給資源打標簽用來做篩選和關(guān)聯(lián)。spec 是具體期望狀態(tài)的描述不同類型的資源spec 長得完全不一樣這也是資源文件里內(nèi)容最豐富、最容易寫錯的部分。2.2 命名規(guī)范與 YAML 語法要點寫資源文件還要注意 YAML 本身的語法。YAML 對縮進敏感一個空格錯了文件可能根本解析不了。不同字段之間層級關(guān)系必須清楚同級的 key 縮進要一致??s進推薦用兩個空格不要用 Tab。字段名區(qū)分大小寫不能隨意改變大小寫形式。注釋用 #用于說明某些關(guān)鍵字段的含義方便團隊協(xié)作。name 字段在 k8s 里有自己的命名規(guī)則一般要求小寫字母、數(shù)字、中劃線或點并且在創(chuàng)建后不可變。metadata.name 一旦寫入資源文件再 apply改名字實際上會創(chuàng)建一個新資源而不是原地修改。這個坑我踩過。給 labels 命名時也盡量帶上應(yīng)用名和層級信息比如 app: nginx、tier: frontend這樣后面查日志、做網(wǎng)絡(luò)策略、寫 Service selector 都會省很多事。3. 最常用的五類資源文件示例與解析3.1 Pod 資源文件一切資源的最小運行單元Pod 是 k8s 里最小的調(diào)度單位也是容器的運行載體。一個 Pod 里可以有一個或多個容器這些容器共享網(wǎng)絡(luò)棧和存儲卷。學(xué)習(xí)資源文件從 Pod 開始是最直觀的。apiVersion: v1 kind: Pod metadata: name: nginx-pod labels: app: nginx spec: containers: - name: nginx image: nginx:1.25 ports: - containerPort: 80這份文件創(chuàng)建后集群會調(diào)度一個 Pod里面跑一個來自 nginx:1.25 鏡像的容器端口 80 映射到容器內(nèi)。需要提醒的是只寫一個裸 Pod 意味著它沒有自愈能力節(jié)點宕機、容器異常退出Pod 不會自動重新創(chuàng)建。這也是為什么實際工作中很少直接創(chuàng)建裸 Pod而是用 Deployment 這類控制器來管理 Pod 的生命周期。3.2 Deployment 資源文件工業(yè)級副本管理Deployment 是日常部署中最常用的資源類型。它負責(zé)管理 ReplicaSet再由 ReplicaSet 保證指定數(shù)量的 Pod 副本存在。這部分內(nèi)容適合放一個帶副本數(shù)的 Deployment 示例來做詳細說明。apiVersion: apps/v1 kind: Deployment metadata: name: nginx-deployment labels: app: nginx spec: replicas: 3 selector: matchLabels: app: nginx template: metadata: labels: app: nginx spec: containers: - name: nginx image: nginx:1.25 ports: - containerPort: 80這里值得講清楚的是 template 與 selector 的關(guān)系。selector 告訴 Deployment 哪些 Pod 歸它管而 template 是創(chuàng)建 Pod 的模板。template 的 labels 必須包含 selector.matchLabels 中的標簽否則會創(chuàng)建失敗或者出現(xiàn)“selector does not match template labels”之類的報錯。簡而言之副本要幾個鏡像是什么版本更新策略是什么都寫在 spec 里。創(chuàng)建之后如果只想改鏡像版本可以直接修改 spec.template.spec.containers[0].image再由控制器完成滾動更新。3.3 Service 資源文件穩(wěn)定的訪問入口Deployment 保證 Pod 的數(shù)量和健康但 Pod 的 IP 是漂移的每次重建都可能變化。想要給外部或集群內(nèi)其他服務(wù)提供一個固定的入口就要引入 Service。apiVersion: v1 kind: Service metadata: name: nginx-service spec: selector: app: nginx ports: - port: 80 targetPort: 80 type: ClusterIPService 通過 selector 里的標簽關(guān)聯(lián)到對應(yīng)的 Pod當(dāng) Pod 的 IP 發(fā)生變化時Service 會自動更新它維護的 Endpoints 列表。port 是 Service 對外暴露的端口targetPort 是容器內(nèi)應(yīng)用監(jiān)聽的端口兩者可以不同。type 有 ClusterIP、NodePort、LoadBalancer 等幾種ClusterIP 是默認值只在集群內(nèi)部可達想從集群外部訪問可以根據(jù)環(huán)境換成 NodePort 或 LoadBalancer。3.4 ConfigMap 與 Secret配置與敏感信息的解耦實際部署中鏡像往往是同一個但不同環(huán)境的參數(shù)不一樣。把所有配置硬編碼進鏡像會導(dǎo)致環(huán)境之間互相污染每次改配置都要重新構(gòu)建鏡像。ConfigMap 和 Secret 就是為了解決這個問題存在的。ConfigMap 適合放非敏感的配置內(nèi)容比如日志級別、開關(guān)項、外部服務(wù)地址。Secret 適合放需要加密存儲的敏感信息比如數(shù)據(jù)庫密碼、訪問密鑰。這兩類資源都可以通過環(huán)境變量或掛載文件的方式注入到 Pod 中。ConfigMap 示例apiVersion: v1 kind: ConfigMap metadata: name: app-config data: LOG_LEVEL: info APP_MODE: prod在 Deployment 中引用方式env: - name: LOG_LEVEL valueFrom: configMapKeyRef: name: app-config key: LOG_LEVELSecret 的寫法和 ConfigMap 非常像只是 data 里的值要求 base64 編碼。要注意的是Secret 只是對內(nèi)容做了編碼并不是加密生產(chǎn)環(huán)境還需要配合加密存儲方案和嚴格的權(quán)限控制。不要真的把生產(chǎn)密碼以明文 base64 形式寫進倉庫然后以為沒人看得到。3.5 多資源組合的完整示例一個完整的應(yīng)用通常由 Deployment 和 Service 配合組成。放在同一個 YAML 文件中可以用 --- 分隔多個資源文檔也可以用 kubectl apply 指定多個文件來一次性生效。apiVersion: apps/v1 kind: Deployment metadata: name: demo-web labels: app: demo-web spec: replicas: 2 selector: matchLabels: app: demo-web template: metadata: labels: app: demo-web spec: containers: - name: demo-web image: nginx:1.25 ports: - containerPort: 80 --- apiVersion: v1 kind: Service metadata: name: demo-web-svc spec: selector: app: demo-web ports: - port: 80 targetPort: 80這樣一個組合文件已經(jīng)是一個能跑通的最小應(yīng)用了。后續(xù)加配置掛載、加健康檢查、加資源限制都可以在這個基礎(chǔ)上逐步擴展。組合文件的好處是復(fù)現(xiàn)方便一個文件代表一個完整應(yīng)用的聲明團隊之間交流起來效率很高。4. 從零到一一個資源文件創(chuàng)建應(yīng)用的完整流程4.1 編寫文件與檢查語法選一個最小的場景來演示。假設(shè)我要在集群里部署一個 nginx 服務(wù)叫 nginx-demo副本 2 個通過 NodePort 暴露端口方便臨時訪問。第一步自然是寫文件我一般會先檢查語法能不能被正確解析。kubectl 提供了 dry-run 和 validate 參數(shù)來幫忙做靜態(tài)檢查。kubectl create --dry-runclient -f nginx-demo.yaml -o yaml這個命令會裝作創(chuàng)建資源但實際不發(fā)送到集群只做客戶端側(cè)解析。輸出如果包含完整 YAML 內(nèi)容說明文件基本沒問題。如果想做服務(wù)端校驗就把 dry-run 換成 server或者用 kubectl apply --dry-runserver。這一步能提前暴露出一批 apiVersion、字段拼寫等問題省得真正 apply 之后反復(fù)刪改。4.2 應(yīng)用資源與驗證狀態(tài)文件檢查無誤后執(zhí)行kubectl apply -f nginx-demo.yaml正常情況下會輸出 deployment.apps/nginx-demo created 和 service/nginx-demo-svc created 之類的結(jié)果。但這還不算完資源創(chuàng)建成功不等于應(yīng)用就起來了。緊接著要查看實際狀態(tài)kubectl get pods -l appdemo-web kubectl get svc這里看的是 Pod 是否 Running 且 ReadyService 是否有可用的 Endpoints。我習(xí)慣用 kubectl get pod -o wide 看一下 Pod 被調(diào)度到了哪個節(jié)點、IP 是什么。如果 Pod 一直處于 Pending、ContainerCreating、CrashLoopBackOff就需要進入排查環(huán)節(jié)。對于 NodePort 類型的 Service集群節(jié)點上會開一個 30000-32767 范圍內(nèi)的隨機端口通過任意節(jié)點的這個端口就能訪問到應(yīng)用。訪問不通時優(yōu)先檢查安全組和防火墻是否放行了這個端口這類問題常發(fā)生在云環(huán)境。4.3 更新與回滾應(yīng)用運行了一段時間后要升級鏡像版本比如從 nginx:1.25 升級到 nginx:1.26。改文件里的 image 字段然后重新 applykubectl apply -f nginx-demo.yamlDeployment 會觸發(fā)滾動更新默認策略是“先啟動新 Pod等它 Ready 后再銷毀舊 Pod”這個過程從外部看服務(wù)是不中斷的。如果想指定某一次更新也可以用 kubectl set image 命令直接改。升級出了問題怎么辦Deployment 自帶回滾機制。查歷史版本kubectl rollout history deployment/nginx-demo回滾到指定版本kubectl rollout undo deployment/nginx-demo --to-revision2回滾本身也是提交一次新的更新它會將資源狀態(tài)恢復(fù)到指定歷史版本所對應(yīng)的描述。這個機制對日常上線非常有用但前提是每次變更都用 kubectl apply 更新資源文件而不是靠手工 edit 改了一堆過后又忘了改了什么。5. 常見問題與排查技巧實錄5.1 鏡像拉取失敗Pod 狀態(tài)停留在 ImagePullBackOff 或 ErrImagePull 時第一個動作是查看 Pod 詳情kubectl describe pod pod-nameEvents 里通常會直接給出失敗原因。鏡像問題常見的有三類鏡像名或標簽寫錯、鏡像不存在、私有倉庫需要認證。前面兩項改寫正確 image 即可私有倉庫需要在資源文件里配置 imagePullSecrets。這件事在沒有配置好之前describe 的 Events 里會給出類似 unauthorized 或 401 的錯誤提示指向非常明顯。5.2 資源一直 Pending 或 CrashLoopBackOffPending 說明調(diào)度沒完成常見原因是節(jié)點資源不足、存在沒有匹配的污點、或者 PVC 沒有綁定。describe 同樣會給出一部分線索比如 0/3 nodes are available。這時候可以看節(jié)點資源使用情況kubectl top nodes。缺資源就擴容或清理污點問題則要重新審視調(diào)度策略。CrashLoopBackOff 表示容器啟動后反復(fù)崩潰退出。先看日志kubectl logs pod-name --previous--previous 很關(guān)鍵因為當(dāng)前容器可能已經(jīng)重啟過之前的日志里往往藏著真正的原因。配置錯誤、啟動命令失敗、依賴的數(shù)據(jù)庫或接口連不上是幾類主要誘因。如果你在 YAML 里配置了 livenessProbe 和 readinessProbe也要考慮探針是否太嚴格、啟動時間是否不夠?qū)е鲁绦蛎髅髌饋砹藚s被 kubelet 判定失敗后殺掉。5.3 服務(wù)訪問不通Service 建好了但應(yīng)用訪問不到需要逐層排查。先確認后端 Pod 有沒有就緒kubectl get endpoints svc-name如果 Endpoints 列表為空說明 selector 的標簽沒匹配到任何 Pod。這是最常見的問題Deployment 的 template 里寫的標簽是 app: nginxService 的 selector 不小心寫成了 name: nginx匹配失敗。同樣的端口對不對也要驗證targetPort 畢竟指的是容器內(nèi)端口不是 Service 端口。用一個臨時 Pod 進去 curl 一下能把問題隔離得更干凈。集群內(nèi)流量走通后再測試 NodePort 或 LoadBalancer 的對外鏈路。NodePort 的訪問方式通常是節(jié)點IP:NodePort不是 Pod IP:Port。我記得第一次調(diào)試時一直在宿主機上 curl 容器 IP 的端口當(dāng)然通不了因為那只是集群內(nèi)部網(wǎng)絡(luò)。逐個鏈路驗證才能快速定位問題出在 Pod、Service 還是節(jié)點路由層面。5.4 檢查項速查表把日常排查步驟整理成一張表適合遇到問題時按圖索驥現(xiàn)象第一步做什么重點關(guān)注資源 apply 報錯kubectl apply --dry-runserverapiVersion、字段拼寫Pod Pendingkubectl describe pod節(jié)點資源、污點、PVCPod CrashLoopBackOffkubectl logs --previous啟動命令、配置依賴鏡像拉取失敗kubectl describe podimage 名稱、tag、私有倉庫認證Service 不通kubectl get endpointsselector 匹配、targetPort外部訪問不通檢查 NodePort/防火墻/安全組網(wǎng)絡(luò)策略、端口放行這張表不是標準答案而是我自己的習(xí)慣性排查路徑。實際過程中kubectl describe 和 kubectl logs 兩個命令能解決八成問題關(guān)鍵是順著狀態(tài)變化一步步縮小范圍。6. 幾個實操經(jīng)驗與避坑補充6.1 資源文件一定要納入版本管理我見過不少同學(xué)手工 kubectl edit 在線改資源集群里改得花團錦簇倉庫里的 YAML 還是三個月前的版本。等哪天集群要重建或者需要快速擴容手里沒有可信的配置就只能對著終端一遍遍 describe 反推。把資源文件放在 Git 倉庫里每次變更走提交評審集群狀態(tài)與倉庫內(nèi)容保持一致這是我從混亂走向可控的第一步。工程實踐上也可以加一套簡單的自動化檢查提交時用 kubeconform 或者 kubectl dry-run 校驗格式減少低級錯誤進入集群的機會。6.2 命名空間資源要寫清楚資源文件里沒有聲明 namespace 時kubectl apply 默認會應(yīng)用到當(dāng)前 context 的 namespace很容易出現(xiàn)“以為自己部署到 test 環(huán)境實際發(fā)到了 default”的烏龍。建議在 metadata 里顯式加 namespace 字段或者用 kubectl apply -n 指定別依賴默認值。尤其在多環(huán)境共用一個集群的情況下該問題相當(dāng)常見且代價不小。6.3 給容器設(shè)置資源請求與限制資源文件里不寫 requests 和 limits容器在節(jié)點上就可能“吃滿”所有可用資源影響其他 Pod 運行。生產(chǎn)環(huán)境如果不加以約束很容易出現(xiàn)故障雪崩。給容器加資源限制是每個資源文件都值得做的動作resources: requests: cpu: 100m memory: 128Mi limits: cpu: 200m memory: 256Mirequests 是調(diào)度時的資源預(yù)留值limits 是運行時可用的上限。cpu 的 100m 表示 0.1 個核心memory 單位可以直接用 Mi、Gi。這類值不要拍腦袋先用壓測拿到應(yīng)用的資源畫像再留出緩沖。限制設(shè)得過小容器會被頻繁殺掉這是很多 CrashLoopBackOff 的隱藏原因。6.4 善用 kubectl explain 查字段寫 YAML 時總會有字段記不準的情況。與其去搜索引擎翻舊文檔不如直接使用 kubectl explainkubectl explain deployment.spec這個命令會展示該字段的類型、說明、是否必填而且版本一定和當(dāng)前集群匹配。我寫文件遇到復(fù)雜的嵌套字段時幾乎都會敲一遍它。這個習(xí)慣會極大減少“照著網(wǎng)上的寫法套用到舊版本”的痛苦。6.5 不要把 Secret 內(nèi)容寫進鏡像倉庫ConfigMap 和 Secret 的設(shè)計初衷是讓配置與鏡像解耦。有人為了省事把數(shù)據(jù)庫連接串直接放進 Dockerfile 的 ENV或者把 Secret 文件硬編碼進應(yīng)用鏡像這基本等于把鑰匙配在鎖旁邊。Secret 的配置引用、權(quán)限管控、輪換機制雖然需要額外學(xué)習(xí)成本但和安全事故的代價相比這筆成本非常劃算。哪怕只是做個人項目也建議從一開始就培養(yǎng)正確的配置管理習(xí)慣。7. 總結(jié)形式的個人體會就不要了說說我現(xiàn)在的習(xí)慣寫了很多最后說點實在的。我現(xiàn)在拿到一個部署需求第一反應(yīng)不是敲 kubectl run而是先想清楚它的資源模型是無狀態(tài)服務(wù)還是有狀態(tài)應(yīng)用需要暴露端口嗎配置里有沒有敏感信息然后把對應(yīng)的資源文件一次寫好apply 之后盯著狀態(tài)變化確認產(chǎn)出。這套流程看似麻煩實際上比命令行點來點去更省心因為它可復(fù)現(xiàn)、可評審、可回滾。還有一個日常小技巧文件里加注釋。YAML 里的 # 注釋寫清楚每個關(guān)鍵字段為什么這么定比如這個副本數(shù)為什么是 3、這個內(nèi)存 limit 為什么是 256Mi。過半年再回來看你會發(fā)現(xiàn)這些注釋比任何文檔都管用因為它記錄的是當(dāng)時決策的上下文。k8s 的學(xué)習(xí)曲線確實陡峭但資源文件就是那條值得先爬上去的主干道。把基礎(chǔ)資源吃透再往后接觸 Helm、Operator、服務(wù)網(wǎng)格這些概念時會發(fā)現(xiàn)它們的底層邏輯依然是“聲明期望狀態(tài)、由控制器去調(diào)和”。今天講的這些字段和排查方法夠你支撐起最早期的部署工作了。剩下的場景遇到一個學(xué)一個坑踩完了經(jīng)驗也就留下了。