前端部署發展史

  • 2019 年 11 月 13 日
  • 筆記

前端一說起刀耕火種,那肯定緊隨着前端工程化這一話題。隨着 react/vue/angulares6+webpackbabeltypescript 以及 node 的發展,前端已經在逐漸替代過去 scriptcdn 開發的方式了,掀起了工程化這一大浪潮。得益於工程化的發展與開源社區的良好生態,前端應用的可用性與效率得到了很大提高。

前端以前是刀耕火種,那前端應用部署在以前也是刀耕火種。那前端應用部署的發展得益於什麼,隨前端工程化帶來的副產品?

這只是一部分,而更重要的原因是 devops 的崛起。

為了更清晰地理解前端部署的發展史,了解部署時運維和前端(或者更廣泛地說,業務開發人員)的職責劃分,當每次前端部署發生改變時,可以思考兩個問題

  1. 緩存,前端應用中http 的 response header 由誰來配?得益於工程化發展,可以對打包後得到帶有 hash 值的文件可以做永久緩存
  2. 跨域,/api 的代理配置由誰來配?在開發環境前端可以開個小服務,啟用 webpack-dev-server 配置跨域,那生產環境呢

這兩個問題都是前端面試時的高頻問題,但話語權是否掌握在前端手裡

時間來到 React 剛剛發展起來的這一年,這時已經使用 React 開發應用,使用 webpack 來打包。但是前端部署,仍是刀耕火種

刀耕火種

一台跳板機

一台生產環境服務器

一份部署腳本

前端調着他的 webpack,開心地給運維發了部署郵件並附了一份部署腳本,想着第一次不用套後端的模板,第一次前端可以獨立部署。想着自己基礎盤進一步擴大,前端不禁開心地笑了

運維照着着前端發過來的部署郵件,一遍又一遍地拉着代碼,改着配置,寫着 try_files, 配着 proxy_pass

這時候,前端靜態文件由 nginx 託管,nginx 配置文件大致長這個樣子

