From fb1e0c67c4fdaeb61627c5a66dfabd636120a864 Mon Sep 17 00:00:00 2001 From: shfshanyue Date: Thu, 3 Feb 2022 16:42:24 +0800 Subject: [PATCH 1/2] update --- .github/workflows/nodejs.yml | 76 +++--- code/graphql/demo.js | 29 --- code/graphql/demo2.js | 18 -- code/graphql/index.md | 51 ---- frontend-engineering/deploy/ci-ci.md | 25 +- frontend-engineering/deploy/ci-env.md | 45 +++- frontend-engineering/deploy/ci-intro.md | 13 +- frontend-engineering/deploy/ci-preview.md | 223 +++++++++++++++--- frontend-engineering/deploy/cra-docker.md | 2 + frontend-engineering/deploy/cra-route.md | 31 ++- frontend-engineering/deploy/end.md | 1 + frontend-engineering/deploy/index.md | 43 ++-- frontend-engineering/deploy/k8s-intro.md | 83 ++++++- frontend-engineering/deploy/k8s-rollback.md | 57 ----- frontend-engineering/deploy/oss-rclone.md | 130 ++++++++-- frontend-engineering/deploy/oss.md | 91 +++++-- frontend-engineering/deploy/simple-intro.md | 62 +++-- frontend-engineering/deploy/simple-nginx.md | 28 ++- frontend-engineering/deploy/traefik-domain.md | 45 +++- frontend-engineering/deploy/traefik.md | 39 ++- package.json | 2 +- 21 files changed, 744 insertions(+), 350 deletions(-) delete mode 100644 code/graphql/demo.js delete mode 100644 code/graphql/demo2.js delete mode 100644 code/graphql/index.md create mode 100644 frontend-engineering/deploy/end.md delete mode 100644 frontend-engineering/deploy/k8s-rollback.md diff --git a/.github/workflows/nodejs.yml b/.github/workflows/nodejs.yml index 0d34c7a..be95f36 100644 --- a/.github/workflows/nodejs.yml +++ b/.github/workflows/nodejs.yml @@ -1,46 +1,46 @@ -name: deploy to aliyun oss +# name: deploy to aliyun oss # on: # push: # schedule: # - cron: '30 20 * * *' -jobs: - build: +# jobs: +# build: - runs-on: ubuntu-latest +# runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v1 - - uses: bahmutov/npm-install@v1 - - run: ls -lah - # 下载 git submodule - - uses: srt32/git-actions@v0.0.3 - with: - args: git submodule update --init --recursive - # 使用 node:10 - - name: use Node.js 14.x - uses: actions/setup-node@v1 - with: - node-version: 14 - - run: | - node -v - # npm install - - name: npm install - run: npm install - - name: build - run: npm run build - # 设置阿里云OSS的 id/secret,存储到 github 的 secrets 中 - - name: setup aliyun oss - uses: manyuanrong/setup-ossutil@master - with: - endpoint: oss-cn-beijing.aliyuncs.com - access-key-id: ${{ secrets.OSS_KEY_ID }} - access-key-secret: ${{ secrets.OSS_KEY_SECRET }} - - name: 删除冗余文件 - run: ossutil rm oss://shanyue-blog/assets -rf - if: github.event_name == 'schedule' - - name: 复制文件到阿里云OSS - run: ossutil cp -rf .vuepress/dist oss://shanyue-blog/ - - name: 设置永久缓存 - run: ossutil set-meta oss://shanyue-blog/assets cache-control:"max-age=31536000" --update -rf +# steps: +# - uses: actions/checkout@v1 +# - uses: bahmutov/npm-install@v1 +# - run: ls -lah +# # 下载 git submodule +# - uses: srt32/git-actions@v0.0.3 +# with: +# args: git submodule update --init --recursive +# # 使用 node:10 +# - name: use Node.js 14.x +# uses: actions/setup-node@v1 +# with: +# node-version: 14 +# - run: | +# node -v +# # npm install +# - name: npm install +# run: npm install +# - name: build +# run: npm run build +# # 设置阿里云OSS的 id/secret,存储到 github 的 secrets 中 +# - name: setup aliyun oss +# uses: manyuanrong/setup-ossutil@master +# with: +# endpoint: oss-cn-beijing.aliyuncs.com +# access-key-id: ${{ secrets.OSS_KEY_ID }} +# access-key-secret: ${{ secrets.OSS_KEY_SECRET }} +# - name: 删除冗余文件 +# run: ossutil rm oss://shanyue-blog/assets -rf +# if: github.event_name == 'schedule' +# - name: 复制文件到阿里云OSS +# run: ossutil cp -rf .vuepress/dist oss://shanyue-blog/ +# - name: 设置永久缓存 +# run: ossutil set-meta oss://shanyue-blog/assets cache-control:"max-age=31536000" --update -rf diff --git a/code/graphql/demo.js b/code/graphql/demo.js deleted file mode 100644 index c87d95d..0000000 --- a/code/graphql/demo.js +++ /dev/null @@ -1,29 +0,0 @@ -const { - graphql, - GraphQLSchema, - GraphQLObjectType, - GraphQLString, -} = require('graphql') - -const schema = new GraphQLSchema({ - query: new GraphQLObjectType({ - name: 'RootQueryType', - fields: { - hello: { - type: GraphQLString, - resolve() { - return 'world' - } - } - } - }) -}) - -const query = '{ hello }' - -graphql(schema, query).then(result => { - // { - // data: { hello: "world" } - // } - console.log(result) -}) diff --git a/code/graphql/demo2.js b/code/graphql/demo2.js deleted file mode 100644 index 1bcfaba..0000000 --- a/code/graphql/demo2.js +++ /dev/null @@ -1,18 +0,0 @@ -const graphql = require('graphql') - -const schema = graphql.buildSchema(` - type Query { - hello (a: String): String - } -`) - -const root = { - hello (root, args) { - console.log(root, args, '.....') - return 'hello, world' - } -} - -graphql(schema, '{ hello (a: "hhh") }', root).then((result) => { - console.log(result) -}) \ No newline at end of file diff --git a/code/graphql/index.md b/code/graphql/index.md deleted file mode 100644 index b5dd5ba..0000000 --- a/code/graphql/index.md +++ /dev/null @@ -1,51 +0,0 @@ ---- -title: graphql.js 源码解析 -date: 2019-08-24 10:00 - ---- - -`graphql.js` 可以视作由两大部分组成 - -1. `query`,可以视作 rest 中的 API,它可以与客户端相结合,提供查询语句 -1. `schema`,可以视作 rest 中对应 API 的逻辑层,它可以与服务端相结合,根据查询语句提供结果 - -以下是一个简单的 `hello, world` 的示例 - -```javascript -const { - graphql, - GraphQLSchema, - GraphQLObjectType, - GraphQLString, -} = require('graphql') - -// schema,由 web 框架实现时,这部分放在服务器端里 -const schema = new GraphQLSchema({ - query: new GraphQLObjectType({ - name: 'RootQueryType', - fields: { - hello: { - type: GraphQLString, - resolve() { - return 'world' - } - } - } - }) -}) - -// query,由 web 框架实现时,这部分放在客户端里 -const query = '{ hello }' - -// 查询,这部分放在服务端里 -graphql(schema, query).then(result => { - // { - // data: { hello: "world" } - // } - console.log(result) -}) -``` - -在这里分析一下以上简单代码的源码解析 - -## diff --git a/frontend-engineering/deploy/ci-ci.md b/frontend-engineering/deploy/ci-ci.md index 4129dbb..426475f 100644 --- a/frontend-engineering/deploy/ci-ci.md +++ b/frontend-engineering/deploy/ci-ci.md @@ -49,6 +49,27 @@ on: - 'feature/**' ``` +## 分支的合并策略与 CI (主分支保护规则) + +**生产环境的代码必须通过 CI 检测才能上线**,但这也需要我们进行手动设置。 + +一般而言,我们会设置以下策略加强代码的质量管理。 + +1. 主分支禁止直接 PUSH 代码 +1. 代码都必须通过 PR 才能合并到主分支 +1. **分支必须 CI 成功才能合并到主分支** +1. 代码必须经过 Code Review (关于该 PR 下的所有 Review 必须解决) +1. 代码必须两个人同意才能合并到主分支 + +在 Gitlab 与 Github 中均可进行设置: + ++ [Github: Managing a branch protection rule](https://docs.github.com/en/repositories/configuring-branches-and-merges-in-your-repository/defining-the-mergeability-of-pull-requests/managing-a-branch-protection-rule) ++ [Gitlab: Merge when pipeline succeeds](https://docs.gitlab.com/ee/user/project/merge_requests/merge_when_pipeline_succeeds.html) + +如下示例,未通过 CI,不允许 Merge。可见示例 [PR #22](https://github.com/shfshanyue/cra-deploy/pull/22)。 + +![](https://cdn.jsdelivr.net/gh/shfshanyue/assets/2022-02-11/clipboard-2703.b42555.webp) + ## 任务的并行与串行 在 CI 中,互不干扰的任务并行执行,可以节省很大时间。如 Lint 和 Test 无任何交集,就可以并行执行。 @@ -238,7 +259,9 @@ Lint 和 Test 仅是 CI 中最常见的阶段。为了保障我们的前端代 有些细心并知识面广泛的同学可能注意到了,某些 CI 工作也可在 Git Hooks 完成,确实如此。 -它们的最大的区别在于一个是客户端检查,一个是服务端检查。而客户端检查是天生不可信任的。而针对 `git hooks` 而言,很容易通过 `git commit --no-verify` 而跳过。 +它们的最大的区别在于一个是客户端检查,一个是服务端检查。而客户端检查是天生不可信任的。 + +而针对 `git hooks` 而言,很容易通过 `git commit --no-verify` 而跳过。 ![](https://cdn.jsdelivr.net/gh/shfshanyue/assets@master/src/image.png) diff --git a/frontend-engineering/deploy/ci-env.md b/frontend-engineering/deploy/ci-env.md index f1032d7..888f34f 100644 --- a/frontend-engineering/deploy/ci-env.md +++ b/frontend-engineering/deploy/ci-env.md @@ -1,8 +1,12 @@ # CI 中的环境变量 +在以前诸多章节中都会使用到环境变量。比如在 OSS 篇使用环境变量存储云服务的权限。在前端的异常监控服务中还会用到 Git 的 Commit/Tag 作为 Release 方便定位代码,其中 Commit/Tag 的名称即可从环境变量中获取。 + +而在后续章节还会使用分支名称作为功能测试分支的前缀。 + ## 环境变量 -在 Linux 系统中,通过 `env` 可列出所有环境变量,我们可对环境变量进行修改与获取操作。 +在 Linux 系统中,通过 `env` 可列出所有环境变量,我们可对环境变量进行修改与获取操作,如 `export` 设置环境变量,`${}` 操作符获取环境变量。 ``` bash $ env @@ -11,10 +15,16 @@ USER=shanyue $ echo $USER shanyue +# 或者通过 printenv 获取环境变量 +$ printenv USER + $ export USER=shanyue2 $ echo $USER shanyue2 + +# 获取环境变量 Name 默认值为 shanyue +$ echo ${NAME:-shanyue} ``` 我们在前后端,都会用到大量的环境变量。环境变量可将非应用层内数据安全地注入到应用当中。在 node.js 中可通过以下表达式进行获取。 @@ -27,7 +37,7 @@ process.env.USER CI 作为与 Git 集成的工具,其中注入了诸多与 Git 相关的环境变量。以下列举一条常用的环境变量 -如 Github Actions 中 +如 [Github Actions virables](https://docs.github.com/en/actions/learn-github-actions/environment-variables#default-environment-variables) 中 | 环境变量 | 描述 | | --- | --- | @@ -37,8 +47,7 @@ CI 作为与 Git 集成的工具,其中注入了诸多与 Git 相关的环境 | `GITHUB_SHA` | 当前的 Commit Id。 `ffac537e6cbbf934b08745a378932722df287a53`. | | `GITHUB_REF_NAME` | 当前的分支名称。| -如 [Gitlab CI envirables](https://docs.gitlab.com/ee/ci/variables/predefined_variables.html) 中 - +如 [Gitlab CI virables](https://docs.gitlab.com/ee/ci/variables/predefined_variables.html) 中 | 环境变量 | 描述 | | --- | --- | @@ -86,7 +95,11 @@ $ CI=true npm run build ![](https://cdn.jsdelivr.net/gh/shfshanyue/assets/2022-01-11/clipboard-9125.9b3a8e.webp) -+ [Github CI Run](https://github.com/shfshanyue/cra-deploy/runs/4771781199?check_suite_focus=true) +> PS: 本次 Action 执行结果 [Github Actions Run](https://github.com/shfshanyue/cra-deploy/runs/4771781199?check_suite_focus=true) + +为了验证此类环境变量,我们可以通过 CI 进行验证。 + +另外,在 Github Actions 中还可以使用 `Context` 获取诸多上下文信息,可通过 `${{ toJSON(github) }}` 进行获取。 ``` yaml name: CI Env Check @@ -101,4 +114,24 @@ jobs: - run: echo $GITHUB_EVENT_NAME - run: echo $GITHUB_SHA - run: echo $GITHUB_REF_NAME -``` \ No newline at end of file + - run: echo $GITHUB_HEAD_REF + - name: Dump GitHub context + run: echo '${{ toJSON(github) }}' +``` + +## 一个项目的环境变量管理 + +一个项目中的环境变量,可通过以下方式进行设置 + +1. 本地/宿主机拥有环境变量 +1. CI 拥有环境环境变量,当然 CI Runner 可认为是宿主机,CI 也可传递环境变量 (命令式或者通过 Github/Gitlab 手动操作) +1. Dockerfile 可传递环境变量 +1. docker-compose 可传递环境变量 +1. kubernetes 可传递环境变量 (env、ConfigMap、secret) +1. 一些配置服务,如 [consul](https://github.com/hashicorp/consul)、[vault](https://github.com/hashicorp/vault) + +而对于一些前端项目而言,可如此进行配置 + +1. 敏感数据放在 [vault] 或者 k8s 的 [secket] 中注入环境变量 +1. Git/OS 相关通过 CI 注入环境变量 +1. 非敏感数据可放置在 `.env` 中 diff --git a/frontend-engineering/deploy/ci-intro.md b/frontend-engineering/deploy/ci-intro.md index 01f3dc4..528e9b4 100644 --- a/frontend-engineering/deploy/ci-intro.md +++ b/frontend-engineering/deploy/ci-intro.md @@ -83,7 +83,7 @@ github 提供了以下配置的服务器作为构建服务器,可以说相当 on: push ``` -更多 Github Actions 事件可以参考官方文档 [Events that trigger workflows](https://help.github.com/en/actions/automating-your-workflow-with-github-actions/events-that-trigger-workflows#about-workflow-events) +更多 Github Actions Event 可以参考官方文档 [Events that trigger workflows](https://help.github.com/en/actions/automating-your-workflow-with-github-actions/events-that-trigger-workflows#about-workflow-events) ``` yaml # 仅仅当 master 代码发生变更时,用以自动化部署 @@ -193,11 +193,14 @@ deploy: - master script: # 构建镜像 - - docker build -t devtools-app-image + - docker build -t cra-deploy-app . # 推送镜像 - - docker push devtools-app-image - # 拉取并部署,devtools-app-servie 将会拉取远程的 devtools-app-image 镜像,进行服务部署 - - deploy devtools-app-service . + - docker push cra-deploy-app + # 拉取镜像并部署,deploy 为一个伪代码命令,在实际项目中可使用 helm、kubectl + - deploy cra-deploy-app . + + # - kubectl apply -f app.yaml + # - helm install cra-app cra-app-chart ``` ## 小结 diff --git a/frontend-engineering/deploy/ci-preview.md b/frontend-engineering/deploy/ci-preview.md index bb3e3b8..6e0b8d4 100644 --- a/frontend-engineering/deploy/ci-preview.md +++ b/frontend-engineering/deploy/ci-preview.md @@ -2,7 +2,7 @@ 关于 Preview,我在前几篇文章提到过几次,**即每一个功能分支都配有对应的测试环境**。 -> 如果不了解 Preview 的话,可以看看我在 [cra-deploy](https://github.com/shfshanyue/cra-deploy) 的一个 [PR](https://github.com/shfshanyue/cra-deploy/pull/1)。 +> PS: 如果不了解 Preview 的话,可以看看我在 [cra-deploy](https://github.com/shfshanyue/cra-deploy) 的一个 [PR #21](https://github.com/shfshanyue/cra-deploy/pull/21)。 > > ![](https://cdn.jsdelivr.net/gh/shfshanyue/assets/2022-01-13/clipboard-1458.98c150.webp) @@ -12,19 +12,13 @@ 1. `dev`:测试环境,本地业务迭代开发结束并交付给测试进行功能测试的环境,在 `dev.shanyue.tech` 类似的二级域名进行测试。此时环境的面向对象主要是测试人员。 1. `prod`:生产环境,线上供用户使用的环境,在 `shanyue.tech` 类似的地址。此时环境的面向对象主要是用户。 -那什么是多分支环境部署呢?这要从 `Git Workflow` 说起 +这里我们增加一个功能分支测试环境,对应于 `feature` 分支。每个 `feature` 分支都会有一个环境,一个特殊的测试环境。如对功能 `feature-A` 的开发在 `feature-A.cra.dev.shanyue.tech` 进行测试。 -## 多分支环境部署 - -CI,使项目变得更加自动化,充分减少程序员的手动操作,并且在产品快速迭代的同时提高代码质量。 - -基于 CICD 的工作流也大大改善了 Git 的工作流。其中就增加了一个基于分支的前端环境: - -1. 功能分支环境,对应于 `feature` 分支。每个 `feature` 分支都会有一个环境,一个特殊的测试环境。如对功能 `feature-A` 的开发在 `cra.feature-A.dev.shanyue.tech` 进行测试。 +本篇文章将实践对 [cra-deploy](https://github.com/shfshanyue/cra-deploy) 的 `feature-preview` 分支部署在 `feature-preview.cra.shanyue.tech` 中作为示例。 **那实现多分支环境部署?** -## 基于 docker 进行部署 +## 基于 docker/compose 进行部署 回忆之前关于部署的章节内容,我们可以根据以下 `docker-compose.yaml` 进行部署,并配置为 `cra.shanyue.tech`。 @@ -48,54 +42,151 @@ networks: name: traefik_default ``` -注意,**此时我们通过容器中的 `labels` 来配置域名。如果我们对不同的分支,配置不同的 `labels`,岂不可以完成这件事?** +则仔细一思索,不难得出 docker-compose 的解决方案。 -回忆之前关于 CI 的章节内容,我们在构建服务器中,**可通过环境变量获取到当前仓库的当前分支**,我们使用它进行功能分支环境部署。 +1. 对不同分支根据分支名配置不同的 service +1. 对每个 service 根据分支名配置响应的 `labels` + +回忆之前关于 CI 的章节内容,我们在构建服务器中,**可通过环境变量获取到当前仓库的当前分支**,我们基于分支名称进行功能分支环境部署。 + +假设 `COMMIT_REF_NAME` 为指向当前分支名称的环境变量,如此 ``` yaml -labels: - - "traefik.http.routers.cra.rule=Host(`cra.${CI_COMMIT_REF_NAME}.shanyue.tech`)" +version: "3" +services: + cra-preview-${COMMIT_REF_NAME}: + build: + context: . + dockerfile: router.Dockerfile + labels: + # 配置域名: Preview + - "traefik.http.routers.cra-preview-${COMMIT_REF_NAME}.rule=Host(`${COMMIT_REF_NAME}.cra.shanyue.tech`)" + - traefik.http.routers.cra-preview-${COMMIT_REF_NAME}.tls=true + - traefik.http.routers.cra-preview-${COMMIT_REF_NAME}.tls.certresolver=le + +# 一定要与 traefik 在同一网络下 +networks: + default: + external: + name: traefik_default ``` -大功告成,我们在本地/个人服务器来尝试一下吧。 +大功告成,但还有一点问题: `在 Service Name 上无法使用环境变量`。 -## deploy 命令的封装 +``` bash +$ docker-compose -f preview.docker-compose.yaml up +WARNING: The COMMIT_REF_NAME variable is not set. Defaulting to a blank string. +ERROR: The Compose file './preview.docker-compose.yaml' is invalid because: +Invalid service name 'cra-app-${COMMIT_REF_NAME}' - only [a-zA-Z0-9\._\-] characters are allowed +``` -但是无论基于那种方式的部署,我们总是可以在给它封装一层来简化操作,一来方便运维管理,一来方便开发者直接接入。如把部署抽象为一个命令,我们这里暂时把这个命令命名为 `deploy`,`deploy` 这个命令可能基于 `kubectl/heml` 也有可能基于 `docker-conpose`。 +## docker-compose.yaml 中的环境变量替换 -该命令最核心 API 如下: +在 `docker-compose.yaml` 中不支持将 `Service` 作为环境变量,因此 `docker-compose up` 启动容器失败。 + +我们可以写一段脚本将文件中的环境变量进行替换,但完全没有这个必要,**因为有一个内置于操作系统的命令 `envsubst` 专职于文件内容的环境变量替换**。 + +> PS: 如果系统中无自带 `envsubst` 命令,可使用[第三方 envsubst](https://github.com/a8m/envsubst) 进行替代。 + +注意,以下命令中的 `COMMIT_REF_NAME` 环境变量为当前分支名称,可通过 git 命令获取。而在 CI 当中,可直接通过 CI 相关环境变量获得,无需通过命令。 ``` bash -$ deploy service-name --host :host +$ cat preview.docker-compose.yaml | COMMIT_REF_NAME=$(git rev-parse --abbrev-ref HEAD) envsubst +version: "3" +services: + cra-preview-feature-preview: + build: + context: . + dockerfile: router.Dockerfile + labels: + # 配置域名: Preview + - "traefik.http.routers.cra-preview-master.rule=Host(`cra.master.shanyue.tech`)" + - traefik.http.routers.cra-preview-master.tls=true + - traefik.http.routers.cra-preview-master.tls.certresolver=le + +# 一定要与 traefik 在同一网络下 +networks: + default: + external: + name: traefik_default + +# 将代理文件进行环境变量替换后,再次输出为 temp.docker-compose.yaml 配置文件 +$ cat preview.docker-compose.yaml | COMMIT_REF_NAME=$(git rev-parse --abbrev-ref HEAD) envsubst > temp.docker-compose.yaml + +# 根据配置文件启动容器服务 +$ docker-compose -f temp.docker-compose.yaml up --build ``` -假设要部署一个应用 `shanyue-feature-A`,设置它的域名为 `feature-A.dev.shanyue.tech`,则这个部署前端的命令为: +## environment -``` bash -$ deploy shanyue-feature-A --host feature-A.dev.shanyue.tech +在我们实施了 Preview/Production 后,我们希望可以看到在 PR 的评论或者其它地方可以看到我们的部署地址。这就是 **Environtment**。 + ++ [Github Actions: environment](https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idenvironment) ++ [Using environments for deployment](https://docs.github.com/en/actions/deployment/targeting-different-environments/using-environments-for-deployment) + +在 CI 中配置 `environment` 为期望的部署地址,则可以在每次部署成功后,便可以看到其地址。 + +``` yaml +environment: + name: review/$COMMIT_REF_NAME + url: http://$COMMIT_REF_NAME.cra.shanyue.tech ``` -现在只剩下了一个问题:找到当前分支。 +![](https://cdn.jsdelivr.net/gh/shfshanyue/assets/2022-02-04/clipboard-4252.4ac063.webp) + +而在 Github 中,你甚至可以看到每个 Environment 的部署历史。 + +![Deployment History](https://cdn.jsdelivr.net/gh/shfshanyue/assets/2022-02-04/clipboard-9131.f7971f.webp) ## 基于 CICD 的多分支部署 -在 CICD 中很可根据环境变量获取当前分支名,详情可参考上一篇文章: [CI 中的环境变量](./ci-env.md)。 +在 CICD 中可根据环境变量获取当前分支名,详情可参考上一篇文章: [CI 中的环境变量](./ci-env.md)。 在 Gitlab CI 中可以通过环境变量 `CI_COMMIT_REF_SLUG` 获取,*该环境变量还会做相应的分支名替换*,如 `feature/A` 到 `feature-a` 的转化。 -在 Github Actions 中可以通过环境变量 `GITHUB_REF_NAME` 获取。 +在 Github Actions 中可以通过环境变量 `GITHUB_REF_NAME`/`GITHUB_HEAD_REF` 获取。 > `CI_COMMIT_REF_SLUG`: $CI_COMMIT_REF_NAME lowercased, shortened to 63 bytes, and with everything except 0-9 and a-z replaced with -. No leading / trailing -. Use in URLs, host names and domain names. -> https://docs.github.com/en/actions/learn-github-actions/expressions +> [Github Actions Default Environment Variables](https://docs.github.com/en/actions/learn-github-actions/environment-variables#default-environment-variables) + +以下是一个基于 `github actions` 的多分支部署的简单示例: -以下是一个基于 `gitlab CI` 的一个多分支部署的简单示例 +> PS: 该 CI 配置位于 [cra-deploy/preview.yaml](https://github.com/shfshanyue/cra-deploy/blob/master/.github/workflows/preview.yaml) ``` yaml -deploy-for-feature: - - +# 为了试验,此处作为单独的 Workflow,在实际工作中可 Install -> Lint、Test -> Preview 串行检验 +name: Preview + +# 执行 CI 的时机: 当 git push 到 feature-* 分支时 +on: + push: + branches: + - feature-* + +# 执行所有的 jobs +jobs: + preview: + # 该 Job 在自建的 Runner 中执行 + runs-on: self-hosted + environment: + # 获取 CICD 中的变量: Context + # https://docs.github.com/en/actions/learn-github-actions/expressions + name: preview/${{ github.ref_name }} + url: https://${{ github.ref_name }}.cra.shanyue.tech + steps: + # 切出代码,使用该 Action 将可以拉取最新代码 + - uses: actions/checkout@v2 + - name: Preview + run: | + cat preview.docker-compose.yaml | envsubst > docker-compose.yaml + docker-compose up --build -d cra-preview-${COMMIT_REF_NAME} + env: + COMMIT_REF_NAME: ${{ github.ref_name }} ``` +以下是一个基于 `gitlab CI` 的多分支部署的简单示例: + ``` yaml deploy-for-feature: stage: deploy @@ -103,21 +194,55 @@ deploy-for-feature: refs: - /^feature-.*$/ script: - - deploy shanyue-$CI_COMMIT_REF_SLUG --host https://$CI_COMMIT_REF_SLUG.sp.dev.smartstudy.com + # 在 CI 中可直接修改为 docker-compose.yaml,因在 CI 中都是一次性操作 + - cat preview.docker-compose.yaml | envsubst > docker-compose.yaml + - docker-compose up --build -d + # 部署环境展示,可在 Pull Request 或者 Merge Request 中直接查看 environment: name: review/$CI_COMMIT_REF_NAME - url: http://$CI_COMMIT_REF_SLUG.dev.shanyue.tech + url: http://$CI_COMMIT_REF_SLUG.cra.shanyue.tech ``` -## environment +## 自动 Stop Preview + +当新建了一个功能分支,并将它 push 到仓库后,CI 将在测试环境部署服务器将会自动启动一个容器。即便该分支已被合并,然而该分支对应的功能分支测试地址仍然存在,其对应的容器也仍然存在。 + +而当业务迭代越来越频繁,功能分支越来越多时,将会有数十个容器在服务器中启动,这将造成极大的服务器资源浪费。 + +当然,我们可以将已经合并到主分支的功能分支所对应的容器进行手动停止,但是不够智能。 + +我们可以通过 CI 做这件事情: **当 PR 被合并后,自动将该功能分支所对应的 Docker 容器进行关停**。 + +> PS: 该 CI 配置位于 [cra-deploy/stop-preview.yaml](https://github.com/shfshanyue/cra-deploy/blob/master/.github/workflows/stop-preview.yaml) + +> PS2: [Stop Preview 所对应的 Action](https://github.com/shfshanyue/cra-deploy/runs/5066759058?check_suite_focus=true) -+ [Github Actions: environment](https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idenvironment) -+ [Using environments for deployment](https://docs.github.com/en/actions/deployment/targeting-different-environments/using-environments-for-deployment) ``` yaml -environment: - name: review/$COMMIT_BRANCH_NAME - url: http://$COMMIT_BRANCH_NAME.dev.shanyue.tech +# 为了避免服务器资源浪费,每次当 PR 被合并或者关闭时,自动停止对应的 Preview 容器 +name: Stop Preview + +on: + pull_request: + types: + # 当 feature 分支关闭时, + - closed + +jobs: + stop-preview: + runs-on: self-hosted + steps: + # - name: stop preview + # # 根据 Label 找到对应的容器,并停止服务,因为无需代码,所以用不到 checkout + # run: docker ps -f label="com.docker.compose.service=cra-preview-${COMMIT_REF_NAME}" -q | xargs docker stop + + - uses: actions/checkout@v2 + - name: stop preview + run: | + cat preview.docker-compose.yaml | envsubst > docker-compose.yaml + docker-compose stop + env: + COMMIT_REF_NAME: ${{ github.head_ref }} ``` ## 基于 k8s 的多分支部署 @@ -173,12 +298,32 @@ spec: targetPort: 80 ``` +## deploy 命令的封装 + +但是无论基于那种方式的部署,我们总是可以在给它封装一层来简化操作,一来方便运维管理,一来方便开发者直接接入。如把部署抽象为一个命令,我们这里暂时把这个命令命名为 `deploy`,`deploy` 这个命令可能基于 `kubectl/heml` 也有可能基于 `docker-conpose`。 + +该命令最核心 API 如下: + +``` bash +$ deploy service-name --host :host +``` + +假设要部署一个应用 `shanyue-feature-A`,设置它的域名为 `feature-A.dev.shanyue.tech`,则这个部署前端的命令为: + +``` bash +$ deploy shanyue-feature-A --host feature-A.dev.shanyue.tech +``` + +现在只剩下了一个问题:找到当前分支。 + ## 小结 -随着 CICD 的发展,对快速迭代以及代码质量提出了更高的要求,而基于分支的多测试环境则成为了刚需。对于该环境的搭建,思路也很清晰 +随着 CICD 的发展、对快速迭代以及代码质量提出了更高的要求,基于分支的分支测试环境则成为了刚需。 + +对于该环境的搭建,思路也很清晰 1. 借用现有的 CICD 服务,如 `github actions` 或者 `gitlab CI` 获取当前分支信息 -1. 借用 Docker 快速部署前端或者后端,根据分支信息启动不同的容器,并配置标签 +1. 借用 Docker 快速部署前端或者后端,根据分支信息启动不同的服务,根据 Docker 启动服务并配置标签 1. 根据容器的标签与当前 Git 分支对前端后端设置不同的域名 另外,这个基于容器的思路不仅仅使用于前端,同样也适用于后端。而现实的业务中复杂多样,如又分为已下几种,这需要在项目的使用场景中灵活处理。 diff --git a/frontend-engineering/deploy/cra-docker.md b/frontend-engineering/deploy/cra-docker.md index b85a9bb..00a3331 100644 --- a/frontend-engineering/deploy/cra-docker.md +++ b/frontend-engineering/deploy/cra-docker.md @@ -6,6 +6,8 @@ *部署一个 [Creact React APP](https://github.com/facebook/create-react-app) 单页应用。* +实际上,即使你们技术栈是 Vue 也无所谓,本系列文章很少涉及 React 相关内容,只要你们项目时单页应用即可。 + > PS: 本项目以 [cra-deploy](https://github.com/shfshanyue/simple-deploy) 仓库作为实践,配置文件位于 [simple.Dockerfile](https://github.com/shfshanyue/cra-deploy/blob/master/simple.Dockerfile) ## 单页应用的静态资源 diff --git a/frontend-engineering/deploy/cra-route.md b/frontend-engineering/deploy/cra-route.md index 3cef0fb..c13abcb 100644 --- a/frontend-engineering/deploy/cra-route.md +++ b/frontend-engineering/deploy/cra-route.md @@ -91,7 +91,7 @@ location / { 此时,可解决服务器端路由问题。 -## 永久缓存 +## 长期缓存 (Long Term Cache) 在 CRA 应用中,`./build/static` 目录均由 webpack 构建产生,资源路径将会带有 hash 值。 @@ -115,6 +115,8 @@ $ tree ./build/static 此时可通过 `expires` 对它们配置一年的长期缓存,它实际上是配置了 `Cache-Control: max-age=31536000` 的响应头。 +那为什么带有 hash 的资源可设置长期缓存呢: **资源的内容发生变更,他将会生成全新的 hash 值,即全新的资源路径。** + ``` conf location /static { expires 1y; @@ -123,7 +125,12 @@ location /static { ## nginx 配置文件 -`nginx.conf` 文件需要维护在项目当中,经过路由问题的解决与缓存配置外,最终配置如下。 +总结缓存策略如下: + +1. 带有 hash 的资源一年长期缓存 +1. 非带 hash 的资源,需要配置 Cache-Control: no-cache,**避免浏览器默认为强缓存** + +`nginx.conf` 文件需要维护在项目当中,经过路由问题的解决与缓存配置外,最终配置如下: > 该 nginx 配置位于 [cra-deploy/nginx.conf](https://github.com/shfshanyue/cra-deploy/blob/master/nginx.conf) @@ -205,6 +212,26 @@ services: 此时对于**非带** hash 资源, `Cache-Control: no-cache` 响应头已配置。 +## 百尺竿头更进一步 + +在前端部署流程中,一些小小的配置能大幅度提升性能,列举一二,感兴趣的同学可进一步探索。 + +构建资源的优化: + +1. 使用 terser 压缩 Javascript 资源 +1. 使用 cssnano 压缩 CSS 资源 +1. 使用 sharp/CDN 压缩 Image 资源或转化为 Webp +1. 使用 webpack 将小图片转化为 DataURI +1. 使用 webpack 进行更精细的分包,避免一行代码的改动使大量文件的缓存失效 + +网络性能的优化: + +1. HTTP2,HTTP2多路复用、头部压缩功能提升网络性能 +1. OSCP Stapling,减少浏览器端的 OSCP 查询(可验证证书合法性) +1. TLS v1.3,TLS 握手时间从 2RTT 优化到了 1RTT,并可 0-RTT Resumption +1. HSTS,无需301跳转,直接使用 HTTPS,但更重要的是安全性能 +1. Brotli,相对 gzip 更高性能的压缩算法 + ## 小结 其实,从这里开始,前端部署与传统前端部署已逐渐显现了天壤之别。 diff --git a/frontend-engineering/deploy/end.md b/frontend-engineering/deploy/end.md new file mode 100644 index 0000000..10964d9 --- /dev/null +++ b/frontend-engineering/deploy/end.md @@ -0,0 +1 @@ +# 扬帆起航 diff --git a/frontend-engineering/deploy/index.md b/frontend-engineering/deploy/index.md index c441ac4..a43339f 100644 --- a/frontend-engineering/deploy/index.md +++ b/frontend-engineering/deploy/index.md @@ -1,31 +1,36 @@ ## 部署篇 -为了保障本专栏系列的知识密度,因此不对 docker/docker-compose/traefik/cicd 等基础用法做过多解释。 +本专栏需要您了解一些前置知识,如 docker/docker-compose/traefik/cicd 等基础用法。 -本专栏仅有十三篇文章,保证做到有图文轻松阅读便于理解,有代码示例保障能真实跑得起来。 +在学习本专栏过程中,您可以随时查阅文档,在文章涉及到的相关配置,会指明具体配置所对应的文档地址。 + +本专栏尽量做到图文轻松阅读便于理解,并有**代码示例保障能真实跑得起来**。 1. 每段代码都可运行 1. 每篇文章都可实践 1. 每次实践都有示例 -示例代码开源,置于 Github 中。 +示例代码开源,置于 Github 中,演示如何对真实项目进行部署上线。 + ++ [simple-deploy](https://github.com/shfshanyue/simple-deploy): 了解最简单的部署,不涉及打包等内容。 ++ [cra-deploy](https://github.com/shfshanyue/cra-deploy): 了解如何部署单页应用,这里以 [create-react-app](https://github.com/facebook/create-react-app) 为例,但实际上所讲述东西与 React 无关,仅与单页应用有关。 + +本专栏分为以下若干篇,其中前三篇以[simple-deploy]作为示例项目,而余下若干篇以[cra-deploy](https://github.com/shfshanyue/cra-deploy)作为示例项目。 -PS: 服务编排两篇文章涉及到个人服务器可自行购买,CICD 五篇涉及到 Gitlab CI/Github Actions 可自行阅读文档。 +其中有几篇文章需要个人服务器资源,以下有所标明。 -1. 极简部署: 如何在宿主机环境(裸机)进行部署 +1. 极简部署: 在宿主机环境(裸机)进行部署 1. 极简部署: docker/docker-compose 部署极简版 1. 极简部署: 基于 nginx 镜像部署 -1. 部署 CRA: Docker 缓存优化技术以及多阶段构建 -1. 部署 CRA: nginx 配置、路由修复与持久化缓存优化 -1. 对象存储云服务: 将静态资源部署在 OSS/CDN -1. 对象存储云服务: 部署时间与云服务优化 -1. 托管服务: 静态资源托管服务 -1. 服务编排: 服务发现与服务网关搭建 -1. 服务编排: 前端应用域名配置 -1. CICD: 自动部署 -1. CICD: 强化前端质量保障工程 Lint/Test -1. CICD: CI Cache -1. CICD: env -1. CICD: Preview -1. k8s: 简单概念介绍 -1. k8s: 灰度部署、金丝雀、滚动更新与回滚 +1. 部署 CRA: Docker 缓存优化以及多阶段构建 +1. 部署 CRA: nginx 配置、路由修复与长期缓存优化 +1. 对象存储云服务: 静态资源部署在 OSS/CDN 云服务。**必须需要**云服务,可自行购买。 +1. 对象存储云服务: 静态资源上传时间与空间优化。 +1. 服务编排: 服务发现与服务网关 Traefik 搭建。**最好需要**个人服务器,可自行购买。如果没有,可在本地进行测试。 +1. 服务编排: 前端应用域名配置。**必须需要**个人域名,可自行购买。 +1. CICD: CICD 功能基础配置介绍与自动部署实践 +1. CICD: 在 CI 中实践前端质量保障工程 +1. CICD: 在 CI 中充分利用 Cache +1. CICD: CI 中的环境变量 +1. CICD: 使用 CI 实现功能分支测试环境 Preview。**必须需要**个人服务器及个人域名。 +1. k8s: 简单概念介绍及使用 k8s 部署前端应用。可本地使用 minikube 进行模拟。 diff --git a/frontend-engineering/deploy/k8s-intro.md b/frontend-engineering/deploy/k8s-intro.md index cbea012..39dd0c1 100644 --- a/frontend-engineering/deploy/k8s-intro.md +++ b/frontend-engineering/deploy/k8s-intro.md @@ -35,6 +35,9 @@ Deployment 可以更好地实现弹性扩容,负载均衡、回滚等功能。 ``` bash $ docker build -t cra-deploy-app -f router.Dockerfile . + +# 实际环节需要根据 CommitId 或者版本号作为镜像的 Tag +$ docker build -t cra-deploy-app:$(git rev-parse --short HEAD) -f router.Dockerfile . ``` 我们将配置文件存为 `k8s-app.yaml`,以下是配置文件个别字段释义: @@ -70,14 +73,16 @@ spec: 我们使用 `kubectl apply` 部署生效后查看 `Pod` 以及 `Deployment` 状态。 +其中每一个 Pod 都有一个 IP,且应用每次升级后 Pod IP 都会发生该表,那应该如何配置该应用对外访问? + ``` bash $ kubectl apply -f k8s-app.yaml $ kubectl get pods --selector "app=cra" -o wide NAME READY STATUS RESTARTS AGE IP -cra-deployment-555dc66769-2kk7p 1/1 Running 0 40m 172.17.0.8 -cra-deployment-555dc66769-fq9gd 1/1 Running 0 40m 172.17.0.9 -cra-deployment-555dc66769-zhtp9 1/1 Running 0 40m 172.17.0.10 +cra-deployment-555dc66769-2kk7p 1/1 Running 0 10m 172.17.0.8 +cra-deployment-555dc66769-fq9gd 1/1 Running 0 10m 172.17.0.9 +cra-deployment-555dc66769-zhtp9 1/1 Running 0 10m 172.17.0.10 # READY 3/3 表明全部部署成功 $ kubectl get deploy cra-deployment @@ -85,13 +90,81 @@ NAME READY UP-TO-DATE AVAILABLE AGE cra-deployment 3/3 3 3 42m ``` +从上述命令,列出其中一个 Pod 名是 `cra-deployment-555dc66769-zhtp9`。 + +其中 `cra-deployment` 是 `Deployment` 名,而该前端应用每次上线升级会部署一个 `Replica Sets`,如本次为 `cra-deployment-555dc66769`。 + ### Service +`Service` 可通过 `spec.selector` 匹配合适的 Deployment 使其能够通过统一的 `Cluster-IP` 进行访问。 + +``` yaml +apiVersion: v1 +kind: Service +metadata: + name: cra-service +spec: + selector: + # 根据 Label 匹配应用 + app: cra + ports: + - protocol: TCP + port: 80 + targetPort: 80 +``` + +根据 `kubectl get service` 可获取 IP,在 k8s 集群中可通过 `curl 10.102.82.153` 直接访问。 +``` bash +$ kubectl get service -o wide +NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE SELECTOR +cra-service ClusterIP 10.102.82.153 80/TCP 10m app=cra + +$ curl --head 10.102.82.153 +HTTP/1.1 200 OK +Server: nginx/1.21.4 +Date: Mon, 14 Feb 2022 04:46:24 GMT +Content-Type: text/html +Content-Length: 644 +Last-Modified: Wed, 26 Jan 2022 10:10:51 GMT +Connection: keep-alive +ETag: "61f11e2b-284" +Expires: Mon, 14 Feb 2022 04:46:23 GMT +Cache-Control: no-cache +Accept-Ranges: bytes +``` + +而且,所有的服务可以通过 `..svc.cluster.local` 进行服务发现。在集群中的任意一个 Pod 中通过域名访问服务 ``` bash -$ kubectl get service +# 通过 kebectl exec 可进入任意 Pod 中 +$ kubectl exec -it cra-deployment-555dc66769-2kk7p sh + +# 在 Pod 中执行 curl,进行访问 +$ curl --head cra-service.default.svc.cluster.local +HTTP/1.1 200 OK +Server: nginx/1.21.4 +Date: Mon, 14 Feb 2022 06:05:41 GMT +Content-Type: text/html +Content-Length: 644 +Last-Modified: Wed, 26 Jan 2022 10:10:51 GMT +Connection: keep-alive +ETag: "61f11e2b-284" +Expires: Mon, 14 Feb 2022 06:05:40 GMT +Cache-Control: no-cache +Accept-Ranges: bytes ``` -### Replica Set +对外可通过 `Ingress` 或者 `Nginx` 提供服务。 + +## 回滚 +如何进行回滚? + +那我们可以对上次版本重新部署一遍。比如在 Gitlab CI 中,我们可以通过点击升级前版本的手动部署按钮,对升级前版本进行重新部署。但是,此时流程有点长。 + +此时可以使用 `kubectl rollout` 直接进行回滚。 + +``` bash +$ kubectl rollout undo deployment/nginx-deployment +``` diff --git a/frontend-engineering/deploy/k8s-rollback.md b/frontend-engineering/deploy/k8s-rollback.md deleted file mode 100644 index 0d27a5d..0000000 --- a/frontend-engineering/deploy/k8s-rollback.md +++ /dev/null @@ -1,57 +0,0 @@ -# 回滚、滚动升级、灰度部署与后端协作 - -## 灰度部署与金丝雀 - -AB Test - -1. 50% 的流量用于功能A。对功能 A 切分支单独开发 -1. 50% 的流量用于功能B。对功能 B 切分支单独开发 - -那如何使仅有一半的访问到功能 A,而另一半的人访问到功能 B 呢? - -我们可将项目部署为 `kubernetes` - -+ 功能 A 使用一个 Deployment,部署 1 个 Pod。并暴露在服务 `cra-service-test-A` -+ 功能 B 使用一个 Deployment,部署 1 个 Pod。并暴露在服务 `cra-service-test-B` - -对两个服务进行**同等权重的负载均衡**即可。 - -``` yaml -upstream: - - cra-service-test-A we - - cra-service-test-B -``` - -那什么是金色雀呢?而金丝雀实际上可理解为 1/10 (或者更小)的流量为新的金丝群版本(作为内测版/公测版),而剩下流量为主版本。 - -> 金丝雀发布这一术语源于煤矿工人把笼养的金丝雀带入矿井的传统。矿工通过金丝雀来了解矿井中一氧化碳的浓度,如果一氧化碳的浓度过高,金丝雀就会中毒,从而使矿工知道应该立刻撤离。——《DevOps实践指南》 - -而无论是 AB Test、灰度部署还是金色雀,其本质上是对服务的流量控制。 - -## AB Test - - - -## 滚动升级 (Rolling Update) - -假设前端发布了 `Version 1.0` 后,并部署成功。 - -此时前端发布了 `Version 2.0`,如何保证流量可以平滑过度,不会因新版本无法提供服务而产生白屏等异常。 - -在早期一些小公司中使用 nginx 部署服务,往往先把 `Version 1.0` 版本停止服务,再对 `Version 2.0` 服务进行部署,**此时中间服务升级的过程将无法停止服务**。如果新版版有异常问题无法正常启动,则白屏时间更长。而滚动升级可以解决这个问题。 - -我们来看一下滚动升级的流程: - -1. 使用 `Version 1` 继续提供服务 -2. 对 `Version 2` 进行启动,启动后进行健康检查(Health Check),如果健康检查通过,则开始提供服务 -3. 对 `Version 2` 停止服务 - -> 思考题: 那此时如果一条请求到了 Version 1,而此时 Version 1 的服务刚好停止呢? - -## 回滚 - -如何进行回滚? - -那我们可以对上次版本重新部署一遍。比如在 Gitlab CI 中,我们可以通过点击升级前版本的手动部署按钮,对升级前版本进行重新部署。 - -但是,此时流程有点长。 diff --git a/frontend-engineering/deploy/oss-rclone.md b/frontend-engineering/deploy/oss-rclone.md index b8f6ec5..e2cd26d 100644 --- a/frontend-engineering/deploy/oss-rclone.md +++ b/frontend-engineering/deploy/oss-rclone.md @@ -1,58 +1,150 @@ # 部署 CRA: 部署时间与云服务优化 -当公司内一个将静态资源部署云服务的前端项目持续跑了 N 年后,可能出现几种情况。 +当公司内将一个静态资源部署云服务的前端项目持续跑了 N 年后,部署了上万次后,可能出现几种情况。 -1. 将静态资源推送到 OSS 用时过长。如构建后的资源全部上传到对象存储,然而**有些资源内容并未发生变更**,将会导致过多的上传时间。 -1. OSS 对象存储中冗余资源越来越多。**前端每改一行代码,便会生成一个新的资源,而旧资源将会在 OSS 不断堆积,占用额外体积。** 从而导致更昂贵的云服务费用。 +1. 时间过长。如构建后的资源全部上传到对象存储,然而**有些资源内容并未发生变更**,将会导致过多的上传时间。 +1. 冗余资源。**前端每改一行代码,便会生成一个新的资源,而旧资源将会在 OSS 不断堆积,占用额外体积。** 从而导致更多的云服务费用。 ## 静态资源上传优化 在前端构建过程中存在无处不在的缓存 -1. 当源文件内容为发生更改时,将不会对 Module 重新使用 Loader 等进行编译,不会重新编译。这是利用了 webpack5 的持久化缓存。 +1. 当源文件内容未发生更改时,将不会对 Module 重新使用 Loader 等进行重新编译。这是利用了 webpack5 的持久化缓存。 1. 当源文件内容未发生更改时,构建生成资源的 hash 将不会发生变更。此举有利于 HTTP 的 Long Term Cache。 -那对比生成资源的哈希,如未发生变更,则不向 OSS 进行上产。**这一步将会提升静态资源上传时间,进而提升每一次前端部署的时间。** +那对比生成资源的哈希,如未发生变更,则不向 OSS 进行上传操作。**这一步将会提升静态资源上传时间,进而提升每一次前端部署的时间。** -**对于构建后含有 hash 的资源,其实对比文件名即可了解资源是否发生变更。** +**对于构建后含有 hash 的资源,对比文件名即可了解资源是否发生变更。** -伪代码如下 +> PS: 该脚本路径位于 [cra-deploy/scripts/uploadOSS.mjs](https://github.com/shfshanyue/cra-deploy/blob/master/scripts/uploadOSS.mjs) + +伪代码如下: + +``` js +// 判断文件 (Object)是否在 OSS 中存在 +// 对于带有 hash 的文件而言,如果存在该文件名,则在 OSS 中存在 +// 对于不带有 hash 的文件而言,可对该 Object 设置一个 X-OSS-META-MTIME 或者 X-OSS-META-HASH 每次对比来判断该文件是否存在更改,本函数跳过 +// 如果再严谨点,将会继续对比 header 之类 +async function isExistObject (objectName) { + try { + await client.head(objectName) + return true + } catch (e) { + return false + } +} +``` + +而对于是否带有 hash 值,设置不同的关于缓存的响应头。 + +``` js +// objectName: static/css/main.079c3a.css +// withHash: 该文件名是否携带 hash 值 +async function uploadFile (objectName, withHash = false) { + const file = resolve('./build', objectName) + // 如果路径名称不带有 hash 值,则直接判断在 OSS 中不存在该文件名,需要重新上传 + const exist = withHash ? await isExistObject(objectName) : false + if (!exist) { + const cacheControl = withHash ? 'max-age=31536000' : 'no-cache' + // 为了加速传输速度,这里使用 stream + await client.putStream(objectName, createReadStream(file), { + headers: { + 'Cache-Control': cacheControl + } + }) + console.log(`Done: ${objectName}`) + } else { + // 如果该文件在 OSS 已存在,则跳过该文件 (Object) + console.log(`Skip: ${objectName}`) + } +} +``` + +另外,我们可以通过 [p-queue](https://github.com/sindresorhus/p-queue) 控制资源上传的并发数量。 ``` js -function uploadFile (file: string) {} -function isFileExistsOSS (file: string): boolean {} +const queue = new PQueue({ concurrency: 10 }) -if (!isFileExistsOSS(file)) { - uploadFile(file) +for await (const entry of readdirp('./build', { depth: 0, type: 'files' })) { + queue.add(() => uploadFile(entry.path)) } ``` -而对于不含有 hash 的资源,则对比内容 hash。 +## Rclone: 按需上传 -## Rclone +[Rclone](https://github.com/rclone/rclone),`rsync for cloud storage`,是使用 Go 语言编写的一款高性能云文件同步的命令行工具,可理解为云存储版本的 rsync,或者更高级的 ossutil。 + +它支持以下功能: + +1. 按需复制,每次仅仅复制更改的文件 +1. 断点续传 +1. 压缩传输 + +``` bash +# 将资源上传到 OSS Bucket +$ rclone copy --exclude 'static/**' --header 'Cache-Control: no-cache' build alioss:/shanyue-cra --progress + +# 将带有 hash 资源上传到 OSS Bucket,并且配置长期缓存 +$ rclone copy --header 'Cache-Control: max-age=31536000' build/static alioss:/shanyue-cra/static --progress +``` + +为求方便,可将两条命令维护到 `npm scripts` 中 ``` js -$ rclone +{ + "scripts": { + "oss:rclone": "rclone copy --exclude 'static/**' --header 'Cache-Control: no-cache' build alioss:/shanyue-cra --progress && rclone copy --header 'Cache-Control: max-age=31536000' build/static alioss:/shanyue-cra/static --progress", + } +} ``` ## 删除 OSS 中冗余资源 -此处要保障生产环境与开发环境的 OSS 资源进行隔离。 +在生产环境中,OSS 只需保留最后一次线上环境所依赖的资源。(多版本共存情况下除外) -在生产环境中,OSS 只需保留最后一次线上环境所依赖的资源。可根据OSS 所有资源与最后一次构建生成的资源一一对比文件名,进行删除。 +此时可根据 OSS 中所有资源与最后一次构建生成的资源一一对比文件名,进行删除。 ``` js +// 列举出来最新被使用到的文件: 即当前目录 +// 列举出来OSS上的所有文件,遍历判断该文件是否在当前目录,如果不在,则删除 +async function main() { + const files = await getCurrentFiles() + const objects = await getAllObjects() + for (const object of objects) { + // 如果当前目录中不存在该文件,则该文件可以被删除 + if (!files.includes(object.name)) { + await client.delete(object.name) + console.log(`Delete: ${object.name}`) + } + } +} +``` +通过 npm scripts 进行简化: + +``` js +{ + "scripts": { + "oss:rclone": "rclone copy --exclude 'static/**' --header 'Cache-Control: no-cache' build alioss:/shanyue-cra --progress && rclone copy --header 'Cache-Control: max-age=31536000' build/static alioss:/shanyue-cra/static --progress", + } +} ``` +而对于清除任务可通过**定时任务周期性删除 OSS 上的冗余资源**,比如通过 CRON 配置每天凌晨两点进行删除。由于该脚本定时完成,所以无需考虑性能问题,故不适用 `p-queue` 进行并发控制 + 而有一种特殊情况,可能不适合此种方法。生产环境发布了多个版本的前端,如 AB 测试,toB 面向不同大客户的差异化开发与部署,此时可针对不同版本对应不同的 `output.path` 来解决。 > `output.path` 可通过环境变量注入 webpack 选项,而环境变量可通过以下命令置入。(或置入 .env) ``` bash -export BRANCH=$(git branch --show-current) +export COMMIT_SHA=$(git rev-parse --short HEAD) -export BRANCH=$(git rev-parse --abbrev-ref HEAD) +export COMMIT_REF_NAME=$(git branch --show-current) +export COMMIT_REF_NAME=$(git rev-parse --abbrev-ref HEAD) ``` -在测试环境中,虽然有很多个分支,但是不重要,想删的时候把 OSS 清掉都没关系。 +以上两个环境变量非常重要,将会在以后篇章经常用到。 + +## 小结 + +通过对 OSS 进行优化后,OSS 篇基本完结。接下来,如何将部署自动化完成呢? diff --git a/frontend-engineering/deploy/oss.md b/frontend-engineering/deploy/oss.md index 41fcc7a..62b9d13 100644 --- a/frontend-engineering/deploy/oss.md +++ b/frontend-engineering/deploy/oss.md @@ -1,8 +1,8 @@ # 将静态资源推至 OSS -本篇文章需要一个阿里云 OSS (对象存储服务)服务,一个月几毛钱,可自行购买。我们将会把静态资源上传至 OSS,并对 OSS 提供 CDN 服务。 +本篇文章需要 OSS(Object Storage) 云服务服务,一个月几毛钱,可自行购买。我们可以把静态资源上传至 OSS,并对 OSS 提供 CDN 服务。 -本篇文章还是以项目 [cra-deploy](https://github.com/shfshanyue/cra-deploy) 示例,并将静态资源上传至 OSS 处理。其地址为 +本篇文章还是以项目 [cra-deploy](https://github.com/shfshanyue/cra-deploy) 示例,并将静态资源上传至 OSS 处理。 ## PUBLIC_PATH 与 webpack 的处理 @@ -48,11 +48,11 @@ export PUBLIC_URL=https://cdn.shanyue.tech + aliyun_access_key_id + aliyun_access_key_secret -在将静态资源上传至云服务时,我们需要 AccessKey 获得权限用以上传。可参考文档[创建AccessKey](https://help.aliyun.com/document_detail/53045.html) +在将静态资源上传至云服务时,我们需要 AccessKey/AccessSecret 获得权限用以上传。可参考文档[创建AccessKey](https://help.aliyun.com/document_detail/53045.html) ### Bucket -`Bucket` 是 OSS 中的存储空间。对于生产环境,可对每一个项目创建单独的 Bucket,而在测试环境,多个项目可共用 Bucket。 +`Bucket` 是 OSS 中的存储空间。**对于生产环境,可对每一个项目创建单独的 Bucket**,而在测试环境,多个项目可共用 Bucket。 在创建 Bucket 时,需要注意以下事项。 @@ -62,19 +62,34 @@ export PUBLIC_URL=https://cdn.shanyue.tech 最终的 PUBLIC_URL 为 `$Bucket.$Endpoint`,比如本篇文章示例项目的 PUBLIC_URL 为 `shanyue-cra.oss-cn-beijing.aliyuncs.com`。 +但是,你也可以配置 CNAME 记录并使用自己的域名。 + +在以下命令行及代码示例中,我们将 cra-deploy 项目的静态资源全部上传至 `shanyue-cra` 该 Bucket 中。 + ## 将资源推送到 OSS: ossutil -在 OSS 上创建一个 Bucket,通过 `ossutil` 将资源上传至 OSS。 +在 OSS 上创建一个 Bucket,通过官方工具 [ossutil](https://help.aliyun.com/document_detail/50452.html) 将静态资源上传至 OSS。 + [ossutil 安装](https://help.aliyun.com/document_detail/120075.htm) + [ossutil 文档](https://help.aliyun.com/document_detail/50452.html) +在进行资源上传之前,需要通过 `ossutil config` 进行权限配置。 + +``` bash +$ ossutil config -i $ACCESS_KEY_ID -k $ACCESS_KEY_SECRET -e $ENDPOINT +``` + +命令 `ossutil cp` 可将本地资源上传至 OSS。而缓存策略与前篇文章保持一致: + +1. 带有 hash 的资源一年长期缓存 +1. 非带 hash 的资源,需要配置 Cache-Control: no-cache,**避免浏览器默认为强缓存** + ``` bash # 将资源上传到 OSS Bucket $ ossutil cp -rf --meta Cache-Control:no-cache build oss://shanyue-cra/ # 将带有 hash 资源上传到 OSS Bucket,并且配置长期缓存 -# 注意此时 build/static 上传了两遍 +# 注意此时 build/static 上传了两遍 (可通过脚本进行优化) $ ossutil cp -rf --meta Cache-Control:max-age=31536000 build/static oss://shanyue-cra/static ``` @@ -92,21 +107,29 @@ $ ossutil cp -rf --meta Cache-Control:max-age=31536000 build/static oss://shanyu 另有一种方法,通过官方提供的 SDK: [ali-oss](https://github.com/ali-sdk/ali-oss) 可对资源进行精准控制: -1. 对每一个资源进行精准控制 -1. 仅仅上传进行更改的文件 -1. 使用 `p-map` 控制 N 个资源同时上传 +1. 对每一条资源进行精准控制 +1. 仅仅上传变更的文件 +1. 使用 [p-queue](https://github.com/sindresorhus/p-queue) 控制 N 个资源同时上传 ``` js { scripts: { - 'oss:sdk': 'node ./scripts/uploadOSS.js' + 'oss:script': 'node ./scripts/uploadOSS.js' } } ``` -脚本此处省略。 +脚本略过不提。 -## Dockerfile +> PS: 上传 OSS 的配置文件位于 [scripts/uploadOSS.js](https://github.com/shfshanyue/simple-deploy/blob/master/scripts/uploadOSS.js) 中,可通过它使用脚本控制静态资源上传。 + +## Dockerfile 与环境变量 + +> PS: 该 Dockerfile 配置位于 [cra-deploy/oss.Dockerfile](https://github.com/shfshanyue/cra-deploy/blob/master/oss.Dockerfile) + +由于 `Dockerfile` 同代码一起进行管理,我们**不能将敏感信息写入 Dockerfile**。 + +故这里使用 [ARG](https://docs.docker.com/engine/reference/builder/#arg) 作为变量传入。而 ARG 可通过 `docker build --build-arg` 抑或 `docker-compose` 进行传入。 ``` dockerfile FROM node:14-alpine as builder @@ -136,7 +159,15 @@ ADD nginx.conf /etc/nginx/conf.d/default.conf COPY --from=builder code/build /usr/share/nginx/html ``` -## docker-compose +## docker-compose 配置 + +> PS: 该 compose 配置位于 [cra-deploy/docker-compose.yaml](https://github.com/shfshanyue/cra-deploy/blob/master/docker-compose.yaml) + +在 `docker-compose` 配置文件中,通过 `build.args` 可对 `Dockerfile` 进行传参。 + +而 `docker-compose.yaml` 同样不允许出现敏感数据,此时**通过环境变量进行传参**。在 `build.args` 中,默认从同名环境变量中取值。 + +> PS: 在本地可通过环境变量传值,那在 CI 中呢,在生产环境中呢?待以后 CI 篇进行揭晓。 ``` yaml version: "3" @@ -146,6 +177,7 @@ services: context: . dockerfile: oss.Dockerfile args: + # 此处默认从环境变量中传参 - ACCESS_KEY_ID - ACCESS_KEY_SECRET - ENDPOINT=oss-cn-beijing.aliyuncs.com @@ -153,4 +185,35 @@ services: - 8000:80 ``` -**好像,容器里好像就剩下一个 index.html 了?** +RUN 起来,成功! + +``` bash +$ docker-compose up --build oss +``` + +## 免费的托管服务平台 + +经过几篇文章的持续优化,当我们使用对象存储服务之后,实际上在我们的镜像中仅仅只剩下几个文件。 + ++ `index.html` ++ `robots.txt` ++ `favicon.ico` ++ 等 + +那我们可以再进一步,将所有静态资源都置于公共服务中吗? + +可以,实际上 OSS 也可以如此配置,但是较为繁琐,如 Rewrite 规则等配置。 + +如果,你既没有个人服务器,也没有属于个人的域名,可将自己所做的前端网站置于以下免费的托管服务平台。 + +1. Vercel +1. Github Pages +1. Netlify + +## 小结 + +通过本篇文章,我们已将静态资源部署至 CDN(近乎等同于 CDN),与大部分公司的生产环境一致。 + +但在测试环境中最好还是建议无需上传至 OSS,毕竟上传至 OSS 需要额外的时间,且对于测试环境无太大意义。 + +但实际上 OSS 在**上传及存储阶段**,还可以进一步优化,请看下一篇文章。 diff --git a/frontend-engineering/deploy/simple-intro.md b/frontend-engineering/deploy/simple-intro.md index 0522afc..0f4a27d 100644 --- a/frontend-engineering/deploy/simple-intro.md +++ b/frontend-engineering/deploy/simple-intro.md @@ -4,7 +4,7 @@ 初学如何部署前端,如同学习编程一样,第一步先根据最简单页面进行部署。 -比如本文,先部署一个最简单 HTML,以下我称它为**hello 版前端应用** +比如本系列专栏,先部署一个最简单 HTML 如下所示,只有几行代码,我称它为**hello 版前端应用**。 ``` html @@ -19,7 +19,7 @@ ``` -> PS: 本项目以 [simple-deploy](https://github.com/shfshanyue/simple-deploy) 仓库作为实践,服务文件位于 [server-fs.js](https://github.com/shfshanyue/simple-deploy/blob/master/server-fs.js) +> PS: 本文以 [simple-deploy](https://github.com/shfshanyue/simple-deploy) 仓库作为实践 ## HTTP 报文 @@ -56,12 +56,14 @@ Keep-Alive: timeout=5 ``` -## 一段简单的服务器代码 +## 一段简单的服务器部署代码 -作为前端,以我们最为熟悉的 Node 为例,写一段最简单的前端服务。 +作为前端,以我们最为熟悉的 Node 为例,写一段最简单的前端部署服务。 该服务监听本地的 3000 端口,并返回我们的*hello 版前端应用*。 +> PS: 该段服务器 nodejs 代码位于 [simple-deploy/server.js](https://github.com/shfshanyue/simple-deploy/blob/master/server.js) + ``` js const http = require('node:http') @@ -88,14 +90,21 @@ server.listen(3000, () => { **恭喜你,部署成功!** -是了,作为前端,你每天借助于 `webpack-dev-server/vite` 都在做着同样的事情。 +但是前端静态资源总是以文件的形式出现,我们对代码进一步优化。 + +## 一段稍微复杂的服务器代码: 文件系统 + +当然,部署前端作为**纯静态资源**,需要我们使用文件系统(fs)去读取资源并将数据返回。 + +在代码中,html 以前是个字符串,现在通过 nodejs 文件系统读取文件的相关 API `fs.readFileSync('./index.html')` 进行获取,代码如下。 -当然,作为**纯静态资源**,需要我们使用文件系统(fs)去读取资源并将数据进行返回。 +> PS: 该段服务器 nodejs 代码位于 [simple-deploy/server-fs.js](https://github.com/shfshanyue/simple-deploy/blob/master/server-fs.js) ``` js const http = require('node:http') const fs = require('node:fs') +// 上段代码这里是一段字符串,而这里通过读取文件获取内容 const html = fs.readFileSync('./index.html') const server = http.createServer((req, res) => res.end(html)) @@ -104,51 +113,62 @@ server.listen(3000, () => { }) ``` -当然,对于前端这类纯静态资源,自己写代码无论从开发效率还是性能而言都是极差的。 +当然,对于前端这类纯静态资源,**自己写代码无论从开发效率还是性能而言都是极差的**。 -因此,有诸多工具专门针对静态资源进行服务,比如 [serve](https://github.com/vercel/serve) +比如将文件系统修改为 `ReadStream` 的形式将会提升该静态服务器的性能,代码如下。 + +```js +const server = http.createServer((req, res) => { + // 此处需要手动处理下 Content-Length + fs.createReadStream('./index.html').pipe(res) +}) +``` + +因此,有诸多工具专门针对静态资源进行服务,比如 [serve](https://github.com/vercel/serve),比如 [nginx](https://nginx.org/en/)。 ``` bash $ npx serve . ``` +在 create-react-app 构建成功后,它会提示使用 `serve` 进行部署。 + ![Creact React APP 构建后,提示使用 serve 进行部署](https://cdn.jsdelivr.net/gh/shfshanyue/assets/2021-12-31/clipboard-3980.619061.webp) **然而,Javascript 的性能毕竟有限,使用 `nginx` 作为静态资源服务器拥有更高的性能。** -但是对于本地环境而言,还是 `serve` 要方便很多啊。 +但是对于本地环境而言,还是 [serve](https://github.com/vercel/serve) 要方便很多啊。 ## 部署的简单理解 那什么是部署呢,为什么说你刚才部署成功? -假设此时有一台拥有 IP 地址的服务器,登录上去,使用 `nodejs` 运行刚才的代码,则外网的人可通过 `IP:3000` 访问该页面。 +假设此时你有一台拥有公共 IP 地址的服务器,在这台服务器使用 `nodejs` 运行刚才的代码,则外网的人可通过 `IP:3000` 访问该页面。 那这可理解为部署,使得所有人都可以访问。 -你将服务器作为你的工作环境,通过 `npm run dev` 运行代码,所有人都可访问他,部署成功。看来你离所有人都可访问的部署只差一台服务器。 +假设你将该服务器作为你的工作环境,通过 `npm run start` 运行代码并通过,所有人都可访问他,部署成功。看来你离所有人都可访问的部署只差一台拥有公共 IP 的服务器。 不管怎么说,你现在已经可以通过裸机(宿主机)部署一个简单的前端应用了。 ## 一些疑问 -*那为什么还需要 nginx 呢?* +*问: 那既然通过 `npm start` 可以启动服务并暴露端口对外提供五福,那为什么还需要 nginx 呢?* -**你需要管理诸多服务(比如A网站、B网站),通过 nginx 进行路由转发至不同的服务,这也就是反向代理** +**你需要管理诸多服务(比如A网站、B网站),通过 nginx 进行路由转发至不同的服务,这也就是反向代理**,另外 nginx 还可以提供 TLS、HTTP2 等功能。 -当然,如果你不在意别人通过端口号去访问,不用 nginx 等反向代理器也是可以的。 +当然,如果你不介意别人通过端口号去访问你的应用,不用 nginx 等反向代理器也是可以的。 ![反向代理](https://cdn.jsdelivr.net/gh/shfshanyue/assets/2021-12-31/Nginx.e7035d.webp) -*那在服务器可以 npm run dev 部署吗?* +*问: 我确实不介意别人通过 IP:Port 的方式来访问我的应用,那在服务器可以 npm run dev 部署吗?* 可以,但是非常不推荐。`npm run dev` 往往需要监听文件变更并重启服务,此处需要消耗较大的内存及CPU等性能。 针对 Typescript 写的后端服务器,不推荐在服务器中直接使用 `ts-node` 而需要事先编译的理由同样如此。 -当然,如果你在意性能也是可以的。 +当然,如果你也不介意性能问题也是可以的。 -*那为什么需要 Docker 部署?* +*问: 那为什么需要 Docker 部署?* 用以隔离环境。 @@ -157,3 +177,11 @@ $ npx serve . 假设你有三个 Node 服务,分别用 node10、node12、node14 编写,你需要在服务器分别安装三个版本 nodejs,非常麻烦。 而有了 Docker,就没有这种问题。 + +对于前端而言,此时你可以通过由自己在项目中单独维护 `nginx.conf` 进行一些 nginx 的配置,大大提升前端的自由性和灵活度,而无需通过运维或者后端来进行。 + +## 小结 + +本篇文章介绍了了一些对于前端部署的简单介绍,并使用 nodejs 写了两段代码用以提供静态服务,来加深理解。 + +而在下篇文章中,我们将介绍如何使用 Docker 将仅有十几行代码的 **hello 版前端应用** 跑起来。 diff --git a/frontend-engineering/deploy/simple-nginx.md b/frontend-engineering/deploy/simple-nginx.md index 93f2af9..99342b9 100644 --- a/frontend-engineering/deploy/simple-nginx.md +++ b/frontend-engineering/deploy/simple-nginx.md @@ -1,6 +1,6 @@ # 基于 nginx 镜像构建容器 -正如上一篇章所言,对于仅仅提供静态资源服务的前端,实际上是不必将 node.js 作为运行环境的。 +正如上一篇章所言,对于仅仅提供静态资源服务的前端,实际上是不必将 nodejs 作为运行环境的。 在实际生产经验中,一般选择体积更小,性能更好基于 nginx 的镜像。 @@ -18,11 +18,7 @@ 那我们完全可以在本地通过 docker 来简单学习下 nginx。如此,既学习了 docker,又实践了 nginx。 -**如果你初学 nginx,强烈建议使用 docker 进行学习** - -**如果你初学 nginx,强烈建议使用 docker 进行学习** - -**如果你初学 nginx,强烈建议使用 docker 进行学习** +**如果你初学 nginx,强烈建议使用 docker 进行学习** 本篇文章最后会附上如何启动 nginx 镜像用以学习。 通过以下一行命令可进入 `nginx` 的环境当中,并且了解 nginx 的目录配置,*该命令将在以下段落用到*。 @@ -168,9 +164,19 @@ simple-deploy_node-app_1 simple-deploy_node-app latest 14054cb0f1d8 13 ## 通过 Docker 学习 Nginx 配置 -我们将注意力集中在**静态资源**与**nginx配置**两个点,在本地进行维护。 +最后,推荐一种高效学习 nginx 的方法: **在本地使用 nginx 镜像并挂载 nginx 配置启动容器**。 + +无需 Linux 环境,也无需自购个人服务器,你可以通过该方法快速掌握以下 nginx 的常用配置。 + +1. 如何配置静态资源缓存策略 +1. 如何配置 CORS +1. 如何配置 gzip/brotli 配置 +1. 如何配置路由匹配 Location +1. 如何配置 Rewrite、Redirect 等 + +我们将注意力集中在**静态资源**与**nginx配置**两个点,在本地进行更新及维护,并通过 `Volume` 的方式挂载到 nginx 容器中。 -并通过 `Volume` 的方式挂载到 nginx 容器中。配置文件如下: +配置文件如下,通过此配置可在 Docker 环境中学习 nginx 的各种指令。 > PS: docker-compose 配置文件位于 [simple-deploy](https://github.com/shfshanyue/simple-deploy/blob/master/learn-nginx.docker-compose.yaml) 中,可通过它实践 nginx 的配置 @@ -186,6 +192,12 @@ services: - .:/usr/share/nginx/html ``` +通过 `docker-compose` 启动该容器,如果需要修改配置,验证配置是否生效,可通过 `docker-compose` 重新启动该容器。 + +``` bash +$ docker-compose -f learn-nginx.docker-compose.yaml up learn-nginx +``` + --- 此时,已成功通过 `nginx` 镜像部署成功,镜像体积也由 `133MB` 下降到 `23.2MB`。然而此三篇文章仅仅部署了一个 `hello` 版的页面。 diff --git a/frontend-engineering/deploy/traefik-domain.md b/frontend-engineering/deploy/traefik-domain.md index 00e7eaf..9817790 100644 --- a/frontend-engineering/deploy/traefik-domain.md +++ b/frontend-engineering/deploy/traefik-domain.md @@ -4,18 +4,27 @@ 回到我们的 `create-react-app` 部署示例,我们如何将此项目可使他们在互联网通过域名进行访问? -我们将它部署到 `cra.shanyue.tech` 中作为示例。在此之前,我需要做两件事 +我们将它部署到 中作为示例。在此之前,我需要做两件事 1. `cra.shanyue.tech` 域名属于我个人。域名可自行在域名提供商进行购买。 -2. `cra.shanyue.tech` 域名通过 A 记录指向搭建好 traefik 网关的服务器的 IP 地址 +2. `cra.shanyue.tech` 域名通过 A 记录指向搭建好 traefik 网关的服务器的 IP 地址。此处需要通过域名提供商的控制台进行配置。 ![](https://cdn.jsdelivr.net/gh/shfshanyue/assets/2022-01-09/clipboard-9961.2a6ea3.webp) ## 启动服务 我们在容器中配置 `labels` 即可配置域名,启动容器域名即可生效。而无需像传统 nginx 方式需要手动去配置 `proxy_pass`。 +而在 `traefik`,在 `container labels` 中配置 `traefik.http.routers` 可为不同的路由注册域名。 + +``` yaml +labels: + - "traefik.http.routers.cra.rule=Host(`cra.shanyue.tech`)" +``` + 编辑 `domain.docker-compose.yaml`,配置文件如下。 +> PS: 该配置文件位于 [cra-deploy/domain.docker-compose.yaml](https://github.com/shfshanyue/cra-deploy/blob/master/domain.docker-compose.yaml) + ``` yaml {9,14} version: "3" services: @@ -39,32 +48,48 @@ networks: 根据 `docker-compose up` 启动服务,此时可在互联网访问。 ``` bash -$ docker-compose -f domain.docker-compose.yaml up +$ docker-compose -f domain.docker-compose.yaml up domain ``` -访问 `https://cra.shanyue.tech` 成功。 +访问 成功。 ## 如何配置多域名 +在 nginx 中可以通过 [server_name](https://nginx.org/en/docs/http/server_names.html) 配置多域名。 + +在 traefik 中通过 `traefik.https.routers` 可配置多域名。 + ++ cra.shanyue.tech ++ preview.cra.shanyue.tech ++ feature-a.cra.shanyue.tech + ``` yaml labels: - "traefik.http.routers.cra.rule=Host(`cra.shanyue.tech`)" - - "traefik.http.routers.cra-preview.rule=Host(`cra.preview.shanyue.tech`)" + - "traefik.http.routers.cra-preview.rule=Host(`preview.cra.shanyue.tech`)" + - "traefik.http.routers.cra-feature-a.rule=Host(`feature-a.cra.shanyue.tech`)" ``` -## 为什么我们可以仅仅设置 Label 来配置域名 +在我们启动 traefik 时,traefik 容器将 `/var/run/docker.sock` 挂载到容器当中。 + +通过 `docker.sock` 调用 [Docker Engine API](https://docs.docker.com/engine/api/v1.40/#tag/Image) 可将同一网络下所有容器信息列举出来。 + +``` bash +# 列举出所有容器的标签信息 +$ curl --unix-socket /var/run/docker.sock http:/containers/json | jq '.[] | .Labels' +``` ## 小结 -目前为止,终于将一个前端应用部署到了互联网。此时除了一些部署知识外,还需要一些服务器资源,包括 +目前为止,终于将一个前端应用使用域名进行部署。此时除了一些部署知识外,还需要一些服务器资源,包括 -1. 一台拥有公网 IP 地址的服务器 -1. 一个域名 +1. 一台拥有公网IP地址的服务器 +1. 一个自己申请的域名 当然,针对前端开发者而言,更重要的还是 1. 如何使用 docker 将它跑起来 -2. **如何将它更快地跑起来** +2. 如何将它更快地跑起来 3. **如何自动将它跑起来** 下一篇文章内容便是 CICD 相关。 diff --git a/frontend-engineering/deploy/traefik.md b/frontend-engineering/deploy/traefik.md index 086e29d..6d6afd6 100644 --- a/frontend-engineering/deploy/traefik.md +++ b/frontend-engineering/deploy/traefik.md @@ -1,24 +1,28 @@ # 服务编排: 服务发现与 Treafik 网关搭建 -假设你在服务器中,现在维护了 N 个容器,每个容器包含一个服务。 +通过该专栏的前序文章,我们已经很熟练地在服务器中通过 Docker 进行前端应用的部署。但如何使它对外提供访问呢? -但好像,除了使用容器启动服务外,和传统方式并无二致。 +假设你在服务器中,现在维护了 N 个前端应用,起了 N 个容器。但好像,除了使用容器启动服务外,和传统方式并无二致,以前管理进程,现在管理容器。 -对,差了一个服务的编排功能。 +对,还差一个服务的编排功能。 比如 -1. 我使用 docker 跑了 N 个服务,我怎么了解所有的服务以及他们的状态呢? +1. 我使用 docker 跑了 N 个服务,我怎么了解所有的服务的健康状态及路由呢? 1. 我使用 docker 新跑了一个服务,如何让它被其它服务所感知或直接被互联网所访问呢? -这就需要一个基于服务发现的网关建设: traefik +这就需要一个基于服务发现的网关建设: [Traefik](https://github.com/traefik/traefik) ## traefik 搭建 -[traefik](https://github.com/traefik/traefik) 目前在 Github 拥有 36K 星星,可以放心使用。 +[traefik](https://github.com/traefik/traefik) 是一个现代化的反向代理与负载均衡器,它可以很容易地同 Docker 集成在一起使用。每当 Docker 容器部署成功,便可以自动在网络上进行访问。 + +目前 traefik 在 Github 拥有 36K 星星,可以放心使用。 ![](https://cdn.jsdelivr.net/gh/shfshanyue/assets/2022-01-08/clipboard-0525.255635.webp) +配置一下 `docker compose` 可启动 traefik 服务。 + ``` yaml version: '3' @@ -33,13 +37,13 @@ services: - /var/run/docker.sock:/var/run/docker.sock ``` -使用 `docker-compose up` 启动 traefik,此时会默认新建一个 `traefik_network` 的网络。这个网络名称很重要,要记住。 +使用 `docker-compose up` 启动 traefik 后,此时会默认新建一个 `traefik_network` 的网络。这个网络名称很重要,要记住。 ![](https://cdn.jsdelivr.net/gh/shfshanyue/assets/2022-01-08/clipboard-5259.7e350b.webp) ## 启动一个任意的服务 -启动一个 whoami 的简易版服务 +启动一个 [whoami](https://hub.docker.com/r/containous/whoami) 的简易版 Web 服务,它将会在页面上打印出一些头部信息。 ``` yaml version: '3' @@ -59,14 +63,17 @@ networks: name: traefik_default ``` -那 `whoami` 这个 `http` 服务做了什么事情呢 +那 `whoami` 服务做了什么事情呢 1. 暴露了一个 `http` 服务,主要提供一些 `header` 以及 `ip` 信息 1. 配置了容器的 `labels`,设置该服务的 `Host` 为 `whoami.shanyue.local`,给 `traefik` 提供标记 此时我们可以通过主机名 `whoami.docker.localhost` 来访问 `whoami` 服务,我们使用 `curl` 做测试 +> PS: `whoami.docker.localhost` 可以是任意域名,此处仅做测试。如果你拥有个人域名,替换成个人域名后,可在任意互联网处进行访问。 + ``` bash +# 通过 -H 来指定 Host $ curl -H Host:whoami.shanyue.local http://127.0.0.1 Hostname: f4b29ed568da IP: 127.0.0.1 @@ -89,11 +96,13 @@ X-Real-Ip: 172.20.0.1 **此时如果把 `Host` 配置为自己的域名,则已经可以使用自己的域名来提供服务。** -由于本系列文章重点在于部署,因此对于 Traefik 将不再过多研究 +由于本系列文章重点在于部署,因此对于 Traefik 以下两点将不再过多研究 1. 如何配置 https 1. 如何配置 Dashboard +使用以下配置文件,直接配置生效。 + ## 终极配置文件 终极配置文件已经配置好了 LTS、Access Log 等,但是细节就不讲了,直接上配置。 @@ -283,4 +292,12 @@ $ chmod 600 acme.json $ touch .env $ docker-compose up -``` \ No newline at end of file +``` + +## 小结 + +此时,一个方向代理的 Traefix 已经完美配置。当部署一个前端应用后,将会自动实现以下功能: + +1. TLS。部署域名将可直接使用 HTTPS 进行访问。 +1. AccessLog。会自动收集每个服务的请求日志。 +1. 自动收集每个服务的健康状态。 diff --git a/package.json b/package.json index e46e807..f15f039 100644 --- a/package.json +++ b/package.json @@ -6,7 +6,7 @@ "scripts": { "dev": "vuepress dev", "build": "vuepress build", - "deploy": "rclone copy .vuepress/dist alioss:/shanyue-blog --progress" + "oss:rclone": "rclone copy --exclude 'assets/**' --header 'Cache-Control: no-cache' .vuepress/dist alioss:/shanyue-blog --progress && rclone copy --header 'Cache-Control: max-age=31536000' .vuepress/dist/assets alioss:/shanyue-blog/assets --progress" }, "repository": { "type": "git", From aff1b63ecb4989c0128218308ac8254229407f9a Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 18 Feb 2022 06:03:11 +0000 Subject: [PATCH 2/2] Bump url-parse from 1.5.1 to 1.5.7 Bumps [url-parse](https://github.com/unshiftio/url-parse) from 1.5.1 to 1.5.7. - [Release notes](https://github.com/unshiftio/url-parse/releases) - [Commits](https://github.com/unshiftio/url-parse/compare/1.5.1...1.5.7) --- updated-dependencies: - dependency-name: url-parse dependency-type: indirect ... Signed-off-by: dependabot[bot] --- package-lock.json | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/package-lock.json b/package-lock.json index 70101f8..d7258b9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -14988,9 +14988,9 @@ } }, "node_modules/url-parse": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.5.1.tgz", - "integrity": "sha512-HOfCOUJt7iSYzEx/UqgtwKRMC6EU91NFhsCHMv9oM03VJcVo2Qrp8T8kI9D7amFf1cu+/3CEhgb3rF9zL7k85Q==", + "version": "1.5.7", + "resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.5.7.tgz", + "integrity": "sha512-HxWkieX+STA38EDk7CE9MEryFeHCKzgagxlGvsdS7WBImq9Mk+PGwiT56w82WI3aicwJA8REp42Cxo98c8FZMA==", "dev": true, "dependencies": { "querystringify": "^2.1.1", @@ -29501,9 +29501,9 @@ } }, "url-parse": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.5.1.tgz", - "integrity": "sha512-HOfCOUJt7iSYzEx/UqgtwKRMC6EU91NFhsCHMv9oM03VJcVo2Qrp8T8kI9D7amFf1cu+/3CEhgb3rF9zL7k85Q==", + "version": "1.5.7", + "resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.5.7.tgz", + "integrity": "sha512-HxWkieX+STA38EDk7CE9MEryFeHCKzgagxlGvsdS7WBImq9Mk+PGwiT56w82WI3aicwJA8REp42Cxo98c8FZMA==", "dev": true, "requires": { "querystringify": "^2.1.1",