前端自動化部署
本文參考自: 作者:yeyan1996 鏈接:https://juejin.cn/post/684516... 來源:稀土掘金 著作權歸作者所有。商業轉載請聯繫作者獲得授權,非商業轉載請註明出處。
docker簡介
開發5分鐘,打包半小時, 早已是前端的痛點, 更着, 開發者自身環境的差異會導致最終的產物也有不同
docker 可以靈活的創建/銷燬/管理多個“服務器”,這些“服務器”被稱為 容器 (container)
在容器中你可以做任何服務器可以做的事,例如在有 node 環境的容器中運行 npm run build 打包項目,在有 nginx 環境的容器中部署項目,在有 mysql 環境的容器中做數據存儲等等
一旦服務器安裝了 docker ,就可以自由創建任意多的容器,上圖中 docker 的 logo 形象的展示了它們之間的關係,🐳就是 docker,上面的一個個集裝箱就是容器
本機安裝docker
官方下載地址: Get Started with Docker | Docker
下載時最好順手也註冊一個dockerHub
我的電腦是windows, 安裝完成後點擊docker圖標啓動docker, 在終端輸入docker, 看到如下輸出則代表docker正常運行
-
補充:
在此你打開docker, 可能會看到docker是紅色的, 此時你可以在終端進入docker目錄, 運行
DockerCli.exe -SwitchDaemon-
cd "C:\Program Files\Docker\Docker"
-
DockerCli.exe -SwitchDaemon
// 執行失敗時執行
./DockerCli.exe -SwitchDaemon
如果執行上述步驟後, docker仍然是紅色, 而且無法執行docker其他命令(出現error), 例如
那就只能依照軟件提示安裝Linux內核
點擊進入網址後依照操作進行下載安裝 https://aka.ms/wsl2kernel
-
下載最新的適用於x64計算機的更新包
如果不確定自己計算機的類型, 打開PowerShell輸入
systeminfo | find "系統類型", 如果你看到如下輸出,則代表計算機是x64類型的:下載安裝完成後在PowerShell運行如下命令, 將WSL2設為默認版本
wsl --set-default-version 2
接着再次執行:
cd "C:\Program Files\Docker\Docker"
DockerCli.exe -SwitchDaemon
如果還是出現錯誤, 那就重啓電腦再試一次吧(我有一次就是這樣, 重啓之後執行上述命令就好了)
-
docker概念:
- 鏡像(image)
- 容器(container)
- 倉庫(repositiory)
容器可以類比一個服務器, 鏡像則是創建容器的模板, 一個docker鏡像可以創建多個容器
有兩種獲取鏡像的方式:
- Dockerfile文件創建
- 使用dockerHub或其他倉庫上現有的鏡像
創建docker鏡像
首先創建一個hello-docker目錄, 在目錄中創建index.html和Dockerfile文件
<!--index.html-->
<h1>Hello docker</h1>
# Dockerfile
FROM nginx
COPY index.html /usr/share/nginx/html/index.html
EXPOSE 80
-
Dockerfile內容解析:
- FROM nginx:基於官方 nginx 鏡像
- COPY index.html /usr/share/nginx/html/index.html:將當前目錄下 index.html 替換容器中 /usr/share/nginx/html/index.html 文件,
/usr/share/nginx/html是容器中 nginx 默認存放網頁文件的目錄,訪問容器 80 端口會展示該目錄下 index.html 文件 - EXPOSE 80:容器對外暴露 80 端口,主要起聲明作用,真實端口映射還需要在創建容器時定義
-
目前的文件結構:
hello-docker |____index.html |____Dockerfile
在當前目錄(項目目錄)運行以下命令創建doker鏡像
docker build . -t test-image:latest
-
命令解析:
- build:創建 docker 鏡像
- .:使用當前目錄下的 dockerfile 文件
- -t:使用 tag 標記版本
- test-image:latest:創建名為
test-image的鏡像,並標記為 latest(最新)版本
創建完成後, 可以通過docker images命令查看所有鏡像
創建docker容器
鏡像成功創建後, 運行以下命令可以創建一個docker容器
docker run -d -p 8081:80 --name test-container test-image:latest
-
命令解析:
- run:創建並運行 docker 容器
- -d: 後台運行容器
- 8081:80:將當前服務器的 8081 端口(冒號前的 8081),映射到容器的 80 端口(冒號後的 80)
- --name:給容器命名,便於之後定位容器
- test-image:latest:基於
test-image最新版本的鏡像創建容器
通過docker ps -a命令查看所有容器
現在在本地瀏覽器輸入: localhost:8081即可訪問服務內容(即項目中的index.html)
-
docker其他命令
-
停止、啓動、殺死、重啓一個容器
docker stop Name或者ID
docker start Name或者ID
docker kill Name或者ID
docker restart name或者ID -
刪除所有停止的容器
docker container prune
-
查看所有 name 以 docker 開頭的 docker 容器,並只輸出容器名
docker ps -a -f "name=^docker" --format="{{.Names}}"
-
停止 name 為 docker-container 的容器
docker stop docker-container
-
刪除 name 為 docker-container 的容器(停止狀態的容器才能被刪除)
docker rm docker-container
-
dockerHub
dockerhub是存儲鏡像的倉庫, 開發者可以將 Dockerfile 生成的鏡像上傳到 dockerhub 來存儲自定義鏡像,也可以直接使用官方提供的鏡像
-
使用官方鏡像啓動一個容器
docker pull nginx
docker run -d -p 81:80 --name nginx-container nginx第一步拉取了官方的 nginx 鏡像
第二步用基於官方 nginx 鏡像創建名為
nginx-container的容器這裏使用 81 端口映射到容器的 80 端口,訪問
localhost:81可以看到 nginx 啓動頁面
docker的好處
docker將環境統一起來, 保證生成環境和開發環境項目均能正常運行
開發者將開發環境用docker鏡像上傳到docker倉庫, 在生成環境拉取並運行相同環境即可保持環境一直
-
提交名為
docker-test-image的鏡像, 鏡像名前加上dockerhub賬號作為前綴docker push wzc520pyfm/docker-test-image:latest
-
服務器拉取賬號
wzc520pyfm下的docker-test-image鏡像docker pull wzc520pyfm/docker-test-image:latest
docker也有版本控制, 在創建鏡像時可以使用tag標記版本, 如果某個版本的環境有問題, 可以快速回滾到之前的版本
docker將項目構建需要的環境放在容器中, 與服務器隔離
容器創建和銷燬都十分高效
高效的前端自動化部署
沒有自動化的部署, 我們需要 npm run build生成構建產物(dist), 將dist文件上傳到服務器, 同時還需要將代碼提交到倉庫(團隊合作總要提交的吧).
實現自動化部署後, 我們要做的僅僅是git push提交代碼到倉庫, 其餘均由腳本自動執行.
-
腳本需要做的事:
- 自動更新鏡像
- 鏡像中自動運行
npm run build生成構建產物 - 自動創建容器
登錄Linux雲服務器
參考各大雲服務器廠商官方文檔, 附上騰訊雲CentOS登錄指南文檔: 輕量應用服務器 使用遠程登錄軟件登錄 Linux 實例 - 操作指南 - 文檔中心 - 騰訊雲 (tencent.com)
我的是騰訊雲CentOS 7.6 64位的操作系統, 學生購買雲服務器有優惠, 應該是99/年
Linux服務器安裝必要的系統工具
安裝必要工具
sudo yum install -y yum-utils
添加軟件軟件源, 使用阿里雲鏡像
sudo yum-config-manager --add-repo http://mirrors.aliyun.com/doc...
Linux安裝docker
安裝docker
sudo yum install docker-ce docker-ce-cli containerd.io
開啓docker服務
sudo systemctl start docker
運行hello-world項目
sudo docker run hello-world
如果能夠看到輸出Hello from Docker!, 證明Docker安裝成功
Linux安裝git
用於從代碼倉庫拉取代碼
yum install git
Linux安裝nvm
前端自動化部署, 那當然處理邏輯是用js來寫, node可以讓js在服務端運行
nvm: 管理node版本
官方地址: nvm-sh/nvm:節點版本管理器 - 符合 POSIX 標準的 bash 腳本,用於管理多個主動節點.js版本 (github.com)
首先運行安裝命令:
curl -o- https://raw.githubusercontent... | bash
安裝時輸出示例如下:
[root@VM-12-9-centos /]# curl -o- https://raw.githubusercontent... | bash
% Total % Received % Xferd Average Speed Time Time Time CurrentDload Upload Total Spent Left Speed100 13226 100 13226 0 0 7281 0 0:00:01 0:00:01 --:--:-- 7283
=> Downloading nvm from git to '/root/.nvm'
=> Cloning into '/root/.nvm'...
remote: Enumerating objects: 278, done.
remote: Counting objects: 100% (278/278), done.
remote: Compressing objects: 100% (245/245), done.
remote: Total 278 (delta 31), reused 101 (delta 20), pack-reused 0
Receiving objects: 100% (278/278), 142.25 KiB | 54.00 KiB/s, done.
Resolving deltas: 100% (31/31), done.
=> Compressing and cleaning up git repository=> nvm source string already in /root/.bashrc
=> Appending bash_completion source string to /root/.bashrc
=> Close and reopen your terminal to start using nvm or run the following to use it now:export NVM_DIR="$HOME/.nvm"
[ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh" # This loads nvm
[ -s "$NVM_DIR/bash_completion" ] && \. "$NVM_DIR/bash_completion" # This loads nvm bash_completion
程序自動地嘗試將環境變量添加到正確的位置, 在此之後, 我們需要手動運行使能命令,讓環境變量生效
source ~/.bashrc
驗證是否安裝成功
command -v nvm
正常輸出示例:
[root@VM-12-9-centos /]# command -v nvm
nvm
Linux安裝node
下載、編譯、安裝最新版本的node: (本次操作執行此命令)
nvm install node # "node" is an alias for the latest version
如果需要安裝特定版本的node, 請運行:
nvm install 14.7.0 # or 16.3.0, 12.22.1, etc
安裝的第一個版本將成為默認版本。新 shell 將從節點的默認版本(例如) 開始。
其他命令:
-
切換node版本
nvm use 14.17.3
-
列出已安裝的node版本
nvm ls
-
卸載指定版本的node
nvm uninstall 14.17.3
Linux安裝pm2
pm2可以讓我們的js腳本在服務器後台運行
npm i pm2 -g
創建前端項目
本地創建一個簡單的前端基礎項目
vue create docker-test
創建完成後將demo上傳到github (建議創建public共有倉庫, 這樣可以免去鑑權直接clone, 如果創建了private私有倉庫, 在運行時需要輸入密碼, 代碼運行時當然不希望這樣, 解決辦法是使用token, 參見: (37條消息) 【突發】解決remote: Support for password authentication was removed on August 13, 2021. Please use a perso_日積月累,天道酬勤-CSDN博客) , 接下來配置webhook
webhook
github倉庫有一個hook(鈎子), 它會在當前倉庫觸發某些事件時, 發送一個post形式的http請求
當倉庫有提交代碼時,通過將 webhook 請求地址指向雲服務器 IP 地址,雲服務器就能知道項目有更新,之後運行相關代碼實現自動化部署
-
配置webhook
-
測試是否配置成功
- 本地修改代碼並提交到倉庫
參數主要涉及當前倉庫和本地提交的信息,這裏我們只用 repository.name 獲取更新的倉庫名即可
請求如何處理?
當我們的服務器收到項目更新後發送的post請求後, 需要創建/更新鏡像來實現自動化部署
創建Dockerfile
在本地項目裏新建一個Dockerfile文件, 用於之後創建鏡像
# dockerfile
# build stage
FROM registry.cn-hangzhou.aliyuncs.com/dyjutil/node:v14.8.0 as build-stage
WORKDIR /app
COPY package*.json ./
RUN npm install
COPY . .
RUN npm run build
# production stage
FROM nginx:stable-alpine as production-stage
COPY --from=build-stage /app/dist /usr/share/nginx/html
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]
-
配置解析
- FROM registry.cn-hangzhou.aliyuncs.com/dyjutil/node:v14.8.0 as build-stage:採用阿里版本境像的階段命名為
build-stage - WORKDIR /app:將工作區設為 /app,和其他系統文件隔離
- COPY package*.json ./:拷貝 package.json/package-lock.json 到容器的 /app 目錄
- RUN npm install:運行
npm install在容器中安裝依賴 - COPY . .:拷貝其他文件到容器 /app 目錄,分兩次拷貝是因為保持 node_modules 一致
- RUN npm run build:運行
npm run build在容器中構建
這裏用到了 docker 一個技巧:多階段構建
將構建分為兩個階段,第一階段基於 node 鏡像,第二階段基於 nginx 鏡像
- FROM nginx:lts-alpine as production-stage:基於 nginx
stable-alpine版本鏡像,並將有 nginx 環境的階段命名為production-stage - COPY --from=build-stage /app/dist /usr/share/nginx/html:通過 --form 參數可以引用
build-stage階段生成的產物,將其複製到 /usr/share/nginx/html - EXPOSE 80:容器對外暴露 80 端口
- CMD ["nginx", "-g", "daemon off;"]:容器創建時運行
nginx -g daemon off命令,一旦 CMD 對應的命令結束,容器就會被銷燬,所以通過 daemon off 讓 nginx 一直在前台運行
- FROM registry.cn-hangzhou.aliyuncs.com/dyjutil/node:v14.8.0 as build-stage:採用阿里版本境像的階段命名為
通過scp命令, 將Dockerfile文件複製到雲服務器上
scp ./Dockerfile root@121.5.110.8:/root
創建.dockerignore
.dockerignore 可以在創建鏡像複製文件時忽略複製某些文件
在本地項目裏新建.dockerignore
# .dockerignore
node_modules
接着將.dockerignore文件也複製到雲服務器上
scp ./.dockerignore root@121.5.110.8:/root
創建http服務器並編寫自動部署腳本
使用node來開啓簡單的http服務器處理webhook發送的post請求,腳本需要包含創建http服務器、拉取倉庫代碼、創建鏡像和容器。
在本地項目新建index.js
const http = require("http");
const { execSync } = require("child_process");
const path = require("path");
const fs = require("fs");
// 遞歸刪除目錄
function deleteFolderRecursive(path) {
if (fs.existsSync(path)) {
fs.readdirSync(path).forEach(function (file) {
const curPath = path + "/" + file;
if (fs.statSync(curPath).isDirectory()) {
// recurse
deleteFolderRecursive(curPath);
} else {
// delete file
fs.unlinkSync(curPath);
}
});
fs.rmdirSync(path);
}
}
const resolvePost = (req) =>
new Promise((resolve) => {
let chunk = "";
req.on("data", (data) => {
chunk += data;
});
req.on("end", () => {
resolve(JSON.parse(chunk));
});
});
http
.createServer(async (req, res) => {
console.log("receive request");
console.log(req.url);
if (req.method === "POST" && req.url === "/") {
const data = await resolvePost(req);
const projectDir = path.resolve(`./${data.repository.name}`);
deleteFolderRecursive(projectDir);
// 拉取倉庫最新代碼 data.repository.name 即 webhook 中記錄倉庫名的屬性
execSync(
`git clone https://github.com/wzc520pyfm/${data.repository.name}.git ${projectDir}`,
{
stdio: "inherit",
}
);
// 複製 Dockerfile 到項目目錄
fs.copyFileSync(
path.resolve(`./Dockerfile`),
path.resolve(projectDir, "./Dockerfile")
);
// 複製 .dockerignore 到項目目錄
fs.copyFileSync(
path.resolve(__dirname, `./.dockerignore`),
path.resolve(projectDir, "./.dockerignore")
);
// 創建 docker 鏡像
execSync(`docker build . -t ${data.repository.name}-image:latest `, {
stdio: "inherit",
cwd: projectDir,
});
// 銷燬 docker 容器
execSync(
`docker ps -a -f "name=^${data.repository.name}-container" --format="{{.Names}}" | xargs -r docker stop | xargs -r docker rm`,
{
stdio: "inherit",
}
);
// 創建 docker 容器 -- 這裏使用了服務器的8888端口
execSync(
`docker run -d -p 8888:80 --name ${data.repository.name}-container ${data.repository.name}-image:latest`,
{
stdio: "inherit",
}
);
console.log("deploy success");
}
res.end("ok");
})
.listen(3000, () => {
console.log("server is ready");
});
在銷燬 docker 容器部分用到了 linux 的管道運算符和 xargs 命令,過濾出以 docker-test 開頭容器(用 docker-test 倉庫的代碼製作的鏡像創建的容器),停止,刪除並重新創建它們
最後, 通過scp將index上傳到雲服務器上
scp ./index.js root@121.5.110.8:/root
現在, 我們的項目結構為:
雲服務器上使用pm2運行index.js
pm2 start index.js
-
pm2命令
pm2 list查看運行的pm2服務pm2 logs查看pm2日誌pm2 flush清除pm2日誌pm2 start index.js運行index.js文件pm2 stop id停止指定id的pm2服務
小插曲
因為我們需要通過服務器的8888端口訪問部署的項目, 倉庫需要通過服務器3000端口通知服務器代碼更新, 所以服務器需要放通8888和3000端口, 下面介紹騰訊雲服務器放通端口的步驟:
接着就可以訪問 http://服務器ip:8888 看到頁面, 如果無響應, 嘗試向倉庫push一次代碼, 查看pm2日誌是否成功拉取代碼並更新鏡像, 如果出現網絡問題無法拉取代碼, 最好是改用gitee倉庫, gitee的webhook配置訪問與github一致.
接下來對項目中的App.vue稍作更改,並提交倉庫, 測試自動化部署
重新打開 http://服務器ip:8888
可以看到內容已經更新
示例代碼
wzc520pyfm/docker-test - 碼雲 - 開源中國 (gitee.com)
關注 Dockerfile ,.dockerignore, index.js 文件
距離真實環境仍有一定差距
上述 demo 只創建了單個 docker 容器,當項目更新時,由於容器需要經過銷燬和創建的過程,會存在一段時間頁面無法訪問情況
而實際投入生產時一般會創建多個容器,並逐步更新每個容器,配合負載均衡將用户的請求映射到不同端口的容器上,確保線上的服務不會因為容器的更新而宕機
另外基於 github 平台也有非常成熟的 CI/CD 工具,例如
- travis-ci
- circleci
通過 yml 配置文件,簡化上文中註冊 webhook 和編寫更新容器的 index.js 腳本的步驟
# .travis.yml
language: node_js
node_js:
- 8
branchs:
only:
- master
cache:
directories:
- node_modules
install:
- yarn install
scripts:
- yarn test
- yarn build
另外隨着環境的增多,容器也會逐漸增加,docker 也推出了更好管理多個容器的方式 docker-compose