server {    listen 80;    server_name shanyue.tech;      location / {      # 避免非root路徑404      try_files $uri $uri/ /index.html;    }      # 解決跨域    location /api {      proxy_pass http://api.shanyue.tech;    }      # 為帶 hash 值的文件配置永久緩存    location ~* .(?:css|js)$ {        try_files $uri =404;        expires 1y;        add_header Cache-Control "public";    }      location ~ ^.+..+$ {        try_files $uri =404;    }  }

不過…經常有時候跑不起來

運維抱怨着前端的部署腳本沒有標好 node 版本,前端嚷嚷着測試環境沒問題

這個時候運維需要費很多心力放在部署上,甚至測試環境的部署上,前端也要費很多心力放在運維如何部署上。這個時候由於怕影響線上環境,上線往往選擇在深夜,前端和運維身心俱疲

不過向來如此

魯迅說,向來如此,那便對么。

這個時候,無論跨域的配置還是緩存的配置,都是運維來管理,運維不懂前端。但配置方式卻是前端在提供,而前端並不熟悉 nginx

使用 docker 構建鏡像

docker 的引進,很大程度地解決了部署腳本跑不了這個大BUG。dockerfile 即部署腳本,部署腳本即 dockerfile。這也很大程度緩解了前端與運維的摩擦,畢竟前端越來越靠譜了,至少部署腳本沒有問題了 (笑

這時候,前端不再提供靜態資源,而是提供服務,一個 http 服務

前端寫的 dockerfile 大致長這個樣子

FROM node:alpine    # 代表生產環境  ENV PROJECT_ENV production  # 許多 package 會根據此環境變量,做出不同的行為  # 另外,在 webpack 中打包也會根據此環境變量做出優化,但是 create-react-app 在打包時會寫死該環境變量  ENV NODE_ENV production  WORKDIR /code  ADD . /code  RUN npm install && npm run build && npm install -g http-server  EXPOSE 80    CMD http-server ./public -p 80

單單有 dockerfile 也跑不起來,另外前端也開始維護一個 docker-compose.yaml,交給運維執行命令 docker-compose up -d 啟動前端應用。前端第一次寫 dockerfiledocker-compose.yaml,在部署流程中扮演的角色越來越重要。想着自己基礎盤進一步擴大,前端又不禁開心地笑了

version: "3"  services:    shici:      build: .      expose:        - 80

運維的 nginx 配置文件大致長這個樣子

server {    listen 80;    server_name shanyue.tech;      location / {      proxy_pass http://static.shanyue.tech;    }      location /api {      proxy_pass http://api.shanyue.tech;    }  }

運維除了配置 nginx 之外,還要執行一個命令: docker-compose up -d

這時候再思考文章最前面兩個問題

  1. 緩存,由於從靜態文件轉換為服務,緩存開始交由前端控制 (但是鏡像中的 http-server 不太適合做這件事情)
  2. 跨域,跨域仍由運維在 nginx 中配置

前端可以做他應該做的事情中的一部分了,這是一件令人開心的事情

當然,前端對於 dockerfile 的改進也是一個慢慢演進的過程,那這個時候鏡像有什麼問題呢?

  1. 構建鏡像體積過大
  2. 構建鏡像時間過長

使用多階段構建優化鏡像

這中間其實經歷了不少坎坷,其中過程如何,詳見我的另一篇文章: 如何使用 docker 部署前端應用

其中主要的優化也是在上述所提到的兩個方面

  1. 構建鏡像體積由 1G+ 變為 10M+
  2. 構建鏡像時間由 5min+ 變為 1min (視項目複雜程度,大部分時間在構建時間與上傳靜態資源時間)
FROM node:alpine as builder    ENV PROJECT_ENV production  ENV NODE_ENV production    WORKDIR /code    ADD package.json /code  RUN npm install --production    ADD . /code    # npm run uploadCdn 是把靜態資源上傳至 oss 上的腳本文件,將來會使用 cdn 對 oss 加速  RUN npm run build && npm run uploadCdn    # 選擇更小體積的基礎鏡像  FROM nginx:alpine  COPY --from=builder code/public/index.html code/public/favicon.ico /usr/share/nginx/html/  COPY --from=builder code/public/static /usr/share/nginx/html/static

那它怎麼做的

  1. ADD package.json /code, 再 npm install --production 之後 Add 所有文件。充分利用鏡像緩存,減少構建時間
  2. 多階段構建,大大減小鏡像體積

另外還可以有一些小優化,如

  • npm cache 的基礎鏡像或者 npm 私有倉庫,減少 npm install 時間,減小構建時間
  • npm install --production 只裝必要的包

前端看着自己優化的 dockerfile,想着前幾天還被運維吵,說什麼磁盤一半的空間都被前端的鏡像給佔了,想着自己節省了前端鏡像幾個數量級的體積,為公司好像省了不少服務器的開銷,想着自己的基礎盤進一步擴大,又不禁開心的笑了

這時候再思考文章最前面兩個問題

  1. 緩存,緩存由前端控制,緩存在oss上設置,將會使用 cdn 對 oss 加速。此時緩存由前端寫腳本控制
  2. 跨域,跨域仍由運維在 nginx 中配置

CI/CD 與 gitlab

此時前端成就感爆棚,運維呢?運維還在一遍一遍地上線,重複着一遍又一遍的三個動作用來部署

  1. 拉代碼
  2. docker-compose up -d
  3. 重啟 nginx

運維覺得再也不能這麼下去了,於是他引進了 CI: 與現有代碼倉庫 gitlab 配套的 gitlab ci

  • CIContinuous Integration,持續集成
  • CDContinuous Delivery,持續交付

重要的不是 CI/CD 是什麼,重要的是現在運維不用跟着業務上線走了,不需要一直盯着前端部署了。這些都是 CI/CD 的事情了,它被用來做自動化部署。上述提到的三件事交給了 CI/CD

.gitlab-ci.ymlgitlab 的 CI 配置文件,它大概長這個樣子

deploy:    stage: deploy    only:      - master    script:      - docker-compose up --build -d    tags:      - shell

CI/CD 不僅僅更解放了業務項目的部署,也在交付之前大大加強了業務代碼的質量,它可以用來 linttestpackage 安全檢查,甚至多特性多環境部署,我將會在我以後的文章寫這部分事情

我的一個服務器渲染項目 shfshanyue/shici 以前在我的服務器中就是以 docker/docker-compose/gitlab-ci 的方式部署,有興趣的可以看看它的配置文件

如果你有個人服務器的話,也建議你做一個自己感興趣的前端應用和配套的後端接口服務,並且配套 CI/CD 把它部署在自己的自己服務器上

而你如果希望結合 githubCI/CD,那可以試一試 github + github action

另外,也可以試試 drone.ci,如何部署可以參考我以前的文章: github 上持續集成方案 drone 的簡介及部署

使用 kubernetes 部署

隨着業務越來越大,鏡像越來越多,docker-compose 已經不太能應付,kubernetes 應時而出。這時服務器也從1台變成了多台,多台服務器就會有分佈式問題

一門新技術的出現,在解決以前問題的同時也會引進複雜性。

k8s 部署的好處很明顯: 健康檢查,滾動升級,彈性擴容,快速回滾,資源限制,完善的監控等等

那現在遇到的新問題是什麼?

構建鏡像的服務器,提供容器服務的服務器,做持續集成的服務器是一台!

需要一個私有的鏡像倉庫,這是運維的事情,harbor 很快就被運維搭建好了,但是對於前端部署來說,複雜性又提高了

先來看看以前的流程:

  1. 前端配置 dockerfiledocker-compose
  2. 生產環境服務器的 CI runner 拉代碼(可以看做以前的運維),docker-compose up -d 啟動服務。然後再重啟 nginx,做反向代理,對外提供服務

以前的流程有一個問題: 構建鏡像的服務器,提供容器服務的服務器,做持續集成的服務器是一台!,所以需要一個私有的鏡像倉庫,一個能夠訪問 k8s 集群的持續集成服務器

流程改進之後結合 k8s 的流程如下

  1. 前端配置 dockerfile,構建鏡像,推到鏡像倉庫
  2. 運維為前端應用配置 k8s 的資源配置文件,kubectl apply -f 時會重新拉取鏡像,部署資源

運維問前端,需不需要再擴大下你的基礎盤,寫一寫前端的 k8s 資源配置文件,並且列了幾篇文章

前端看了看後端十幾個 k8s 配置文件之後,搖搖頭說算了算了

這個時候,gitlab-ci.yaml 差不多長這個樣子,配置文件的權限由運維一人管理

deploy:    stage: deploy    only:      - master    script:      - docker build -t harbor.shanyue.tech/fe/shanyue      - docker push harbor.shanyue.tech/fe/shanyue      - kubectl apply -f https://k8s-config.default.svc.cluster.local/shanyue.yaml    tags:      - shell

這時候再思考文章最前面兩個問題

  1. 緩存,緩存由前端控制
  2. 跨域,跨域仍由運維控制,在後端 k8s 資源的配置文件中控制 Ingress

使用 helm 部署

這時前端與運維已不太往來,除了偶爾新起項目需要運維幫個忙以外

但好景不長,突然有一天,前端發現自己連個環境變量都沒法傳!於是經常找運維修改配置文件,運維也不勝其煩

於是有了 helm,如果用一句話解釋它,那它就是一個帶有模板功能的 k8s 資源配置文件。作為前端,你只需要填參數。更多詳細的內容可以參考我以前的文章 使用 helm 部署 k8s 資源

假如我們使用 bitnami/nginx 作為 helm chart,前端可能寫的配置文件長這個樣子

image:    registry: harbor.shanyue.tech    repository: fe/shanyue    tag: 8a9ac0    ingress:    enabled: true    hosts:    - name: shanyue.tech      path: /      tls:    - hosts:        - shanyue.tech      secretName: shanyue-tls        # livenessProbe:      #   httpGet:      #     path: /      #     port: http      #   initialDelaySeconds: 30      #   timeoutSeconds: 5      #   failureThreshold: 6      #      # readinessProbe:      #   httpGet:      #     path: /      #     port: http      #   initialDelaySeconds: 5      #   timeoutSeconds: 3      #   periodSeconds: 5

這時候再思考文章最前面兩個問題

  1. 緩存,緩存由前端控制
  2. 跨域,跨域由後端控制,配置在後端 Chart 的配置文件 values.yaml

到了這時前端和運維的職責所在呢?

前端需要做的事情有:

  1. 寫前端構建的 dockerfile,這只是一次性的工作,而且有了參考
  2. 使用 helm 部署時指定參數

那運維要做的事情呢

  1. 提供一個供所有前端項目使用的 helm chart,甚至不用提供,如果運維比較懶那就就使用 bitnami/nginx 吧。也是一次性工作
  2. 提供一個基於 helm 的工具,禁止業務過多的權限,甚至不用提供,如果運維比較懶那就直接使用 helm

這時前端可以關注於自己的業務,運維可以關注於自己的雲原生,職責劃分從未這般清楚

統一前端部署平台

後來運維覺得前端應用的本質是一堆靜態文件,較為單一,容易統一化,來避免各個前端鏡像質量的參差不齊。於是運維準備了一個統一的 node 基礎鏡像,做了一個前端統一部署平台,而這個平台可以做什麼呢

  1. CI/CD: 當你 push 代碼到倉庫的特定分支會自動部署
  2. http headers: 你可以定製資源的 http header,從而可以做緩存優化
  3. http redirect/rewrite: 如果一個 nginx,這樣可以配置 /api,解決跨域問題
  4. hostname: 你可以設置域名
  5. CDN: 把你的靜態資源推到 CDN
  6. https: 為你準備證書
  7. Prerender: 結合 SPA,做預渲染

前端再也不需要構建鏡像,上傳 CDN 了,他只需要寫一份配置文件就可以了,大致長這個樣子

build:    command: npm run build    dist: /dist    hosts:  - name: shanyue.tech    path: /    headers:  - location: /*    values:    - cache-control: max-age=7200  - location: assets/*    values:    - cache-control: max-age=31536000    redirects:  - from : /api    to: https://api.shanyue.tech    status: 200

此時,前端只需要寫一份配置文件,就可以配置緩存,配置 proxy,做應該屬於前端做的一切,而運維也再也不需要操心前端部署的事情了

前端看着自己剛剛寫好的配置文件,悵然若失的樣子…

不過一般只有大廠會有這麼完善的前端部署平台,如果你對它有興趣,你可以嘗試下 netlify,可以參考我的文章: 使用 netlify 部署你的前端應用

服務端渲染與後端部署

大部分前端應用本質上是靜態資源,剩下的少部分就是服務端渲染了,服務端渲染的本質上是一個後端服務,它的部署可以視為後端部署

後端部署的情況更為複雜,比如

  1. 配置服務,後端需要訪問敏感數據,但又不能把敏感數據放在代碼倉庫。你可以在 environment variablesconsul 或者 k8s configmap 中維護
  2. 上下鏈路服務,你需要依賴數據庫,上游服務
  3. 訪問控制,限制 IP,黑白名單
  4. RateLimit
  5. 等等

我將在以後的文章分享如何在 k8s 中部署一個後端

小結

隨着 devops 的發展,前端部署越來越簡單,可控性也越來越高,建議所有人都稍微學習一下 devops 的東西。

道阻且長,行則將至。

相關文章