## 專案CI/CD 針對[erp專案](https://github.com/ryanimay/ERP-Base)的部屬整理 專案原先本地開發是依附外部MSSQL還有Docker起的Redis 期望是專案做容器化,完整流程: 1. 本地開發、推code 2. [git收到push,觸發webhook,發送到jenkins](###1._綁Github_Webhook) 4. jenkins觸發任務,跑pipline script: 1. pull最新sourceCode(FreeStyle會自動拉,[但pipline需要手動做](###Pipeline)) 2. [打包、建構最新image](###2._CI打包上Dockerhub) 3. 把最新的image推到Docker Registry 4. 觸發kubectl(K8S)抓最新的image,建構、更新服務 <font style='color:red;'>專案開發,版控都做了,這邊從初期建Jenkins開始,按順序往下</font> ## Jenkins 這邊是用docker起,比較需要注意的是要在jenkins容器內裝docker客戶端和各種專案運行打包需要的套件(比如後端需要打包maven所以這邊要在jenkins內裝maven), 先看dockerfile: ```dockerfile= FROM jenkins/jenkins:lts # 安装 Docker 客户端和 Maven USER root RUN apt-get update && apt-get install -y docker.io maven && apt-get clean # 把 Jenkins 用戶加到 Docker群組 RUN usermod -aG docker jenkins # 切回 Jenkins 默認用户 USER jenkins ``` 除了安裝額外套件外,還有一部分要做修改就是權限問題 要在jenkins內用docker語法要有編輯<font style='color:red;'>/var/run/docker.sock</font>的權限 但docker.sock預設只有給root權限,所以這邊用 <font style='color:red;'>RUN usermod -aG docker jenkins</font> 把jenkins加上docker group 後續再把docker.sock也改成docker group,同組就能調用了 然後建docker-compose,抓剛剛建的dockerfile,然後利用健康檢查,確保啟動後做權限修改 這邊的修改主要是針對/var/run/docker.sock(docker嵌套) 1. 設定嵌套文件的group,確保和jenkins同組 2. 設定權限為660(默認就是660,同組可用,但之前權限原因不明跑掉,所以這邊固定都運行一次確保權限正確) ```yaml= version: '3.8' services: jenkins: build: context: jenkins dockerfile: Dockerfile container_name: jenkins restart: always image: jenkins ports: - "8080:8080" - "50000:50000" volumes: - jenkins_home:/var/jenkins_home - /var/run/docker.sock:/var/run/docker.sock networks: - my-network healthcheck: test: ["CMD", "curl", "-f", "http://localhost:8080/login"] interval: 30s timeout: 10s retries: 5 entrypoint: ["/bin/bash", "-c", "exec /usr/local/bin/jenkins.sh"] command: ["/bin/bash", "-c", "while [ ! -f /tmp/ready ]; do sleep 5; done; chmod 660 /var/run/docker.sock; chown root:docker /var/run/docker.sock; touch /tmp/done"] deploy: resources: limits: cpus: '1.0' memory: 2G volumes: jenkins_home: networks: my-network: driver: bridge ``` 這邊要特別注意 ```bash= deploy: resources: limits: cpus: '1.0' memory: 2G ``` ==這段,是限制jenkins容器占用資源上限,要加的原因是因為jenkin作業在運行的時候如果作業內容包含外部套鍵或依賴安裝,會占用大量資源,導致docker CPU過載,會連帶導致其他容器故障== 啟動方式這邊用bash跑或是直接指令<font style='color:red;'>docker compose up --build -d</font> ```bash= docker-compose build docker-compose up -d ``` 特別注意docker-compose中的<font style='color:red;'>**restart: always**</font> 如果沒加,每次重新開機啟動docker都要再手動啟動jenkins, 加上之後就是docker啟動時會自動運行服務 然後運行bash建立容器啟動後就可以看相對port url ### 基本使用(Free-Style) 這邊是localhost:8080 ![image](https://hackmd.io/_uploads/SkwP-fU1ke.png) ▲初始介面,從jenkins默認位置找預設密碼,輸入 ``` docker exec jenkins cat /var/jenkins_home/secrets/initialAdminPassword ``` ![image](https://hackmd.io/_uploads/H14ffGUJkx.png) 通過之後要安裝插件 ![image](https://hackmd.io/_uploads/ByKdMf8y1l.png) 這邊選擇推薦外掛就好 ![image](https://hackmd.io/_uploads/HJFxXGLyyg.png) 就等待安裝 ![image](https://hackmd.io/_uploads/BkcOmfL1kl.png) 安裝完成,建立第一個管理員帳戶 ![image](https://hackmd.io/_uploads/HJdBX5i1Jg.png) 設定端點url ![image](https://hackmd.io/_uploads/Bk-u79sJJg.png) 然後就可以開始用了 先來實驗簡單任務 ![image](https://hackmd.io/_uploads/S1gK5nsk1l.png) 點擊左上新增一個作業 ![image](https://hackmd.io/_uploads/HkQnqnj1yl.png) 比較常用的是FreeStyle和Pipeline * FreeStyle: 大部分功能都有UI支援,操作比較簡單,適合小專案 * [Pipeline](###Pipeline): 使用Groovy語法寫操作,功能多,可以做並行或是條件判斷,可以整合其他工具,可以方便做版控(Jenkinsfile),適合較複雜的專案 這邊先選FreeStyle ![image](https://hackmd.io/_uploads/Bk7Fohiykx.png) 輸入Git的repository url和要關注的分支 <font style='color:red;'>這邊的作用是,當作業觸發時,會自動拉取指定Repository的指定分支的程式碼 到Jenkins的工作倉庫(默認路徑/var/jenkins_home/workspace/該作業名稱/)</font> 建置方式選Git,Build Steps選Shell ![image](https://hackmd.io/_uploads/ry4CshsJ1l.png) ![image](https://hackmd.io/_uploads/Skelhnjy1l.png) ![image](https://hackmd.io/_uploads/H1qLhhjyyl.png) 這邊先暫時輸入shell的提示字,簡單測試 之後存檔,會跳轉到新建專案的首頁,點選馬上建置 ![image](https://hackmd.io/_uploads/r1OTnhiJ1e.png) --- ![image](https://hackmd.io/_uploads/SJvWp3ik1g.png) 建置歷程就會跑出執行紀錄 點擊進入後可以看到這次觸發執行的詳細內容, ![image](https://hackmd.io/_uploads/BylBahik1x.png) 點擊主控台輸出(Console output),可以看到專案的終端印出剛剛輸入的提示字 就是剛剛shell輸入的信息 ![image](https://hackmd.io/_uploads/r1oCp3sJ1x.png) 代表有成功調通, <font style='color:red;'>但這邊只是手動觸發,還沒有綁定Git的Webhook(當Git有新的push就會觸發作業)</font> ### 1._綁Github_Webhook 目前設定完的部分只有單向的從Jenkins拉Github repository 但還要從Github設定發送通知(有新的push就會觸發Jenkins)才能完整的做到即時更新 然後Github要綁定的方式是要綁Jenkins的url,但因為在本地測試是沒有公開網域的(本地localhost沒辦法被外網辨識),所以這邊要用到外掛插件[ngrok](https://ngrok.com/) 作用是可以暫時(有時效性,可以拿來測試)把本地localhost暴露給外網 ![image](https://hackmd.io/_uploads/B1PSMeakkx.png) 直接下載的方式會沒辦法運行,測試都是要帶入token所以要先註冊登入 會綁google authenticator,註冊過程就不多說 登入後看到登入頁面有載點 ![image](https://hackmd.io/_uploads/BkU1Xx61ke.png) 點擊下載並且解壓縮會有一個exe檔案 ![image](https://hackmd.io/_uploads/S1EHmeTkyx.png) 點擊運行會開啟cmd,複製ngrok下載頁面底下會有token設定的語法,直接複製執行 ![image](https://hackmd.io/_uploads/HyQ4Eepykg.png) 之後就是cmd直接下語法,範例是綁定localhost:8080 ![image](https://hackmd.io/_uploads/HkyYVlakJx.png) 送出之後會顯示ngrok相關資訊並掛起本地相對port,給出一個對外域名 ![image](https://hackmd.io/_uploads/rJXzHeay1x.png) 複製之後可以直接貼在瀏覽器上測試,應該就可以靠這段域名連上本地8080了(jenkins) ![image](https://hackmd.io/_uploads/Hk7Orgpy1x.png) 這樣就取得了一段公開域名,可以來設置Github的webhook 先開啟Github要綁的repository,點選Setting中的webhook,Add webhook ![image](https://hackmd.io/_uploads/B1E7UxT1yg.png) 輸入密碼驗證後,可以看到上面的敘述,簡單來說就是repository會發送請求 下面可以選擇細節(發送路徑、請求格式、觸發場合) <font style='color:red;'>Payload URL的部分放上剛剛ngrok產出的公開域名,並且結尾要加上/github-webhook/</font> event就選push吧 ![image](https://hackmd.io/_uploads/S18CDl6JJl.png) 好了之後存檔,webhook就建立完成了,<font style='color:green;'>√綠勾勾</font>代表有接通 ![image](https://hackmd.io/_uploads/Bk6-Yg6kkg.png) 然後就可以來測試了 實際在專案下個commit推看看 ![image](https://hackmd.io/_uploads/HkJN3xTkJx.png) push之後jenkins上的對應作業應該就可以看到建置歷程有新動作 ![image](https://hackmd.io/_uploads/HJt35e6kkl.png) 一樣點擊Console output就可以看到該次觸發的詳細內容 ![image](https://hackmd.io/_uploads/H1JW2l61ke.png) 然後可以手動看docker的jenkins容器下 (默認路徑/var/jenkins_home/workspace/作業名稱)有沒有成功拉到SourceCode ![image](https://hackmd.io/_uploads/BJ4aaeT1Jl.png) 也可以用DockerDesktop看 ![image](https://hackmd.io/_uploads/B1J7pg6kJe.png) 這樣就大致完成了第一步的==Github Webhook觸發Jenkins== ### 2._CI打包上Dockerhub 已經確定可以用Jenkins監控git做到改動即時觸發作業,接下來要做的事情就是在jenkins上 操作docker打包並且把docker images推上dockerhub給其他服務使用 1. 先來測試打包,這邊是用springboot+mvn專案,因為要用mvn package所以jenkins內必須安裝maven,一開始範例建立jenkins的dockerfile已經有先裝好了 這邊測試就直接點擊組態 ![image](https://hackmd.io/_uploads/ByEL7bp1ke.png) 修改觸發時的shell,加上打包指令 ![image](https://hackmd.io/_uploads/HyNjfWaJJg.png) 實際觸發就會發現運行了mvn打包,也可以順帶運行程式碼中的自動測試,確保每次git push沒有爆炸 ![image](https://hackmd.io/_uploads/HJGxN-pkyl.png) 可以看到開始建構專案 ![image](https://hackmd.io/_uploads/rki6mZ6Jye.png) 測試打包成功,可以進到工作目錄看看target底下有沒有打包的jar ![image](https://hackmd.io/_uploads/SJCd4ZpJ1g.png) 2. 打包完再來就是做dockerfile,把打包專案做成docker images 這部分就沒什麼,就抓jar(依照dockerfile位置,我是也放進版控,固定抓/target/xxx.jar) 放進工作目錄,指定port,加上啟動指令 ```java= FROM openjdk:17-jdk-alpine WORKDIR /app COPY /target/erp-0.0.1.jar /app/erp-0.0.1.jar EXPOSE 8081 ENTRYPOINT ["java", "-Dfile.encoding=UTF-8", "-jar", "erp-0.0.1.jar"] ``` 3. 再來就是用dockerfile建構images並且推到Dockerhub上,這邊步驟比較長 要先註冊[Dockerhub](https://hub.docker.com/),註冊流程這邊就不提 ![image](https://hackmd.io/_uploads/SkHVvb6y1x.png) 註冊完成登入後,點選repository,創建一個空倉庫,這邊用private就好 ![image](https://hackmd.io/_uploads/SksiPb6kke.png) 創建就填個倉庫名<font style='color:red;'>※免費帳戶的private庫僅限1個,所以如果有其他專案就只能公開了</font> 不然就是要用其他docker repository() ![image](https://hackmd.io/_uploads/SJKUF-TJkx.png) 保存之後repository內就可以看到範例的推送指令,這邊先告一個段落 ![image](https://hackmd.io/_uploads/HJc2FWTkkl.png) 再來是Jenkins內要配置Dockerhub的帳密,當然是也可以直接寫在指令內但怕有安全疑慮 Jenkins首頁點選管理Jenkins->點Cridential ![image](https://hackmd.io/_uploads/SkmTqbaJke.png) 直接點global新增Cridential ![image](https://hackmd.io/_uploads/BJRVo-Ty1x.png) --- ![image](https://hackmd.io/_uploads/r1JW2Wpyyl.png) 可以依照想要設置的類型選擇不同的配置 如果只是單獨一段文字(只配置密碼)應該secret text就可以了 這邊因為帳號密碼都要配置就選Username with password ![image](https://hackmd.io/_uploads/S1x93b6kJl.png) 主要就填帳號密碼,scope是作用域,簡單來說System是供jenkins系統內部使用, 作業沒辦法用,所以這邊選Global,詳細點 **?** 都有說明 ![image](https://hackmd.io/_uploads/HJxV6WpJ1g.png) 儲存之後可以看到新增的Cridential ![image](https://hackmd.io/_uploads/HJ_DRWpJ1g.png) 再來就是開啟剛剛建置的作業,開啟組態設定,找到建置環境設 然後就是Cridentials選擇剛剛新增的Cridential,並幫變數取名 ![image](https://hackmd.io/_uploads/rylfwkCkyg.png) 這部分可以依照需求類型選擇,看是單一文字還是帳密 ![image](https://hackmd.io/_uploads/r1nHDkA1yl.png) 之後就可以在shell的地方呼叫到Jenkins內配置的Cridential,可以避免寫明文和重複動作 最後就是看到shell的區塊,原先只做到打包應該是長這樣: ![image](https://hackmd.io/_uploads/SJf7u1RJkl.png) 現在分別加上 1. 登入dockerhub 2. dockerfile建置image <font style='color:red;'>(這邊的dockerfile位置是放在/var/jenkins_home/workspace/該作業名資料夾/,我是直接上Git,放在第一層,拉下來就可以直接調用到)</font> 3. 把images推到dockerhub的語法 這邊指定dockerhub的語法就是(帳戶名/倉庫名:版號標籤),完整如下: ![image](https://hackmd.io/_uploads/B1mep1CJJl.png) 儲存完之後就可以手動觸發作業看看結果有沒有成功 登入+建置image: ![image](https://hackmd.io/_uploads/rylB31Cy1e.png) 推上dockerhub: ![image](https://hackmd.io/_uploads/SkTLnyRkkg.png) 成功之後看看dockerhub的倉庫有沒有更新: ![image](https://hackmd.io/_uploads/Bysu2yAkkg.png) ==這樣就成功完成CI的部分,本地推code之後自動跑測試 > 打包 > 更新線上的包== ### Pipeline 和Free-style比起來,使用Groovy語法寫操作,功能多,可以做並行或是條件判斷,可以整合其他工具,可以方便做版控(Jenkinsfile),適合較複雜的專案 後面預計是要整合Kubernetes做完整CI/CD,這邊先把專案類型改pipeline 一樣先新增專案,選pipeline ![image](https://hackmd.io/_uploads/rJlfF5VeJg.png) 前面General的部分都差不多,就是填desc和Git、還有觸發方式之類的 ![image](https://hackmd.io/_uploads/H1gnK9Nlke.png) 下面pipeline就是比較特殊的了,這邊有分 * Pipeline script: 直接把Pipeline腳本寫在jenkins作業內 * Pipeline script from SCM(Source Code Management): 把Pipeline腳本寫成Jenkinsfile,放入版控,用引入檔案的方式配置操作 ![image](https://hackmd.io/_uploads/S1qgs54e1g.png) 這邊先用==Pipeline script==,後面再加入版控 關於pipeline scrpt語法結構,[網路上很多介紹](https://ithelp.ithome.com.tw/articles/10338284),不詳細講 要提的是Pipeline Syntax,可以依需求產出語法不用自己寫 ![image](https://hackmd.io/_uploads/SJcQa9Ngkl.png) 進去之後依需求選擇輸入對應資訊,Generate產出語法 ![image](https://hackmd.io/_uploads/H1NcTcNl1l.png) 一樣先簡單測試pipeline script ![image](https://hackmd.io/_uploads/H1WkkiVgyl.png) 手動觸發,成功 ![image](https://hackmd.io/_uploads/SyPaAqExJg.png) 接下來就是把所有內容從Freestyle搬移到pipline script 要注意,<font style='color:red;'>pipline不會自動拉取sourceCode,要自己在script區塊做</font> 整理完像這樣: ![image](https://hackmd.io/_uploads/H1XsEoEgye.png) 其中變數比較特別,因為是使用cridentials,類型是Username with password 要拿username和password,默認字樣是<font style='color:red;'>_USR</font>和<font style='color:red;'>_PSW</font>, 然後也可以改成用withCredentials的方式像這樣: ![image](https://hackmd.io/_uploads/BJnZu3EeJe.png) 差異是 * environment: 全域使用,每個stage都可以調用到 * withCredentials: 只能在宣告的區塊使用,出了區域就會無效,安全性比較高 :::danger 然後既然提到pull sourceCode,這邊就要提一下webhook完整的觸發流程: 當github webhook綁定jenkins後,當指定動作發生(pull),github會發起請求給jenkins,內含觸發相關資訊,jenkins接受到後會做的動作是: 1. 比對repository,觸發相對應作業 2. 比對pipeline script 1. 當script沒指定分支,但有寫pull repository,默認動作就是來什麼接什麼, 觸發的是A分支就pull branch A,觸發的是B分支就pull branch B 2. 當script有指定分支,jenkins會只關注這個分支的變更, 並且比對上次pull的commit hash和當前repository對應分支有沒有改變 1. 如果有改變,就會pull,並接著往下執行,jenkins也會產出建置歷程,GitHub Hook Log會顯示有更改 ![image](https://hackmd.io/_uploads/BkT0U6Ngkx.png) 2. 如果commit hash都沒變,代表內容沒變化,jenkins就會中斷任務,不執行後續script,也不會產出建置歷程,只能從GitHub Hook Log看到動作 ![image](https://hackmd.io/_uploads/SyPVDTEgkg.png) ::: 但以上動作都是建立於沒有詳細限制運行,只單純依靠配置的git來切分,如果要更詳細區分,Pipeline可以寫條件判斷,補如說當觸發分支為A才進行動作 ![image](https://hackmd.io/_uploads/BJrYF6Vx1e.png) 這樣還可以用參數化建置的方式來直接區分手動執行的分支,每個分支要做什麼事情來區隔環境 比如說 ```bash= when{ branch 'A' } //跑測試 when{ branch 'B' } //打包 ``` ### Pipeline from SCM 上面簡單試完pipeline script,但有個問題就是這樣語法是存在jenkins內,不好做修改或是版控 所以接下來就是選擇pipeline的第二個選項,把語法加到版控內(git) ![image](https://hackmd.io/_uploads/rJ8j8RBeJx.png) 選擇之後就可以看到,畫面就沒有寫語法的地方了,反而是要寫git repository和分支 用意就是會從git的位置去找語法的檔案,這邊也可以指定檔案命名來做切換區分 ![image](https://hackmd.io/_uploads/B1YLwArgJe.png) 好了就存檔,然後來改文件 至於文件,其實就只是把之前pipeline script寫的語法移到單一文件內 ![image](https://hackmd.io/_uploads/HyAauCrlkl.png) push之後可以測試觸發 Console output就會寫到jenkinsfile來源 ![image](https://hackmd.io/_uploads/BJzBd0Hxkg.png) 就是剛剛設置的git路徑,可以和先前單純配置語法的建置歷程做比較 這樣Jenkins CI(Pipeline)的部分到這邊暫時告一段落,接下來準備K8S CD的部分 ## Kubernetes ### 名詞解釋 已經做完CI的部分,接下來是CD,先說說一些最基本的名詞解釋 * pod: k8s中最小的單位,裡面可以運行一個或多個容器(container),會運行到多個的情況通常是因為容器間的耦合相依不可分割(比如容器和代理服務一同部屬,或是服務監控),還有因為同一個pod內相互調用只需要透過localhost,共享資源方便,不過也有可能是單純服務小、或是懶 * deployment: 用來操作pod,算是pod的外層,可以同時運行多個pod 1. 依照其中<font style='color:red;'>replicas</font>屬性配置可以做到<font style='color:red;'>自動管理pod生命週期和數量</font>,並且管理附載均衡、平衡服務流量 2. 基於歷史紀錄可以做到版本控制和回滾,歷史紀錄數量由配置<font style='color:red;'>spec.revisionHistoryLimit</font>控制 3. 當replicas數量設置大於1時,可以做到滾動更新,系統會一個一個替換新舊pod來達到無縫銜接 * service: 服務(container)實際是運行在pod上,容器間溝通調用是要pod之間溝通,但由於pod是交由k8s自動控管生命週期,當發生不可預期狀況重新建立或是創建,IP會浮動變化,又尤其要做到自動分配流量平衡,所以不會直接調度pod的IP,改為配置一個對外部的service中間層,固定IP,再透過service附載均衡調用各個pod * configMap、secret: 用於在k8s內做儲存,差別是configmap比較是用來做沒有安全疑慮的配置(環境變數、初始化文件),secret用於儲存敏感資訊(帳號密碼、憑證),會經過base64,不會存明文 ![image](https://hackmd.io/_uploads/SyyJmbCeyg.png) * namespace: 可以當作是k8s中的硬性隔離方式,當多專案或是多團隊共用k8s,或是區隔環境(prod、dev...),==一般情況下==,不同namespace間的資源是不能相互調用的,除非透過一些特殊配置或方式,可以用<font style='color:red;'>kubectl get</font>指令加上<font style='color:red;'>--all-namespaces</font>查看各部件所屬namespace ![image](https://hackmd.io/_uploads/Hy9BBW0lyl.png) <font style='color:red;'>自動管理</font>==的意思就是,不能手動停止容器服務了,就算手動停止,k8s也會幫你重啟,真的要操作就只能透過k8s== [基礎覺得這邊講得蠻好理解的,帶圖解,可以看](https://ithelp.ithome.com.tw/m/articles/10264285) ### Minikube 先做簡單測試,首先需要下載安裝3個東西 1. [Virtualbox](https://www.virtualbox.org/)(如果有docker非必要)-提供VM,如果安裝過成出現提示缺失[看這](https://blog.csdn.net/jiemashizhen/article/details/135901335) ==如果本機有裝docker是可以以docker為驅動,就不需要裝,這邊就是使用docker作為驅動== 2. [kubectl](https://kubernetes.io/docs/reference/kubectl/)(Kubernetes Controller)-Kubernetes的存取指令, 如果有安裝DockerDesktop會自動安裝,可以在cmd用kubectl version --client檢查 ![image](https://hackmd.io/_uploads/Hyg2o70yyx.png) 但要確定版本和Kubernetes可以支援, 如果要替換就是官網下載.exe檔,下指令where kubectl替換檔案就可以了 3. [miniKube](https://github.com/kubernetes/minikube)-輕量Kubernetes,需要依附本機VM啟動,[官方完整範例](https://minikube.sigs.k8s.io/docs/start/?arch=%2Fwindows%2Fx86-64%2Fstable%2F.exe+download) 執行minikube version 確認是否有成功安裝,如果出現context提示錯誤,執行docker context use default 重新指向context就可以排除(原因不明) ![image](https://hackmd.io/_uploads/rkOCJkBlkg.png) 都安裝完之後可以運行<font style='color:red;'>minikube start</font> 有遇到什麼問題就再解決,這邊遇到無法拉取遠端image先不管 ![image](https://hackmd.io/_uploads/Hka9R4Ay1x.png) 可以確認當前驅動VM ![image](https://hackmd.io/_uploads/SkTDQrCk1e.png) 啟動成功 ### 簡單測試 跟著[官方範例](https://minikube.sigs.k8s.io/docs/start/?arch=%2Fwindows%2Fx86-64%2Fstable%2F.exe+download)實際測試運行沒問題 * 本機有docker desktop就會以此為vm ![image](https://hackmd.io/_uploads/S1jB-kSg1e.png) 最後測試執行成功,連線應該會顯示 ![image](https://hackmd.io/_uploads/ryceMxrxke.png) ### 完整建構 這邊是直接使用docker desktop內的k8s來建,雖然說是single node但夠用了 構想是yml建deployment區分服務,然後pod都先開單一個,先不管同步問題 總共4個服務依序: [DB](####DB(MSSQL)) > [redis](####redis) > [後端](####後端服務) > [前端](####前端服務) ![image](https://hackmd.io/_uploads/Bk9KrNvl1l.png) 如果是小型或是只想簡單部屬可以就採用方案1,DB甚至可以不做容器單純外部服務 但這邊主要是想練習完整性,所以採用方案2 完整流程就會變成,主要控管容器Jenkins(CI)+K8S(CD),然後剩下所有服務容器都交由K8S管控 我的建構順序是都先本地測試deployment.yml沒問題,後面在寫進Jenkinsfile yml格式驗證可以刷[這個](https://codebeautify.org/yaml-validator#google_vignette) #### DB(MSSQL) 按順序來: ==DB== > [redis](####redis) > [後端](####後端服務) > [前端](####前端服務) 用[官方image](https://learn.microsoft.com/zh-tw/sql/linux/quickstart-install-connect-docker?view=sql-server-ver16&tabs=cli&pivots=cs1-bash) 配置前須要先下指令: ```bash= kubectl create secret generic mssql-secret --from-literal=MSSQL_SA_PASSWORD=MyPassword ``` 關於敏感資訊,要用手動配置在k8s服務器secret的方式做,因為配置文件後續也可能會做版控,不要直接寫明文,新增完可以下指令確認有沒有成功 ![image](https://hackmd.io/_uploads/rkgHDIuve1g.png) 然後創建deployment.yml: ```yml= apiVersion: v1 kind: ConfigMap metadata: name: sql-scripts data: init.sql: | IF NOT EXISTS (SELECT * FROM sys.databases WHERE name = 'localdb') BEGIN CREATE DATABASE localdb COLLATE Latin1_General_100_CI_AS_SC_UTF8; END --- apiVersion: apps/v1 kind: Deployment metadata: name: mssql spec: replicas: 1 selector: matchLabels: app: mssql template: metadata: labels: app: mssql spec: containers: - name: mssql image: mcr.microsoft.com/mssql/server:2022-latest env: - name: ACCEPT_EULA value: "Y" - name: MSSQL_SA_PASSWORD valueFrom: secretKeyRef: name: mssql-secret key: MSSQL_SA_PASSWORD ports: - containerPort: 1433 volumeMounts: - name: sql-init mountPath: /docker-entrypoint-initdb.d command: ["/bin/bash", "-c", "/opt/mssql/bin/sqlservr & while ! /opt/mssql-tools18/bin/sqlcmd -S localhost -U SA -P $(MSSQL_SA_PASSWORD) -C -Q 'SELECT 1' &>/dev/null; do echo 'Waiting for SQL Server to start...'; sleep 5; done; /opt/mssql-tools18/bin/sqlcmd -S localhost -U SA -P $(MSSQL_SA_PASSWORD) -C -i /docker-entrypoint-initdb.d/init.sql && tail -f /dev/null"] volumes: - name: sql-init configMap: name: sql-scripts --- apiVersion: v1 kind: Service metadata: name: mssql spec: type: ClusterIP ports: - port: 1433 targetPort: 1433 selector: app: mssql ``` 三大區塊,依序就是 1. 用ConfigMap做初始化的sql語法 2. 做Deployment,內含一個pod,container用官方image,並且把剛剛做的ConfigMap放進路徑下,寫command讓容器運行後自動執行初始化語法 3. 暴露service端口給集群內部調用 這邊會把剛剛創建的secret掛到環境變數上使用,就不用寫明文 創建完yml之後就可以選擇執行其一 ```bash= #create是新增,如果已經存在,運行create就會出錯 kubectl create -f 檔案名稱.yml #apply是新增或更新,如果已經存在,會進行覆蓋更新 kubectl apply -f 檔案名稱.yml ``` 運行之後會顯示到底更新或是新增了什麼 ![image](https://hackmd.io/_uploads/rkCKvwDxkg.png) 可以再下指令查詢 ```bash= kubectl get all ``` ![image](https://hackmd.io/_uploads/H1aVOPPxkg.png) 可以看到當前k8s內所有服務詳細資訊(pod, service, deployment...) 本地測試: 目前的狀態可以看到service暴露mssql的pod給集群內部調用,但是外部是連不到的 可以用==kubectl port-forward service名稱 本地port:servicePort== 把service的port掛到本地測試容器有沒有成功啟動 ![image](https://hackmd.io/_uploads/rJLRYPPgJl.png) 掛出來就可以實際連線看看,但這邊要注意,因為不是本地啟動,要用<font style='color:red;'>127.0.0.1,1433</font>連線 用localhost連不到,用:port也會連不到==要用「,」port== ![image](https://hackmd.io/_uploads/SkxPqwwg1e.png) 也可以用docker desktop查到服務 ![image](https://hackmd.io/_uploads/HkQi5wDeyx.png) 成功啟動就可以進行下一步 #### redis [DB](####DB(MSSQL)) > ==redis== > [後端](####後端服務) > [前端](####前端服務) redis就沒什麼特別的,就抓redis的公共image,然後一樣service暴露6379 ```yml= apiVersion: apps/v1 kind: Deployment metadata: name: demo-redis labels: app: demo-redis spec: replicas: 1 selector: matchLabels: app: demo-redis template: metadata: labels: app: demo-redis spec: containers: - name: demo-redis image: redis ports: - containerPort: 6379 --- apiVersion: v1 kind: Service metadata: name: demo-redis spec: selector: app: demo-redis ports: - port: 6379 targetPort: 6379 type: ClusterIP ``` 然後一樣運行==kubectl apply -f 檔案名.yml==執行 ![image](https://hackmd.io/_uploads/rkBsXdPg1l.png) 成功接著往下 #### 後端服務 [DB](####DB(MSSQL)) > [redis](####redis) > ==後端== > [前端](####前端服務) 我的後端設計是依賴於資料庫和redis,前面都配置好了再接著往下 這邊要手動配置secret的有兩個 ```bash= #後端專案有用jasypt,容器啟動要帶入密鑰 kubectl create secret generic jasypt --from-literal=JASYPT_ENCRYPTOR_PASSWORD=密鑰 #前面jenkins做的範例image是私有庫,要帶登入資訊才能使用 kubectl create secret docker-registry dockerhub \ --docker-username=<your-docker-username> \ --docker-password=<your-docker-password> \ --docker-email=<your-email> \ --docker-server=https://index.docker.io/v1/ ``` 然後一樣寫yml: ```yml= apiVersion: apps/v1 kind: Deployment metadata: name: erp-base labels: app: erp-base spec: replicas: 1 selector: matchLabels: app: erp-base template: metadata: labels: app: erp-base spec: imagePullSecrets: - name: dockerhub # 用剛剛建的secret containers: - name: erp-base image: ryanimay840121/erp-base:latest # 上面jenkins範例的image env: - name: JASYPT_ENCRYPTOR_PASSWORD valueFrom: secretKeyRef: # 用剛剛建的secret name: jasypt key: JASYPT_ENCRYPTOR_PASSWORD ports: - containerPort: 8081 --- apiVersion: v1 kind: Service metadata: name: erp-base spec: type: ClusterIP ports: - port: 8081 targetPort: 8081 selector: app: erp-base ``` 運行之後可以檢查是否有連接上依賴的內部服務,這部分就慢慢排查 #### 前端服務 [DB](####DB(MSSQL)) > [redis](####redis) > [後端](####後端服務) > ==前端== 大部分後端差不多,只是一樣要從前面jenkins做CI推Docker Registry開始, 比較特殊的部分是前端是要透過nginx做代理,nginx.conf要注意 這邊proxy_pass指向的domain就是設成k8s的<font style='color:red;'>service_name:port</font> [關於location路徑](https://nginx.org/en/docs/http/ngx_http_core_module.html#location) ```conf= server { # 監聽port(前端發出的請求) listen 8082; location / { root /usr/share/nginx/html; try_files $uri $uri/ /index.html; } location /api/ { # /api請求代理到後端 proxy_pass http://erp-base:8081/; } location /erp_base/ws { # WebSocket 代理到後端 proxy_pass http://erp-base:8081/erp_base/ws; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection "Upgrade"; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; } } ``` 然後遇到websocket的代理問題比較複雜,[可以看這裡](https://stackoverflow.com/questions/77635244/failed-to-load-resource-neterr-name-not-resolved-error) 簡單來說就是連線url要寫<font style='color:red;'>localhost:前端port</font>然後讓nginx代理到後端,像這樣 ![image](https://hackmd.io/_uploads/SJkf3Oe-kl.png) 這樣可以被nginx監聽,代理到後端websocket進行連接 ## 整合Jenkins+Kubernetes 到這邊為止,已經完成jenkins的CI部分和驗證完kubernetes的yml建構還有pod之間的溝通調通 剩下就是整合讓jenkins的pipeline調度k8s進行建構的操作 總體來說運行環境上應該會有jenkins + kubernetes(不一定都是同在docker上甚至不同機器),分別負責CI/CD,所有容器都是透過這兩部分進行操作,然後要讓流程自動化,就是讓負責CI的jenkins去觸發k8s打包部屬 要在Jenkins中調用到k8s(遠端),就要配置相關憑證 先在Jenkins中下載插件 ![image](https://hackmd.io/_uploads/rkK2ccgZJl.png) 安裝完之後打開管理,新增一個cloud ![image](https://hackmd.io/_uploads/S11Y1jlWkg.png) ![image](https://hackmd.io/_uploads/Hkepgixb1e.png) 就可以看到要配置的相關屬性 然後在k8s端下指令,獲取k8s配置,這邊是直接輸出到本地文件 ```bash= kubectl config view --minify --flatten > ./yourPath/kubeconfig.yml ``` 打開文件可以看到相關訊息 ![image](https://hackmd.io/_uploads/HkiP35lbye.png) 畫線是比較重要的部分 * certificate-authority-data: 輸入指令,把cluster訊息轉成base64證書格式 ```bash= echo <certificate-authority-data> | base64 -d > ./ca.crt ``` 然後把輸出的文件內容複製到jenkins的cloud配置上 ![image](https://hackmd.io/_uploads/rJsU8oeZ1l.png) 完成後再來是client-certificate * client-certificate: 一樣輸入指令,把user相關訊息包成base64證書 ```bash= echo <client-certificate-data> | base64 -d > ./client.crt echo <client-key-data> | base64 -d > ./client.key ``` 然後把剛剛產出的三個文件包成PKC,特別注意<font style='color:red;'>-passout pass:輸入密碼</font>,打包時要帶上密碼 ```bash= openssl pkcs12 -export -out ./cert.pfx -inkey ./client.key -in ./client.crt -certfile ./ca.crt -passout pass:輸入密碼 ``` 輸出成功就會長這樣 ![image](https://hackmd.io/_uploads/S1MZhjx-Jg.png) 之後回到Jenkins的cloud配置頁,點選新增cridentials ![image](https://hackmd.io/_uploads/B1jQ6jxZyx.png =30%x) 選擇上傳PKC並且加上剛剛建檔時的密碼,應該就可以解析內容了,好了就新增 ![image](https://hackmd.io/_uploads/HyMkknxW1e.png =80%x) 點擊test Connection就可以看到有沒有成功連上k8s ![image](https://hackmd.io/_uploads/rkkUx2gZJx.png) 現在已經連上遠端k8s了,剩下就是直接把原先建立好的pipeline語法加上kubectl部署的區塊 然後需要做CI/CD的主要是前後端專案,這部分建構的yml我是推上git所以CI拉sourceCode之後可以直接再jenkins的workspace拿到檔案,直接使用就好 接下來部分比較特別,jenkins調用k8s的方式不是直接執行指令,比較像是遠端進入k8s內(所以前面才要配置Cloud),創建一個<font style='color:red;'>臨時pod(默認名稱==jnlp==)</font>用來執行pipeline指令,所以因為是獨立pod,要使用kubectl指令有兩種方式: 1. ==必須先在默認容器內安裝kubectl== 2. 創建另一個容器以kubectl為鏡像,執行指令時指定kubectl容器 這邊是用第一個方法,按步驟來,先修改之前建立的Jenkinsfile(以後端專案為例): ```yml= pipeline { agent any stages { #... #...先前段落 #新增以下區塊,部屬 stage('Deploy to Kubernetes') { #這邊指定到kubernetes agent { kubernetes { #pod命名 label 'k8s-agent' #使用默認容器 defaultContainer 'jnlp' } } steps { echo "Installing kubectl..." //安裝kubectl sh 'curl -LO "https://dl.k8s.io/release/$(curl -L -s https://dl.k8s.io/release/stable.txt)/bin/linux/amd64/kubectl"' sh 'chmod u+x ./kubectl' //用kubectl執行 echo "Deploying to Kubernetes..." sh './kubectl apply -f ./erp-base-deployment.yml' echo "Deployment applied successfully!" } } } ``` 如果是在本地執行測試,這邊運行後碰到的第一個錯誤應該是==localhost==, 因為在jenkins中是沒辦法辨識localhost的,要修改系統的URL ![image](https://hackmd.io/_uploads/rJsxsMMbyl.png) ![image](https://hackmd.io/_uploads/Skpfjzfbyg.png) 如果是本地localhost,這邊要用<font style='color:red;'>host.docker.internal:port</font>才能找到 改完之後再運行一次,這邊就可以看到自動創建的臨時pod和相關資訊 ![image](https://hackmd.io/_uploads/HyaS3GfZJe.png) 就是剛剛Jenkinsfile寫的pod名和剛剛配置的URL,有成功連到 然後再往下執行,應該是會在==kubectl apply -f==指令發生錯誤,顯示權限不足 因為jenkins默認應該是default角色,只有讀取權限,像這樣 ![image](https://hackmd.io/_uploads/ryw3vffZye.png) 但因為進入k8s時的角色默認就是沒有編輯權限了,所以也沒辦法在Jenkinsfile中直接做修改權限 要在kubernetes的服務器上運行這段 ```bash= kubectl create clusterrolebinding default-admin --clusterrole=cluster-admin --serviceaccount=default:default ``` 內容是直接開放所有權限給default角色 ![image](https://hackmd.io/_uploads/Hy7t_MfbJl.png) 再執行一次作業就可以成功操作了,讚!! 但這邊還是有一些待改善的地方: 1. 關於SourceCode,Jenkins默認是會在每次碰到agent就會拉取一次確定最新源碼, 就拿這邊來說,最一開始會pull code、打包建構丟上dockerhub,後續進入k8s容器時會再pull一次源碼,目前寫法是把Jenkinsfile和專案源碼放在同一個Git倉庫,拉兩次一樣的東西從理論上來說因為是環境區隔所以好像是必要的,但總感覺怪怪的 2. 後續期望是丟到AWS或GCP上做Global Web Application,再慢慢更新 ## kubernetes-dashboard 額外提一下dashboard,是k8s的UI介面,可以看到k8s內相關服務或是配置 使用方式需要另外安裝 ```shell= kubectl apply -f https://raw.githubusercontent.com/kubernetes/dashboard/v2.5.1/aio/deploy/recommended.yaml ``` 安裝完成可以下指令確認,<font style='color:red;'>官方版本v2.x.x之後,安裝默認namespave是kubernetes-dashboard</font> ```bash= kubectl get all -n kubernetes-dashboard ``` 應該就可以看到相關服務了,再來就是運行指令 ```bash= kubectl proxy ``` 可以直接在本地訪問kubernetes API和dashboard 訪問路徑[http://localhost:8001/api/v1/namespaces/kubernetes-dashboard/services/https:kubernetes-dashboard:/proxy/](http://localhost:8001/api/v1/namespaces/kubernetes-dashboard/services/https:kubernetes-dashboard:/proxy/) 應該就可以看到dashboard介面,但是需要權限進入 ![image](https://hackmd.io/_uploads/rkC294D-kx.png) 這邊選擇用Token的方式,接下來就是要創建一個用於觀看dashboard的帳戶 寫一個yml來創建 ```yml= #建ServiceAccount, namespace用kubernetes-dashboard apiVersion: v1 kind: ServiceAccount metadata: name: dashboard-admin-sa namespace: kubernetes-dashboard --- #基於Kubernetes RBAC做權限配置 apiVersion: rbac.authorization.k8s.io/v1 #給ClusterRoleBinding的權限,因為要看整個k8s集群內所有服務資源 kind: ClusterRoleBinding #命名 metadata: name: dashboard-admin-sa-binding #指定上面建的ServiceAccount subjects: - kind: ServiceAccount name: dashboard-admin-sa namespace: kubernetes-dashboard #給定最高權限的cluster-admin角色 roleRef: kind: ClusterRole name: cluster-admin apiGroup: rbac.authorization.k8s.io ``` 然後運行指令 ```bash= #執行yml創建 kubectl apply -f xxx.yml #確認serviceaccount kubectl get serviceaccount -n kubernetes-dashboard ``` 這樣應該就可以看到有新增了帳戶 那再官方版本v1.x.x之前,都是創建帳戶就會自動建立Token但後來因為怕安全疑慮就改掉了 ![image](https://hackmd.io/_uploads/B1xRiEDZkg.png) 改版之前應該都是會自動創建secret,現在就都需要手動創建Token來登入 下指令 ```bash= #kubectl create token serviceaccount名稱 -n namespace kubectl create token dashboard-admin-sa -n kubernetes-dashboard ``` 終端上應該就會出現一串密文,直接複製上dashboard的登入UI,就可以成功登入 ![image](https://hackmd.io/_uploads/rktj34vWkg.png) 這樣就可以直接用UI看相關服務和配置,==本地測試要記得開kubctl proxy== 雲端服務一般就是會配nginx或是ingress ## Docker-compose、Dockerfile 單純用docker運行容器的過程紀錄 如果不做jenkin或是k8s就是看這 ### MSSQL 如果原先本地有啟動MSSQL的話,是會默認啟動的, 如果要改用docker起要手動關閉本地MSSQL不然會占用1433 ![image](https://hackmd.io/_uploads/Sy5LGp71yx.png) ▲組態管理員,要手動右鍵關閉 然後就可以開始操作docker 詳細也可以看官方說明,有每個指定參數的詳細解說 https://learn.microsoft.com/zh-tw/sql/linux/quickstart-install-connect-docker?view=sql-server-ver16&tabs=cli&pivots=cs1-bash cmd指令 ``` docker pull mcr.microsoft.com/mssql/server:2022-latest ``` ▲先抓mssql images ``` docker run -e "ACCEPT_EULA=Y" \ -e "MSSQL_SA_PASSWORD=" \ -p 1433:1433 \ --name mssql \ -d mcr.microsoft.com/mssql/server:2022-latest ``` ▲建Container 然後寫個初始化sql(範例名稱:init.sql), 後續可以綁到docker-Compose上 ```sql= IF NOT EXISTS (SELECT * FROM sys.databases WHERE name = 'dbname') BEGIN CREATE DATABASE dbname COLLATE Latin1_General_100_CI_AS_SC_UTF8; END ``` !! 特別注意<font style='color:red;'>COLLATE Latin1_General_100_CI_AS_SC_UTF8</font> MSSQL建庫的時候要指定編碼,不然預設編碼是SQL_Latin1_General_CP1_CI_AS 中文都會變成亂碼 整理成docker-compose如下 ```yaml= version: '3.8' services: mssql: image: mcr.microsoft.com/mssql/server:2022-latest container_name: mssql environment: #不確定有沒有差別,但就是指定編碼 - LANG=C.UTF-8 - LC_ALL=C.UTF-8 - ACCEPT_EULA=Y - MSSQL_SA_PASSWORD=${MSSQL_SA_PASSWORD} ports: - "1433:1433" volumes: #持久化 - sqlserver_data:/var/opt/mssql #想用預設執行的方式 - ./init.sql:/docker-entrypoint-initdb.d/init.sql networks: - my-network networks: my-network: driver: bridge ``` ${MSSQL_SA_PASSWORD}是寫再bash的環境變數 ![image](https://hackmd.io/_uploads/SJrpj_iyJg.png) 然後實際運行會發現一個問題,MSSQL默認是沒有執行初始化sql的方式 docker-entrypoint-initdb.d是針對PostgreSQL和MySQL的初始化方式 後續參考這篇 https://stackoverflow.com/questions/69941444/how-to-have-docker-compose-init-a-sql-server-database 用healthcheck的方式,綁定depends_on觸發command執行,完整如下 ```yaml= version: '3.8' services: mssql: image: mcr.microsoft.com/mssql/server:2022-latest container_name: mssql environment: - ACCEPT_EULA=Y - MSSQL_SA_PASSWORD=${MSSQL_SA_PASSWORD} ports: - "1433:1433" volumes: - sqlserver_data:/var/opt/mssql - ./init.sql:/docker-entrypoint-initdb.d/init.sql networks: - my-network healthcheck: test: ["CMD-SHELL", "/opt/mssql-tools18/bin/sqlcmd -S mssql -U sa -P ${MSSQL_SA_PASSWORD} -Q 'SELECT 1' -C"] interval: 10s timeout: 5s retries: 10 mssql.configurator: image: mcr.microsoft.com/mssql/server:2022-latest depends_on: mssql: condition: service_healthy volumes: - ./init.sql:/docker-entrypoint-initdb.d/init.sql command: > bash -c ' /opt/mssql-tools18/bin/sqlcmd -S mssql -U sa -P ${MSSQL_SA_PASSWORD} -d master -i /docker-entrypoint-initdb.d/init.sql -C; echo "mssql done!"; ' networks: - my-network networks: my-network: driver: bridge ``` 要注意的地方有兩點 1. sqlcmd的位置可能會依mssql版本不同有差異,要檢查容器下,像這邊就是在 /opt/mssql-tools18/bin/sqlcmd ![image](https://hackmd.io/_uploads/r1kq2dsykx.png) 2. docker起mssql默認連接方式是SSL/TLS,用指令操作加上-C直接忽略證書驗證,這可能會有安全隱患但這方面不熟悉,就先這樣跳過 ### Redis 沒什麼特別的, 就直接下指令 ``` docker pull redis-latest ``` ▲先抓mssql images ```yaml= version: '3.8' services: redis: image: redis container_name: demo-redis ports: - "6379:6379" networks: - my-network networks: my-network: driver: bridge ``` ▲整理成docker-compose ### Springboot專案(後端) 測試是先手動打包jar來測試,後續再搭配jenkins做自動部屬 ```bash= #依專案開發使用的版本 FROM openjdk:17-jdk-alpine WORKDIR /app COPY erp-0.0.1.jar /app/erp-0.0.1.jar EXPOSE 8081 ENTRYPOINT ["java", "-jar", "erp-0.0.1.jar"] ``` ▲先做dockerfile ```yaml= version: '3.8' services: erp-base: build: context: erp-base dockerfile: dockerfile container_name: erp-base image: erp-base ports: - "8081:8081" environment: - JASYPT_ENCRYPTOR_PASSWORD=${JASYPT_ENCRYPTOR_PASSWORD} networks: - my-network networks: my-network: driver: bridge ``` ▲整理成docker-compose 因為專案有使用jsaypt做脫敏,啟動要帶入密鑰,這邊用${JASYPT_ENCRYPTOR_PASSWORD} 傳入,寫在bash的環境變數 ### Vue3專案(前端) 前端部署相對比較複雜一點,不能像後端專案一樣打包jar部屬,因為全部靜態資源都要用到,感覺比較像是把所有前端資源丟給nginx,讓nginx啟動 整體流程大概就是: -> 前端Vue專案開發完成 -> Vue經過編譯、打包成靜態資源文件(.js、.css 文件) -> 放在nginx底下的靜態資源目錄(/usr/share/nginx/html),給nginx作為靜態資源託管 -> 啟動nginx 所以我的作法是初始部屬(編譯打包)直接用本地專案資料夾,後續再交給jenkins做自動部署 1. 先做nginx.conf ```bash= server { #監聽本地端口localhost:80(前端) listen 80; server_name localhost; location / { root /usr/share/nginx/html; try_files $uri $uri/ /index.html; } location /api/ { # /api請求代理到後端,這邊要用[服務器名稱:port] proxy_pass http://erp-base:8081/; } } ``` 2. dockerfile(放在本地vue3專案資料夾內,這樣下面路徑設置都是依照前端專案為root) ```bash= #用node做建構 FROM node:16 #工作目錄 WORKDIR /app #複製相關依賴文件到工作目錄 COPY package*.json ./ #npm抓取相關依賴 RUN npm install #複製專案所有程式碼到工作目錄 COPY . . #編譯建構所有Vue.js程式為靜態資源,放在工作目錄/app/dist下 RUN npm run build #建構nginx FROM nginx:alpine #把先前工作目錄下編譯的所有靜態資源複製到nginx下 COPY --from=0 /app/dist /usr/share/nginx/html #配置nginx相關代理配置 COPY nginx.conf /etc/nginx/conf.d/default.conf EXPOSE 80 #運行nginx CMD ["nginx", "-g", "daemon off;"] ``` 3. 一樣寫進docke-compose ```yaml= version: '3.8' services: erp-view: build: context: erp-view dockerfile: dockerfile container_name: erp-view image: erp-view ports: - "80:80" networks: - my-network networks: my-network: driver: bridge ``` ## 一些奇怪的問題 紀錄一下開發中遇到一些奇怪的問題 ### Windows系統Git大小寫 windows系統是大小寫不敏感,意思就是在系統中是會判定Filename.txt和filename.txt是相同的 在操作git的時候就發現問題 ![image](https://hackmd.io/_uploads/rkJjpoOgJl.png) 本地明明文件都找得到但是遠端操作就會顯示路徑錯誤 ![image](https://hackmd.io/_uploads/H1Q_6iOeJg.png) 實際開git bash就可以看到,git中的名稱是大寫開頭,導致bash是找不到文件的 ![image](https://hackmd.io/_uploads/SyVUaoOeJl.png) 所以要手動操作git覆蓋檔案重推,才能改掉遠端倉庫文件大小寫 第一次寫vue對命名方式不太了解的後果XD