## 專案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

▲初始介面,從jenkins默認位置找預設密碼,輸入
```
docker exec jenkins cat /var/jenkins_home/secrets/initialAdminPassword
```

通過之後要安裝插件

這邊選擇推薦外掛就好

就等待安裝

安裝完成,建立第一個管理員帳戶

設定端點url

然後就可以開始用了
先來實驗簡單任務

點擊左上新增一個作業

比較常用的是FreeStyle和Pipeline
* FreeStyle:
大部分功能都有UI支援,操作比較簡單,適合小專案
* [Pipeline](###Pipeline):
使用Groovy語法寫操作,功能多,可以做並行或是條件判斷,可以整合其他工具,可以方便做版控(Jenkinsfile),適合較複雜的專案
這邊先選FreeStyle

輸入Git的repository url和要關注的分支
<font style='color:red;'>這邊的作用是,當作業觸發時,會自動拉取指定Repository的指定分支的程式碼
到Jenkins的工作倉庫(默認路徑/var/jenkins_home/workspace/該作業名稱/)</font>
建置方式選Git,Build Steps選Shell



這邊先暫時輸入shell的提示字,簡單測試
之後存檔,會跳轉到新建專案的首頁,點選馬上建置

---

建置歷程就會跑出執行紀錄
點擊進入後可以看到這次觸發執行的詳細內容,

點擊主控台輸出(Console output),可以看到專案的終端印出剛剛輸入的提示字
就是剛剛shell輸入的信息

代表有成功調通,
<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暴露給外網

直接下載的方式會沒辦法運行,測試都是要帶入token所以要先註冊登入
會綁google authenticator,註冊過程就不多說
登入後看到登入頁面有載點

點擊下載並且解壓縮會有一個exe檔案

點擊運行會開啟cmd,複製ngrok下載頁面底下會有token設定的語法,直接複製執行

之後就是cmd直接下語法,範例是綁定localhost:8080

送出之後會顯示ngrok相關資訊並掛起本地相對port,給出一個對外域名

複製之後可以直接貼在瀏覽器上測試,應該就可以靠這段域名連上本地8080了(jenkins)

這樣就取得了一段公開域名,可以來設置Github的webhook
先開啟Github要綁的repository,點選Setting中的webhook,Add webhook

輸入密碼驗證後,可以看到上面的敘述,簡單來說就是repository會發送請求
下面可以選擇細節(發送路徑、請求格式、觸發場合)
<font style='color:red;'>Payload URL的部分放上剛剛ngrok產出的公開域名,並且結尾要加上/github-webhook/</font>
event就選push吧

好了之後存檔,webhook就建立完成了,<font style='color:green;'>√綠勾勾</font>代表有接通

然後就可以來測試了
實際在專案下個commit推看看

push之後jenkins上的對應作業應該就可以看到建置歷程有新動作

一樣點擊Console output就可以看到該次觸發的詳細內容

然後可以手動看docker的jenkins容器下
(默認路徑/var/jenkins_home/workspace/作業名稱)有沒有成功拉到SourceCode

也可以用DockerDesktop看

這樣就大致完成了第一步的==Github Webhook觸發Jenkins==
### 2._CI打包上Dockerhub
已經確定可以用Jenkins監控git做到改動即時觸發作業,接下來要做的事情就是在jenkins上
操作docker打包並且把docker images推上dockerhub給其他服務使用
1. 先來測試打包,這邊是用springboot+mvn專案,因為要用mvn package所以jenkins內必須安裝maven,一開始範例建立jenkins的dockerfile已經有先裝好了
這邊測試就直接點擊組態

修改觸發時的shell,加上打包指令

實際觸發就會發現運行了mvn打包,也可以順帶運行程式碼中的自動測試,確保每次git push沒有爆炸

可以看到開始建構專案

測試打包成功,可以進到工作目錄看看target底下有沒有打包的jar

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/),註冊流程這邊就不提

註冊完成登入後,點選repository,創建一個空倉庫,這邊用private就好

創建就填個倉庫名<font style='color:red;'>※免費帳戶的private庫僅限1個,所以如果有其他專案就只能公開了</font>
不然就是要用其他docker repository()

保存之後repository內就可以看到範例的推送指令,這邊先告一個段落

再來是Jenkins內要配置Dockerhub的帳密,當然是也可以直接寫在指令內但怕有安全疑慮
Jenkins首頁點選管理Jenkins->點Cridential

直接點global新增Cridential

---

可以依照想要設置的類型選擇不同的配置
如果只是單獨一段文字(只配置密碼)應該secret text就可以了
這邊因為帳號密碼都要配置就選Username with password

主要就填帳號密碼,scope是作用域,簡單來說System是供jenkins系統內部使用,
作業沒辦法用,所以這邊選Global,詳細點 **?** 都有說明

儲存之後可以看到新增的Cridential

再來就是開啟剛剛建置的作業,開啟組態設定,找到建置環境設
然後就是Cridentials選擇剛剛新增的Cridential,並幫變數取名

