做軟件的人:“工作體驗好,好事才能來。”
1 太長不讀
從2023年8月到10月,我花了3個月自學docker和k8s。踩了一路坑,到10月22日終于把一個帶有vue.js 3前端、spring boot后端以及postgres數據庫的shopping list web app,部署到azure k8s service云上,并能正常運行。
之所以說踩了一路坑,是因為網上分享的k8s部署web app的樣例,都是部署一個web服務。講ingress nginx controller的樣例雖然會涉及兩個微服務,但在這種根據path設定將請求分配給兩個hello world的web微服務場景中,兩個微服務之間,是沒有前后端之間的依賴關系的。另外前后端之間的CORS跨源資源共享該如何解決,也找不到我這種前后端分離的web app場景下的直接資料,只能自己摸索。
在爬出坑后,很愿意寫一系列避坑指南文章分享給大家。雖然不知小伙伴們是否愿意讀,但我很想把這一系列文章,寫成網上通過實例講docker和k8s入門的最好的文章,而且每年至少更新一次。
這一系列文章的目標讀者,是對docker或k8s不太熟悉的做軟件的人,不僅包括常寫代碼的程序員,也包括不常寫代碼的測試工程師和運維工程師,如圖1所示。
K8s和Docker能解決做軟件的人的什么痛點?這也是我為啥要花這么多時間寫這一系列文章的原因。因為k8s和Docker可以讓咱們做軟件的人,能體驗更好地做軟件。
“工作體驗好,好事才能來。”
想想咱們做軟件的人常說的下面幾句話。
“在我這運行得好好的,怎么你那兒不行?” docker image能將代碼的所有依賴庫都打包到一起,并能讓代碼在容器中獨立運行。這樣就能實現你在測試環境中所測試的image,就是你在生產環境所部署的,從而能在很大程度上解決因為依賴庫在不同環境下的差異,而導致這里能運行,那里不能運行的問題。
“這是誰改了配置又不告訴大家?” Docker和k8s都強調基礎設施即代碼,即配置不是靠做軟件的人拍腦袋臨時手工敲的,而是靠寫成與代碼同等地位的配置文件,通過團隊代碼評審,保存到版本庫中,并讓機器執行。這樣能讓配置的更改廣而告之,配置的執行有據可查。同時也便于讓機器讀取,自動執行,而無須手工一遍一遍敲同樣的命令。
“測試環境太少得排隊使用。” 有了本地docker compose,做軟件的人可以利用其占用存儲空間小,運行速度快的特點,在本地電腦以docker image的方式,最大限度模擬生產環境的方式,測試要發布的軟件,而無須排隊等公司共享的測試環境。這樣能更早地發現bug,減少因很晚才發現所導致的大量返工成本。同樣,在內部使用了k8s云集群的企業,也能利用云的多租戶特點,快速為需要測試環境的做軟件的人,分配測試環境,從而解決測試環境少的問題。
可見,Docker和k8s能讓咱們做軟件的人,工作體驗更好,好事才更能來。
“為何選用Shopping List Web app作為樣例項目?”這個樣例源自我在自學vue.js時所學的待辦清單todo list app樣例,如圖2所示。我把todo list改造為shopping list。兩者的功能近似,都是為用戶提供一個備忘清單,有一個web界面可以增刪改查。這容易理解。此外,這個樣例能代表前后端分離的web app典型架構。另外,這個樣例能表現最小化的云原生微服務之間的依賴關系,比如前端微服務依賴后端微服務,而后端微服務又依賴于數據庫微服務。這便于學習如何使用新興的故障注入實驗工具,進行混沌工程實踐。
“我對java和vue.js不熟,能讀懂這一系列文章嗎?”能。因為文章的代碼命名寫得足夠表意,一看就懂。另外,這一系列文章不涉及前后端具體的編程,而重點關注如何把開發好的代碼用docker打成image,并部署到本地docker compose和k8s云集群上。這些都與前后端所使用的編程語言關系不大,所以文章內容適用于所有使用JSON/HTTP協議的前后端分離的web app的技術棧。
“我不會編程,能讀懂這一系列文章嗎?”能。因為文章不涉及前后端功能的代碼編寫,而主要涉及配置文件和命令行工具的使用,適合程序員、測試工程師和運維工程師閱讀。
“我是做測試或運維的,還需要按照文章的描述,在本地開發環境里跑通嗎?”需要。因為在本文所描述的避坑的過程中,你會發現之前代碼中的配置有問題。當你需要在源代碼里更改配置,并重新構建docker image時,你就需要知道如何操作。
這一系列文章的第一篇,會針對macOS、Windows10和Ubuntu這3種操作系統,分別推出3個版本。
這一系列文章可以分為三篇。這三篇的標題如下:
K8s部署前后端分離的web應用避坑系列指南之一:在本地開發環境、本地docker compose和k8s云集群里跑通購物清單應用(macOS/Windows10/Ubuntu-2023版分別寫)
K8s部署前后端分離的web應用避坑系列指南之二:解讀購物清單應用Dockerfile和docker-compose.yml文件
K8s部署前后端分離的web應用避坑系列指南之三:解讀購物清單應用k8s的deployment、service和ingress配置文件
這一系列指南相關的源代碼在這里下載:https://github.com/wubin28/shopping-list-web-app。
要想找到這一系列文章的最新版本,可以在知乎搜“體驗更好地做軟件”專欄。
2 深入閱讀
注意,本文一萬三千字。分享了8個避坑指南。需要拿著mac邊讀邊練。沒點決心堅持不下來。慎入。
2.1 需求描述
這一系列文章所選用的web app,是一個購物清單shopping list web app。它的用戶是有購物需求的顧客小吾。一天,小吾發現家里的瓶裝水快沒了,就想著晚上下班路過超市時,順便買幾瓶。但這時老板來微信喊他開會。他很快就把買瓶裝水的事情忘掉干干凈凈。等下班路過超市,他光顧著給老婆買打折的巧克力。等到家要喝水了,發現水沒買。咱們這個shopping list web app,就能為小吾解決上面的痛點。當他想要買水時,就可以馬上在app里添加一條買水的購物項。過了一會兒又想買點香蕉,那就再加一條。等他到了超市,再查看一下這個清單,要買的東西就不會忘了。
2.2 從源代碼開始分三步部署到k8s
現在咱們有了這一系列指南相關的源代碼。該如何將它部署到k8s呢?
可以分三步:
第一步,在本地開發環境里跑通;
第二步,在本地docker compose里跑通;
第三步,在k8s云集群里跑通。
為何不能一次就從源代碼直接部署到k8s呢?當然這樣做也可以,但前提是你確信部署上去后,將來再也沒有新需求或修bug而去修改源代碼并重新部署。對于坑坑洼洼的docker和k8s學習之旅,你覺得這可能嗎?
所以你需要知道當新需求來了或要修bug時,該如何把修改過的代碼,在本地開發環境里調試通。這是進行第一輪自測。畢竟,本地電腦是你的地盤兒。在本地電腦上調試程序,比在k8w云集群里要方便得多。這是第一步的意義。
之后,你需要知道如何將通過了第一輪自測的代碼,構建成docker image,并在本地docker compose里跑通,為之后將docker image部署到k8s做第二輪自測。畢竟,本地docker compose也在你的地盤兒上。這是第二步的意義。
最后,你需要知道如何將通過了第二輪自測的docker image,部署到k8s云集群并跑通,為之后部署到生產k8s云集群環境做第三輪自測。這個項目的k8s云集群,我選用了微軟的azure k8s service,免費使用1個月。這也算是我的地盤兒。在這里自測,會比在運維團隊地盤兒里的生產k8s云集群環境,要方便多了。這是第三步的意義。
2.3 在本地開發環境里跑通
2.3.1 在本地開發環境里的架構
Shopping List Web App在本地開發環境里的架構,如果用c4 model(https://c4model.com/)畫出來,就如圖3和圖4所示。
圖3是站在整個web app的邊界,向外看的context圖。在系統外,有user和admin這兩種用戶在使用系統。User使用系統來管理購物清單。Admin使用系統來管理購物清單數據。
圖4是站在整個web app的邊界,向內看的container圖。在系統內,有4個容器。注意c4 model里的container的概念,和docker的container的概念,是不同的。前者是代表架構圖中運行的應用或數據存儲系統,后者代表封裝了所有代碼和依賴庫能獨立運行的軟件運行單元。User通過前端shopping-list-front-end來查看和修改購物清單。而前端shopping-list-front-end將用戶對購物清單的操作請求,發給后端shopping-list-api。后端shopping-list-api再訪問數據庫postgres查詢和更新數據。Admin通過使用pgadmin數據庫管理工具來直接管理postgres數據庫中的數據。
2.3.2 本地開發環境準備
我所使用的Mac,是Apple M1 Pro,32G內存。對于在本機跑docker compose和前后端應用,只要不同時開著intellij idea和webstorm,8G內存應該是夠用了。
[小心坑!不要直接使用官網安裝包安裝工具]
我之前安裝jdk的習慣,一直是先確定要裝哪個版本的jdk,比如jdk11,然后在oracle官網上找到jdk11的下載頁面,下載對應操作系統的安裝包,然后解壓或安裝。
但后來發現,jdk版本更新得很頻繁。如果想用現在的主流版本jdk17,就得再從官網下載并安裝jdk17,然后手工在/.zshrc或/.bashrc里修改JAVA_HOME和PATH環境變量。這樣才能從jdk11切換到jdk17。如果有老舊項目又需要用jdk11,又得手工修改環境變量。這太累了。
而像git這樣的工具,雖然版本更新得不那么頻繁,如果你也是從官網下載安裝包安裝,等過了幾個月,想升級版本時,經常會忘記當初是如何安裝的,導致難以卸載并重新安裝。
所以像jdk和node.js甚至git這樣的工具,一般情況下不建議直接從官網下載安裝包安裝,而是使用熱門的包管理器來安裝。這樣當要切換同一工具的不同版本、升級版本和卸載時,就方便多了。
如果你在macOS上的git、jdk和node.js/npm之前是直接使用官網安裝包安裝的,而沒有使用包管理器來安裝,那么推薦你設法把它們先卸載,然后使用下面的包管理器來安裝。否則,你就要冒因工具版本與我所用的不一致,而導致各種問題的風險。而我下面描述的工具版本,是經過我測試過了的。
用包管理器homebrew安裝文件版本管理工具git 2.42.0以便下載本項目代碼
安裝homebrew方法參見:https://brew.sh/。我用的homebrew版本是4.1.16。
安裝git:brew install git
,參見:https://formulae.brew.sh/formula/git。
驗證git是否工作:運行命令git -v
,我用的git版本是2.42.0。
下載代碼:運行git clone https://github.com/wubin28/shopping-list-web-app.git
,就能把代碼下載到項目文件夾shoppling-list-web-app
中。
以后就把shoppling-list-web-app
叫做項目文件夾。
進入到這個文件夾,運行命令ls -alF
,你會看到,這個文件夾里有3個子文件夾。
drwx------ 15 binwu staff 480 Oct 23 16:22 back-end/
drwx------ 23 binwu staff 736 Oct 23 13:47 front-end/
drwx------ 3 binwu staff 96 Oct 23 08:27 infrastructure/
其中,
infrastructure文件夾存放了運行docker compose和k8s的配置文件,如docker-compose.yml
。
back-end存放了后端代碼、后端Dockerfile和其他配置文件。
front-end存放了前端代碼、前端Dockerfile和其他配置文件。
Dockerfile是一種配置文件,用于把源代碼構建為docker image,以便以容器化的方式進行部署。
用包管理器sdkman安裝后端開發工具jdk 17.0.8.1-tem以便在本地進行后端構建
安裝sdkman方法參見:https://sdkman.io/install。我用的sdkman的版本是script: 5.18.2,native: 0.4.2。
安裝jdk:sdk install java
。
查看所安裝的jdk版本:sdk list java
。
使用所安裝的jdk版本:sdk use java 17.0.8.1-tem
,參見:https://sdkman.io/usage。
驗證jdk是否工作:java -version
。我用的jdk版本是openjdk version "17.0.8.1" 2023-08-24。
用包管理器nvm安裝前端工具node.js和npm以便在本地進行前端構建
安裝nvm方法參見:https://github.com/nvm-sh/nvm。我用的nvm版本是0.39.5。
安裝node.js/npm:nvm install --lts
。
驗證前端工具node.js是否工作:node -v
。我用的node.js版本是v18.18.0。
驗證前端構建工具npm是否工作:npm -v
。我用的npm版本是9.8.1。
安裝docker desktop以便用容器方式運行postgres數據庫及其管理工具
參見:https://docs.docker.com/desktop/install/mac-install/。我用的docker desktop for macOS版本是v4.24.2。
驗證docker desktop是否工作:看docker desktop是否能正常啟動。
2.3.3 在本地開發環境里跑通shopping list web app
啟動docker desktop
在容器中運行postgres數據庫和能查看數據庫中數據的pgadmin以便在本地開發環境里運行gradle構建和測試
[小心坑!不要再使用官網安裝包安裝數據庫和管理工具]
在實現新功能和修bug的時候,如果能在本地運行一個數據庫和數據庫管理工具,就能很方便地進行自測。你當然可以從官網下載數據庫和管理工具的安裝包,在本地電腦上安裝。但如前面安裝jdk類似,將來卸載或升級,會比較麻煩。在容器化的時代,如果想使用數據庫及其管理工具,你完全可以從http://hub.docker.com(又叫Docker hub)上,下載數據庫和管理工具的docker image文件,然后在本地電腦用簡單的一行命令,啟動相應的容器,來使用數據庫及其管理工具。將來卸載或升級,也是運行一行命令的事兒,多方便。
有人會問:容器里跑數據庫,要是關閉或刪除容器,那數據不就丟了?其實不用擔心,你可以為數據庫容器設置一個位于本地硬盤中的volume,以便保存持久化的數據。只要你不刪除這個volume,數據庫容器關閉后再啟動,仍然能夠獲取之前的數據。
在本地開發環境里跑通shopping list web app,首先要把postgres數據庫和pgadmin管理工具啟動起來。因為之后的后端app在使用gradle進行構建時,會運行自動化測試,需要訪問數據庫。如果在后端app構建時不啟動postgres數據庫,那么gradle構建會失敗。
要運行這兩個容器,需要下載代碼。在本地電腦的terminal里,進入項目文件夾,運行命令cd infrastructure
進入這個子文件夾。然后再運行命令docker compose up postgres pgadmin
啟動postgres數據庫和pgadmin管理工具。這個命令會讀取當前文件夾下面的docker-compose.yml文件中的postgres和pgadmin服務,并啟動起來。我會在系列文章的第二篇,解讀docker-compose.yml文件。
驗證容器:在docker desktop的container界面里,能看到運行起來的兩個容器,如圖5所示。
驗證數據庫:
打開瀏覽器訪問pdadmin數據庫管理工具鏈接http://localhost:5050/
,用戶名:admin@gmail.com,密碼:admin@gmail.com。這個用戶名和密碼是在docker-compose.yml文件中的pdadmin服務中設置好的。
鼠標右擊Servers -> Register -> Server… -> General里的Name: 隨便寫一個,比如shopping-list -> Connection里面的Host name/address: postgres -> Port: 5432 -> Maintenance database: postgres -> Username: postgres -> Password: postgres -> 允許Save password -> 點擊Save按鈕 -> 點擊剛剛創建的shopping-list服務器,就能在數據庫出現問題時查看數據庫里的數據,如圖6所示。這里的Username和Password也是在docker-compose.yml文件中的postgres服務中設置好的。
在本地開發環境啟動后端app
重新打開一個terminal,進入項目文件夾,然后進入后端代碼文件夾:cd back-end
。啟動后端app:./gradlew bootRun
。
驗證后端app:打開瀏覽器訪問http://localhost:8081/swagger-ui.html
,如果能看到OpenAPI definition頁面,就表示后端已經起了。可以在這個頁面試用一下GET /api/v1/shopping-items
接口,應該返回[]
空記錄。
在本地開發環境啟動前端app
重新打開一個terminal,進入項目文件夾,然后進入前端代碼文件夾:cd front-end
。先運行命令npm install
,安裝package.json文件所設置的依賴庫。
只有等依賴庫安裝好了,才能運行命令npm run dev
啟動前端app。之后,屏幕會出現提示諸如Local: http://localhost:5173/
的信息。
驗證前端app:打開瀏覽器訪問http://localhost:5173
,能看到ShoppingList頁面。在Item輸入框中輸入“a banana”,點擊Add按鈕,會出現什么結果?”a banana”竟然沒有出現在下面的清單里!
[小心坑!CORS問題導致前端無法訪問后端]
此時為何無法插入數據?可以用快捷鍵Cmd+Option+I打開Developer Tools界面,在Network頁簽的Console里,能看到前端訪問后端時出現了CORS錯誤信息Access to XMLHttpRequest at 'http://localhost:8081/api/v1/shopping-items' from origin 'http://localhost:5173' has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource.
CORS(跨源資源共享)是瀏覽器的一種安全設置。如果后端app配置好了CORS,那么后端app就能告訴瀏覽器:“雖然訪問我的這個請求來自前端app,但我信任它,所以你可以放心地加載和展示我所提供的信息。”于是瀏覽器就能順利展示前端app訪問后端app所獲取的數據。而上面的錯誤信息表明,用戶從前端app的網址(http://localhost:5173)訪問后端app網址(http://localhost:8081)里的信息,被瀏覽器攔截了。這說明后端app沒有設置好CORS特定的權限來告訴瀏覽器:“前端這個請求是允許的,你可以放心接收。”
如何查看后端app的CORS配置呢?此時可以查看后端代碼back-end/src/main/java/com/wuzhenben/shoppinglist
文件夾下的ShoppingListApplicationConfig.java
文件。此文件的allowedOrigins(“http://localhost:8080”)
,設置了后端app允許前端app從http://localhost:8080
這個origin來訪問它。而除此之外的origin,瀏覽器就給用戶報上面的CORS錯誤,并拒絕訪問。
此時要解決這個問題,該怎么辦?既然后端已經允許前端app從http://localhost:8080
這個origin來訪問,那么如果讓前端在8080號端口運行,是不是就能解決問題?
此時可以按Ctrl+C中止前端app。然后運行下面的命令,讓前端app在8080號端口啟動:npm run dev -- --port 8080
。屏幕出現提示Local: http://localhost:8080/
。
再次驗證前端app:打開瀏覽器訪問http://localhost:8080
,在Item輸入框中再次輸入“a banana”,點擊Add按鈕。”a banana”果真出現在下面的清單里!你也可以試試點擊a banana右邊的radio button,把這個購物項設置為已購買,或者點擊Delete按鈕,刪除這個購物項。
此時還可以用快捷鍵Cmd+Option+I打開Developer Tools界面,在Network頁簽的Console里,就看不到任何錯誤信息了。
你還可以用瀏覽器訪問http://localhost:5050/
,用之前配置好的pgadmin數據庫管理工具,看看shoppingList數據庫中是否存入了你在前端app所添加的購物項。
[小心坑!docker desktop的kubernetes里的配置會搗亂]
有一天,我使用了上面的步驟,讓前端app在端口8080上啟動。但當打開瀏覽器訪問http://localhost:8080
時,又是前端無法訪問后端。打開瀏覽器chrome里的Developer Tools一看,發現network里的console報以下錯誤:Access to XMLHttpRequest at 'http://shopping-list-api-ingress:8081/api/v1/shopping-items' from origin 'http://localhost:8080' has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource.
。看起來又是CORS問題。
我想試試后端的CORS配置是否起作用了。于是我在顯示前端頁面的Chrome瀏覽器的console里,輸入命令fetch(“http://localhost:8081/api/v1/shopping-items”).then(a => a.text()).then(console.log)
,來從后端app獲取所有購物項數據,結果發現能夠獲取到,返回結果是[{“id":1,"item":"a banana”,”purchased”:true}]
。因為我是從http://localhost:8080
頁面的console里運行的fetch命令,這就說明后端代碼ShoppingListApplicationConfig
類中的CORS的設置起作用了。
那究竟是什么原因導致報CORS問題呢?仔細再看錯誤信息,說來自前端的請求,要訪問后端http://shopping-list-api-ingress:8081/api/v1/shopping-items
接口路徑時,出現了CORS問題。但后端接口路徑明明是http://localhost:8081/api/v1/shopping-items
啊。這是怎么回事?另外,我的代碼里,也沒有出現過shopping-list-api-ingress
這樣的字符串。那為何前端app在訪問后端app時,卻使用了http://shopping-list-api-ingress:8081/api/v1/shopping-items
這樣的陌生路徑?
仔細回憶,才想起來,前兩天在docker desktop里試用了本地電腦kubernetes(簡稱k8s)集群功能,并在里面配置了名為shopping-list-api-ingress
的ingress配置。ingress的解釋,參見本文2.5.2。
看起來前端在訪問后端時,使用了這個ingress,從而導致CORS錯誤。要是我把docker desktop里的kubernetes給關掉,是不是就會好了。于是在docker desktop的settings中,選擇Kubernetes,再把Enable Kubernetes的勾選項取消勾選,重啟docker desktop。這樣就刪除了那個ingress。為了保險,再清除一下瀏覽器cache。再次訪問前端。一切正常!
如果你有興趣,可以用Insomnia或postman驗證后端app接口。之前clone下來的代碼里,有一個Insomnia_2023-10-06.json
文件,可以安裝Insomnia(參見:[https://insomnia.rest/),創建一個collection,并在里面import這個文件來驗證后端app接口。相比postman來說,Insomnia對于初學者更加輕量和易用。
至此,shopping list web app就已經在本地開發環境里跑通了。
清理現場
如果不清理現場,一直讓兩個容器和兩個服務一直跑著,有點耗內存。此時可以在前端、后端和運行本地docker compose命令的terminal界面里,按Ctrl+C,來終止這三個程序的運行。最后在運行本地docker compose命令的terminal界面里,運行命令docker compose down
,來終止并刪除postgres和pgadmin容器以及相關網絡資源,然后在docker desktop界面里驗證一下。這兩個容器果然消失了。
2.4 在本地docker compose里跑通
2.4.1 在本地docker compose里的架構
架構圖沒變,還是見圖3和圖4.雖然Shopping list web app在本地docker compose里的架構,與在本地開發環境里的架構,在c4 model架構圖中的畫法相同,但在實現層面有差異。前者的前端和后端app,是運行在docker container里的。而后者則運行在npm和gradle命令所啟動的服務中。
2.4.2 本地docker compose環境準備
在macOS上,只要安裝好了docker desktop,你就準備好了本地docker compose運行環境。可以在docker desktop界面里查看docker compose所啟動的容器,以及相應的image。
2.4.3 在本地docker compose里跑通shopping list web app
免費注冊Docker hub賬號以便推送docker image為部署k8s做準備
Docker hub是Docker公司搞的一個存儲docker image的公共注冊(registry)中心。Docker公司把容器化搞火了之后,很多做軟件的公司,就把它們的軟件產品,做成docker image,并推送到Docker hub。你之前所用的postgres和pgadmin的image,都是從這個中心拉取的。你在Docker hub上注冊賬號后,也可以把你構建的docker image推送到Docker hub上。
這樣做有什么好處?因為這樣一來,在k8s云集群里跑通shopping list web app時,k8s云集群就能從Docker hub里拉取你所構建的前后端app的docker image。免費注冊Docker hub賬號,參見:https://hub.docker.com/。
構建后端docker image并推送到docker hub
構建后端docker image,分為三步。
第一步,用gradle構建后端app,生成jar包。
先生成jar包,再構建docker image的好處,是能讓image僅包含運行后端所需要的jar包。這樣能讓image文件盡量小。
進入項目文件夾,運行命令cd infrastructure
進入這個子文件夾。然后再運行命令docker compose up postgres pgadmin
啟動postgres數據庫和pgadmin管理工具。
然后新打開一個terminal窗口,進入項目文件夾,運行cd ../back-end
,進入后端文件夾。
運行命令./gradlew clean build
構建后端app。之后可以在文件夾build/libs
里,找找所生成的jar包,文件名是shoppinglist-0.0.1-SNAPSHOT.jar。
第二步,構建docker image。
運行命令docker buildx build --build-arg JAR_FILE=build/libs/shoppinglist-0.0.1-SNAPSHOT.jar -t <docker-hub-username>/shopping-list-api:v1.0.docker-compose .
來構建后端docker image。可以運行命令docker image ls
查看新構建的帶有v1.0.docker-compose
tag的image。
這個命令中,docker buildx build
是對舊的docker build
命令的擴展,提供了后者所沒有的緩存的導入和導出,以及并發構建多個image的功能。
在參數-t <docker-hub-username>/shopping-list-api:v1.0.docker-compose
中,-t
指給image加一個tag。這個tag就是參數中v1.0.docker-compose
,用于標識這個image。你要把<docker-hub-username>替換為你的Docker hub用戶名。而整個<docker-hub-username>/shopping-list-api
,表示這個image將來推送到Docker hub上的鏡像庫(repository)名稱。</docker-hub-username>
[小心坑!docker buildx命令最后的那個小數點不要忘了]
上面命令最后有一個不起眼的小數點。千萬不要把它忘了。這代表把當前文件夾作為build的上下文,以找到諸如jar文件這樣的構建資源。
[小心坑!如何知道所構建的image對應的是代碼庫中的哪些代碼?]
我們知道,隨著不斷提交,代碼庫中的代碼總是在不斷變化。如果有一天,你推送到Docker hub中的image里有bug,你想打開對應的源代碼看一下。但距離你構建這個image已經過去好幾天了,你也往代碼庫里提交了不少代碼。當初構建這個image的代碼也改了不少。此時你該如何在代碼庫中,還原當初構建這個image時的代碼?解決的辦法,就是你在運行上面的docker buildx命令,構建了docker image后,就立即運行命令git tag -a v1.0.docker-compose -m “v1.0.docker-compose”
,在git庫里打一個同名的tag。這樣通過識別這個tag,你就能把image和代碼對應上了。最后別忘了運行命令git push origin v1.0.docker-compose
把這個tag推送到遠程git庫中。
第三步,把docker image推送到Docker hub。
運行命令docker login
登錄Docker hub。然后運行命令docker push <docker-hub-username>/shopping-list-api:v1.0.docker-compose
,將構建好的image推送到Docker hub。你可以登錄Docker hub,看看后端帶有v1.0.docker-compose
這個tag的image是否已經在上面了。
構建前端docker image并推送到docker hub
構建前端docker image,分為兩步。
第一步,構建docker image。
有人可能會問,為何不是先用命令npm run build
來構建前端app?答案是這個命令,以及納入前端的Dockerfile文件里了。我會在第二篇文章中,解讀這個文件。
運行cd ../front-end
,進入前端文件夾。運行命令docker buildx build -t <docker-hub-username>/shopping-list-front-end:v1.0.docker-compose .
來構建后端docker image。這里的參數解讀和前面講的一樣。可以運行命令docker image ls
查看新構建的帶有v1.0.docker-compose
tag的image。
第二步,把docker image推送到Docker hub。
運行命令docker push <docker-hub-username>/shopping-list-front-end:v1.0.docker-compose
,將構建好的image推送到Docker hub。你可以登錄Docker hub,看看前端帶有v1.0.docker-compose
這個tag的image是否已經在上面了。
在本地docker compose里跑通shopping list web app
在本地docker compose里跑通的命令很簡單,進入項目文件夾,運行命令cd infrastructure
進入infrastructure子文件夾,再運行命令docker compose up
來啟動postgres、pgadmin、shopping-list-api和shopping-list-front-end這四個容器即可。此時可以在docker desktop里查看這4個容器的運行狀態。還可以在瀏覽器里訪問http://localhost:8080/
來試用購物列表web app。
至此,shopping list web app在本地docker compose里跑通了。
清理現場
進入項目文件夾,運行命令cd infrastructure
進入infrastructure子文件夾,再運行命令docker compose down
可以停止和刪除4個容器。
2.5 在k8s云集群里跑通
在k8s云集群里跑前后端分離的web app,有兩種選擇。
第一種,是使用云廠商所提供的免費試用的服務。
第二種,是使用在本地電腦上運行的諸如minikube這樣的單node的服務。
因為要真正體驗上云,所以我選擇了第一種。
各大云廠商都會提供1~3個月不等的k8s云集群免費試用。本文選用了微軟的azure k8s service。免費試用1個月,提供2個node。按照之前講解的習慣,此時應該展示shopping list web app在k8s云集群里的架構。但為了再現我踩坑的經過,讓講解更有趣,我打算把架構放到最后再講。
2.5.1 K8s云集群環境準備
注冊Azure k8s service云平臺賬號
Azure k8s service云平臺免費注冊方法參見:https://azure.microsoft.com/。
注冊完后,可以創建一個名為my-k8s-cluster-1
的k8s service,以及名為my-azure-resource-group-1
的resource group。然后登錄主頁https://portal.azure.com/#home
,就能看到你所擁有的資源,如圖7所示。
打開docker desktop kubernetes讓kubectl能正常工作
接下來,你需要安裝工具kubectl,以便從macOS連上k8s云集群。做法是在docker desktop里,點擊settings,選擇Kubernetes,然后把Enable Kubernetes左邊的勾選框勾上。之后點擊Apply & reset按鈕。
驗證docker desktop k8s能否正常工作:等reset結束后,你能在docker desktop的主界面左下角的小鯨魚圖標上方,看到一個綠色背景的小橫條,上面有k8s的舵輪圖標。綠色背景,表示docker desktop k8s運行正常。另外,你可以打開一個terminal窗口,在里面輸入命令kubectl version -o yaml
。如果能看到clientVersion和serverVersion,就說明操作k8s的命令kubectl能正常工作了。
連上azure k8s service云平臺
要從你的Mac連上azure k8s service云平臺,需要改一個配置文件。這個文件是你的mac電腦的~/.kube
文件夾下的config文件。你可以用你喜歡用的編輯器,打開這個文件。里面只有你的docker desktop所提供的一個k8s集群,名字就是docker-desktop。你要連azure k8s service云平臺,就需要把這個文件,替換為azure k8s service云平臺的同名配置文件。或者在這個文件中,添加azure k8s service云平臺的配置。即這個文件可以有多個k8s集群的配置,此時就能用kubectl命令,在兩個k8s集群之間切換。因為在本文中,我們不用docker desktop k8s所提供的單node的本地集群,所以為簡單起見,可以把你mac上的~/.kube/config
文件先備份,然后用azure k8s service云平臺的同名配置文件將其替換。
那如何獲取azure k8s service云平臺的配置文件?方法是你需要在瀏覽器里,登錄你的azure k8s service云平臺。在頁面上方搜索框的右側,有一個Cloud shell圖標。點擊這個圖標,就能在屏幕下方,看到一個黑色背景的命令行界面出現。點擊命令行界面上方的兩個大括號{}圖標Open editor,就能在左側打開一個文件樹。在文件樹中,找到.kube文件夾并打開,然后點擊config文件。右側就會出現這個文件的內容。你把這個文件的內容全部復制出來,保存到mac電腦的~/.kube/config
文件末尾,并把這個文件原先的內容刪除。再次提醒,在刪除原內容前,一定要備份。
一旦改好了config文件,你就可以連接azure k8s service云平臺了。
運行命令kubectl config get-contexts
,可以看到你所連接的azure k8s service云平臺。
運行命令kubectl get nodes
,可以查看azure k8s service云平臺給你分配了兩個node,狀態都是ready。
2.5.2 在k8s云集群里跑通shopping list web app時踩坑
我是如何踩坑的
初次在k8s上部署前后端分離的web app,最自然的方式,就是按照在docker compose里部署的架構,來部署。但這樣想,就踩進了一個坑。在討論坑之前,先看看在k8s云集群里跑通與在本地跑通之間的差異。
在k8s云集群里跑通shopping list web app,與在本地docker compose里跑通,有什么差異呢?有3個差異。
第一個差異,是后端app所依賴的數據庫主機名,不再是localhost,而是k8s云集群里postgres數據庫的內部service名。這需要改動back-end/src/main/resources/application.properties文件,將里面的localhost,替換為${DB_HOST}。即通過在下面介紹的deployment配置文件設置的DB_HOST環境變量,來確定postgres數據庫的service名。
第二個差異,是后端的CORS的配置中的allowedOrigins,不再是http://localhost:8080
,而應該是前端app在k8s云集群中的對外域名和端口號。
第三個差異,前端前端app所依賴的后端app的主機名和端口,也不再是localhost:8081,同樣也變成了k8s云集群里后端app的service名。這需要改動前端代碼的3個文件。首先,front-end/src/components/ShoppingList.vue文件中的localhost:8081,需要改為%%API_URL%%。這也是通過在下面介紹的deployment配置文件設置的API_URL環境變量,來確定后端app的服務名。為了能夠在js代碼中,替換后端app的服務名,需要改動front-end/Dockerfile和新增front-end/entrypoint.sh文件。
第二個差異,就是一個坑。后端的CORS的配置中的allowedOrigins,該如何配前端app在k8s云集群中的對外域名和端口號?我沒有為這個項目申請域名。域名也不能寫成內部service名,因為內部名無法用于外部訪問。能把域名寫成ip地址嗎?在云集群中,ip地址經常會發生變化。每次ip變了就去改配置,多麻煩。這個坑該如何爬出來?
我還真的把postgres、后端shopping-list-api和前端shopping-list-web-app都部署到k8s云集群里,并讓前端擁有一個外部IP。結果發現,當我用瀏覽器訪問前端外部IP的8080端口時,瀏覽器果然報了CORS錯誤:Access to XMLHttpRequest at ‘http://shopping-list-api/api/v1/shopping-items' from origin ‘http://20.72.168.185:8080’ has been blocked by CORS policy: No ‘Access-Control-Allow-Origin’ header is present on the requested resources.
后來也是查了很多資料,在朋友圈求助,經過朋友們的提醒,并嘗試了一下,發現為shopping list web app配置ingress能解決這個難題。
在k8s里,ingress是一種規則和配置的集合,它能幫助外部的網絡請求,來查找到和訪問集群內的服務。可以把它想象成一個交通指揮員,它知道如何根據特定的規則把外面來的車輛(網絡請求)引導到正確的停車位(服務)。
2.5.3 在k8s云集群里的架構
在k8s云集群里,就難以使用pgadmin數據庫管理工具了。所以圖8的context架構圖只有user。
咱們這個web app,用戶不再直接訪問前端app的對外IP和端口,而是直接訪問ingress nginx controller的對外IP和端口。之后,ingress nginx controller會把用戶的請求,根據請求的path不同,分發給前端app和后端app。而前后端app就不必擁有對外的IP和端口了。
既然用戶所使用的瀏覽器,只看到ingress nginx controller所對外暴露的IP和端口,那么之后前端app訪問后端app獲取數據,就都在同一個ingress nginx controller所對外暴露的IP和端口下,這樣對瀏覽器來說,就不存在CORS的跨域問題了。如圖9所示。
2.5.4 如何從坑里爬出來
要從坑里爬出來,就需要新增k8s的deployment、service和ingress的配置文件,以便使用kubectl命令將ingress和postgres、shopping-list-api和shopping-list-front-end這3個微服務部署到k8s上。
注意,ingress不是微服務,而是k8s里的一組規則。
另外,每個微服務的k8s部署,一般都需要一個deployment文件和一個service文件。前者供k8s為這個微服務創建pod,后者供k8s為這個微服務的pod分配穩定的ip地址以及DNS名稱。即使容器實例被替換,ip地址以及DNS名稱也不會改變。
Pod是k8s管理的最小單元,里面推薦只運行一個docker container,這樣才算微服務。
配置ingress需要一個ingress配置文件。因為要在k8s里配置3個微服務,所以需要新增3個deployment文件和3個service文件。
此外,原先在本地使用的pgadmin數據庫管理工具,在k8s云集群中,就不再使用了。
改動的代碼文件列表如下:
back-end/src/main/resources/application.properties(改動)
Back-end/src/main/ShoppingListApplicationConfig.java(改動)
front-end/Dockerfile(改動)
front-end/src/components/ShoppingList.vue(改動)
front-end/entrypoint.sh(新增)
infrastructure/deployment-postgres.yml(新增)
infrastructure/ingress.yml(新增)
infrastructure/deployment-shopping-list-api.yml(新增)
infrastructure/deployment-shopping-list-front-end.yml(新增)
infrastructure/service-postgres.yml(新增)
infrastructure/service-shopping-list-api.yml(新增)
infrastructure/service-shopping-list-front-end.yml(新增)
為了減輕你寫代碼的負擔,我把這些改動和新增保存到了分支for-azure-k8s-service中。運行命令git checkout for-azure-k8s-service
就能看到進行了這些改動和新增后的代碼。
由于代碼改動涉及后端和前端,所以要重新構建后端和前端的docker image。
構建后端docker image并推送到docker hub
首先把數據庫跑起來,以便構建代碼時運行測試。進入項目文件夾,運行命令cd infrastructure
進入這個子文件夾。然后再運行命令docker compose up postgres pgadmin
啟動postgres數據庫和pgadmin管理工具。
然后新打開一個terminal窗口,進入項目文件夾,運行cd ../back-end
,進入后端文件夾。因為后端app所依賴的數據庫主機名,現在已經改為環境變量${DB_HOST}了,所以在構建前,需要在terminal窗口中,運行命令export DB_HOST=localhost
來設置環境變量。
之后,可以運行命令./gradlew clean build
來生成后端jar包。
然后運行命令docker buildx build --build-arg JAR_FILE=build/libs/shoppinglist-0.0.1-SNAPSHOT.jar -t <docker-hub-username>/shopping-list-api:v1.1.k8s .
來構建后端docker image。注意,為了和之前為docker compose構建image做區分,上面命令中的tag改為v1.1.k8s
。可以運行命令docker image ls
查看新構建的帶有v1.1.k8s
tag的image。
運行命令docker login
登錄Docker hub。然后運行命令docker push <docker-hub-username>/shopping-list-api:v1.1.k8s
,將構建好的image推送到Docker hub。你可以登錄Docker hub,看看后端shopping-list-api帶有v1.1.k8s
這個tag的image是否已經在上面了。
構建前端docker image并推送到docker hub
[小心坑!如果用arm64架構的mac構建image而不做架構設定會怎樣?]
我按之前為docker compose構建前端docker image的方式,為azure k8s service構建了前端docker image。但等我把前端的deployment文件apply到k8s云集群時,pod在啟動時總是報一個奇怪的錯誤:exec /usr/local/bin/docker-entrypoint.sh: exec format error。
把這個image拉下來,運行一個容器,然后進去看文件docker-entrypoint.sh的內容,也看不出所以然。后來查了半天,才知道原因在于我用arm64架構的mac在構建image時,沒有指定所構建的image應該是amd64架構的。
如果用arm64架構的mac構建image,而不在命令中做架構設定,那么所構建的image就只能用于arm64架構的容器運行系統里,這也是我之前能正常在mac上的docker compose里運行不帶架構設定而構建出的image的容器的原因。但我在azure k8s service云集群里所申請的資源,一般都是只能運行amd64架構的容器。
要爬出這個坑需要做兩件事。
第一,需要在~/.docker/config.json文件中,增加下面的配置,以便讓docker buildx能夠支持在Mac arm64架構的電腦上,構建amd64架構的image。
{ "experimental": "enabled" }
第二,在docker buildx命令中,增加指定架構的參數。可以在項目文件夾中,運行cd ../front-end
,進入前端文件夾。運行命令docker buildx build --platform linux/amd64 -t <docker-hub-username>/shopping-list-front-end:v1.1.k8s.amd64 .
來構建前端docker image。可以運行命令docker image ls
查看新構建的帶有v1.1.k8s.amd64
tag的image。還可以運行命令docker inspect wubin28/shopping-list-front-end:v1.1.k8s.amd64 | grep “Architecture"
查看這個image是否真的是amd64架構的。
運行命令docker push <docker-hub-username>/shopping-list-front-end:v1.1.k8s.amd64
,將構建好的image推送到Docker hub。你可以登錄Docker hub,看看前端shopping-list-front-end帶有v1.1.k8s.amd64
這個tag的image是否已經在上面了。
在k8s云集群上配置postgres、shopping-list-api和shopping-list-front-end三個微服務和ingress并運行
要在Mac的terminal里連上azure k8s service進行操作,需要安裝azure-cli工具。可以運行brew update
和brew install azure-cli
進行安裝。安裝完后,可以運行命令az --version
來驗證安裝是否成功。然后可以運行az login
來登錄azure k8s service云平臺。
我們在k8s云集群里為這個web app所創建的資源,最好都放到一個namespace里,這樣便于管理。將來不用云服務了,要刪除一個namespace里所有資源以便省錢,也就是運行一條命令的事兒。具體如何做,見下文“清理現場”。
因為每個命令一般都有掛上NAMESPACE`來創建這個namespace。
前面講到,在一個操作系統里安裝工具,最好用包管理器。這樣便于維護工具的版本。對于云計算操作系統k8s來說,helm就是這樣的包管理工具。我們可以用helm來安裝ingress-nginx:
helm repo add ingress-nginx-repo https://kubernetes.github.io/ingress-nginx
helm repo update
helm install ingress-nginx-release ingress-nginx-repo/ingress-nginx \
-n $NAMESPACE \
--set controller.service.annotations."service\.beta\.kubernetes\.io/azure-load-balancer-health-probe-request-path"=/healthz
安裝完后,可以運行helm list -n $NAMESPACE
驗證一下。
接下來就可以用kubectl,運行下面命令,來往k8s云集群里部署postgres、shopping-list-api、shopping-list-front-end和ingress了。
`cd ../infrastructure`
部署postgres的deployment:`kubectl apply -f ./deployment-postgres.yml --namespace $NAMESPACE`
驗證image是否正確:`kubectl get deployments -o wide -n $NAMESPACE`
驗證pod是否正常啟動:`kubectl get pods -o wide -n $NAMESPACE`
部署postgres的service:`kubectl apply -f ./service-postgres.yml --namespace $NAMESPACE`
驗證服務是否正常啟動:`kubectl get services -o wide -n $NAMESPACE`
部署shopping-list-api的deployment:`kubectl apply -f ./deployment-shopping-list-api.yml --namespace $NAMESPACE`
驗證image是否正確:`kubectl get deployments -o wide -n $NAMESPACE`
驗證pod是否正常啟動:`kubectl get pods -o wide -n $NAMESPACE`
部署shopping-list-api的service:`kubectl apply -f ./service-shopping-list-api.yml --namespace $NAMESPACE`
驗證服務是否正常啟動:`kubectl get services -o wide -n $NAMESPACE`
部署shopping-list-front-end的deployment:`kubectl apply -f ./deployment-shopping-list-front-end.yml --namespace $NAMESPACE`
驗證image是否正確:`kubectl get deployments -o wide -n $NAMESPACE`
驗證pod是否正常啟動:`kubectl get pods -o wide -n $NAMESPACE`
部署shopping-list-front-end的service:`kubectl apply -f ./service-shopping-list-front-end.yml --namespace $NAMESPACE`
驗證服務是否正常啟動:`kubectl get services -o wide -n $NAMESPACE`
部署ingress:`kubectl apply -f ./ingress.yml --namespace $NAMESPACE`
查看ingress的狀態:kubectl get ingresses -n $NAMESPACE
查看ingress的詳情:kubectl describe ingress <ingress name> -n $NAMESPACE
如果一切順利,沒有出錯,那么就可以運行命令`kubectl get services -o wide -n $NAMESPACE`,查看ingress nginx controller對外暴露的IP和端口,以便讓我們試用web app。假設我們查看到的IP是20.72.130.209。而端口一般是80。
打開瀏覽器,訪問http://20.72.130.209/
。如果一切正常,就能在上面愉快地管理購物項了。
清理現場
運行命令kubectl delete namespace $NAMESPACE
,就可以刪除該namespace下所有資源。
如果你的azure k8s service云服務免費試用快到期了,記得刪除下面的資源:my-k8s-cluster-1、my-azure-resource-group-1和Azure subscription 1。
[小心坑!在免費期到期前不要忘記刪除k8s云集群中的所有資源]
在微軟、谷歌、亞馬遜、阿里、騰訊這樣的云平臺申請了帶有免費試用期的賬號,如果暫時不用,在試用期到期前,一定記得刪除k8s云集群中的所有資源,否則就太破費了。你會遇到云刺客。
本文Windows 10和ubuntu版,等我有空了再寫。
因篇幅所限,本文并未解讀所使用的docker compose和k8s的配置文件。我會在接下來的兩篇文章中,進行解讀。敬請關注。
要想找到這一系列文章的最新版本,可以在知乎搜“體驗更好地做軟件”專欄。
如果你喜歡這一系列文章,歡迎點贊和收藏,并在留言區寫下為何喜歡,以便我將來寫更多你喜歡的文章。
如果你不喜歡,也歡迎你留言告訴我哪里可以再改進。