---
# System prepended metadata

title: 專案CI/CD

---

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