這部分可以依照需求類型選擇,看是單一文字還是帳密

之後就可以在shell的地方呼叫到Jenkins內配置的Cridential,可以避免寫明文和重複動作
最後就是看到shell的區塊,原先只做到打包應該是長這樣:

現在分別加上
1. 登入dockerhub
2. dockerfile建置image
<font style='color:red;'>(這邊的dockerfile位置是放在/var/jenkins_home/workspace/該作業名資料夾/,我是直接上Git,放在第一層,拉下來就可以直接調用到)</font>
3. 把images推到dockerhub的語法
這邊指定dockerhub的語法就是(帳戶名/倉庫名:版號標籤),完整如下:

儲存完之後就可以手動觸發作業看看結果有沒有成功
登入+建置image:

推上dockerhub:

成功之後看看dockerhub的倉庫有沒有更新:

==這樣就成功完成CI的部分,本地推code之後自動跑測試 > 打包 > 更新線上的包==
### Pipeline
和Free-style比起來,使用Groovy語法寫操作,功能多,可以做並行或是條件判斷,可以整合其他工具,可以方便做版控(Jenkinsfile),適合較複雜的專案
後面預計是要整合Kubernetes做完整CI/CD,這邊先把專案類型改pipeline
一樣先新增專案,選pipeline

前面General的部分都差不多,就是填desc和Git、還有觸發方式之類的

下面pipeline就是比較特殊的了,這邊有分
* Pipeline script:
直接把Pipeline腳本寫在jenkins作業內
* Pipeline script from SCM(Source Code Management):
把Pipeline腳本寫成Jenkinsfile,放入版控,用引入檔案的方式配置操作

