前端部署發展史

shanyue發表於2019-11-07

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

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

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

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

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

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

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

如果本篇文章能夠對你有所幫助,可以幫我在 shfshanyue/op-note 上點個 star

刀耕火種

一臺跳板機

一臺生產環境伺服器

一份部署指令碼

前端調著他的 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

使用 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 的東西。

道阻且長,行則將至。

相關文章


我是山月,一個喜歡跑步與爬山的程式設計師,我會定期分享全棧文章在個人公眾號中,歡迎交流

歡迎關注公眾號山月行,我會定期分享一些前後端以及運維的文章

相關文章