這邊先用==Pipeline script==,後面再加入版控
關於pipeline scrpt語法結構,[網路上很多介紹](https://ithelp.ithome.com.tw/articles/10338284),不詳細講
要提的是Pipeline Syntax,可以依需求產出語法不用自己寫

進去之後依需求選擇輸入對應資訊,Generate產出語法

一樣先簡單測試pipeline script

手動觸發,成功

接下來就是把所有內容從Freestyle搬移到pipline script
要注意,<font style='color:red;'>pipline不會自動拉取sourceCode,要自己在script區塊做</font>
整理完像這樣:

其中變數比較特別,因為是使用cridentials,類型是Username with password
要拿username和password,默認字樣是<font style='color:red;'>_USR</font>和<font style='color:red;'>_PSW</font>,
然後也可以改成用withCredentials的方式像這樣:

差異是
* 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會顯示有更改

2. 如果commit hash都沒變,代表內容沒變化,jenkins就會中斷任務,不執行後續script,也不會產出建置歷程,只能從GitHub Hook Log看到動作

:::
但以上動作都是建立於沒有詳細限制運行,只單純依靠配置的git來切分,如果要更詳細區分,Pipeline可以寫條件判斷,補如說當觸發分支為A才進行動作

這樣還可以用參數化建置的方式來直接區分手動執行的分支,每個分支要做什麼事情來區隔環境
比如說
```bash=
when{
branch 'A'
}
//跑測試
when{
branch 'B'
}
//打包
```
### Pipeline from SCM
上面簡單試完pipeline script,但有個問題就是這樣語法是存在jenkins內,不好做修改或是版控
所以接下來就是選擇pipeline的第二個選項,把語法加到版控內(git)

選擇之後就可以看到,畫面就沒有寫語法的地方了,反而是要寫git repository和分支
用意就是會從git的位置去找語法的檔案,這邊也可以指定檔案命名來做切換區分

好了就存檔,然後來改文件
至於文件,其實就只是把之前pipeline script寫的語法移到單一文件內

push之後可以測試觸發
Console output就會寫到jenkinsfile來源

就是剛剛設置的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,不會存明文

* namespace: 可以當作是k8s中的硬性隔離方式,當多專案或是多團隊共用k8s,或是區隔環境(prod、dev...),==一般情況下==,不同namespace間的資源是不能相互調用的,除非透過一些特殊配置或方式,可以用<font style='color:red;'>kubectl get</font>指令加上<font style='color:red;'>--all-namespaces</font>查看各部件所屬namespace

<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檢查

但要確定版本和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就可以排除(原因不明)

都安裝完之後可以運行<font style='color:red;'>minikube start</font>
有遇到什麼問題就再解決,這邊遇到無法拉取遠端image先不管

可以確認當前驅動VM

啟動成功
### 簡單測試
跟著[官方範例](https://minikube.sigs.k8s.io/docs/start/?arch=%2Fwindows%2Fx86-64%2Fstable%2F.exe+download)實際測試運行沒問題
* 本機有docker desktop就會以此為vm

最後測試執行成功,連線應該會顯示

### 完整建構
這邊是直接使用docker desktop內的k8s來建,雖然說是single node但夠用了
構想是yml建deployment區分服務,然後pod都先開單一個,先不管同步問題
總共4個服務依序: [DB](####DB(MSSQL)) > [redis](####redis) > [後端](####後端服務) > [前端](####前端服務)

如果是小型或是只想簡單部屬可以就採用方案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的方式做,因為配置文件後續也可能會做版控,不要直接寫明文,新增完可以下指令確認有沒有成功

然後創建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
```
運行之後會顯示到底更新或是新增了什麼

可以再下指令查詢
```bash=
kubectl get all
```

可以看到當前k8s內所有服務詳細資訊(pod, service, deployment...)
本地測試:
目前的狀態可以看到service暴露mssql的pod給集群內部調用,但是外部是連不到的
可以用==kubectl port-forward service名稱 本地port:servicePort==
把service的port掛到本地測試容器有沒有成功啟動

掛出來就可以實際連線看看,但這邊要注意,因為不是本地啟動,要用<font style='color:red;'>127.0.0.1,1433</font>連線
用localhost連不到,用:port也會連不到==要用「,」port==

也可以用docker desktop查到服務

成功啟動就可以進行下一步
#### 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==執行

成功接著往下
#### 後端服務
[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代理到後端,像這樣

這樣可以被nginx監聽,代理到後端websocket進行連接
## 整合Jenkins+Kubernetes
到這邊為止,已經完成jenkins的CI部分和驗證完kubernetes的yml建構還有pod之間的溝通調通
剩下就是整合讓jenkins的pipeline調度k8s進行建構的操作
總體來說運行環境上應該會有jenkins + kubernetes(不一定都是同在docker上甚至不同機器),分別負責CI/CD,所有容器都是透過這兩部分進行操作,然後要讓流程自動化,就是讓負責CI的jenkins去觸發k8s打包部屬
要在Jenkins中調用到k8s(遠端),就要配置相關憑證
先在Jenkins中下載插件

安裝完之後打開管理,新增一個cloud


就可以看到要配置的相關屬性
然後在k8s端下指令,獲取k8s配置,這邊是直接輸出到本地文件
```bash=
kubectl config view --minify --flatten > ./yourPath/kubeconfig.yml
```
打開文件可以看到相關訊息

畫線是比較重要的部分
* certificate-authority-data:
輸入指令,把cluster訊息轉成base64證書格式
```bash=
echo <certificate-authority-data> | base64 -d > ./ca.crt
```
然後把輸出的文件內容複製到jenkins的cloud配置上

完成後再來是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:輸入密碼
```
輸出成功就會長這樣

之後回到Jenkins的cloud配置頁,點選新增cridentials

選擇上傳PKC並且加上剛剛建檔時的密碼,應該就可以解析內容了,好了就新增

點擊test Connection就可以看到有沒有成功連上k8s

現在已經連上遠端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


如果是本地localhost,這邊要用<font style='color:red;'>host.docker.internal:port</font>才能找到
改完之後再運行一次,這邊就可以看到自動創建的臨時pod和相關資訊

就是剛剛Jenkinsfile寫的pod名和剛剛配置的URL,有成功連到
然後再往下執行,應該是會在==kubectl apply -f==指令發生錯誤,顯示權限不足
因為jenkins默認應該是default角色,只有讀取權限,像這樣

但因為進入k8s時的角色默認就是沒有編輯權限了,所以也沒辦法在Jenkinsfile中直接做修改權限
要在kubernetes的服務器上運行這段
```bash=
kubectl create clusterrolebinding default-admin --clusterrole=cluster-admin --serviceaccount=default:default
```
內容是直接開放所有權限給default角色

再執行一次作業就可以成功操作了,讚!!
但這邊還是有一些待改善的地方:
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介面,但是需要權限進入

這邊選擇用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但後來因為怕安全疑慮就改掉了

改版之前應該都是會自動創建secret,現在就都需要手動創建Token來登入
下指令
```bash=
#kubectl create token serviceaccount名稱 -n namespace
kubectl create token dashboard-admin-sa -n kubernetes-dashboard
```
終端上應該就會出現一串密文,直接複製上dashboard的登入UI,就可以成功登入

這樣就可以直接用UI看相關服務和配置,==本地測試要記得開kubctl proxy==
雲端服務一般就是會配nginx或是ingress
## Docker-compose、Dockerfile
單純用docker運行容器的過程紀錄
如果不做jenkin或是k8s就是看這
### MSSQL
如果原先本地有啟動MSSQL的話,是會默認啟動的,
如果要改用docker起要手動關閉本地MSSQL不然會占用1433

▲組態管理員,要手動右鍵關閉
然後就可以開始操作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的環境變數

然後實際運行會發現一個問題,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

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的時候就發現問題

本地明明文件都找得到但是遠端操作就會顯示路徑錯誤

實際開git bash就可以看到,git中的名稱是大寫開頭,導致bash是找不到文件的

所以要手動操作git覆蓋檔案重推,才能改掉遠端倉庫文件大小寫
第一次寫vue對命名方式不太了解的後果XD